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
|
||||
|
||||
|
||||
## 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({
|
||||
tag: 'ion-route'
|
||||
})
|
||||
export class Route {
|
||||
|
||||
@Prop() url = '';
|
||||
@Prop() component: string;
|
||||
@Prop() redirectTo: string;
|
||||
@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
|
||||
|
||||
|
||||
## Events
|
||||
|
||||
#### ionRouteChanged
|
||||
|
||||
|
||||
## Methods
|
||||
|
||||
#### 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 { flattenRouterTree, readRedirects, readRoutes } from './utils/parser';
|
||||
import { readNavState, writeNavState } from './utils/dom';
|
||||
import { chainToPath, generatePath, parsePath, readPath, writePath } from './utils/path';
|
||||
import { RouteChain, RouteRedirect } from './utils/interfaces';
|
||||
import { chainToPath, parsePath, readPath, writePath } from './utils/path';
|
||||
import { RouteChain, RouteRedirect, RouterEventDetail } from './utils/interfaces';
|
||||
import { routeRedirect, routerIDsToChain, routerPathToChain } from './utils/matching';
|
||||
import { printRoutes } from './utils/debug';
|
||||
|
||||
|
||||
@Component({
|
||||
@ -13,9 +14,14 @@ import { routeRedirect, routerIDsToChain, routerPathToChain } from './utils/matc
|
||||
export class Router {
|
||||
|
||||
private routes: RouteChain[];
|
||||
private previousPath: string[] = null;
|
||||
private redirects: RouteRedirect[];
|
||||
private busy = false;
|
||||
private init = false;
|
||||
private state = 0;
|
||||
private timer: any;
|
||||
|
||||
@Element() el: HTMLElement;
|
||||
|
||||
@Prop({ context: 'config' }) config: Config;
|
||||
@Prop({ context: 'dom' }) dom: DomController;
|
||||
@ -23,26 +29,33 @@ export class Router {
|
||||
@Prop() base = '';
|
||||
@Prop() useHash = true;
|
||||
|
||||
@Element() el: HTMLElement;
|
||||
@Event() ionRouteChanged: EventEmitter<RouterEventDetail>;
|
||||
|
||||
componentDidLoad() {
|
||||
this.init = true;
|
||||
this.onRouteChanged();
|
||||
}
|
||||
|
||||
@Listen('ionRouteDataChanged')
|
||||
protected onRouteChanged() {
|
||||
if (!this.init) {
|
||||
return;
|
||||
}
|
||||
const tree = readRoutes(this.el);
|
||||
this.routes = flattenRouterTree(tree);
|
||||
this.redirects = readRedirects(this.el);
|
||||
|
||||
if (Build.isDev) {
|
||||
console.debug('%c[@ionic/core]', 'font-weight: bold', `ion-router registered ${this.routes.length} 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(', ')})`);
|
||||
}
|
||||
printRoutes(this.routes);
|
||||
}
|
||||
|
||||
// perform first write
|
||||
this.dom.raf(() => {
|
||||
console.debug('[OUT] page load -> write nav state');
|
||||
// schedule write
|
||||
if (this.timer) {
|
||||
cancelAnimationFrame(this.timer);
|
||||
this.timer = undefined;
|
||||
}
|
||||
this.timer = requestAnimationFrame(() => {
|
||||
this.timer = undefined;
|
||||
this.onPopState();
|
||||
});
|
||||
}
|
||||
@ -53,76 +66,91 @@ export class Router {
|
||||
this.state++;
|
||||
window.history.replaceState(this.state, document.title, document.location.href);
|
||||
}
|
||||
this.writeNavStateRoot(this.readPath());
|
||||
this.writeNavStateRoot(this.getPath());
|
||||
}
|
||||
|
||||
@Method()
|
||||
navChanged(isPop: boolean) {
|
||||
if (!this.busy) {
|
||||
console.debug('[IN] nav changed -> update URL');
|
||||
const { ids, pivot } = this.readNavState();
|
||||
const chain = routerIDsToChain(ids, this.routes);
|
||||
if (chain) {
|
||||
const path = chainToPath(chain);
|
||||
this.writePath(path, isPop);
|
||||
|
||||
if (chain.length > ids.length) {
|
||||
// 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));
|
||||
}
|
||||
navChanged(isPop: boolean): Promise<boolean> {
|
||||
if (this.busy) {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
console.debug('[IN] nav changed -> update URL');
|
||||
const { ids, pivot } = readNavState(document.body);
|
||||
const chain = routerIDsToChain(ids, this.routes);
|
||||
if (!chain) {
|
||||
console.warn('no matching URL for ', ids.map(i => i.id));
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
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()
|
||||
push(url: string, backDirection = false) {
|
||||
const path = parsePath(url);
|
||||
this.writePath(path, backDirection);
|
||||
this.setPath(path, backDirection);
|
||||
return this.writeNavStateRoot(path);
|
||||
}
|
||||
|
||||
private writeNavStateRoot(path: string[]): Promise<any> {
|
||||
private writeNavStateRoot(path: string[]): Promise<boolean> {
|
||||
if (this.busy) {
|
||||
return Promise.resolve();
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
const redirect = routeRedirect(path, this.redirects);
|
||||
let redirectFrom: string[] = null;
|
||||
if (redirect) {
|
||||
this.writePath(redirect.to, true);
|
||||
this.setPath(redirect.to, true);
|
||||
redirectFrom = redirect.path;
|
||||
path = redirect.to;
|
||||
}
|
||||
const direction = window.history.state >= this.state ? 1 : -1;
|
||||
const node = document.querySelector('ion-app');
|
||||
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) {
|
||||
return Promise.resolve();
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
this.busy = true;
|
||||
return writeNavState(node, chain, 0, direction)
|
||||
.catch(err => console.error(err))
|
||||
.then(() => this.busy = false);
|
||||
return writeNavState(node, chain, 0, direction).then(changed => {
|
||||
this.busy = false;
|
||||
return changed;
|
||||
});
|
||||
}
|
||||
|
||||
private readNavState() {
|
||||
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
|
||||
private setPath(path: string[], isPop: boolean) {
|
||||
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);
|
||||
}
|
||||
|
||||
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';
|
||||
|
||||
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) {
|
||||
return Promise.resolve();
|
||||
return Promise.resolve(direction === 0);
|
||||
}
|
||||
const route = chain[index];
|
||||
const node = searchNavNode(root);
|
||||
if (!node) {
|
||||
return Promise.resolve();
|
||||
return Promise.resolve(direction === 0);
|
||||
}
|
||||
return node.componentOnReady()
|
||||
.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 promise = (nextEl)
|
||||
? writeNavState(nextEl, chain, index + 1, direction)
|
||||
: Promise.resolve();
|
||||
: Promise.resolve(direction === 0);
|
||||
|
||||
if (result.markVisible) {
|
||||
return promise.then(() => result.markVisible());
|
||||
return promise.then((c) => {
|
||||
result.markVisible();
|
||||
return c;
|
||||
});
|
||||
}
|
||||
return promise;
|
||||
});
|
||||
|
@ -6,6 +6,12 @@ export interface NavOutlet {
|
||||
getContainerEl(): HTMLElement | null;
|
||||
}
|
||||
|
||||
export interface RouterEventDetail {
|
||||
from: string[]|null;
|
||||
redirectedFrom: string[]|null;
|
||||
to: string[];
|
||||
}
|
||||
|
||||
export interface RouteRedirect {
|
||||
path: string[];
|
||||
to: string[];
|
||||
|
@ -245,7 +245,7 @@ export class Tabs implements NavOutlet {
|
||||
if (router) {
|
||||
return router.navChanged(false);
|
||||
}
|
||||
return Promise.resolve();
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
private shouldSwitch(selectedTab: HTMLIonTabElement) {
|
||||
|
Reference in New Issue
Block a user