diff --git a/core/src/components/router/router.tsx b/core/src/components/router/router.tsx index 45e00d3718..bcb29e3fe4 100644 --- a/core/src/components/router/router.tsx +++ b/core/src/components/router/router.tsx @@ -2,6 +2,7 @@ import { Component, ComponentInterface, Element, Event, EventEmitter, Listen, Me import { AnimationBuilder, BackButtonEvent, RouteChain, RouterDirection, RouterEventDetail } from '../../interface'; import { debounce } from '../../utils/helpers'; +import { NavigationHookResult } from '../route/route-interface'; import { ROUTER_INTENT_BACK, ROUTER_INTENT_FORWARD, ROUTER_INTENT_NONE } from './utils/constants'; import { printRedirects, printRoutes } from './utils/debug'; @@ -186,6 +187,7 @@ export class Router implements ComponentInterface { return true; } + // This handler gets called when a `ion-route-redirect` component is added to the DOM or if the from or to property of such node changes. private onRedirectChanged() { const path = this.getPath(); if (path && findRouteRedirect(path, readRedirects(this.el))) { @@ -193,6 +195,7 @@ export class Router implements ComponentInterface { } } + // This handler gets called when a `ion-route` component is added to the DOM or if the from or to property of such node changes. private onRoutesChanged() { return this.writeNavStateRoot(this.getPath(), ROUTER_INTENT_NONE); } @@ -202,7 +205,7 @@ export class Router implements ComponentInterface { if (win.history.state === null) { this.state++; - win.history.replaceState(this.state, win.document.title, win.document.location && win.document.location.href); + win.history.replaceState(this.state, win.document.title, win.document.location?.href); } const state = win.history.state; @@ -211,11 +214,11 @@ export class Router implements ComponentInterface { if (state > lastState || (state >= lastState && lastState > 0)) { return ROUTER_INTENT_FORWARD; - } else if (state < lastState) { + } + if (state < lastState) { return ROUTER_INTENT_BACK; - } else { - return ROUTER_INTENT_NONE; } + return ROUTER_INTENT_NONE; } private async writeNavStateRoot(path: string[] | null, direction: RouterDirection, animation?: AnimationBuilder): Promise { @@ -281,7 +284,7 @@ export class Router implements ComponentInterface { // // When the beforeLeave hook does not return true (to allow navigating) then that value is returned early and the beforeEnter is executed. // Otherwise the beforeEnterHook hook of the target route is executed. - private async runGuards(to: string[] | null = this.getPath(), from?: string[] | null) { + private async runGuards(to: string[] | null = this.getPath(), from?: string[] | null): Promise { if (from === undefined) { from = parsePath(this.previousPath).segments; } diff --git a/core/src/components/router/utils/dom.ts b/core/src/components/router/utils/dom.ts index 1f4f3e6f3c..c2f724062a 100644 --- a/core/src/components/router/utils/dom.ts +++ b/core/src/components/router/utils/dom.ts @@ -88,5 +88,5 @@ const searchNavNode = (root: HTMLElement | undefined): NavOutletElement | undefi return root as NavOutletElement; } const outlet = root.querySelector(QUERY); - return outlet ? outlet : undefined; + return outlet ?? undefined; }; diff --git a/core/src/components/router/utils/parser.ts b/core/src/components/router/utils/parser.ts index 3af2ffc203..e749c897b8 100644 --- a/core/src/components/router/utils/parser.ts +++ b/core/src/components/router/utils/parser.ts @@ -1,6 +1,16 @@ import { RouteChain, RouteNode, RouteRedirect, RouteTree } from './interface'; import { parsePath } from './path'; +const readProp = (el: HTMLElement, prop: string): string | null | undefined => { + if (prop in el) { + return (el as any)[prop]; + } + if (el.hasAttribute(prop)) { + return el.getAttribute(prop); + } + return null; +}; + export const readRedirects = (root: Element): RouteRedirect[] => { return (Array.from(root.children) as HTMLIonRouteRedirectElement[]) .filter(el => el.tagName === 'ION-ROUTE-REDIRECT') @@ -17,46 +27,33 @@ export const readRoutes = (root: Element): RouteChain[] => { return flattenRouterTree(readRouteNodes(root)); }; -export const readRouteNodes = (root: Element, node = root): RouteTree => { +export const readRouteNodes = (node: Element): RouteTree => { return (Array.from(node.children) as HTMLIonRouteElement[]) .filter(el => el.tagName === 'ION-ROUTE' && el.component) .map(el => { - const component = readProp(el, 'component'); - if (component == null) { - throw new Error('component missing in ion-route'); - } + const component = readProp(el, 'component') as string; return { path: parsePath(readProp(el, 'url')).segments, id: component.toLowerCase(), params: el.componentProps, beforeLeave: el.beforeLeave, beforeEnter: el.beforeEnter, - children: readRouteNodes(root, el) + children: readRouteNodes(el) }; }); }; -export const readProp = (el: HTMLElement, prop: string): string | null | undefined => { - if (prop in el) { - return (el as any)[prop]; - } - if (el.hasAttribute(prop)) { - return el.getAttribute(prop); - } - return null; -}; - export const flattenRouterTree = (nodes: RouteTree): RouteChain[] => { - const routes: RouteChain[] = []; + const chains: RouteChain[] = []; for (const node of nodes) { - flattenNode([], routes, node); + flattenNode([], chains, node); } - return routes; + return chains; }; -const flattenNode = (chain: RouteChain, routes: RouteChain[], node: RouteNode) => { - const s = chain.slice(); - s.push({ +const flattenNode = (chain: RouteChain, chains: RouteChain[], node: RouteNode) => { + chain = chain.slice(); + chain.push({ id: node.id, path: node.path, params: node.params, @@ -65,10 +62,10 @@ const flattenNode = (chain: RouteChain, routes: RouteChain[], node: RouteNode) = }); if (node.children.length === 0) { - routes.push(s); + chains.push(chain); return; } - for (const sub of node.children) { - flattenNode(s, routes, sub); + for (const child of node.children) { + flattenNode(chain, chains, child); } }; diff --git a/core/src/components/router/utils/path.ts b/core/src/components/router/utils/path.ts index 588fcd2097..e4fae43860 100644 --- a/core/src/components/router/utils/path.ts +++ b/core/src/components/router/utils/path.ts @@ -1,6 +1,7 @@ import { ROUTER_INTENT_FORWARD } from './constants'; import { ParsedRoute, RouteChain, RouterDirection } from './interface'; +// Join the non empty segments with "/". export const generatePath = (segments: string[]): string => { const path = segments .filter(s => s.length > 0) @@ -9,6 +10,26 @@ export const generatePath = (segments: string[]): string => { return '/' + path; }; +const generateUrl = (segments: string[], useHash: boolean, queryString?: string) => { + let url = generatePath(segments); + if (useHash) { + url = '#' + url; + } + if (queryString !== undefined) { + url += '?' + queryString; + } + return url; +} + +export const writePath = (history: History, root: string, useHash: boolean, path: string[], direction: RouterDirection, state: number, queryString?: string) => { + const url = generateUrl([...parsePath(root).segments, ...path], useHash, queryString); + if (direction === ROUTER_INTENT_FORWARD) { + history.pushState(state, '', url); + } else { + history.replaceState(state, '', url); + } +}; + export const chainToPath = (chain: RouteChain): string[] | null => { const path = []; for (const route of chain) { @@ -27,25 +48,12 @@ export const chainToPath = (chain: RouteChain): string[] | null => { return path; }; -export const writePath = (history: History, root: string, useHash: boolean, path: string[], direction: RouterDirection, state: number, queryString?: string) => { - let url = generatePath([ - ...parsePath(root).segments, - ...path - ]); - if (useHash) { - url = '#' + url; - } - if (queryString !== undefined) { - url = url + '?' + queryString; - } - if (direction === ROUTER_INTENT_FORWARD) { - history.pushState(state, '', url); - } else { - history.replaceState(state, '', url); - } -}; - -export const removePrefix = (prefix: string[], path: string[]): string[] | null => { +// Remove the prefix segments from the path segments. +// +// Return: +// - null when the path segments do not start with the passed prefix, +// - the path segments after the prefix otherwise. +const removePrefix = (prefix: string[], path: string[]): string[] | null => { if (prefix.length > path.length) { return null; } @@ -53,7 +61,7 @@ export const removePrefix = (prefix: string[], path: string[]): string[] | null return path; } for (let i = 0; i < prefix.length; i++) { - if (prefix[i].length > 0 && prefix[i] !== path[i]) { + if (prefix[i] !== path[i]) { return null; } } @@ -64,15 +72,8 @@ export const removePrefix = (prefix: string[], path: string[]): string[] | null }; export const readPath = (loc: Location, root: string, useHash: boolean): string[] | null => { - let pathname = loc.pathname; - if (useHash) { - const hash = loc.hash; - pathname = (hash[0] === '#') - ? hash.slice(1) - : ''; - } - const prefix = parsePath(root).segments; + const pathname = useHash ? loc.hash.slice(1) : loc.pathname; const path = parsePath(pathname).segments; return removePrefix(prefix, path); };