mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-20 04:14:21 +08:00
feat(ion-router): dynamic routes
This commit is contained in:
@ -49,6 +49,11 @@ string
|
|||||||
string
|
string
|
||||||
|
|
||||||
|
|
||||||
|
## Events
|
||||||
|
|
||||||
|
#### ionRouteDataChanged
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
----------------------------------------------
|
----------------------------------------------
|
||||||
|
|
||||||
|
@ -1,12 +1,25 @@
|
|||||||
import { Component, Prop } from '@stencil/core';
|
import { Component, Event, Prop } from '@stencil/core';
|
||||||
|
import { EventEmitter } from 'ionicons/dist/types/stencil.core';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
tag: 'ion-route'
|
tag: 'ion-route'
|
||||||
})
|
})
|
||||||
export class Route {
|
export class Route {
|
||||||
|
|
||||||
@Prop() url = '';
|
@Prop() url = '';
|
||||||
@Prop() component: string;
|
@Prop() component: string;
|
||||||
@Prop() redirectTo: string;
|
@Prop() redirectTo: string;
|
||||||
@Prop() componentProps: {[key: string]: any};
|
@Prop() componentProps: {[key: string]: any};
|
||||||
|
|
||||||
|
@Event() ionRouteDataChanged: EventEmitter;
|
||||||
|
|
||||||
|
componentDidLoad() {
|
||||||
|
this.ionRouteDataChanged.emit();
|
||||||
|
}
|
||||||
|
componentDidUnload() {
|
||||||
|
this.ionRouteDataChanged.emit();
|
||||||
|
}
|
||||||
|
componentDidUpdate() {
|
||||||
|
this.ionRouteDataChanged.emit();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -29,6 +29,11 @@ string
|
|||||||
boolean
|
boolean
|
||||||
|
|
||||||
|
|
||||||
|
## Events
|
||||||
|
|
||||||
|
#### ionRouteChanged
|
||||||
|
|
||||||
|
|
||||||
## Methods
|
## Methods
|
||||||
|
|
||||||
#### navChanged()
|
#### navChanged()
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
import { Build, Component, Element, Listen, Method, Prop } from '@stencil/core';
|
import { Build, Component, Element, Event, EventEmitter, Listen, Method, Prop } from '@stencil/core';
|
||||||
import { Config, DomController } from '../../index';
|
import { Config, DomController } from '../../index';
|
||||||
import { flattenRouterTree, readRedirects, readRoutes } from './utils/parser';
|
import { flattenRouterTree, readRedirects, readRoutes } from './utils/parser';
|
||||||
import { readNavState, writeNavState } from './utils/dom';
|
import { readNavState, writeNavState } from './utils/dom';
|
||||||
import { chainToPath, generatePath, parsePath, readPath, writePath } from './utils/path';
|
import { chainToPath, parsePath, readPath, writePath } from './utils/path';
|
||||||
import { RouteChain, RouteRedirect } from './utils/interfaces';
|
import { RouteChain, RouteRedirect, RouterEventDetail } from './utils/interfaces';
|
||||||
import { routeRedirect, routerIDsToChain, routerPathToChain } from './utils/matching';
|
import { routeRedirect, routerIDsToChain, routerPathToChain } from './utils/matching';
|
||||||
|
import { printRoutes } from './utils/debug';
|
||||||
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@ -13,9 +14,14 @@ import { routeRedirect, routerIDsToChain, routerPathToChain } from './utils/matc
|
|||||||
export class Router {
|
export class Router {
|
||||||
|
|
||||||
private routes: RouteChain[];
|
private routes: RouteChain[];
|
||||||
|
private previousPath: string[] = null;
|
||||||
private redirects: RouteRedirect[];
|
private redirects: RouteRedirect[];
|
||||||
private busy = false;
|
private busy = false;
|
||||||
|
private init = false;
|
||||||
private state = 0;
|
private state = 0;
|
||||||
|
private timer: any;
|
||||||
|
|
||||||
|
@Element() el: HTMLElement;
|
||||||
|
|
||||||
@Prop({ context: 'config' }) config: Config;
|
@Prop({ context: 'config' }) config: Config;
|
||||||
@Prop({ context: 'dom' }) dom: DomController;
|
@Prop({ context: 'dom' }) dom: DomController;
|
||||||
@ -23,26 +29,33 @@ export class Router {
|
|||||||
@Prop() base = '';
|
@Prop() base = '';
|
||||||
@Prop() useHash = true;
|
@Prop() useHash = true;
|
||||||
|
|
||||||
@Element() el: HTMLElement;
|
@Event() ionRouteChanged: EventEmitter<RouterEventDetail>;
|
||||||
|
|
||||||
componentDidLoad() {
|
componentDidLoad() {
|
||||||
|
this.init = true;
|
||||||
|
this.onRouteChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Listen('ionRouteDataChanged')
|
||||||
|
protected onRouteChanged() {
|
||||||
|
if (!this.init) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const tree = readRoutes(this.el);
|
const tree = readRoutes(this.el);
|
||||||
this.routes = flattenRouterTree(tree);
|
this.routes = flattenRouterTree(tree);
|
||||||
this.redirects = readRedirects(this.el);
|
this.redirects = readRedirects(this.el);
|
||||||
|
|
||||||
if (Build.isDev) {
|
if (Build.isDev) {
|
||||||
console.debug('%c[@ionic/core]', 'font-weight: bold', `ion-router registered ${this.routes.length} routes`);
|
printRoutes(this.routes);
|
||||||
for (const chain of this.routes) {
|
|
||||||
const path: string[] = [];
|
|
||||||
chain.forEach(r => path.push(...r.path));
|
|
||||||
const ids = chain.map(r => r.id);
|
|
||||||
console.debug(`%c ${generatePath(path)}`, 'font-weight: bold; padding-left: 20px', '=>\t', `(${ids.join(', ')})`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// perform first write
|
// schedule write
|
||||||
this.dom.raf(() => {
|
if (this.timer) {
|
||||||
console.debug('[OUT] page load -> write nav state');
|
cancelAnimationFrame(this.timer);
|
||||||
|
this.timer = undefined;
|
||||||
|
}
|
||||||
|
this.timer = requestAnimationFrame(() => {
|
||||||
|
this.timer = undefined;
|
||||||
this.onPopState();
|
this.onPopState();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -53,76 +66,91 @@ export class Router {
|
|||||||
this.state++;
|
this.state++;
|
||||||
window.history.replaceState(this.state, document.title, document.location.href);
|
window.history.replaceState(this.state, document.title, document.location.href);
|
||||||
}
|
}
|
||||||
this.writeNavStateRoot(this.readPath());
|
this.writeNavStateRoot(this.getPath());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Method()
|
@Method()
|
||||||
navChanged(isPop: boolean) {
|
navChanged(isPop: boolean): Promise<boolean> {
|
||||||
if (!this.busy) {
|
if (this.busy) {
|
||||||
console.debug('[IN] nav changed -> update URL');
|
return Promise.resolve(false);
|
||||||
const { ids, pivot } = this.readNavState();
|
}
|
||||||
const chain = routerIDsToChain(ids, this.routes);
|
console.debug('[IN] nav changed -> update URL');
|
||||||
if (chain) {
|
const { ids, pivot } = readNavState(document.body);
|
||||||
const path = chainToPath(chain);
|
const chain = routerIDsToChain(ids, this.routes);
|
||||||
this.writePath(path, isPop);
|
if (!chain) {
|
||||||
|
console.warn('no matching URL for ', ids.map(i => i.id));
|
||||||
if (chain.length > ids.length) {
|
return Promise.resolve(false);
|
||||||
// readNavState() found a pivot that is not initialized
|
|
||||||
console.debug('[IN] pivot uninitialized -> write partial nav state');
|
|
||||||
return this.writeNavState(pivot, chain.slice(ids.length), 0);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.warn('no matching URL for ', ids.map(i => i.id));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
const path = chainToPath(chain);
|
||||||
|
this.setPath(path, isPop);
|
||||||
|
|
||||||
|
const promise = (chain.length > ids.length)
|
||||||
|
? this.writeNavState(pivot, chain.slice(ids.length), 0)
|
||||||
|
: Promise.resolve(true);
|
||||||
|
|
||||||
|
return promise.then(() => {
|
||||||
|
this.emitRouteChange(path, null);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@Method()
|
@Method()
|
||||||
push(url: string, backDirection = false) {
|
push(url: string, backDirection = false) {
|
||||||
const path = parsePath(url);
|
const path = parsePath(url);
|
||||||
this.writePath(path, backDirection);
|
this.setPath(path, backDirection);
|
||||||
return this.writeNavStateRoot(path);
|
return this.writeNavStateRoot(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
private writeNavStateRoot(path: string[]): Promise<any> {
|
private writeNavStateRoot(path: string[]): Promise<boolean> {
|
||||||
if (this.busy) {
|
if (this.busy) {
|
||||||
return Promise.resolve();
|
return Promise.resolve(false);
|
||||||
}
|
}
|
||||||
const redirect = routeRedirect(path, this.redirects);
|
const redirect = routeRedirect(path, this.redirects);
|
||||||
|
let redirectFrom: string[] = null;
|
||||||
if (redirect) {
|
if (redirect) {
|
||||||
this.writePath(redirect.to, true);
|
this.setPath(redirect.to, true);
|
||||||
|
redirectFrom = redirect.path;
|
||||||
path = redirect.to;
|
path = redirect.to;
|
||||||
}
|
}
|
||||||
const direction = window.history.state >= this.state ? 1 : -1;
|
const direction = window.history.state >= this.state ? 1 : -1;
|
||||||
const node = document.querySelector('ion-app');
|
|
||||||
const chain = routerPathToChain(path, this.routes);
|
const chain = routerPathToChain(path, this.routes);
|
||||||
return this.writeNavState(node, chain, direction);
|
return this.writeNavState(document.body, chain, direction).then(changed => {
|
||||||
|
if (changed) {
|
||||||
|
this.emitRouteChange(path, redirectFrom);
|
||||||
|
}
|
||||||
|
return changed;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private writeNavState(node: any, chain: RouteChain, direction: number): Promise<any> {
|
private writeNavState(node: any, chain: RouteChain, direction: number): Promise<boolean> {
|
||||||
if (this.busy) {
|
if (this.busy) {
|
||||||
return Promise.resolve();
|
return Promise.resolve(false);
|
||||||
}
|
}
|
||||||
this.busy = true;
|
this.busy = true;
|
||||||
return writeNavState(node, chain, 0, direction)
|
return writeNavState(node, chain, 0, direction).then(changed => {
|
||||||
.catch(err => console.error(err))
|
this.busy = false;
|
||||||
.then(() => this.busy = false);
|
return changed;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private readNavState() {
|
private setPath(path: string[], isPop: boolean) {
|
||||||
const root = document.querySelector('ion-app') as HTMLElement;
|
|
||||||
return readNavState(root);
|
|
||||||
}
|
|
||||||
|
|
||||||
private writePath(path: string[], isPop: boolean) {
|
|
||||||
// busyURL is used to prevent reentering in the popstate event
|
|
||||||
this.state++;
|
this.state++;
|
||||||
writePath(window.history, this.base, this.useHash, path, isPop, this.state);
|
writePath(window.history, this.base, this.useHash, path, isPop, this.state);
|
||||||
}
|
}
|
||||||
|
|
||||||
private readPath(): string[] | null {
|
private getPath(): string[] | null {
|
||||||
return readPath(window.location, this.base, this.useHash);
|
return readPath(window.location, this.base, this.useHash);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private emitRouteChange(path: string[], redirectedFrom: string[]|null) {
|
||||||
|
const from = this.previousPath;
|
||||||
|
const to = path.slice();
|
||||||
|
this.previousPath = to;
|
||||||
|
this.ionRouteChanged.emit({
|
||||||
|
from,
|
||||||
|
redirectedFrom,
|
||||||
|
to: to
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
12
core/src/components/router/utils/debug.ts
Normal file
12
core/src/components/router/utils/debug.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { generatePath } from './path';
|
||||||
|
import { RouteChain } from './interfaces';
|
||||||
|
|
||||||
|
export function printRoutes(routes: RouteChain[]) {
|
||||||
|
console.debug('%c[@ionic/core]', 'font-weight: bold', `ion-router registered ${routes.length} routes`);
|
||||||
|
for (const chain of routes) {
|
||||||
|
const path: string[] = [];
|
||||||
|
chain.forEach(r => path.push(...r.path));
|
||||||
|
const ids = chain.map(r => r.id);
|
||||||
|
console.debug(`%c ${generatePath(path)}`, 'font-weight: bold; padding-left: 20px', '=>\t', `(${ids.join(', ')})`);
|
||||||
|
}
|
||||||
|
}
|
@ -1,13 +1,13 @@
|
|||||||
import { NavOutlet, NavOutletElement, RouteChain, RouteID } from './interfaces';
|
import { NavOutlet, NavOutletElement, RouteChain, RouteID } from './interfaces';
|
||||||
|
|
||||||
export function writeNavState(root: HTMLElement, chain: RouteChain|null, index: number, direction: number): Promise<void> {
|
export function writeNavState(root: HTMLElement, chain: RouteChain|null, index: number, direction: number): Promise<boolean> {
|
||||||
if (!chain || index >= chain.length) {
|
if (!chain || index >= chain.length) {
|
||||||
return Promise.resolve();
|
return Promise.resolve(direction === 0);
|
||||||
}
|
}
|
||||||
const route = chain[index];
|
const route = chain[index];
|
||||||
const node = searchNavNode(root);
|
const node = searchNavNode(root);
|
||||||
if (!node) {
|
if (!node) {
|
||||||
return Promise.resolve();
|
return Promise.resolve(direction === 0);
|
||||||
}
|
}
|
||||||
return node.componentOnReady()
|
return node.componentOnReady()
|
||||||
.then(() => node.setRouteId(route.id, route.params, direction))
|
.then(() => node.setRouteId(route.id, route.params, direction))
|
||||||
@ -18,10 +18,13 @@ export function writeNavState(root: HTMLElement, chain: RouteChain|null, index:
|
|||||||
const nextEl = node.getContainerEl();
|
const nextEl = node.getContainerEl();
|
||||||
const promise = (nextEl)
|
const promise = (nextEl)
|
||||||
? writeNavState(nextEl, chain, index + 1, direction)
|
? writeNavState(nextEl, chain, index + 1, direction)
|
||||||
: Promise.resolve();
|
: Promise.resolve(direction === 0);
|
||||||
|
|
||||||
if (result.markVisible) {
|
if (result.markVisible) {
|
||||||
return promise.then(() => result.markVisible());
|
return promise.then((c) => {
|
||||||
|
result.markVisible();
|
||||||
|
return c;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return promise;
|
return promise;
|
||||||
});
|
});
|
||||||
|
@ -6,6 +6,12 @@ export interface NavOutlet {
|
|||||||
getContainerEl(): HTMLElement | null;
|
getContainerEl(): HTMLElement | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface RouterEventDetail {
|
||||||
|
from: string[]|null;
|
||||||
|
redirectedFrom: string[]|null;
|
||||||
|
to: string[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface RouteRedirect {
|
export interface RouteRedirect {
|
||||||
path: string[];
|
path: string[];
|
||||||
to: string[];
|
to: string[];
|
||||||
|
@ -245,7 +245,7 @@ export class Tabs implements NavOutlet {
|
|||||||
if (router) {
|
if (router) {
|
||||||
return router.navChanged(false);
|
return router.navChanged(false);
|
||||||
}
|
}
|
||||||
return Promise.resolve();
|
return Promise.resolve(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private shouldSwitch(selectedTab: HTMLIonTabElement) {
|
private shouldSwitch(selectedTab: HTMLIonTabElement) {
|
||||||
|
Reference in New Issue
Block a user