mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2026-03-13 10:22:08 +08:00
feat(router): url based routing
This commit is contained in:
115
packages/core/package-lock.json
generated
115
packages/core/package-lock.json
generated
@@ -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": {
|
||||
|
||||
@@ -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)$": "<rootDir>/node_modules/@stencil/core/testing/jest.preprocessor.js"
|
||||
"^.+\\.(js|ts|tsx)$": "<rootDir>/node_modules/@stencil/core/testing/jest.preprocessor.js"
|
||||
},
|
||||
"testRegex": "src/.*\\.spec\\.(ts|tsx|js)$",
|
||||
"moduleFileExtensions": [
|
||||
"ts",
|
||||
"tsx",
|
||||
"js",
|
||||
"jsx"
|
||||
"jsx",
|
||||
"d.ts"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
31
packages/core/src/components.d.ts
vendored
31
packages/core/src/components.d.ts
vendored
@@ -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,
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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<number, Nav>();
|
||||
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 (
|
||||
<slot></slot>
|
||||
);
|
||||
const dom = [<slot></slot>];
|
||||
if (this.useRouter) {
|
||||
dom.push(<ion-router-controller></ion-router-controller>);
|
||||
}
|
||||
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<any> {
|
||||
// if there is a menu controller dom element, hydrate it, otherwise move on
|
||||
// TODO ensure ion-menu-controller is the name
|
||||
|
||||
@@ -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<void> {
|
||||
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 <slot></slot>;
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
|
||||
@@ -18,9 +18,9 @@ export class PageThree {
|
||||
|
||||
protected render() {
|
||||
return [<ion-header>
|
||||
<ion-navbar>
|
||||
<ion-toolbar>
|
||||
<ion-title>Page Three</ion-title>
|
||||
</ion-navbar>
|
||||
</ion-toolbar>
|
||||
</ion-header>,
|
||||
<ion-content>
|
||||
Page Three Content
|
||||
|
||||
@@ -23,9 +23,9 @@ export class PageTwo {
|
||||
|
||||
protected render() {
|
||||
return [<ion-header>
|
||||
<ion-navbar>
|
||||
<ion-toolbar>
|
||||
<ion-title>Page Two</ion-title>
|
||||
</ion-navbar>
|
||||
</ion-toolbar>
|
||||
</ion-header>,
|
||||
<ion-content fullscreen={true}>
|
||||
Page Two Content
|
||||
|
||||
@@ -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<RouterEntry>;
|
||||
@Event() ionRouteRemoved: EventEmitter<string>;
|
||||
|
||||
// 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 <p></p>;
|
||||
|
||||
if(match.url == this.url) {
|
||||
console.log(` <ion-route> Rendering route ${this.url}`, router, match);
|
||||
return (<ChildComponent props={this.componentProps} />);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
*/
|
||||
protected ionViewDidUnload() {
|
||||
this.ionRouteRemoved.emit(this.path);
|
||||
}
|
||||
}
|
||||
|
||||
102
packages/core/src/components/router/router-controller.tsx
Normal file
102
packages/core/src/components/router/router-controller.tsx
Normal file
@@ -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<any> {
|
||||
const node = document.querySelector('ion-app') as HTMLElement;
|
||||
return this.writeNavState(node, this.readURL());
|
||||
}
|
||||
|
||||
private writeNavState(node: any, url: string[]): Promise<any> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
183
packages/core/src/components/router/router-utils.tsx
Normal file
183
packages/core/src/components/router/router-utils.tsx
Normal file
@@ -0,0 +1,183 @@
|
||||
|
||||
export interface NavElement extends HTMLElement {
|
||||
setRouteId(id: any, data?: any): Promise<void>;
|
||||
getRoutes(): RouterEntries;
|
||||
getState(): NavState;
|
||||
|
||||
componentOnReady(): Promise<HTMLElement>;
|
||||
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<void> {
|
||||
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;
|
||||
}
|
||||
@@ -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('<ion-router> 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('<ion-router> rendering')
|
||||
return (
|
||||
<slot></slot>
|
||||
);
|
||||
}
|
||||
*/
|
||||
}
|
||||
241
packages/core/src/components/router/test/router-utils.spec.ts
Normal file
241
packages/core/src/components/router/test/router-utils.spec.ts
Normal file
@@ -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', () => {
|
||||
|
||||
});
|
||||
});
|
||||
@@ -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 {
|
||||
<ion-button onClick={() => this.setHighlight(true)}>enabled</ion-button>
|
||||
<ion-button onClick={() => this.setHighlight(false)}>disabled</ion-button>
|
||||
</p>
|
||||
<p><a href='#/not-found'>Not found</a></p>
|
||||
<p><a href='#/'>/</a></p>
|
||||
<p><a href='#/tab2'>/tab2</a></p>
|
||||
<p><a href='#/tab3'>/tab3</a></p>
|
||||
<p><a href='#/tab4'>/tab4</a></p>
|
||||
<p><a href='#/tab4/paginaaaa-two'>/tab4/paginaaaa-two</a></p>
|
||||
<f></f>
|
||||
<f></f>
|
||||
<f></f>
|
||||
|
||||
@@ -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<ViewController> {
|
||||
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 <ion-nav></ion-nav>;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
// --------------------------------------------------
|
||||
|
||||
|
||||
@@ -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 = <any>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<any> {
|
||||
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<void> {
|
||||
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 = [
|
||||
<div class='tabs-inner'>
|
||||
|
||||
@@ -5,14 +5,37 @@
|
||||
<title>Tab - Basic</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||
<script src="/dist/ionic.js"></script>
|
||||
<script type="text/javascript">
|
||||
window.Ionic.config = {useRouter: true};
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<ion-app>
|
||||
<ion-tabs>
|
||||
<ion-tab root="page-tab" title="Plain List" icon="star"></ion-tab>
|
||||
<ion-tab root="page-two" title="Schedule" icon="globe"></ion-tab>
|
||||
<ion-tab root="page-three" title="Stopwatch" icon="logo-facebook"></ion-tab>
|
||||
<ion-tab root="page-one" title="Messages" icon="chatboxes"></ion-tab>
|
||||
<ion-tab title="Plain List" icon="star" path="">
|
||||
<page-tab class="ion-page"></page-tab>
|
||||
</ion-tab>
|
||||
|
||||
<ion-tab title="Schedule" icon="globe" path="tab2">
|
||||
<ion-nav root="page-two">
|
||||
<ion-route path="" component="page-two"></ion-route>
|
||||
<ion-route path="path/to/page/three" component="page-three"></ion-route>
|
||||
</ion-nav>
|
||||
</ion-tab>
|
||||
|
||||
<ion-tab title="Stopwatch" icon="logo-facebook" path="tab3">
|
||||
<ion-nav root="page-three">
|
||||
<ion-route path="/" component="page-three"></ion-route>
|
||||
</ion-nav>
|
||||
</ion-tab>
|
||||
|
||||
<ion-tab title="Messages" icon="chatboxes" path="tab4">
|
||||
<ion-nav root="page-tab">
|
||||
<ion-route component="page-one"></ion-route>
|
||||
<ion-route path="paginaaaa-two" component="page-two"></ion-route>
|
||||
</ion-nav>
|
||||
</ion-tab>
|
||||
</ion-tabs>
|
||||
|
||||
<ion-nav-controller></ion-nav-controller>
|
||||
|
||||
5
packages/core/src/index.d.ts
vendored
5
packages/core/src/index.d.ts
vendored
@@ -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';
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
getNextTransitionId,
|
||||
getParentTransitionId,
|
||||
isViewController,
|
||||
resolveRoute,
|
||||
setZIndex,
|
||||
toggleHidden,
|
||||
transitionFactory,
|
||||
@@ -213,13 +214,14 @@ export function nextTransaction(nav: Nav): Promise<any> {
|
||||
|
||||
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<ViewController[]> {
|
||||
export function initializeViewBeforeTransition(nav: Nav, ti: TransitionInstruction): Promise<ViewController[]> {
|
||||
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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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'] },
|
||||
|
||||
Reference in New Issue
Block a user