diff --git a/core/src/components/route/readme.md b/core/src/components/route/readme.md index 811bba7e2a..c008987020 100644 --- a/core/src/components/route/readme.md +++ b/core/src/components/route/readme.md @@ -49,6 +49,11 @@ string string +## Events + +#### ionRouteDataChanged + + ---------------------------------------------- diff --git a/core/src/components/route/route.tsx b/core/src/components/route/route.tsx index 8eaaa3220c..a5df526be2 100644 --- a/core/src/components/route/route.tsx +++ b/core/src/components/route/route.tsx @@ -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(); + } } diff --git a/core/src/components/router/readme.md b/core/src/components/router/readme.md index c0731ed779..4619355fc9 100644 --- a/core/src/components/router/readme.md +++ b/core/src/components/router/readme.md @@ -29,6 +29,11 @@ string boolean +## Events + +#### ionRouteChanged + + ## Methods #### navChanged() diff --git a/core/src/components/router/router.tsx b/core/src/components/router/router.tsx index 7d889aab41..f9b7b29802 100644 --- a/core/src/components/router/router.tsx +++ b/core/src/components/router/router.tsx @@ -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; 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 { + 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 { + private writeNavStateRoot(path: string[]): Promise { 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 { + private writeNavState(node: any, chain: RouteChain, direction: number): Promise { 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 + }); + } } diff --git a/core/src/components/router/utils/debug.ts b/core/src/components/router/utils/debug.ts new file mode 100644 index 0000000000..00b0e37748 --- /dev/null +++ b/core/src/components/router/utils/debug.ts @@ -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(', ')})`); + } +} diff --git a/core/src/components/router/utils/dom.ts b/core/src/components/router/utils/dom.ts index 43b9c663ba..f3bfbd969b 100644 --- a/core/src/components/router/utils/dom.ts +++ b/core/src/components/router/utils/dom.ts @@ -1,13 +1,13 @@ import { NavOutlet, NavOutletElement, RouteChain, RouteID } from './interfaces'; -export function writeNavState(root: HTMLElement, chain: RouteChain|null, index: number, direction: number): Promise { +export function writeNavState(root: HTMLElement, chain: RouteChain|null, index: number, direction: number): Promise { 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; }); diff --git a/core/src/components/router/utils/interfaces.ts b/core/src/components/router/utils/interfaces.ts index 35b932ad10..6b59336902 100644 --- a/core/src/components/router/utils/interfaces.ts +++ b/core/src/components/router/utils/interfaces.ts @@ -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[]; diff --git a/core/src/components/tabs/tabs.tsx b/core/src/components/tabs/tabs.tsx index 4de5f2d38e..aa8fc3e751 100644 --- a/core/src/components/tabs/tabs.tsx +++ b/core/src/components/tabs/tabs.tsx @@ -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) {