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'] },