mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2026-03-13 10:22:08 +08:00
fix(ion-router): fixes routing algorithm
This commit is contained in:
@@ -1,229 +0,0 @@
|
||||
export interface NavOutlet {
|
||||
setRouteId(id: any, data: any, direction: number): Promise<boolean>;
|
||||
getRouteId(): string;
|
||||
getContentElement(): HTMLElement | null;
|
||||
}
|
||||
|
||||
export type NavOutletElement = NavOutlet & HTMLStencilElement;
|
||||
|
||||
export interface RouterEntry {
|
||||
id: any;
|
||||
path: string[];
|
||||
subroutes: RouterEntries;
|
||||
props?: any;
|
||||
}
|
||||
|
||||
export type RouterEntries = RouterEntry[];
|
||||
|
||||
export class RouterSegments {
|
||||
constructor(
|
||||
private path: string[]
|
||||
) {}
|
||||
|
||||
next(): string {
|
||||
if (this.path.length > 0) {
|
||||
return this.path.shift() as string;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
export function writeNavState(root: HTMLElement, chain: RouterEntries, index: number, direction: number): Promise<void> {
|
||||
if (index >= chain.length) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
const route = chain[index];
|
||||
const node = breadthFirstSearch(root);
|
||||
if (!node) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return node.componentOnReady()
|
||||
.then(() => node.setRouteId(route.id, route.props, direction))
|
||||
.then(changed => {
|
||||
if (changed) {
|
||||
direction = 0;
|
||||
}
|
||||
const nextEl = node.getContentElement();
|
||||
if (nextEl) {
|
||||
return writeNavState(nextEl, chain, index + 1, direction);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
export function readNavState(node: HTMLElement) {
|
||||
const stack: string[] = [];
|
||||
let pivot: NavOutlet|null;
|
||||
while (true) {
|
||||
pivot = breadthFirstSearch(node);
|
||||
if (pivot) {
|
||||
const cmp = pivot.getRouteId();
|
||||
if (cmp) {
|
||||
node = pivot.getContentElement();
|
||||
stack.push(cmp.toLowerCase());
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return {
|
||||
stack: stack,
|
||||
pivot: pivot,
|
||||
};
|
||||
}
|
||||
|
||||
export function matchPath(stack: string[], routes: RouterEntries) {
|
||||
const path: string[] = [];
|
||||
for (const id of stack) {
|
||||
const route = routes.find(r => r.id === id);
|
||||
if (route) {
|
||||
path.push(...route.path);
|
||||
routes = route.subroutes;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return {
|
||||
path: path,
|
||||
routes: routes,
|
||||
};
|
||||
}
|
||||
|
||||
export function matchRouteChain(path: string[], routes: RouterEntries): RouterEntries {
|
||||
const chain = [];
|
||||
const segments = new RouterSegments(path);
|
||||
while (routes.length > 0) {
|
||||
const route = matchRoute(segments, routes);
|
||||
if (!route) {
|
||||
break;
|
||||
}
|
||||
chain.push(route);
|
||||
routes = route.subroutes;
|
||||
}
|
||||
return chain;
|
||||
}
|
||||
|
||||
export function matchRoute(segments: RouterSegments, routes: RouterEntries): RouterEntry | null {
|
||||
if (!routes) {
|
||||
return null;
|
||||
}
|
||||
let index = 0;
|
||||
let selectedRoute: RouterEntry|null = null;
|
||||
let ambiguous = false;
|
||||
let segment: string;
|
||||
let l: number;
|
||||
|
||||
while (true) {
|
||||
routes = routes.filter(r => r.path.length > index);
|
||||
if (routes.length === 0) {
|
||||
break;
|
||||
}
|
||||
segment = segments.next();
|
||||
routes = routes.filter(r => r.path[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 readRoutes(root: Element): RouterEntries {
|
||||
return (Array.from(root.children) as HTMLIonRouteElement[])
|
||||
.filter(el => el.tagName === 'ION-ROUTE')
|
||||
.map(el => ({
|
||||
path: parsePath(el.path),
|
||||
id: el.component,
|
||||
props: el.props,
|
||||
subroutes: readRoutes(el)
|
||||
}));
|
||||
}
|
||||
|
||||
export function generatePath(segments: string[]): string {
|
||||
const path = segments
|
||||
.filter(s => s.length > 0)
|
||||
.join('/');
|
||||
|
||||
return '/' + path;
|
||||
}
|
||||
|
||||
export function parsePath(path: string): string[] {
|
||||
if (path === null || path === undefined) {
|
||||
return [''];
|
||||
}
|
||||
const segments = path.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): NavOutletElement | null {
|
||||
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];
|
||||
let node: HTMLElement | undefined;
|
||||
while (node = queue.shift()) {
|
||||
// visit node
|
||||
if (navs.indexOf(node.tagName) >= 0) {
|
||||
return node as NavOutletElement;
|
||||
}
|
||||
|
||||
// queue children
|
||||
const children = node.children;
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
queue.push(children[i] as NavOutletElement);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function writePath(history: History, base: string, usePath: boolean, path: string[], isPop: boolean, state: number) {
|
||||
path = [base, ...path];
|
||||
let url = generatePath(path);
|
||||
if (usePath) {
|
||||
url = '#' + url;
|
||||
}
|
||||
state++;
|
||||
if (isPop) {
|
||||
history.back();
|
||||
history.replaceState(state, null, url);
|
||||
} else {
|
||||
history.pushState(state, null, url);
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
export function readPath(loc: Location, base: string, useHash: boolean): string[] | null {
|
||||
const path = useHash
|
||||
? loc.hash.substr(1)
|
||||
: loc.pathname;
|
||||
|
||||
if (path.startsWith(base)) {
|
||||
return parsePath(path.slice(base.length));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { Component, Element, Listen, Prop } from '@stencil/core';
|
||||
import { RouterEntries, matchPath, matchRouteChain, readNavState, readPath, readRoutes, writeNavState, writePath } from './router-utils';
|
||||
import { Config, DomController } from '../../index';
|
||||
import { flattenRouterTree, readRoutes } from './utils/parser';
|
||||
import { readNavState, writeNavState } from './utils/dom';
|
||||
import { chainToPath, readPath, writePath } from './utils/path';
|
||||
import { RouteChain } from './utils/interfaces';
|
||||
import { routerIDsToChain, routerPathToChain } from './utils/matching';
|
||||
|
||||
|
||||
@Component({
|
||||
@@ -8,7 +12,7 @@ import { Config, DomController } from '../../index';
|
||||
})
|
||||
export class Router {
|
||||
|
||||
private routes: RouterEntries;
|
||||
private routes: RouteChain[];
|
||||
private busy = false;
|
||||
private state = 0;
|
||||
|
||||
@@ -22,7 +26,8 @@ export class Router {
|
||||
|
||||
componentDidLoad() {
|
||||
// read config
|
||||
this.routes = readRoutes(this.el);
|
||||
const tree = readRoutes(this.el);
|
||||
this.routes = flattenRouterTree(tree);
|
||||
|
||||
// perform first write
|
||||
this.dom.raf(() => {
|
||||
@@ -49,16 +54,16 @@ export class Router {
|
||||
return;
|
||||
}
|
||||
console.debug('[IN] nav changed -> update URL');
|
||||
const { stack, pivot } = this.readNavState();
|
||||
const { path, routes } = matchPath(stack, this.routes);
|
||||
if (pivot) {
|
||||
const { ids, pivot } = this.readNavState();
|
||||
const { chain, matches } = routerIDsToChain(ids, this.routes);
|
||||
if (chain.length > matches) {
|
||||
// readNavState() found a pivot that is not initialized
|
||||
console.debug('[IN] pivot uninitialized -> write partial nav state');
|
||||
this.writeNavState(pivot, [], routes, 0);
|
||||
this.writeNavState(pivot, chain.slice(matches), 0);
|
||||
}
|
||||
|
||||
const isPop = ev.detail.isPop === true;
|
||||
this.writePath(path, isPop);
|
||||
this.writePath(chain, isPop);
|
||||
}
|
||||
|
||||
private writeNavStateRoot(): Promise<any> {
|
||||
@@ -66,14 +71,13 @@ export class Router {
|
||||
const currentPath = this.readPath();
|
||||
const direction = window.history.state >= this.state ? 1 : -1;
|
||||
if (currentPath) {
|
||||
return this.writeNavState(node, currentPath, this.routes, direction);
|
||||
const {chain} = routerPathToChain(currentPath, this.routes);
|
||||
return this.writeNavState(node, chain, direction);
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
private writeNavState(node: any, path: string[], routes: RouterEntries, direction: number): Promise<any> {
|
||||
const chain = matchRouteChain(path, routes);
|
||||
|
||||
private writeNavState(node: any, chain: RouteChain, direction: number): Promise<any> {
|
||||
this.busy = true;
|
||||
return writeNavState(node, chain, 0, direction)
|
||||
.catch(err => console.error(err))
|
||||
@@ -85,7 +89,8 @@ export class Router {
|
||||
return readNavState(root);
|
||||
}
|
||||
|
||||
private writePath(path: string[], isPop: boolean) {
|
||||
private writePath(chain: RouteChain, isPop: boolean) {
|
||||
const path = chainToPath(chain);
|
||||
this.state = writePath(window.history, this.base, this.useHash, path, isPop, this.state);
|
||||
}
|
||||
|
||||
|
||||
47
packages/core/src/components/router/test/common.spec.tsx
Normal file
47
packages/core/src/components/router/test/common.spec.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import { RouterSegments, breadthFirstSearch } from '../utils/common';
|
||||
|
||||
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('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);
|
||||
});
|
||||
});
|
||||
|
||||
65
packages/core/src/components/router/test/e2e.spec.tsx
Normal file
65
packages/core/src/components/router/test/e2e.spec.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
import { RouteChain } from '../utils/interfaces';
|
||||
import { routerIDsToChain, routerPathToChain } from '../utils/matching';
|
||||
import { mockRouteElement } from './parser.spec';
|
||||
import { chainToPath, generatePath, parsePath } from '../utils/path';
|
||||
import { flattenRouterTree, readRoutes } from '../utils/parser';
|
||||
import { mockElement } from '@stencil/core/dist/testing';
|
||||
|
||||
describe('ionic-conference-app', () => {
|
||||
|
||||
it('should match conference-app routing', () => {
|
||||
const root = conferenceAppRouting();
|
||||
const tree = readRoutes(root);
|
||||
const routes = flattenRouterTree(tree);
|
||||
|
||||
expect(getRouteIDs('/', routes)).toEqual(['page-tabs', 'tab-schedule', 'page-schedule']);
|
||||
expect(getRouteIDs('/speaker', routes)).toEqual(['page-tabs', 'tab-speaker', 'page-speaker-list']);
|
||||
expect(getRouteIDs('/map', routes)).toEqual(['page-tabs', 'page-map']);
|
||||
expect(getRouteIDs('/about', routes)).toEqual(['page-tabs', 'page-about']);
|
||||
expect(getRouteIDs('/tutorial', routes)).toEqual(['page-tutorial']);
|
||||
|
||||
expect(getRoutePaths(['page-tabs', 'tab-schedule', 'page-schedule'], routes)).toEqual('/');
|
||||
expect(getRoutePaths(['page-tabs', 'tab-speaker', 'page-speaker-list'], routes)).toEqual('/speaker');
|
||||
expect(getRoutePaths(['page-tabs', 'page-map'], routes)).toEqual('/map');
|
||||
expect(getRoutePaths(['page-tabs', 'page-about'], routes)).toEqual('/about');
|
||||
expect(getRoutePaths(['page-tutorial'], routes)).toEqual('/tutorial');
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
function conferenceAppRouting() {
|
||||
const p2 = mockRouteElement('/', 'tab-schedule');
|
||||
const p3 = mockRouteElement('/', 'page-schedule');
|
||||
p2.appendChild(p3);
|
||||
|
||||
const p4 = mockRouteElement('/speaker', 'tab-speaker');
|
||||
const p5 = mockRouteElement('/', 'page-speaker-list');
|
||||
p4.appendChild(p5);
|
||||
|
||||
const p6 = mockRouteElement('/map', 'page-map');
|
||||
const p7 = mockRouteElement('/about', 'page-about');
|
||||
|
||||
const p1 = mockRouteElement('/', 'page-tabs');
|
||||
p1.appendChild(p2);
|
||||
p1.appendChild(p4);
|
||||
p1.appendChild(p6);
|
||||
p1.appendChild(p7);
|
||||
|
||||
const p8 = mockRouteElement('/tutorial', 'page-tutorial');
|
||||
const container = mockElement('div');
|
||||
container.appendChild(p1);
|
||||
container.appendChild(p8);
|
||||
return container;
|
||||
}
|
||||
|
||||
|
||||
|
||||
function getRouteIDs(path: string, routes: RouteChain[]): string[] {
|
||||
return routerPathToChain(parsePath(path), routes).chain.map(r => r.id);
|
||||
}
|
||||
|
||||
function getRoutePaths(ids: string[], routes: RouteChain[]): string {
|
||||
return generatePath(chainToPath(routerIDsToChain(ids, routes).chain));
|
||||
}
|
||||
|
||||
279
packages/core/src/components/router/test/matching.spec.tsx
Normal file
279
packages/core/src/components/router/test/matching.spec.tsx
Normal file
@@ -0,0 +1,279 @@
|
||||
import { RouteChain } from '../utils/interfaces';
|
||||
import { matchesIDs, matchesPath, routerPathToChain } from '../utils/matching';
|
||||
import { mockRouteElement } from './parser.spec';
|
||||
import { mockElement } from '@stencil/core/dist/testing';
|
||||
|
||||
const CHAIN_1: RouteChain = [
|
||||
{ id: '2', path: ['to'], props: undefined },
|
||||
{ id: '1', path: ['path'], props: undefined },
|
||||
{ id: '3', path: ['segment'], props: undefined },
|
||||
{ id: '4', path: [''], props: undefined },
|
||||
];
|
||||
|
||||
const CHAIN_2: RouteChain = [
|
||||
{ id: '2', path: [''], props: undefined },
|
||||
{ id: '1', path: [''], props: undefined },
|
||||
{ id: '3', path: ['segment', 'to'], props: undefined },
|
||||
{ id: '4', path: [''], props: undefined },
|
||||
{ id: '5', path: ['hola'], props: undefined },
|
||||
{ id: '6', path: [''], props: undefined },
|
||||
{ id: '7', path: [''], props: undefined },
|
||||
{ id: '8', path: ['adios', 'que', 'tal'], props: undefined },
|
||||
];
|
||||
|
||||
const CHAIN_3: RouteChain = [
|
||||
{ id: '2', path: ['this', 'to'], props: undefined },
|
||||
{ id: '1', path: ['path'], props: undefined },
|
||||
{ id: '3', path: ['segment', 'to', 'element'], props: undefined },
|
||||
{ id: '4', path: [''], props: undefined },
|
||||
];
|
||||
|
||||
|
||||
|
||||
describe('matchesIDs', () => {
|
||||
it('should match simple set of ids', () => {
|
||||
const chain: RouteChain = CHAIN_1;
|
||||
expect(matchesIDs(['2'], chain)).toBe(1);
|
||||
expect(matchesIDs(['2', '1'], chain)).toBe(2);
|
||||
expect(matchesIDs(['2', '1', '3'], chain)).toBe(3);
|
||||
expect(matchesIDs(['2', '1', '3', '4'], chain)).toBe(4);
|
||||
expect(matchesIDs(['2', '1', '3', '4', '5'], chain)).toBe(4);
|
||||
|
||||
expect(matchesIDs([], chain)).toBe(0);
|
||||
expect(matchesIDs(['1'], chain)).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('matchesPath', () => {
|
||||
it('should match simple path', () => {
|
||||
const chain: RouteChain = CHAIN_3;
|
||||
expect(matchesPath(['this'], chain)).toBe(false);
|
||||
expect(matchesPath(['this', 'to'], chain)).toBe(false);
|
||||
expect(matchesPath(['this', 'to', 'path'], chain)).toBe(false);
|
||||
expect(matchesPath(['this', 'to', 'path', 'segment'], chain)).toBe(false);
|
||||
expect(matchesPath(['this', 'to', 'path', 'segment', 'to'], chain)).toBe(false);
|
||||
expect(matchesPath(['this', 'to', 'path', 'segment', 'to', 'element'], chain)).toBe(true);
|
||||
expect(matchesPath(['this', 'to', 'path', 'segment', 'to', 'element', 'more'], chain)).toBe(false);
|
||||
|
||||
expect(matchesPath([], chain)).toBe(false);
|
||||
expect(matchesPath([''], chain)).toBe(false);
|
||||
expect(matchesPath(['path'], chain)).toBe(false);
|
||||
});
|
||||
|
||||
it('should match simple default route', () => {
|
||||
const chain: RouteChain = CHAIN_2;
|
||||
expect(matchesPath([''], chain)).toBe(false);
|
||||
expect(matchesPath(['segment'], chain)).toBe(false);
|
||||
expect(matchesPath(['segment', 'to'], chain)).toBe(false);
|
||||
expect(matchesPath(['segment', 'to', 'hola'], chain)).toBe(false);
|
||||
expect(matchesPath(['segment', 'to', 'hola', 'adios'], chain)).toBe(false);
|
||||
expect(matchesPath(['segment', 'to', 'hola', 'adios', 'que'], chain)).toBe(false);
|
||||
expect(matchesPath(['segment', 'to', 'hola', 'adios', 'que', 'tal'], chain)).toBe(true);
|
||||
|
||||
expect(matchesPath(['to'], chain)).toBe(false);
|
||||
expect(matchesPath(['path', 'to'], chain)).toBe(false);
|
||||
});
|
||||
|
||||
it('should match simple route 2', () => {
|
||||
const chain: RouteChain = [{ id: '5', path: ['hola'], props: undefined }];
|
||||
expect(matchesPath([''], chain)).toBe(false);
|
||||
expect(matchesPath(['hola'], chain)).toBe(true);
|
||||
expect(matchesPath(['hola', 'hola'], chain)).toBe(true);
|
||||
expect(matchesPath(['hola', 'adios'], chain)).toBe(true);
|
||||
});
|
||||
|
||||
it('should match simple route 3', () => {
|
||||
const chain: RouteChain = [{ id: '5', path: ['hola', 'adios'], props: undefined }];
|
||||
expect(matchesPath([''], chain)).toBe(false);
|
||||
expect(matchesPath(['hola'], chain)).toBe(false);
|
||||
expect(matchesPath(['hola', 'hola'], chain)).toBe(false);
|
||||
expect(matchesPath(['hola', 'adios'], chain)).toBe(true);
|
||||
});
|
||||
|
||||
it('should match simple route 4', () => {
|
||||
const chain: RouteChain = [
|
||||
{ id: '5', path: ['hola'], props: undefined },
|
||||
{ id: '5', path: ['adios'], props: undefined }];
|
||||
|
||||
expect(matchesPath([''], chain)).toBe(false);
|
||||
expect(matchesPath(['hola'], chain)).toBe(false);
|
||||
expect(matchesPath(['hola', 'hola'], chain)).toBe(false);
|
||||
expect(matchesPath(['hola', 'adios'], chain)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('routerPathToChain', () => {
|
||||
it('should match the route with higher priority', () => {
|
||||
const chain3: RouteChain = [{ id: '5', path: ['hola'], props: undefined }];
|
||||
const chain4: RouteChain = [
|
||||
{ id: '5', path: ['hola'], props: undefined },
|
||||
{ id: '5', path: ['adios'], props: undefined }];
|
||||
|
||||
const routes: RouteChain[] = [
|
||||
CHAIN_1,
|
||||
CHAIN_2,
|
||||
chain3,
|
||||
chain4
|
||||
];
|
||||
expect(routerPathToChain(['to'], routes)).toEqual({
|
||||
chain: null,
|
||||
matches: 0,
|
||||
});
|
||||
|
||||
expect(routerPathToChain([''], routes)).toEqual({
|
||||
chain: null,
|
||||
matches: 0,
|
||||
});
|
||||
expect(routerPathToChain(['segment', 'to'], routes)).toEqual({
|
||||
chain: null,
|
||||
matches: 0,
|
||||
});
|
||||
|
||||
expect(routerPathToChain(['hola'], routes)).toEqual({
|
||||
chain: chain3,
|
||||
matches: 1,
|
||||
});
|
||||
expect(routerPathToChain(['hola', 'hola'], routes)).toEqual({
|
||||
chain: chain3,
|
||||
matches: 1,
|
||||
});
|
||||
|
||||
expect(routerPathToChain(['hola', 'adios'], routes)).toEqual({
|
||||
chain: chain4,
|
||||
matches: 2,
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
it('should match the default route', () => {
|
||||
const chain1: RouteChain = [
|
||||
{ id: 'tabs', path: [''], props: undefined },
|
||||
{ id: 'tab1', path: [''], props: undefined },
|
||||
{ id: 'schedule', path: [''], props: undefined }
|
||||
];
|
||||
const chain2: RouteChain = [
|
||||
{ id: 'tabs', path: [''], props: undefined },
|
||||
{ id: 'tab2', path: ['tab2'], props: undefined },
|
||||
{ id: 'page2', path: [''], props: undefined }
|
||||
];
|
||||
|
||||
expect(routerPathToChain([''], [chain1])).toEqual({chain: chain1, matches: 3});
|
||||
expect(routerPathToChain(['tab2'], [chain1])).toEqual({chain: null, matches: 0});
|
||||
|
||||
expect(routerPathToChain([''], [chain2])).toEqual({chain: null, matches: 0});
|
||||
expect(routerPathToChain(['tab2'], [chain2])).toEqual({chain: chain2, matches: 3});
|
||||
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
// describe('matchRoute', () => {
|
||||
// it('should match simple route', () => {
|
||||
// const path = ['path', 'to', 'component'];
|
||||
// const routes: RouteChain[] = [
|
||||
// [{ id: 2, path: ['to'], props: undefined }],
|
||||
// [{ id: 1, path: ['path'], props: undefined }],
|
||||
// [{ id: 3, path: ['segment'], props: undefined }],
|
||||
// [{ id: 4, path: [''], props: undefined }],
|
||||
// ];
|
||||
// const match = routerPathToChain(path, routes);
|
||||
// expect(match).toEqual({ id: 1, path: ['path'], children: [] });
|
||||
// expect(seg.next()).toEqual('to');
|
||||
// });
|
||||
|
||||
// it('should match default route', () => {
|
||||
// const routes: RouteTree = [
|
||||
// { id: 2, path: ['to'], children: [], props: undefined },
|
||||
// { id: 1, path: ['path'], children: [], props: undefined },
|
||||
// { id: 3, path: ['segment'], children: [], props: undefined },
|
||||
// { id: 4, path: [''], children: [], props: undefined },
|
||||
// ];
|
||||
// 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: RouteTree = [
|
||||
// { id: 2, path: ['to', 'to', 'to'], children: [], props: undefined },
|
||||
// { id: 1, path: ['adam', 'manu'], children: [], props: undefined },
|
||||
// { id: 3, path: ['hola', 'adam'], children: [], props: undefined },
|
||||
// { id: 4, path: [''], children: [], props: undefined },
|
||||
// ];
|
||||
// const seg = new RouterSegments(['hola', 'manu', 'adam']);
|
||||
// const match = matchRoute(seg, routes);
|
||||
// expect(match).toBeNull();
|
||||
// });
|
||||
|
||||
// it('should not match if there are not routes', () => {
|
||||
// const routes: RouteTree = [];
|
||||
// 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: RouteTree = [
|
||||
// { id: 1, path: ['adam', 'manu'], children: [], props: undefined },
|
||||
// { id: 3, path: ['hola', 'adam'], children: [], props: undefined },
|
||||
// ];
|
||||
// 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: RouteTree = [
|
||||
// { id: 1, path: ['adam', 'manu'], children: [], props: undefined },
|
||||
// { id: 2, path: ['manu', 'hello'], children: [], props: undefined },
|
||||
// { id: 3, path: ['hello'], children: [], props: undefined },
|
||||
// { id: 4, path: [''], children: [], props: undefined },
|
||||
// ];
|
||||
// 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: RouteTree = [
|
||||
// { id: 1, path: ['adam', 'manu', 'hello', 'menu', 'hello'], children: [], props: undefined },
|
||||
// { id: 2, path: ['adam', 'manu', 'hello', 'menu'], children: [], props: undefined },
|
||||
// { id: 3, path: ['adam', 'manu'], children: [], props: undefined },
|
||||
// ];
|
||||
// const seg = new RouterSegments(['adam', 'manu', 'hello', 'menu', 'hello']);
|
||||
// const 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();
|
||||
// });
|
||||
// });
|
||||
66
packages/core/src/components/router/test/parser.spec.tsx
Normal file
66
packages/core/src/components/router/test/parser.spec.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
import { mockElement } from '@stencil/core/testing';
|
||||
import { flattenRouterTree, readRoutes } from '../utils/parser';
|
||||
import { RouteTree } from '../utils/interfaces';
|
||||
|
||||
describe('readRoutes', () => {
|
||||
it('should read URL', () => {
|
||||
const root = mockElement('div');
|
||||
const r1 = mockRouteElement('/', 'main-page');
|
||||
const r2 = mockRouteElement('/one-page', 'one-page');
|
||||
const r3 = mockRouteElement('secondpage', 'second-page');
|
||||
const r4 = mockRouteElement('/5/hola', '4');
|
||||
const r5 = mockRouteElement('/path/to/five', '5');
|
||||
const r6 = mockRouteElement('/path/to/five2', '6');
|
||||
|
||||
root.appendChild(r1);
|
||||
root.appendChild(r2);
|
||||
root.appendChild(r3);
|
||||
r3.appendChild(r4);
|
||||
r4.appendChild(r5);
|
||||
r4.appendChild(r6);
|
||||
|
||||
const expected: RouteTree = [
|
||||
{ path: [''], id: 'main-page', children: [], props: undefined },
|
||||
{ path: ['one-page'], id: 'one-page', children: [], props: undefined },
|
||||
{ path: ['secondpage'], id: 'second-page', props: undefined, children: [
|
||||
{ path: ['5', 'hola'], id: '4', props: undefined, children: [
|
||||
{ path: ['path', 'to', 'five'], id: '5', children: [], props: undefined },
|
||||
{ path: ['path', 'to', 'five2'], id: '6', children: [], props: undefined }
|
||||
] }
|
||||
] }
|
||||
];
|
||||
expect(readRoutes(root)).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('flattenRouterTree', () => {
|
||||
it('should process routes', () => {
|
||||
const entries: RouteTree = [
|
||||
{ path: [''], id: 'hola', children: [], props: undefined },
|
||||
{ path: ['one-page'], id: 'one-page', children: [], props: undefined },
|
||||
{ path: ['secondpage'], id: 'second-page', props: undefined, children: [
|
||||
{ path: ['5', 'hola'], id: '4', props: undefined, children: [
|
||||
{ path: ['path', 'to', 'five'], id: '5', children: [], props: undefined },
|
||||
{ path: ['path', 'to', 'five2'], id: '6', children: [], props: undefined }
|
||||
] }
|
||||
] }
|
||||
];
|
||||
const routes = flattenRouterTree(entries);
|
||||
expect(routes).toEqual([
|
||||
[{ path: [''], id: 'hola' }],
|
||||
[{ path: ['one-page'], id: 'one-page' }],
|
||||
[{ path: ['secondpage'], id: 'second-page'}, { path: ['5', 'hola'], id: '4'}, { path: ['path', 'to', 'five'], id: '5'}],
|
||||
[{ path: ['secondpage'], id: 'second-page'}, { path: ['5', 'hola'], id: '4'}, { path: ['path', 'to', 'five2'], id: '6'}],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
export function mockRouteElement(path: string, component: string) {
|
||||
const el = mockElement('ion-route');
|
||||
el.setAttribute('path', path);
|
||||
(el as any).component = component;
|
||||
return el;
|
||||
}
|
||||
55
packages/core/src/components/router/test/path.spec.tsx
Normal file
55
packages/core/src/components/router/test/path.spec.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import { generatePath, parsePath } from '../utils/path';
|
||||
|
||||
describe('parseURL', () => {
|
||||
it('should parse empty path', () => {
|
||||
expect(parsePath('')).toEqual(['']);
|
||||
});
|
||||
|
||||
it('should parse empty path (2)', () => {
|
||||
expect(parsePath(' ')).toEqual(['']);
|
||||
});
|
||||
|
||||
it('should parse null path', () => {
|
||||
expect(parsePath(null)).toEqual(['']);
|
||||
});
|
||||
|
||||
it('should parse undefined path', () => {
|
||||
expect(parsePath(undefined)).toEqual(['']);
|
||||
});
|
||||
|
||||
it('should parse relative path', () => {
|
||||
expect(parsePath('path/to/file.js')).toEqual(['path', 'to', 'file.js']);
|
||||
});
|
||||
|
||||
it('should parse absolute path', () => {
|
||||
expect(parsePath('/path/to/file.js')).toEqual(['path', 'to', 'file.js']);
|
||||
});
|
||||
it('should parse relative path', () => {
|
||||
expect(parsePath('/PATH///to//file.js//')).toEqual(['PATH', 'to', 'file.js']);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('generatePath', () => {
|
||||
it('should generate an empty URL', () => {
|
||||
expect(generatePath([])).toEqual('/');
|
||||
expect(generatePath([{ path: '' } as any])).toEqual('/');
|
||||
expect(generatePath([{ path: '/' } as any])).toEqual('/');
|
||||
expect(generatePath([{ path: '//' } as any])).toEqual('/');
|
||||
expect(generatePath([{ path: ' ' } as any])).toEqual('/');
|
||||
});
|
||||
|
||||
it('should genenerate a basic url', () => {
|
||||
const stack = [
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'path/to',
|
||||
'page',
|
||||
'number-TWO',
|
||||
''
|
||||
];
|
||||
expect(generatePath(stack)).toEqual('/path/to/page/number-TWO');
|
||||
|
||||
});
|
||||
});
|
||||
@@ -1,235 +0,0 @@
|
||||
import {
|
||||
RouterEntries, RouterSegments, breadthFirstSearch,
|
||||
generatePath, matchRoute, parsePath
|
||||
} 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(parsePath('')).toEqual(['']);
|
||||
});
|
||||
|
||||
it('should parse empty path (2)', () => {
|
||||
expect(parsePath(' ')).toEqual(['']);
|
||||
});
|
||||
|
||||
it('should parse null path', () => {
|
||||
expect(parsePath(null)).toEqual(['']);
|
||||
});
|
||||
|
||||
it('should parse undefined path', () => {
|
||||
expect(parsePath(undefined)).toEqual(['']);
|
||||
});
|
||||
|
||||
it('should parse relative path', () => {
|
||||
expect(parsePath('path/to/file.js')).toEqual(['path', 'to', 'file.js']);
|
||||
});
|
||||
|
||||
it('should parse absolute path', () => {
|
||||
expect(parsePath('/path/to/file.js')).toEqual(['path', 'to', 'file.js']);
|
||||
});
|
||||
it('should parse relative path', () => {
|
||||
expect(parsePath('/PATH///to//file.js//')).toEqual(['PATH', 'to', 'file.js']);
|
||||
});
|
||||
});
|
||||
|
||||
// describe('readRoutes', () => {
|
||||
// it('should read URL', () => {
|
||||
// const node = (<div>
|
||||
// <ion-route path='/' component='main-page'/>
|
||||
// <ion-route path='/one-page' component='one-page'/>
|
||||
// <ion-route path='secondpage' component='second-page'/>
|
||||
// <ion-route path='/5/hola' component='4'/>
|
||||
// <ion-route path='path/to/five' component='5'/>
|
||||
// </div>) as any;
|
||||
// node.children = node.vchildren;
|
||||
|
||||
// expect(readRoutes(node)).toEqual([
|
||||
// { path: [''], id: 'hola', subroutes: [] },
|
||||
// { path: ['one-page'], id: 'one-page', subroutes: [] },
|
||||
// { path: ['secondpage'], id: 'second-page', subroutes: [] },
|
||||
// { path: ['5', 'hola'], id: '4', subroutes: [] },
|
||||
// { path: ['path', 'to', 'five'], id: '5', subroutes: [] }
|
||||
// ]);
|
||||
// });
|
||||
// });
|
||||
|
||||
describe('matchRoute', () => {
|
||||
it('should match simple route', () => {
|
||||
const seg = new RouterSegments(['path', 'to', 'component']);
|
||||
const routes: RouterEntries = [
|
||||
{ id: 2, path: ['to'], subroutes: [] },
|
||||
{ id: 1, path: ['path'], subroutes: [] },
|
||||
{ id: 3, path: ['segment'], subroutes: [] },
|
||||
{ id: 4, path: [''], subroutes: [] },
|
||||
];
|
||||
const match = matchRoute(seg, routes);
|
||||
expect(match).toEqual({ id: 1, path: ['path'], subroutes: [] });
|
||||
expect(seg.next()).toEqual('to');
|
||||
});
|
||||
|
||||
it('should match default route', () => {
|
||||
const routes: RouterEntries = [
|
||||
{ id: 2, path: ['to'], subroutes: [] },
|
||||
{ id: 1, path: ['path'], subroutes: [] },
|
||||
{ id: 3, path: ['segment'], subroutes: [] },
|
||||
{ id: 4, path: [''], subroutes: [] },
|
||||
];
|
||||
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: RouterEntries = [
|
||||
{ id: 2, path: ['to', 'to', 'to'], subroutes: [] },
|
||||
{ id: 1, path: ['adam', 'manu'], subroutes: [] },
|
||||
{ id: 3, path: ['hola', 'adam'], subroutes: [] },
|
||||
{ id: 4, path: [''], subroutes: [] },
|
||||
];
|
||||
const seg = new RouterSegments(['hola', 'manu', 'adam']);
|
||||
const 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: RouterEntries = [
|
||||
{ id: 1, path: ['adam', 'manu'], subroutes: [] },
|
||||
{ id: 3, path: ['hola', 'adam'], subroutes: [] },
|
||||
];
|
||||
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: RouterEntries = [
|
||||
{ id: 1, path: ['adam', 'manu'], subroutes: [] },
|
||||
{ id: 2, path: ['manu', 'hello'], subroutes: [] },
|
||||
{ id: 3, path: ['hello'], subroutes: [] },
|
||||
{ id: 4, path: [''], subroutes: [] },
|
||||
];
|
||||
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: RouterEntries = [
|
||||
{ id: 1, path: ['adam', 'manu', 'hello', 'menu', 'hello'], subroutes: [] },
|
||||
{ id: 2, path: ['adam', 'manu', 'hello', 'menu'], subroutes: [] },
|
||||
{ id: 3, path: ['adam', 'manu'], subroutes: [] },
|
||||
];
|
||||
const seg = new RouterSegments(['adam', 'manu', 'hello', 'menu', 'hello']);
|
||||
const 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('generatePath', () => {
|
||||
it('should generate an empty URL', () => {
|
||||
expect(generatePath([])).toEqual('/');
|
||||
expect(generatePath([{ path: '' } as any])).toEqual('/');
|
||||
expect(generatePath([{ path: '/' } as any])).toEqual('/');
|
||||
expect(generatePath([{ path: '//' } as any])).toEqual('/');
|
||||
expect(generatePath([{ path: ' ' } as any])).toEqual('/');
|
||||
});
|
||||
|
||||
it('should genenerate a basic url', () => {
|
||||
const stack = [
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'path/to',
|
||||
'page',
|
||||
'number-TWO',
|
||||
''
|
||||
];
|
||||
expect(generatePath(stack)).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);
|
||||
});
|
||||
});
|
||||
|
||||
51
packages/core/src/components/router/utils/common.ts
Normal file
51
packages/core/src/components/router/utils/common.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { NavOutletElement } from './interfaces';
|
||||
|
||||
export class RouterSegments {
|
||||
private path: string[];
|
||||
constructor(path: string[]) {
|
||||
this.path = path.slice();
|
||||
}
|
||||
|
||||
isDefault(): boolean {
|
||||
if (this.path.length > 0) {
|
||||
return this.path[0] === '';
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
next(): string {
|
||||
if (this.path.length > 0) {
|
||||
return this.path.shift() as string;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
const navs = ['ION-NAV', 'ION-TABS'];
|
||||
export function breadthFirstSearch(root: HTMLElement): NavOutletElement | null {
|
||||
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];
|
||||
let node: HTMLElement | undefined;
|
||||
while (node = queue.shift()) {
|
||||
// visit node
|
||||
if (navs.indexOf(node.tagName) >= 0) {
|
||||
return node as NavOutletElement;
|
||||
}
|
||||
|
||||
// queue children
|
||||
const children = node.children;
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
queue.push(children[i] as NavOutletElement);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
48
packages/core/src/components/router/utils/dom.ts
Normal file
48
packages/core/src/components/router/utils/dom.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { breadthFirstSearch } from './common';
|
||||
import { NavOutlet, RouteChain } from './interfaces';
|
||||
|
||||
export function writeNavState(root: HTMLElement, chain: RouteChain, index: number, direction: number): Promise<void> {
|
||||
if (index >= chain.length) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
const route = chain[index];
|
||||
const node = breadthFirstSearch(root);
|
||||
if (!node) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return node.componentOnReady()
|
||||
.then(() => node.setRouteId(route.id, route.props, direction))
|
||||
.then(changed => {
|
||||
if (changed) {
|
||||
direction = 0;
|
||||
}
|
||||
const nextEl = node.getContentElement();
|
||||
if (nextEl) {
|
||||
return writeNavState(nextEl, chain, index + 1, direction);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
export function readNavState(node: HTMLElement) {
|
||||
const stack: string[] = [];
|
||||
let pivot: NavOutlet|null;
|
||||
while (true) {
|
||||
pivot = breadthFirstSearch(node);
|
||||
if (pivot) {
|
||||
const cmp = pivot.getRouteId();
|
||||
if (cmp) {
|
||||
node = pivot.getContentElement();
|
||||
stack.push(cmp.toLowerCase());
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return {
|
||||
ids: stack,
|
||||
pivot: pivot,
|
||||
};
|
||||
}
|
||||
26
packages/core/src/components/router/utils/interfaces.ts
Normal file
26
packages/core/src/components/router/utils/interfaces.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
|
||||
export interface NavOutlet {
|
||||
setRouteId(id: any, data: any, direction: number): Promise<boolean>;
|
||||
getRouteId(): string;
|
||||
getContentElement(): HTMLElement | null;
|
||||
}
|
||||
|
||||
export interface RouteMatch {
|
||||
chain: RouteChain;
|
||||
matches: number;
|
||||
}
|
||||
|
||||
export type NavOutletElement = NavOutlet & HTMLStencilElement;
|
||||
|
||||
export interface RouteEntry {
|
||||
id: string;
|
||||
path: string[];
|
||||
props: any|undefined;
|
||||
}
|
||||
|
||||
export interface RouteNode extends RouteEntry {
|
||||
children: RouteTree;
|
||||
}
|
||||
|
||||
export type RouteChain = RouteEntry[];
|
||||
export type RouteTree = RouteNode[];
|
||||
71
packages/core/src/components/router/utils/matching.ts
Normal file
71
packages/core/src/components/router/utils/matching.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { RouterSegments } from './common';
|
||||
import { RouteChain, RouteMatch } from './interfaces';
|
||||
|
||||
export function matchesIDs(ids: string[], chain: RouteChain): number {
|
||||
const len = Math.min(ids.length, chain.length);
|
||||
let i = 0;
|
||||
for (; i < len; i++) {
|
||||
if (ids[i] !== chain[i].id) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return i;
|
||||
}
|
||||
|
||||
|
||||
export function matchesPath(path: string[], chain: RouteChain): boolean {
|
||||
const segments = new RouterSegments(path);
|
||||
let matchesDefault = false;
|
||||
for (let i = 0; i < chain.length; i++) {
|
||||
const route = chain[i];
|
||||
if (route.path[0] !== '') {
|
||||
for (const segment of route.path) {
|
||||
if (segments.next() !== segment) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
matchesDefault = false;
|
||||
} else {
|
||||
matchesDefault = true;
|
||||
}
|
||||
}
|
||||
if (matchesDefault) {
|
||||
return matchesDefault === segments.isDefault();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
export function routerIDsToChain(ids: string[], chains: RouteChain[]): RouteMatch {
|
||||
let match: RouteChain|null = null;
|
||||
let maxMatches = 0;
|
||||
for (const chain of chains) {
|
||||
const score = matchesIDs(ids, chain);
|
||||
if (score > maxMatches) {
|
||||
match = chain;
|
||||
maxMatches = score;
|
||||
}
|
||||
}
|
||||
return {
|
||||
chain: match,
|
||||
matches: maxMatches,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
export function routerPathToChain(path: string[], chains: RouteChain[]): RouteMatch|null {
|
||||
let match: RouteChain = null;
|
||||
let matches = 0;
|
||||
for (const chain of chains) {
|
||||
if (matchesPath(path, chain)) {
|
||||
if (chain.length > matches) {
|
||||
matches = chain.length;
|
||||
match = chain;
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
chain: match,
|
||||
matches,
|
||||
};
|
||||
}
|
||||
49
packages/core/src/components/router/utils/parser.ts
Normal file
49
packages/core/src/components/router/utils/parser.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { RouteChain, RouteNode, RouteTree } from './interfaces';
|
||||
import { parsePath } from './path';
|
||||
|
||||
|
||||
export function readRoutes(root: Element): RouteTree {
|
||||
return (Array.from(root.children) as HTMLIonRouteElement[])
|
||||
.filter(el => el.tagName === 'ION-ROUTE')
|
||||
.map(el => ({
|
||||
path: parsePath(readProp(el, 'path')),
|
||||
id: readProp(el, 'component'),
|
||||
props: readProp(el, 'props'),
|
||||
children: readRoutes(el)
|
||||
}));
|
||||
}
|
||||
|
||||
export function readProp(el: HTMLElement, prop: string): string|undefined {
|
||||
if (prop in el) {
|
||||
return (el as any)[prop];
|
||||
}
|
||||
if (el.hasAttribute(prop)) {
|
||||
return el.getAttribute(prop);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function flattenRouterTree(nodes: RouteTree): RouteChain[] {
|
||||
const routes: RouteChain[] = [];
|
||||
for (const node of nodes) {
|
||||
flattenNode([], routes, node);
|
||||
}
|
||||
return routes;
|
||||
}
|
||||
|
||||
function flattenNode(chain: RouteChain, routes: RouteChain[], node: RouteNode) {
|
||||
const s = chain.slice();
|
||||
s.push({
|
||||
id: node.id,
|
||||
path: node.path,
|
||||
props: node.props
|
||||
});
|
||||
|
||||
if (node.children.length === 0) {
|
||||
routes.push(s);
|
||||
return;
|
||||
}
|
||||
for (const sub of node.children) {
|
||||
flattenNode(s, routes, sub);
|
||||
}
|
||||
}
|
||||
62
packages/core/src/components/router/utils/path.ts
Normal file
62
packages/core/src/components/router/utils/path.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { RouteChain } from './interfaces';
|
||||
|
||||
export function generatePath(segments: string[]): string {
|
||||
const path = segments
|
||||
.filter(s => s.length > 0)
|
||||
.join('/');
|
||||
|
||||
return '/' + path;
|
||||
}
|
||||
|
||||
export function chainToPath(chain: RouteChain): string[] {
|
||||
const path = [];
|
||||
for (const route of chain) {
|
||||
if (route.path[0] !== '') {
|
||||
path.push(...route.path);
|
||||
}
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
export function writePath(history: History, base: string, usePath: boolean, path: string[], isPop: boolean, state: number) {
|
||||
path = [base, ...path];
|
||||
let url = generatePath(path);
|
||||
if (usePath) {
|
||||
url = '#' + url;
|
||||
}
|
||||
state++;
|
||||
if (isPop) {
|
||||
history.back();
|
||||
history.replaceState(state, null, url);
|
||||
} else {
|
||||
history.pushState(state, null, url);
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
export function readPath(loc: Location, base: string, useHash: boolean): string[] | null {
|
||||
const path = useHash
|
||||
? loc.hash.substr(1)
|
||||
: loc.pathname;
|
||||
|
||||
if (path.startsWith(base)) {
|
||||
return parsePath(path.slice(base.length));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function parsePath(path: string): string[] {
|
||||
if (path === null || path === undefined) {
|
||||
return [''];
|
||||
}
|
||||
const segments = path.split('/')
|
||||
.map(s => s.trim())
|
||||
.filter(s => s.length > 0);
|
||||
|
||||
if (segments.length === 0) {
|
||||
return [''];
|
||||
} else {
|
||||
return segments;
|
||||
}
|
||||
}
|
||||
|
||||
6
packages/core/src/index.d.ts
vendored
6
packages/core/src/index.d.ts
vendored
@@ -74,10 +74,10 @@ export { Range, RangeEvent } from './components/range/range';
|
||||
export { RangeKnob } from './components/range-knob/range-knob';
|
||||
export { ReorderGroup } from './components/reorder-group/reorder-group';
|
||||
export {
|
||||
RouterEntry,
|
||||
RouterEntries,
|
||||
RouteNode,
|
||||
RouteTree,
|
||||
NavOutlet
|
||||
} from './components/router/router-utils';
|
||||
} from './components/router/utils/interfaces';
|
||||
export { Row } from './components/row/row';
|
||||
export { Reorder } from './components/reorder/reorder';
|
||||
export { Scroll } from './components/scroll/scroll';
|
||||
|
||||
Reference in New Issue
Block a user