diff --git a/packages/core/package-lock.json b/packages/core/package-lock.json index 3005091dc2..07a671d3f0 100644 --- a/packages/core/package-lock.json +++ b/packages/core/package-lock.json @@ -14,9 +14,9 @@ } }, "@stencil/core": { - "version": "0.0.8-3", - "resolved": "https://registry.npmjs.org/@stencil/core/-/core-0.0.8-3.tgz", - "integrity": "sha512-y/JQbezu5LOSEzMM57y8BqCnCmZKjPZQA5cZmw9t8MXS/aHxYYpG3jkfVYquQa8jUyTtmwCAWYlKjRxMGmqI7g==", + "version": "0.0.8-5", + "resolved": "https://registry.npmjs.org/@stencil/core/-/core-0.0.8-5.tgz", + "integrity": "sha512-BQjBU44pFXCOEVZO0e55ihdyjBkp6EJ+nwIoxSko6CXoX1a2NuYRHpj3BP+XBljZaLChTI1PQjFbchinytsv5A==", "dev": true, "requires": { "chokidar": "1.7.0", @@ -56,9 +56,9 @@ "dev": true }, "@types/node": { - "version": "8.0.51", - "resolved": "https://registry.npmjs.org/@types/node/-/node-8.0.51.tgz", - "integrity": "sha512-El3+WJk2D/ppWNd2X05aiP5l2k4EwF7KwheknQZls+I26eSICoWRhRIJ56jGgw2dqNGQ5LtNajmBU2ajS28EvQ==", + "version": "8.0.53", + "resolved": "https://registry.npmjs.org/@types/node/-/node-8.0.53.tgz", + "integrity": "sha512-54Dm6NwYeiSQmRB1BLXKr5GELi0wFapR1npi8bnZhEcu84d/yQKqnwwXQ56hZ0RUbTG6L5nqDZaN3dgByQXQRQ==", "dev": true }, "abab": { @@ -734,6 +734,12 @@ } } }, + "browser-stdout": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.0.tgz", + "integrity": "sha1-81HTKWnTL6XXpVZxVCY9korjvR8=", + "dev": true + }, "bser": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/bser/-/bser-2.0.0.tgz", @@ -1515,6 +1521,14 @@ "requires": { "acorn": "5.2.1", "acorn-jsx": "3.0.1" + }, + "dependencies": { + "acorn": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.2.1.tgz", + "integrity": "sha512-jG0u7c4Ly+3QkkW18V+NRDN+4bWHdln30NL1ZL2AvFZZmQe/BfopYCtghCKKVBUSetZ4QKcyA0pY6/4Gw8Pv8w==", + "dev": true + } } }, "esprima": { @@ -2941,6 +2955,12 @@ "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", "dev": true }, + "growl": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.3.tgz", + "integrity": "sha512-hKlsbA5Vu3xsh1Cg3J7jSmX/WaW6A5oBeqzM88oNbCRQFz+zUaXm6yxS4RVytp1scBoJzSYl4YAEOQIt6O8V1Q==", + "dev": true + }, "growly": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz", @@ -4996,6 +5016,28 @@ } } }, + "listr-input": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/listr-input/-/listr-input-0.1.2.tgz", + "integrity": "sha512-GQl3txLCcuKzmkYxMjZ3idiICyyjxZTfDz2k92LXU3byxY1nz249iKbfMKvSmWYUHs85TqWYZCkMJ316OKjTfA==", + "dev": true, + "requires": { + "inquirer": "3.3.0", + "rxjs": "5.5.2", + "through": "2.3.8" + }, + "dependencies": { + "rxjs": { + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-5.5.2.tgz", + "integrity": "sha512-oRYoIKWBU3Ic37fLA5VJu31VqQO4bWubRntcHSJ+cwaDQBwdnZ9x4zmhJfm/nFQ2E82/I4loSioHnACamrKGgA==", + "dev": true, + "requires": { + "symbol-observable": "1.0.4" + } + } + } + }, "listr-silent-renderer": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/listr-silent-renderer/-/listr-silent-renderer-1.1.1.tgz", @@ -5436,6 +5478,50 @@ } } }, + "mocha": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-4.0.1.tgz", + "integrity": "sha512-evDmhkoA+cBNiQQQdSKZa2b9+W2mpLoj50367lhy+Klnx9OV8XlCIhigUnn1gaTFLQCa0kdNhEGDr0hCXOQFDw==", + "dev": true, + "requires": { + "browser-stdout": "1.3.0", + "commander": "2.11.0", + "debug": "3.1.0", + "diff": "3.3.1", + "escape-string-regexp": "1.0.5", + "glob": "7.1.2", + "growl": "1.10.3", + "he": "1.1.1", + "mkdirp": "0.5.1", + "supports-color": "4.4.0" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "diff": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.3.1.tgz", + "integrity": "sha512-MKPHZDMB0o6yHyDryUOScqZibp914ksXwAMYMTHj6KO8UeKsRYNJD3oNCKjTqZon+V488P7N/HzXF8t7ZR95ww==", + "dev": true + }, + "supports-color": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.4.0.tgz", + "integrity": "sha512-rKC3+DyXWgK0ZLKwmRsrkyHVZAjNkfzeehuFWdGGcqGDTZFH73+RH6S/RDAAxl9GusSjZSUWYLmT9N5pzXFOXQ==", + "dev": true, + "requires": { + "has-flag": "2.0.0" + } + } + } + }, "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -5570,9 +5656,9 @@ } }, "np": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/np/-/np-2.16.1.tgz", - "integrity": "sha512-UKOSN81mlC8HL1QxIeWS34a6jxM67ec7GehK7vCJePmkcBuvQvtJtQv9mUnYFzL5LfQ6DIa/X2e2veIjU/TmsQ==", + "version": "2.17.0", + "resolved": "https://registry.npmjs.org/np/-/np-2.17.0.tgz", + "integrity": "sha512-60q9qIyeAsBgMshQ/93vglkIdrlSanG3EysQj3Q3n0X7BWfkz2LMkoaBo9eIuSQHDx/vBlG8t6ZXTlg8Wz2ViA==", "dev": true, "requires": { "any-observable": "0.2.0", @@ -5582,6 +5668,7 @@ "has-yarn": "1.0.0", "inquirer": "3.3.0", "listr": "0.11.0", + "listr-input": "0.1.2", "log-symbols": "1.0.2", "meow": "3.7.0", "read-pkg-up": "2.0.0", @@ -5939,7 +6026,7 @@ "integrity": "sha512-rgO9Zg5LLLkfJF9E6CCmXlSE4UVceloys8JrFqCcHloC3usd/kJCyPDwH2SOlzix2j3xaP9sUX3e8+kvkuleAA==", "dev": true, "requires": { - "@types/node": "8.0.51" + "@types/node": "8.0.53" } }, "path-exists": { @@ -7556,13 +7643,13 @@ "glob": "7.1.2", "lodash.template": "4.4.0", "mkdirp": "0.5.1", - "workbox-sw": "2.1.1" + "workbox-sw": "2.1.2" } }, "workbox-sw": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/workbox-sw/-/workbox-sw-2.1.1.tgz", - "integrity": "sha512-vweBw08fiup2ocJWMCpd2oFlVM5s9c1vdR9PzYkO8lUGNLDU+5vvMblpe3DRBZn11FABF0OriDb6AerXp1M2tw==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/workbox-sw/-/workbox-sw-2.1.2.tgz", + "integrity": "sha512-ZGOM57FWD4mnH7qCSQS7XCPX4RzuyUpqb67j68jw4/VNAmGt/1/hq63Md8N++B3tj5dulgGGDC4EBTU2LpPkIg==", "dev": true }, "worker-farm": { diff --git a/packages/core/package.json b/packages/core/package.json index 93ea9accb8..15fcf8792f 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -10,17 +10,17 @@ "dist/" ], "devDependencies": { - "@stencil/core": "0.0.8-3", + "@stencil/core": "0.0.8-5", "@stencil/dev-server": "0.0.18-0", "@stencil/utils": "latest", - "@types/jest": "^21.1.0", + "@types/jest": "^21.1.6", "ionicons": "^4.0.0-6", - "jest": "^21.1.0", + "jest": "^21.2.1", "mocha": "^4.0.1", - "np": "^2.16.1", - "sass-lint": "^1.11.1", + "np": "^2.17.0", + "sass-lint": "^1.12.1", "selenium-webdriver": "^3.6.0", - "tslint": "^5.7.0", + "tslint": "^5.8.0", "tslint-ionic-rules": "0.0.11" }, "scripts": { @@ -52,14 +52,15 @@ "homepage": "https://github.com/ionic-team/ionic#readme", "jest": { "transform": { - "^.+\\.(ts|tsx)$": "/node_modules/@stencil/core/testing/jest.preprocessor.js" + "^.+\\.(js|ts|tsx)$": "/node_modules/@stencil/core/testing/jest.preprocessor.js" }, "testRegex": "src/.*\\.spec\\.(ts|tsx|js)$", "moduleFileExtensions": [ "ts", "tsx", "js", - "jsx" + "jsx", + "d.ts" ] } } diff --git a/packages/core/src/components.d.ts b/packages/core/src/components.d.ts index 219a43393a..5f0ee9b10f 100644 --- a/packages/core/src/components.d.ts +++ b/packages/core/src/components.d.ts @@ -2423,42 +2423,39 @@ declare global { namespace JSXElements { export interface IonRouteAttributes extends HTMLAttributes { - url?: string, + path?: string, component?: string, - componentProps?: any, - router?: any + props?: any } } } import { - Router as IonRouter -} from './components/router/router'; + RouterController as IonRouterController +} from './components/router/router-controller'; declare global { - interface HTMLIonRouterElement extends IonRouter, HTMLElement { + interface HTMLIonRouterControllerElement extends IonRouterController, HTMLElement { } - var HTMLIonRouterElement: { - prototype: HTMLIonRouterElement; - new (): HTMLIonRouterElement; + var HTMLIonRouterControllerElement: { + prototype: HTMLIonRouterControllerElement; + new (): HTMLIonRouterControllerElement; }; interface HTMLElementTagNameMap { - "ion-router": HTMLIonRouterElement; + "ion-router-controller": HTMLIonRouterControllerElement; } interface ElementTagNameMap { - "ion-router": HTMLIonRouterElement; + "ion-router-controller": HTMLIonRouterControllerElement; } namespace JSX { interface IntrinsicElements { - "ion-router": JSXElements.IonRouterAttributes; + "ion-router-controller": JSXElements.IonRouterControllerAttributes; } } namespace JSXElements { - export interface IonRouterAttributes extends HTMLAttributes { + export interface IonRouterControllerAttributes extends HTMLAttributes { - root?: string, - match?: any } } } @@ -3040,9 +3037,7 @@ declare global { export interface IonTabAttributes extends HTMLAttributes { btnId?: string, - root?: any, - rootParams?: any, - urlPath?: string, + path?: string, title?: string, icon?: string, badge?: string, diff --git a/packages/core/src/components/app/app.scss b/packages/core/src/components/app/app.scss index ec54e0b867..992e98ad75 100644 --- a/packages/core/src/components/app/app.scss +++ b/packages/core/src/components/app/app.scss @@ -231,7 +231,6 @@ ion-page, position: absolute; z-index: $z-index-page-container; - display: block; width: 100%; height: 100%; @@ -239,37 +238,6 @@ ion-page, contain: strict; } -ion-tabs { - display: flex; - - flex-direction: column; -} - -.tabs-inner { - position: relative; - flex: 1; - - contain: strict; -} - -ion-tab { - @include position(0, null, null, 0); - - position: absolute; - z-index: -1; - display: none; - - width: 100%; - height: 100%; - - contain: strict; -} - -ion-tab.show-tab { - z-index: $z-index-page-container; - display: block; -} - // Page Container Structure // -------------------------------------------------- @@ -308,6 +276,17 @@ ion-header { // Misc // -------------------------------------------------- +ion-route, +ion-route-controller, +ion-animation-controller, +ion-nav-controller, +ion-menu-controller, +ion-action-sheet-controller, +ion-alert-controller, +ion-loading-controller, +ion-modal-controller, +ion-picker-controller, +ion-toast-controller, [app-viewport], [overlay-portal], [nav-viewport], diff --git a/packages/core/src/components/app/app.tsx b/packages/core/src/components/app/app.tsx index 3b61d3e239..b4fcc607f2 100644 --- a/packages/core/src/components/app/app.tsx +++ b/packages/core/src/components/app/app.tsx @@ -1,4 +1,4 @@ -import { Component, Element, Listen, Prop } from '@stencil/core'; +import { Component, Element, Listen, Prop, State } from '@stencil/core'; import { Config, Nav, NavContainer } from '../../index'; import { isReady } from '../../utils/helpers'; @@ -17,17 +17,23 @@ const rootNavs = new Map(); export class App { @Element() element: HTMLElement; + + @State() modeCode: string; + @State() hoverCSS: boolean = false; + @State() useRouter: boolean = false; + @Prop({ context: 'config' }) config: Config; - @Listen('body:navInit') - registerRootNav(event: CustomEvent) { - rootNavs.set((event.detail as Nav).navId, (event.detail as Nav)); + + protected componentWillLoad() { + this.modeCode = this.config.get('mode'); + this.useRouter = this.config.getBoolean('useRouter', false); + this.hoverCSS = this.config.getBoolean('hoverCSS', true); } - - - componentWillLoad() { - componentDidLoadImpl(this); + @Listen('body:navInit') + protected registerRootNav(event: CustomEvent) { + rootNavs.set((event.detail as Nav).navId, (event.detail as Nav)); } getActiveNavs(rootNavId?: number): NavContainer[] { @@ -65,10 +71,21 @@ export class App { return null; } + protected hostData() { + return { + class: { + [this.modeCode]: true, + 'enable-hover': this.hoverCSS + } + }; + } + protected render() { - return ( - - ); + const dom = []; + if (this.useRouter) { + dom.push(); + } + return dom; } } @@ -100,15 +117,6 @@ export function getNavByIdOrNameImpl(nav: NavContainer, id: number | string): Na return null; } -export function componentDidLoadImpl(app: App) { - app.element.classList.add(app.config.get('mode')); - // TODO add platform classes - if (app.config.getBoolean('hoverCSS', true)) { - app.element.classList.add('enable-hover'); - } - // TODO fire platform ready -} - export function handleBackButtonClick(): Promise { // if there is a menu controller dom element, hydrate it, otherwise move on // TODO ensure ion-menu-controller is the name diff --git a/packages/core/src/components/nav/nav.tsx b/packages/core/src/components/nav/nav.tsx index 0a6d640295..49660eead4 100644 --- a/packages/core/src/components/nav/nav.tsx +++ b/packages/core/src/components/nav/nav.tsx @@ -1,7 +1,8 @@ import { Component, Element, Event, EventEmitter, Listen, Method, Prop } from '@stencil/core'; import { ComponentDataPair, Config, FrameworkDelegate, NavController, NavOptions, ViewController } from '../../index'; -import { getActiveImpl, getFirstView, getPreviousImpl, getViews, init } from '../../navigation/nav-utils'; -import { isReady } from '../../utils/helpers'; +import { getActiveImpl, getFirstView, getNextNavId, getPreviousImpl, getViews, resolveRoute } from '../../navigation/nav-utils'; +import { assert, isReady } from '../../utils/helpers'; +import { NavState, RouterEntries, RouterEntry } from '../../index'; @Component({ tag: 'ion-nav', @@ -10,10 +11,14 @@ export class Nav { @Element() element: HTMLElement; @Event() navInit: EventEmitter; + @Event() ionNavChanged: EventEmitter; + useRouter: boolean; navId: number; + init = false; + routes: RouterEntries = []; parent: Nav; - views: ViewController[]; + views: ViewController[] = []; transitioning?: boolean; destroyed?: boolean; transitionId?: number; @@ -29,11 +34,31 @@ export class Nav { @Prop({ context: 'config' }) config: Config; constructor() { - init(this); + this.navId = getNextNavId(); + } + + @Listen('ionRouteAdded') + routeAdded(ev: CustomEvent) { + this.addRoute(ev.detail); + } + + @Listen('ionRouteRemoved') + routeRemoved(ev: CustomEvent) { + this.removeRoute(ev.detail); + } + + componentWillLoad() { + this.useRouter = this.config.getBoolean('useRouter', false); } componentDidLoad() { - componentDidLoadImpl(this); + if (this.init) { + return; + } + this.init = true; + if (!this.useRouter) { + componentDidLoadImpl(this); + } } getViews(): ViewController[] { @@ -125,12 +150,61 @@ export class Nav { navInitializedImpl(this, event); } + @Method() + addRoute(route: RouterEntry) { + this.routes.push(route); + } + + @Method() + removeRoute(_: RouterEntry) { + throw 'not implemented'; + } + + @Method() + getState(): NavState { + assert(this.useRouter, 'routing is disabled'); + return getState(this); + } + + @Method() + setRouteId(id: string, _: any = {}): Promise { + assert(this.useRouter, 'routing is disabled'); + const active = this.getActive(); + if (active && active.component === id) { + return Promise.resolve(); + } + return this.setRoot(id); + } + + @Method() + getRoutes(): RouterEntries { + assert(this.useRouter, 'routing is disabled'); + return this.routes; + } protected render() { return ; } } +export function getState(nav: Nav): NavState { + const active = getActiveImpl(nav); + if (!active) { + return null; + } + const component = active.component; + const route = resolveRoute(nav, component); + if (!route) { + console.error('cant reverse route by component', component); + return null; + } + + return { + path: route.path, + focusNode: active.element + }; +} + export function componentDidLoadImpl(nav: Nav) { nav.navInit.emit(nav); if (nav.root) { diff --git a/packages/core/src/components/nav/page-three.tsx b/packages/core/src/components/nav/page-three.tsx index 424037b684..b9196e85cf 100644 --- a/packages/core/src/components/nav/page-three.tsx +++ b/packages/core/src/components/nav/page-three.tsx @@ -18,9 +18,9 @@ export class PageThree { protected render() { return [ - + Page Three - + , Page Three Content diff --git a/packages/core/src/components/nav/page-two.tsx b/packages/core/src/components/nav/page-two.tsx index 586c693e96..a3ef35d5f3 100644 --- a/packages/core/src/components/nav/page-two.tsx +++ b/packages/core/src/components/nav/page-two.tsx @@ -23,9 +23,9 @@ export class PageTwo { protected render() { return [ - + Page Two - + , Page Two Content diff --git a/packages/core/src/components/router/route.tsx b/packages/core/src/components/router/route.tsx index 4255e1c927..b91ccf353a 100644 --- a/packages/core/src/components/router/route.tsx +++ b/packages/core/src/components/router/route.tsx @@ -1,4 +1,5 @@ -import { Component, Prop, State } from '@stencil/core'; +import { Component, Event, EventEmitter, Prop } from '@stencil/core'; +import { RouterEntry, parseURL } from './router-utils'; /** * @name Route @@ -9,45 +10,24 @@ import { Component, Prop, State } from '@stencil/core'; tag: 'ion-route' }) export class Route { - @Prop() url: string; + @Prop() path: string; @Prop() component: string; + @Prop() props: any = {}; - @Prop() componentProps: any = {}; + @Event() ionRouteAdded: EventEmitter; + @Event() ionRouteRemoved: EventEmitter; - // The instance of the router - @Prop() router: any; - - // @Prop() match: any; - @State() match: any = {}; - - protected ionViewWillLoad() { -/* - this.routerInstance = document.querySelector(this.router) - - // HACK - this.routerInstance.addEventListener('ionRouterNavigation', (e) => { - this.match = e.detail; - }) -*/ + protected ionViewDidLoad() { + this.ionRouteAdded.emit({ + path: this.path, + segments: parseURL(this.path), + id: this.component, + props: this.props + }); } - protected render() { -/* - this.match.url = this.routerInstance.routeMatch.url; - const match = this.match - const ChildComponent = this.component - - console.log('Does match match?', match.url, this.url) - - //return

; - - if(match.url == this.url) { - console.log(` Rendering route ${this.url}`, router, match); - return (); - } else { - return null; - } -*/ + protected ionViewDidUnload() { + this.ionRouteRemoved.emit(this.path); } } diff --git a/packages/core/src/components/router/router-controller.tsx b/packages/core/src/components/router/router-controller.tsx new file mode 100644 index 0000000000..4e784d065d --- /dev/null +++ b/packages/core/src/components/router/router-controller.tsx @@ -0,0 +1,102 @@ +import { Component, Listen, Prop } from '@stencil/core'; +import { RouterSegments, generateURL, parseURL, readNavState, writeNavState } from './router-utils'; +import { Config } from '../../index'; + +@Component({ + tag: 'ion-router-controller' +}) +export class RouterController { + + private busy = false; + private enabled = false; + private basePrefix: string = '#'; + + @Prop({ context: 'config' }) config: Config; + + protected ionViewDidLoad() { + const enabled = this.enabled = this.config.getBoolean('useRouter', false); + if (enabled) { + const base = document.querySelector('head > base'); + if (base) { + const baseURL = base.getAttribute('href'); + if (baseURL.length > 0) { + this.basePrefix = baseURL; + } + } + + Context.dom.raf(() => { + console.debug('[OUT] page load -> write nav state'); + this.writeNavStateRoot(); + }); + } + } + + @Listen('window:hashchange') + protected onURLHashChanged() { + if (!this.isBlocked()) { + console.debug('[OUT] hash changed -> write nav state'); + this.writeNavStateRoot(); + } + } + + @Listen('body:ionNavChanged') + protected onNavChanged(ev: CustomEvent) { + if (this.isBlocked()) { + return; + } + console.debug('[IN] nav changed -> update URL'); + const { stack, pivot } = this.readNavState(); + if (pivot) { + // readNavState() found a pivot that is not initialized + console.debug('[IN] pivot uninitialized -> write partial nav state'); + this.writeNavState(pivot, []); + } + + const isPop = ev.detail.isPop === true; + this.setURL(generateURL(stack), isPop); + } + + private setURL(url: string, isPop: boolean) { + url = this.basePrefix + url; + const history = window.history; + if (isPop) { + history.back(); + history.replaceState(null, null, url); + } else { + history.pushState(null, null, url); + } + } + + private isBlocked(): boolean { + return this.busy || !this.enabled; + } + + private writeNavStateRoot(): Promise { + const node = document.querySelector('ion-app') as HTMLElement; + return this.writeNavState(node, this.readURL()); + } + + private writeNavState(node: any, url: string[]): Promise { + const segments = new RouterSegments(url); + this.busy = true; // prevents reentrance + return writeNavState(node, segments) + .catch(err => console.error(err)) + .then(() => this.busy = false); + } + + private readNavState() { + let root = document.querySelector('ion-app') as HTMLElement; + return readNavState(root); + } + + private isHash() { + return this.basePrefix.length > 0 && this.basePrefix[0]; + } + + private readURL(): string[] { + const url = this.isHash() + ? window.location.hash.substr(1) + : window.location.pathname; + return parseURL(url); + } +} diff --git a/packages/core/src/components/router/router-utils.tsx b/packages/core/src/components/router/router-utils.tsx new file mode 100644 index 0000000000..ff77ca3180 --- /dev/null +++ b/packages/core/src/components/router/router-utils.tsx @@ -0,0 +1,183 @@ + +export interface NavElement extends HTMLElement { + setRouteId(id: any, data?: any): Promise; + getRoutes(): RouterEntries; + getState(): NavState; + + componentOnReady(): Promise; + componentOnReady(done: (cmp?: HTMLElement) => void): void; +} + +export interface RouterEntry { + path: string; + id: any; + segments?: string[]; + props?: any; +} + +export type RouterEntries = RouterEntry[]; + +export interface NavState { + path: string; + focusNode: HTMLElement; +} + +export class RouterSegments { + constructor( + private segments: string[] + ) {} + + next(): string { + if (this.segments.length > 0) { + return this.segments.shift(); + } + return ''; + } +} + +export function writeNavState(root: HTMLElement, segments: RouterSegments): Promise { + const node = breadthFirstSearch(root); + if (!node) { + return Promise.resolve(); + } + return node.componentOnReady() + .then(() => node.getRoutes()) + .then(routes => mustMatchRoute(segments, routes)) + .then(route => node.setRouteId(route.id)) + .then(() => { + const state = node.getState(); + if (!state) { + throw new Error('setRouteId failed?'); + } + writeNavState(state.focusNode, segments); + }); +} + +export function readNavState(node: HTMLElement) { + const stack = []; + let pivot: NavElement; + let state: NavState; + while (true) { + pivot = breadthFirstSearch(node); + if (pivot) { + state = pivot.getState(); + if (state) { + node = state.focusNode; + stack.push(state); + } else { + break; + } + } else { + break; + } + } + return { + stack: stack, + pivot: pivot + }; +} + +function mustMatchRoute(segments: RouterSegments, routes: RouterEntries) { + const r = matchRoute(segments, routes); + if (!r) { + throw 'no route found'; + } + return r; +} + +export function matchRoute(segments: RouterSegments, routes: RouterEntries): RouterEntry { + if (!routes) { + return null; + } + let index = 0; + routes = routes.map(initRoute); + let selectedRoute: RouterEntry = null; + let ambiguous = false; + let segment: string; + let l: number; + + while (true) { + routes = routes.filter(r => r.segments.length > index); + if (routes.length === 0) { + break; + } + segment = segments.next(); + routes = routes.filter(r => r.segments[index] === segment); + l = routes.length; + if (l === 0) { + selectedRoute = null; + ambiguous = false; + } else { + selectedRoute = routes[0]; + ambiguous = l > 1; + } + index++; + } + if (ambiguous) { + throw new Error('ambiguious match'); + } + return selectedRoute; +} + +export function generateURL(stack: NavState[]): string { + const segments: string[] = []; + for (let state of stack) { + segments.push(...parseURL(state.path)); + } + const path = segments + .filter(s => s.length > 0) + .join('/'); + + return '/' + path; +} + +export function initRoute(route: RouterEntry): RouterEntry { + if (route.segments === undefined || route.segments === null) { + route.segments = parseURL(route.path); + } + return route; +} + +export function parseURL(url: string): string[] { + if (url === null || url === undefined) { + return ['']; + } + const segments = url.split('/') + .map(s => s.trim()) + .filter(s => s.length > 0); + + if (segments.length === 0) { + return ['']; + } else { + return segments; + } +} + +const navs = ['ION-NAV', 'ION-TABS']; +export function breadthFirstSearch(root: HTMLElement): NavElement { + if (!root) { + console.error('search root is null'); + return null; + } + // we do a Breadth-first search + // Breadth-first search (BFS) is an algorithm for traversing or searching tree + // or graph data structures.It starts at the tree root(or some arbitrary node of a graph, + // sometimes referred to as a 'search key'[1]) and explores the neighbor nodes + // first, before moving to the next level neighbours. + + const queue = [root]; + while (queue.length > 0) { + const node = queue.shift(); + // visit node + if (navs.indexOf(node.tagName) >= 0) { + return node as NavElement; + } + + // queue children + const children = node.children; + for (let i = 0; i < children.length; i++) { + queue.push(children[i] as NavElement); + } + } + return null; +} diff --git a/packages/core/src/components/router/router.tsx b/packages/core/src/components/router/router.tsx deleted file mode 100644 index 5331f5ebc9..0000000000 --- a/packages/core/src/components/router/router.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { Component, Prop, State } from '@stencil/core'; - -/** - * @name Router - * @module ionic - * @description - */ -@Component({ - tag: 'ion-router' -}) -export class Router { - base: string; - - @Prop() root: string = ''; - - @State() routeMatch: any = {}; - - @Prop() - get match() { - return this.routeMatch; - } - - /* - @Prop() - navigateTo(url, data={}) { - window.history.pushState(null, null, url); - this.routeMatch = { - url: '/' + url.replace(this.root, '') - } - console.log('Route match', this.routeMatch); - - console.log('Emitting event'); - Ionic.emit(this.$instance, 'ionRouterNavigation', { detail: this.routeMatch }); - } - - protected ionViewWillLoad() { - console.log(' loaded'); - window.addEventListener('popstate', this.handlePopState.bind(this)); - window.addEventListener('hashchange', this.handleHashChange.bind(this)); - - const initialPath = window.location.pathname - const withoutBase = initialPath.replace(this.root, '') - - this.routeMatch = { - url: "/" + withoutBase - } - console.log('Route match', this.routeMatch); - } - - handlePopState(e) { - console.log('Pop state', e) - } - - handleHashChange(e) { - console.log('Hash change', e) - } - - protected render() { - console.log(' rendering') - return ( - - ); - } - */ -} diff --git a/packages/core/src/components/router/test/router-utils.spec.ts b/packages/core/src/components/router/test/router-utils.spec.ts new file mode 100644 index 0000000000..5810985395 --- /dev/null +++ b/packages/core/src/components/router/test/router-utils.spec.ts @@ -0,0 +1,241 @@ +import { + RouterEntries, RouterEntry, RouterSegments, breadthFirstSearch, + generateURL, initRoute, matchRoute, parseURL +} from '../router-utils'; + +describe('RouterSegments', () => { + it ('should initialize with empty array', () => { + const s = new RouterSegments([]); + expect(s.next()).toEqual(''); + expect(s.next()).toEqual(''); + expect(s.next()).toEqual(''); + expect(s.next()).toEqual(''); + expect(s.next()).toEqual(''); + }); + + it ('should initialize with array', () => { + const s = new RouterSegments(['', 'path', 'to', 'destination']); + expect(s.next()).toEqual(''); + expect(s.next()).toEqual('path'); + expect(s.next()).toEqual('to'); + expect(s.next()).toEqual('destination'); + expect(s.next()).toEqual(''); + expect(s.next()).toEqual(''); + }); +}); + +describe('parseURL', () => { + it('should parse empty path', () => { + expect(parseURL('')).toEqual(['']); + }); + + it('should parse empty path (2)', () => { + expect(parseURL(' ')).toEqual(['']); + }); + + it('should parse null path', () => { + expect(parseURL(null)).toEqual(['']); + }); + + it('should parse undefined path', () => { + expect(parseURL(undefined)).toEqual(['']); + }); + + it('should parse relative path', () => { + expect(parseURL('path/to/file.js')).toEqual(['path', 'to', 'file.js']); + }); + + it('should parse absolute path', () => { + expect(parseURL('/path/to/file.js')).toEqual(['path', 'to', 'file.js']); + }); + it('should parse relative path', () => { + expect(parseURL('/PATH///to//file.js//')).toEqual(['PATH', 'to', 'file.js']); + }); +}); + +describe('initRoute', () => { + it('should initialize empty segments', () => { + const route: RouterEntry = { + id: 'cmp', + path: 'path/to/cmp' + }; + initRoute(route); + expect(route.segments).toEqual(['path', 'to', 'cmp']); + }); + + it('should not initialize valid segments', () => { + const route: RouterEntry = { + id: 'cmp', + path: 'path/to/cmp', + segments: [''] + }; + initRoute(route); + expect(route.segments).toEqual(['']); + }); +}); + +describe('matchRoute', () => { + it('should match simple route', () => { + const seg = new RouterSegments(['path', 'to', 'component']); + const routes = [ + { id: 2, path: 'to' }, + { id: 1, path: 'path' }, + { id: 3, path: 'segment' }, + { id: 4, path: '' }, + ]; + const match = matchRoute(seg, routes); + expect(match).toEqual({ id: 1, path: 'path', segments: ['path'] }); + expect(seg.next()).toEqual('to'); + }); + + it('should match default route', () => { + const routes = [ + { id: 2, path: 'to' }, + { id: 1, path: 'path' }, + { id: 3, path: 'segment' }, + { id: 4, path: '' }, + ]; + const seg = new RouterSegments(['hola', 'path']); + let match = matchRoute(seg, routes); + expect(match).toBeNull(); + + match = matchRoute(seg, routes); + expect(match.id).toEqual(1); + + for (let i = 0; i < 20; i++) { + match = matchRoute(seg, routes); + expect(match.id).toEqual(4); + } + }); + + + it('should not match any route', () => { + const routes = [ + { id: 2, path: 'to/to/to' }, + { id: 1, path: 'adam/manu' }, + { id: 3, path: 'hola/adam' }, + { id: 4, path: '' }, + ]; + const seg = new RouterSegments(['hola', 'manu', 'adam']); + let match = matchRoute(seg, routes); + expect(match).toBeNull(); + }); + + it('should not match if there are not routes', () => { + const routes: RouterEntries = []; + const seg = new RouterSegments(['adam']); + expect(matchRoute(seg, routes)).toBeNull(); + expect(matchRoute(seg, routes)).toBeNull(); + expect(matchRoute(seg, routes)).toBeNull(); + }); + + it('should not match any route (2)', () => { + const routes = [ + { id: 1, path: 'adam/manu' }, + { id: 3, path: 'hola/adam' }, + ]; + const seg = new RouterSegments(['adam']); + expect(matchRoute(seg, routes)).toBeNull(); + expect(matchRoute(seg, routes)).toBeNull(); + expect(matchRoute(seg, routes)).toBeNull(); + }); + + it ('should match multiple segments', () => { + const routes = [ + { id: 1, path: 'adam/manu' }, + { id: 2, path: 'manu/hello' }, + { id: 3, path: 'hello' }, + { id: 4, path: '' }, + ]; + const seg = new RouterSegments(['adam', 'manu', 'hello', 'manu', 'hello']); + let match = matchRoute(seg, routes); + expect(match.id).toEqual(1); + + match = matchRoute(seg, routes); + expect(match.id).toEqual(3); + + match = matchRoute(seg, routes); + expect(match.id).toEqual(2); + + match = matchRoute(seg, routes); + expect(match.id).toEqual(4); + + match = matchRoute(seg, routes); + expect(match.id).toEqual(4); + }); + + it('should match long multi segments', () => { + const routes = [ + { id: 1, path: 'adam/manu/hello/menu/hello' }, + { id: 2, path: 'adam/manu/hello/menu' }, + { id: 3, path: 'adam/manu' }, + ]; + const seg = new RouterSegments(['adam', 'manu', 'hello', 'menu', 'hello']); + let match = matchRoute(seg, routes); + expect(match.id).toEqual(1); + expect(matchRoute(seg, routes)).toBeNull(); + }); + + it('should match long multi segments', () => { + let match = matchRoute(new RouterSegments(['']), null); + expect(match).toBeNull(); + + match = matchRoute(new RouterSegments(['hola']), null); + expect(match).toBeNull(); + }); +}); + + +describe('generateURL', () => { + it('should generate an empty URL', () => { + expect(generateURL([])).toEqual('/'); + expect(generateURL([{ path: '' } as any])).toEqual('/'); + expect(generateURL([{ path: '/' } as any])).toEqual('/'); + expect(generateURL([{ path: '//' } as any])).toEqual('/'); + expect(generateURL([{ path: ' ' } as any])).toEqual('/'); + }); + + it('should genenerate a basic url', () => { + const state = [ + { path: '/' }, + { path: '/ ' }, + { path: '' }, + { path: '/path// to/' }, + { path: '/page ' }, + { path: 'number-TWO/' }, + { path: ' / ' } + ]; + expect(generateURL(state as any)).toEqual('/path/to/page/number-TWO'); + + }); +}); + +describe('breadthFirstSearch', () => { + it('should search in order', () => { + const n1 = { tagName: 'ION-TABS', children: [] as any }; + const n2 = { tagName: 'DIV', children: [n1] }; + const n3 = { tagName: 'ION-NAV', children: [n2] }; + const n4 = { tagName: 'ION-TABS', children: [] as any }; + const n5 = { tagName: 'DIV', children: [n4] }; + const n6 = { tagName: 'DIV', children: [n5, n3] }; + const n7 = { tagName: 'DIV', children: [] as any }; + const n8 = { tagName: 'DIV', children: [n6] }; + const n9 = { tagName: 'DIV', children: [n8, n7] }; + + expect(breadthFirstSearch(n9 as any)).toBe(n3); + expect(breadthFirstSearch(n8 as any)).toBe(n3); + expect(breadthFirstSearch(n7 as any)).toBe(null); + expect(breadthFirstSearch(n6 as any)).toBe(n3); + expect(breadthFirstSearch(n5 as any)).toBe(n4); + expect(breadthFirstSearch(n4 as any)).toBe(n4); + expect(breadthFirstSearch(n3 as any)).toBe(n3); + expect(breadthFirstSearch(n2 as any)).toBe(n1); + expect(breadthFirstSearch(n1 as any)).toBe(n1); + }); +}); + +describe('readNavState', () => { + it('should read state', () => { + + }); +}); diff --git a/packages/core/src/components/tabs/page-tab.tsx b/packages/core/src/components/tabs/page-tab.tsx index 1e701d0df9..6ea5c13597 100644 --- a/packages/core/src/components/tabs/page-tab.tsx +++ b/packages/core/src/components/tabs/page-tab.tsx @@ -1,5 +1,10 @@ import { Component, Element } from '@stencil/core'; +export interface Route { + path: string | null; + component: string; +} + @Component({ tag: 'page-tab' }) @@ -59,6 +64,12 @@ export class PageTab { this.setHighlight(true)}>enabled this.setHighlight(false)}>disabled

+

Not found

+

/

+

/tab2

+

/tab3

+

/tab4

+

/tab4/paginaaaa-two

diff --git a/packages/core/src/components/tabs/tab.tsx b/packages/core/src/components/tabs/tab.tsx index ce8cae05cd..8c7e5e6ac8 100644 --- a/packages/core/src/components/tabs/tab.tsx +++ b/packages/core/src/components/tabs/tab.tsx @@ -125,20 +125,10 @@ export class Tab { */ @Prop() btnId: string; - /** - * @input {Page} Set the root page for this tab. - */ - @Prop() root: any; - - /** - * @input {object} Any nav-params to pass to the root page of this tab. - */ - @Prop() rootParams: any; - /** * @input {string} The URL path name to represent this tab within the URL. */ - @Prop() urlPath: string; + @Prop() path: string; /** * @input {string} The title of the tab button. @@ -194,22 +184,15 @@ export class Tab { * @output {Tab} Emitted when the current tab is selected. */ @Event() ionSelect: EventEmitter; - @Event() ionTabDidLoad: EventEmitter; - @Event() ionTabDidUnload: EventEmitter; - - protected ionViewDidLoad() { - this.ionTabDidLoad.emit(this.el); - } - - protected ionViewDidUnload() { - this.ionTabDidUnload.emit(this.el); - } protected componentDidUpdate() { if (this.init && this.resolveNav) { const nav = this.el.querySelector('ion-nav') as any as StencilElement; - // TODO - fix existing typings issue - nav.componentOnReady(this.resolveNav as any); + if (nav) { + nav.componentOnReady(this.resolveNav); + } else { + this.resolveNav(null); + } this.resolveNav = null; } } @@ -222,36 +205,17 @@ export class Tab { this.active = active; this.selected = active; - const needLifecycle = this.init; - if (active) { - this.init = true; - if (!needLifecycle) { - return this.nav.then(nav => nav.setRoot(this.root, this.rootParams)); - } - } - if (needLifecycle) { - if (active) { - // lifecycle didEnter - } else { - // lifecycle didLeave - } - } - return this.nav; - } - - @Method() - resize() { - this.nav.then(nav => nav.resize()); + return Promise.resolve(); } @Method() goToRoot(opts: any = {}) { - return this.nav.then(nav => nav.setRoot(this.root, this.rootParams, opts)); + return this.nav.then(nav => nav && nav.setRoot(nav.root, null, opts)); } @Method() getActive(): Promise { - return this.nav.then(nav => nav.getActive()); + return this.nav.then(nav => nav && nav.getActive()); } @Method() @@ -270,11 +234,4 @@ export class Tab { } }; } - - protected render() { - if (this.init) { - return ; - } - return null; - } } diff --git a/packages/core/src/components/tabs/tabs.scss b/packages/core/src/components/tabs/tabs.scss index f1e26c151b..aa083afd8a 100644 --- a/packages/core/src/components/tabs/tabs.scss +++ b/packages/core/src/components/tabs/tabs.scss @@ -1,5 +1,38 @@ @import "../../themes/ionic.globals"; + +ion-tabs { + display: flex; + + flex-direction: column; +} + +.tabs-inner { + position: relative; + + flex: 1; + + contain: strict; +} + +ion-tab { + @include position(0, null, null, 0); + + position: absolute; + z-index: -1; + display: none; + + width: 100%; + height: 100%; + + contain: strict; +} + +ion-tab.show-tab { + z-index: $z-index-page-container; + display: block; +} + // Tabbar // -------------------------------------------------- diff --git a/packages/core/src/components/tabs/tabs.tsx b/packages/core/src/components/tabs/tabs.tsx index aaee6bb1ef..39cc3e63c7 100644 --- a/packages/core/src/components/tabs/tabs.tsx +++ b/packages/core/src/components/tabs/tabs.tsx @@ -1,5 +1,5 @@ import { Component, Element, Event, EventEmitter, Listen, Method, Prop, State } from '@stencil/core'; -import { Config } from '../../index'; +import { Config, NavState, RouterEntries } from '../../index'; export interface NavOptions { } // import { isPresent } from '../../utils/helpers'; @@ -147,7 +147,6 @@ export class Tabs { private ids: number = -1; private tabsId: number = (++tabIds); - private selectHistory: string[] = []; @Element() el: HTMLElement; @@ -185,37 +184,18 @@ export class Tabs { * @output {any} Emitted when the tab changes. */ @Event() ionChange: EventEmitter; + @Event() ionNavChanged: EventEmitter; protected ionViewDidLoad() { this.loadConfig('tabsPlacement', 'bottom'); this.loadConfig('tabsLayout', 'icon-top'); this.loadConfig('tabsHighlight', true); - // TODO: handle navigation parent - // if (this.parent) { - // // this Tabs has a parent Nav - // this.parent.registerChildNav(this); - - // } else if (viewCtrl && viewCtrl.getNav()) { - // // this Nav was opened from a modal - // this.parent = viewCtrl.getNav(); - // this.parent.registerChildNav(this); - - // } else if (this._app) { - // // this is the root navcontroller for the entire app - // this._app.registerRootNav(this); - // } - - // // Tabs may also be an actual ViewController which was navigated to - // // if Tabs is static and not navigated to within a NavController - // // then skip this and don't treat it as it's own ViewController - // if (viewCtrl) { - // viewCtrl._setContent(this); - // viewCtrl._setContentRef(elementRef); - // } - // } - this.initTabs(); + const useRouter = this.config.getBoolean('useRouter', false); + if (!useRouter) { + this.initSelect(); + } } protected ionViewDidUnload() { @@ -224,36 +204,19 @@ export class Tabs { @Listen('ionTabbarClick') @Listen('ionSelect') - tabChange(ev: CustomEvent) { + protected tabChange(ev: CustomEvent) { const selectedTab = ev.detail as HTMLIonTabElement; this.select(selectedTab); } - @Listen('ionTabDidLoad') - protected addTab(ev: CustomEvent) { - const tab = ev.detail as HTMLIonTabElement; - const id = `t-${this.tabsId}-${++this.ids}`; - tab.btnId = 'tab-' + id; - tab.id = 'tabpanel-' + id; - this.tabs = [...this.tabs, tab]; - ev.stopPropagation(); - } - - @Listen('ionTabDidUnload') - protected removeTab(ev: CustomEvent) { - const tab = ev.detail; - this.tabs.slice(this.tabs.indexOf(tab)); - ev.stopPropagation(); - } - /** * @param {number|Tab} tabOrIndex Index, or the Tab instance, of the tab to select. */ @Method() - select(tabOrIndex: number | HTMLIonTabElement) { + select(tabOrIndex: number | HTMLIonTabElement): Promise { const selectedTab = (typeof tabOrIndex === 'number' ? this.getByIndex(tabOrIndex) : tabOrIndex); if (!selectedTab) { - return; + return Promise.resolve(); } // Reset rest of tabs @@ -264,24 +227,27 @@ export class Tabs { } selectedTab.selected = true; + // The same selected was selected + // we need to set root in the nested ion-nav if it exist if (this.selectedTab === selectedTab) { - selectedTab.goToRoot(); - } else { - const promise = selectedTab._setActive(true); - const leavingTab = this.selectedTab; - if (leavingTab) { - promise.then(() => { - Context.dom.raf(() => { - leavingTab._setActive(false); - }); - }); - } - this.selectedTab = selectedTab; - this.selectHistory.push(selectedTab.id); - this.ionChange.emit(selectedTab); + return selectedTab.goToRoot(); } + + const leavingTab = this.selectedTab; + this.selectedTab = selectedTab; + + let promise = selectedTab._setActive(true); + if (leavingTab) { + promise = promise.then(() => leavingTab._setActive(false)); + } + + return promise.then(() => { + this.ionChange.emit(selectedTab); + this.ionNavChanged.emit({ isPop: false }); + }); } + /** * @param {number} index Index of the tab you want to get * @returns {HTMLIonTabElement} Returns the tab who's index matches the one passed @@ -309,7 +275,46 @@ export class Tabs { return this.tabs; } + @Method() + getState(): NavState { + const selectedTab = this.getSelected(); + if (!selectedTab) { + return null; + } + return { + path: selectedTab.path, + focusNode: selectedTab + }; + } + + @Method() + getRoutes(): RouterEntries { + return this.tabs.map(t => { + return { + path: t.path, + id: t + }; + }); + } + + @Method() + setRouteId(id: any, _: any = {}): Promise { + if (this.selectedTab === id) { + return Promise.resolve(); + } + return this.select(id); + } + private initTabs() { + const tabs = this.tabs = Array.from(this.el.querySelectorAll('ion-tab')); + for (let tab of tabs) { + const id = `t-${this.tabsId}-${++this.ids}`; + tab.btnId = 'tab-' + id; + tab.id = 'tabpanel-' + id; + } + } + + private initSelect() { // find pre-selected tabs let selectedTab = this.tabs.find(t => t.selected); @@ -324,7 +329,6 @@ export class Tabs { } selectedTab._setActive(true); this.selectedTab = selectedTab; - this.selectHistory.push(selectedTab.id); } private loadConfig(attrKey: string, fallback: any) { @@ -334,34 +338,6 @@ export class Tabs { } } - /** - * Get the previously selected Tab which is currently not disabled or hidden. - * @param {boolean} trimHistory If the selection history should be trimmed up to the previous tab selection or not. - * @returns {HTMLIonTabElement} - */ - @Method() - previousTab(trimHistory: boolean = true): HTMLIonTabElement { - // walk backwards through the tab selection history - // and find the first previous tab that is enabled and shown - for (var i = this.selectHistory.length - 2; i >= 0; i--) { - var id = this.selectHistory[i]; - var tab = this.tabs.find(t => t.id === id); - if (tab && tab.enabled && tab.show) { - if (trimHistory) { - this.selectHistory.splice(i + 1); - } - return tab; - } - } - return null; - } - - @Method() - resize() { - const tab = this.getSelected(); - tab && tab.resize(); - } - protected render() { const dom = [
diff --git a/packages/core/src/components/tabs/test/basic/index.html b/packages/core/src/components/tabs/test/basic/index.html index 9c1ccf3a63..05b08d9547 100644 --- a/packages/core/src/components/tabs/test/basic/index.html +++ b/packages/core/src/components/tabs/test/basic/index.html @@ -5,14 +5,37 @@ Tab - Basic + + - - - - + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/core/src/index.d.ts b/packages/core/src/index.d.ts index 55124734af..eedba36b63 100644 --- a/packages/core/src/index.d.ts +++ b/packages/core/src/index.d.ts @@ -121,6 +121,11 @@ export { Radio, RadioEvent } from './components/radio/radio'; export { RangeKnob } from './components/range/range-knob'; export { Range, RangeEvent } from './components/range/range'; export { ReorderGroup } from './components/reorder/reorder-group'; +export { + RouterEntry, + RouterEntries, + NavState, +} from './components/router/router-utils'; export { ItemReorder } from './components/reorder/reorder'; export { Scroll, ScrollCallback, ScrollDetail } from './components/scroll/scroll'; export { Searchbar } from './components/searchbar/searchbar'; diff --git a/packages/core/src/navigation/nav-controller-functions.ts b/packages/core/src/navigation/nav-controller-functions.ts index 0a41e4013f..bd461fe860 100644 --- a/packages/core/src/navigation/nav-controller-functions.ts +++ b/packages/core/src/navigation/nav-controller-functions.ts @@ -17,6 +17,7 @@ import { getNextTransitionId, getParentTransitionId, isViewController, + resolveRoute, setZIndex, toggleHidden, transitionFactory, @@ -213,13 +214,14 @@ export function nextTransaction(nav: Nav): Promise { let enteringView: ViewController; let leavingView: ViewController; - return initializeViewBeforeTransition(topTransaction).then(([_enteringView, _leavingView]) => { + return initializeViewBeforeTransition(nav, topTransaction).then(([_enteringView, _leavingView]) => { enteringView = _enteringView; leavingView = _leavingView; return attachViewToDom(nav, enteringView, topTransaction.delegate); }).then(() => { return loadViewAndTransition(nav, enteringView, leavingView, topTransaction); - }).then((result: NavResult) => { + }).then((result: NavResult) => { + nav.ionNavChanged.emit({ isPop: false }); return successfullyTransitioned(result, topTransaction); }).catch((err: Error) => { return transitionFailed(err, topTransaction); @@ -473,11 +475,11 @@ export function attachViewToDom(nav: Nav, enteringView: ViewController, delegate return Promise.resolve(); } -export function initializeViewBeforeTransition(ti: TransitionInstruction): Promise { +export function initializeViewBeforeTransition(nav: Nav, ti: TransitionInstruction): Promise { let leavingView: ViewController = null; let enteringView: ViewController = null; return startTransaction(ti).then(() => { - const viewControllers = convertComponentToViewController(ti); + const viewControllers = convertComponentToViewController(nav, ti); ti.insertViews = viewControllers; leavingView = ti.nav.getActive(); enteringView = getEnteringView(ti, ti.nav, leavingView); @@ -731,7 +733,7 @@ export function convertViewsToViewControllers(views: any[]): ViewController[] { }).filter(view => !!view); } -export function convertComponentToViewController(ti: TransitionInstruction): ViewController[] { +export function convertComponentToViewController(nav: Nav, ti: TransitionInstruction): ViewController[] { if (ti.insertViews) { assert(ti.insertViews.length > 0, 'length can not be zero'); const viewControllers = convertViewsToViewControllers(ti.insertViews); @@ -747,6 +749,9 @@ export function convertComponentToViewController(ti: TransitionInstruction): Vie if (viewController.state === STATE_DESTROYED) { throw new Error('The view has already been destroyed'); } + if (nav.useRouter && !resolveRoute(nav, viewController.component)) { + throw new Error('Route not specified for ' + viewController.component); + } } return viewControllers; } @@ -782,6 +787,5 @@ export function getDefaultTransition(config: Config) { return config.get('mode') === 'md' ? buildMdTransition : buildIOSTransition; } - let viewIds = VIEW_ID_START; const DISABLE_APP_MINIMUM_DURATION = 64; diff --git a/packages/core/src/navigation/nav-utils.ts b/packages/core/src/navigation/nav-utils.ts index 7b96955841..c2eb4c083f 100644 --- a/packages/core/src/navigation/nav-utils.ts +++ b/packages/core/src/navigation/nav-utils.ts @@ -1,5 +1,5 @@ import { Transition } from './nav-interfaces'; -import { Animation, AnimationOptions, Config, Nav, TransitionBuilder, ViewController } from '..'; +import { Animation, AnimationOptions, Config, Nav, RouterEntry, TransitionBuilder, ViewController } from '..'; import { isDef } from '../utils/helpers'; export const STATE_NEW = 1; @@ -170,11 +170,6 @@ export function getViews(nav: Nav): ViewController[] { return nav.views ? nav.views : []; } -export function init(nav: Nav) { - nav.navId = getNextNavId(); - nav.views = []; -} - export function getActiveImpl(nav: Nav): ViewController { return nav.views && nav.views.length > 0 ? nav.views[nav.views.length - 1] : null; } @@ -190,4 +185,8 @@ export function getNextNavId() { return navControllerIds++; } +export function resolveRoute(nav: Nav, component: string): RouterEntry { + return nav.routes.find(r => r.id === component); +} + let navControllerIds = NAV_ID_START; diff --git a/packages/core/stencil.config.js b/packages/core/stencil.config.js index da8245eed3..e052e2bbad 100644 --- a/packages/core/stencil.config.js +++ b/packages/core/stencil.config.js @@ -27,6 +27,7 @@ exports.config = { { components: ['ion-popover', 'ion-popover-controller'] }, { components: ['ion-radio', 'ion-radio-group'] }, { components: ['ion-reorder', 'ion-reorder-group'] }, + { components: ['ion-route', 'ion-router-controller'] }, { components: ['ion-searchbar'] }, { components: ['ion-segment', 'ion-segment-button'] }, { components: ['ion-select', 'ion-select-option', 'ion-select-popover'] },