-
Notifications
You must be signed in to change notification settings - Fork 13.5k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix(ion-router): fixes routing algorithm
- Loading branch information
1 parent
dc56a59
commit c8a27b7
Showing
15 changed files
with
840 additions
and
480 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} | ||
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
}); | ||
}); | ||
|
Oops, something went wrong.