Skip to content

Commit 3ce8a67

Browse files
committed
feat(router): reverse lookup with params
1 parent d1263a8 commit 3ce8a67

File tree

17 files changed

+167
-133
lines changed

17 files changed

+167
-133
lines changed

packages/core/src/components/nav/nav.tsx

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { Transition } from './transition';
2525

2626
import iosTransitionAnimation from './animations/ios.transition';
2727
import mdTransitionAnimation from './animations/md.transition';
28+
import { RouteID } from '../router/utils/interfaces';
2829

2930
const TrnsCtrl = new TransitionController();
3031

@@ -215,16 +216,19 @@ export class NavControllerBase implements NavOutlet {
215216
}
216217

217218
@Method()
218-
getRouteId(): string | null {
219-
const element = this.getContentElement();
220-
if (element) {
221-
return element.tagName;
219+
getRouteId(): RouteID|null {
220+
const active = this.getActive();
221+
if (active) {
222+
return {
223+
id: active.element.tagName,
224+
params: active.data
225+
};
222226
}
223227
return null;
224228
}
225229

226230
@Method()
227-
getContentElement(): HTMLElement {
231+
getContainerEl(): HTMLElement {
228232
const active = this.getActive();
229233
if (active) {
230234
return active.element;

packages/core/src/components/nav/readme.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ boolean
4848
#### getByIndex()
4949

5050

51-
#### getContentElement()
51+
#### getContainerEl()
5252

5353

5454
#### getPrevious()

packages/core/src/components/router/router.tsx

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -52,16 +52,20 @@ export class Router {
5252
}
5353
console.debug('[IN] nav changed -> update URL');
5454
const { ids, pivot } = this.readNavState();
55-
const { chain, matches } = routerIDsToChain(ids, this.routes);
56-
if (chain.length > matches) {
57-
// readNavState() found a pivot that is not initialized
58-
console.debug('[IN] pivot uninitialized -> write partial nav state');
59-
this.writeNavState(pivot, chain.slice(matches), 0);
55+
const chain = routerIDsToChain(ids, this.routes);
56+
if (chain) {
57+
if (chain.length > ids.length) {
58+
// readNavState() found a pivot that is not initialized
59+
console.debug('[IN] pivot uninitialized -> write partial nav state');
60+
this.writeNavState(pivot, chain.slice(ids.length), 0);
61+
}
62+
63+
const isPop = ev.detail.isPop === true;
64+
const path = chainToPath(chain);
65+
this.writePath(path, isPop);
66+
} else {
67+
console.warn('no matching URL for ', ids.map(i => i.id));
6068
}
61-
62-
const isPop = ev.detail.isPop === true;
63-
const path = chainToPath(chain);
64-
this.writePath(path, isPop);
6569
}
6670

6771
@Method()
@@ -80,7 +84,7 @@ export class Router {
8084
}
8185
const direction = window.history.state >= this.state ? 1 : -1;
8286
const node = document.querySelector('ion-app');
83-
const {chain} = routerPathToChain(currentPath, this.routes);
87+
const chain = routerPathToChain(currentPath, this.routes);
8488
return this.writeNavState(node, chain, direction);
8589
}
8690

@@ -100,14 +104,12 @@ export class Router {
100104
}
101105

102106
private writePath(path: string[], isPop: boolean) {
103-
this.state = writePath(window.history, this.base, this.useHash, path, isPop, this.state);
107+
// busyURL is used to prevent reentering in the popstate event
108+
this.state++;
109+
writePath(window.history, this.base, this.useHash, path, isPop, this.state);
104110
}
105111

106112
private readPath(): string[] | null {
107113
return readPath(window.location, this.base, this.useHash);
108114
}
109-
110-
render() {
111-
return <slot/>;
112-
}
113115
}

packages/core/src/components/router/test/e2e.spec.tsx

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { RouteChain } from '../utils/interfaces';
1+
import { RouteChain, RouteID } from '../utils/interfaces';
22
import { routerIDsToChain, routerPathToChain } from '../utils/matching';
33
import { mockRouteElement } from './parser.spec';
44
import { chainToPath, generatePath, parsePath } from '../utils/path';
@@ -18,19 +18,37 @@ describe('ionic-conference-app', () => {
1818
expect(getRouteIDs('/about', routes)).toEqual(['page-tabs', 'page-about']);
1919
expect(getRouteIDs('/tutorial', routes)).toEqual(['page-tutorial']);
2020

21-
expect(getRoutePaths(['page-tabs', 'tab-schedule', 'page-schedule'], routes)).toEqual('/');
22-
expect(getRoutePaths(['page-tabs', 'tab-speaker', 'page-speaker-list'], routes)).toEqual('/speaker');
23-
expect(getRoutePaths(['page-tabs', 'page-map'], routes)).toEqual('/map');
24-
expect(getRoutePaths(['page-tabs', 'page-about'], routes)).toEqual('/about');
25-
expect(getRoutePaths(['page-tutorial'], routes)).toEqual('/tutorial');
21+
expect(getRoutePath([
22+
{id: 'PAGE-TABS'},
23+
{id: 'tab-schedule'},
24+
{id: 'page-schedule'}], routes)).toEqual('/');
2625

26+
expect(getRoutePath([
27+
{id: 'page-tabs'},
28+
{id: 'TAB-SPEAKER'}], routes)).toEqual('/speaker');
29+
30+
expect(getRoutePath([
31+
{id: 'page-tabs'},
32+
{id: 'TAB-SPEAKER'},
33+
{id: 'page-speaker-list'}], routes)).toEqual('/speaker');
34+
35+
expect(getRoutePath([
36+
{id: 'page-tabs'},
37+
{id: 'PAGE-MAP'}], routes)).toEqual('/map');
38+
39+
expect(getRoutePath([
40+
{id: 'page-tabs'},
41+
{id: 'page-about'}], routes)).toEqual('/about');
42+
43+
expect(getRoutePath([
44+
{id: 'page-tutorial'}], routes)).toEqual('/tutorial');
2745
});
2846
});
2947

3048

3149
function conferenceAppRouting() {
3250
const p2 = mockRouteElement('/', 'tab-schedule');
33-
const p3 = mockRouteElement('/', 'page-schedule');
51+
const p3 = mockRouteElement('/', 'PAGE-SCHEDULE');
3452
p2.appendChild(p3);
3553

3654
const p4 = mockRouteElement('/speaker', 'tab-speaker');
@@ -56,10 +74,10 @@ function conferenceAppRouting() {
5674

5775

5876
function getRouteIDs(path: string, routes: RouteChain[]): string[] {
59-
return routerPathToChain(parsePath(path), routes).chain.map(r => r.id);
77+
return routerPathToChain(parsePath(path), routes).map(r => r.id);
6078
}
6179

62-
function getRoutePaths(ids: string[], routes: RouteChain[]): string {
63-
return generatePath(chainToPath(routerIDsToChain(ids, routes).chain));
80+
function getRoutePath(ids: RouteID[], routes: RouteChain[]): string {
81+
return generatePath(chainToPath(routerIDsToChain(ids, routes)));
6482
}
6583

packages/core/src/components/router/test/matching.spec.tsx

Lines changed: 10 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -132,34 +132,12 @@ describe('routerPathToChain', () => {
132132
chain3,
133133
chain4
134134
];
135-
expect(routerPathToChain(['to'], routes)).toEqual({
136-
chain: null,
137-
matches: 0,
138-
});
139-
140-
expect(routerPathToChain([''], routes)).toEqual({
141-
chain: null,
142-
matches: 0,
143-
});
144-
expect(routerPathToChain(['segment', 'to'], routes)).toEqual({
145-
chain: null,
146-
matches: 0,
147-
});
148-
149-
expect(routerPathToChain(['hola'], routes)).toEqual({
150-
chain: chain3,
151-
matches: 1,
152-
});
153-
expect(routerPathToChain(['hola', 'hola'], routes)).toEqual({
154-
chain: chain3,
155-
matches: 1,
156-
});
157-
158-
expect(routerPathToChain(['hola', 'adios'], routes)).toEqual({
159-
chain: chain4,
160-
matches: 2,
161-
});
162-
135+
expect(routerPathToChain(['to'], routes)).toEqual(null);
136+
expect(routerPathToChain([''], routes)).toEqual(null);
137+
expect(routerPathToChain(['segment', 'to'], routes)).toEqual(null);
138+
expect(routerPathToChain(['hola'], routes)).toEqual(chain3);
139+
expect(routerPathToChain(['hola', 'hola'], routes)).toEqual(chain3);
140+
expect(routerPathToChain(['hola', 'adios'], routes)).toEqual(chain4);
163141
});
164142

165143
it('should match the default route', () => {
@@ -174,11 +152,11 @@ describe('routerPathToChain', () => {
174152
{ id: 'page2', path: [''], params: undefined }
175153
];
176154

177-
expect(routerPathToChain([''], [chain1])).toEqual({chain: chain1, matches: 3});
178-
expect(routerPathToChain(['tab2'], [chain1])).toEqual({chain: null, matches: 0});
155+
expect(routerPathToChain([''], [chain1])).toEqual(chain1);
156+
expect(routerPathToChain(['tab2'], [chain1])).toEqual(null);
179157

180-
expect(routerPathToChain([''], [chain2])).toEqual({chain: null, matches: 0});
181-
expect(routerPathToChain(['tab2'], [chain2])).toEqual({chain: chain2, matches: 3});
158+
expect(routerPathToChain([''], [chain2])).toEqual(null);
159+
expect(routerPathToChain(['tab2'], [chain2])).toEqual(chain2);
182160
});
183161
});
184162

packages/core/src/components/router/test/parser.spec.tsx

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { RouteTree } from '../utils/interfaces';
55
describe('readRoutes', () => {
66
it('should read URL', () => {
77
const root = mockElement('div');
8-
const r1 = mockRouteElement('/', 'main-page');
8+
const r1 = mockRouteElement('/', 'MAIN-PAGE');
99
const r2 = mockRouteElement('/one-page', 'one-page');
1010
const r3 = mockRouteElement('secondpage', 'second-page');
1111
const r4 = mockRouteElement('/5/hola', '4');
@@ -20,12 +20,12 @@ describe('readRoutes', () => {
2020
r4.appendChild(r6);
2121

2222
const expected: RouteTree = [
23-
{ path: [''], id: 'main-page', children: [], props: undefined },
24-
{ path: ['one-page'], id: 'one-page', children: [], props: undefined },
25-
{ path: ['secondpage'], id: 'second-page', props: undefined, children: [
26-
{ path: ['5', 'hola'], id: '4', props: undefined, children: [
27-
{ path: ['path', 'to', 'five'], id: '5', children: [], props: undefined },
28-
{ path: ['path', 'to', 'five2'], id: '6', children: [], props: undefined }
23+
{ path: [''], id: 'main-page', children: [], params: undefined },
24+
{ path: ['one-page'], id: 'one-page', children: [], params: undefined },
25+
{ path: ['secondpage'], id: 'second-page', params: undefined, children: [
26+
{ path: ['5', 'hola'], id: '4', params: undefined, children: [
27+
{ path: ['path', 'to', 'five'], id: '5', children: [], params: undefined },
28+
{ path: ['path', 'to', 'five2'], id: '6', children: [], params: undefined }
2929
] }
3030
] }
3131
];
@@ -36,12 +36,12 @@ describe('readRoutes', () => {
3636
describe('flattenRouterTree', () => {
3737
it('should process routes', () => {
3838
const entries: RouteTree = [
39-
{ path: [''], id: 'hola', children: [], props: undefined },
40-
{ path: ['one-page'], id: 'one-page', children: [], props: undefined },
41-
{ path: ['secondpage'], id: 'second-page', props: undefined, children: [
42-
{ path: ['5', 'hola'], id: '4', props: undefined, children: [
43-
{ path: ['path', 'to', 'five'], id: '5', children: [], props: undefined },
44-
{ path: ['path', 'to', 'five2'], id: '6', children: [], props: undefined }
39+
{ path: [''], id: 'hola', children: [], params: undefined },
40+
{ path: ['one-page'], id: 'one-page', children: [], params: undefined },
41+
{ path: ['secondpage'], id: 'second-page', params: undefined, children: [
42+
{ path: ['5', 'hola'], id: '4', params: undefined, children: [
43+
{ path: ['path', 'to', 'five'], id: '5', children: [], params: undefined },
44+
{ path: ['path', 'to', 'five2'], id: '6', children: [], params: undefined }
4545
] }
4646
] }
4747
];

packages/core/src/components/router/test/path.spec.tsx

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { generatePath, parsePath } from '../utils/path';
1+
import { chainToPath, generatePath, parsePath } from '../utils/path';
2+
import { RouteChain } from '../utils/interfaces';
23

34
describe('parseURL', () => {
45
it('should parse empty path', () => {
@@ -53,3 +54,39 @@ describe('generatePath', () => {
5354

5455
});
5556
});
57+
58+
59+
describe('chainToPath', () => {
60+
it('should generate a simple URL', () => {
61+
const chain: RouteChain = [
62+
{ id: '2', path: [''], params: undefined },
63+
{ id: '1', path: [''], params: undefined },
64+
{ id: '3', path: ['segment', 'to'], params: undefined },
65+
{ id: '4', path: [''], params: undefined },
66+
{ id: '5', path: ['hola', '', 'hey'], params: undefined },
67+
{ id: '6', path: [''], params: undefined },
68+
{ id: '7', path: [':param'], params: {param: 'name'} },
69+
{ id: '8', path: ['adios', ':name', ':id'], params: {name: 'manu', id: '123'} },
70+
];
71+
expect(chainToPath(chain)).toEqual(
72+
['segment', 'to', 'hola', 'hey', 'name', 'adios', 'manu', '123']
73+
);
74+
});
75+
76+
it('should raise an exception', () => {
77+
const chain: RouteChain = [
78+
{ id: '3', path: ['segment'], params: undefined },
79+
{ id: '8', path: [':name'], params: undefined },
80+
];
81+
expect(() => chainToPath(chain)).toThrowError('missing param name');
82+
});
83+
84+
it('should raise an exception 2', () => {
85+
const chain: RouteChain = [
86+
{ id: '3', path: ['segment'], params: undefined },
87+
{ id: '8', path: [':name', ':id'], params: {name: 'hey'} },
88+
];
89+
expect(() => chainToPath(chain)).toThrowError('missing param id');
90+
});
91+
});
92+

packages/core/src/components/router/utils/common.ts

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,6 @@ export class RouterSegments {
66
this.path = path.slice();
77
}
88

9-
isDefault(): boolean {
10-
if (this.path.length > 0) {
11-
return this.path[0] === '';
12-
}
13-
return true;
14-
}
15-
169
next(): string {
1710
if (this.path.length > 0) {
1811
return this.path.shift() as string;

packages/core/src/components/router/utils/dom.ts

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { breadthFirstSearch } from './common';
2-
import { NavOutlet, RouteChain } from './interfaces';
2+
import { NavOutlet, RouteChain, RouteID } from './interfaces';
33

44
export function writeNavState(root: HTMLElement, chain: RouteChain|null, index: number, direction: number): Promise<void> {
55
if (!chain || index >= chain.length) {
@@ -16,7 +16,7 @@ export function writeNavState(root: HTMLElement, chain: RouteChain|null, index:
1616
if (changed) {
1717
direction = 0;
1818
}
19-
const nextEl = node.getContentElement();
19+
const nextEl = node.getContainerEl();
2020
const promise = (nextEl)
2121
? writeNavState(nextEl, chain, index + 1, direction)
2222
: Promise.resolve();
@@ -29,24 +29,21 @@ export function writeNavState(root: HTMLElement, chain: RouteChain|null, index:
2929
}
3030

3131
export function readNavState(node: HTMLElement) {
32-
const stack: string[] = [];
32+
const ids: RouteID[] = [];
3333
let pivot: NavOutlet|null;
3434
while (true) {
3535
pivot = breadthFirstSearch(node);
3636
if (pivot) {
37-
const cmp = pivot.getRouteId();
38-
if (cmp) {
39-
node = pivot.getContentElement();
40-
stack.push(cmp.toLowerCase());
37+
const id = pivot.getRouteId();
38+
if (id) {
39+
node = pivot.getContainerEl();
40+
ids.push(id);
4141
} else {
4242
break;
4343
}
4444
} else {
4545
break;
4646
}
4747
}
48-
return {
49-
ids: stack,
50-
pivot: pivot,
51-
};
48+
return {ids, pivot};
5249
}

packages/core/src/components/router/utils/interfaces.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@
22
export interface NavOutlet {
33
setRouteId(id: string, data: any, direction: number): Promise<boolean>;
44
markVisible?(): Promise<void>;
5-
getRouteId(): string;
5+
getRouteId(): RouteID|null;
66

7-
getContentElement(): HTMLElement | null;
7+
getContainerEl(): HTMLElement | null;
88
}
99

10-
export interface RouteMatch {
11-
chain: RouteChain;
12-
matches: number;
10+
export interface RouteID {
11+
id: string;
12+
params?: any;
1313
}
1414

1515
export type NavOutletElement = NavOutlet & HTMLStencilElement;

0 commit comments

Comments
 (0)