feat(router): adds parameters

This commit is contained in:
Manu Mtz.-Almeida
2018-03-07 23:03:39 +01:00
parent cc365f829d
commit f29e3f4ea4
10 changed files with 204 additions and 130 deletions

View File

@ -2600,8 +2600,8 @@ declare global {
namespace JSXElements { namespace JSXElements {
export interface IonRouteAttributes extends HTMLAttributes { export interface IonRouteAttributes extends HTMLAttributes {
component?: string; component?: string;
params?: undefined;
path?: string; path?: string;
props?: any;
} }
} }
} }

View File

@ -223,11 +223,6 @@ export class NavControllerBase implements NavOutlet {
return null; return null;
} }
@Method()
markVisible() {
return Promise.resolve();
}
@Method() @Method()
getContentElement(): HTMLElement { getContentElement(): HTMLElement {
const active = this.getActive(); const active = this.getActive();

View File

@ -71,9 +71,6 @@ Return a view controller
#### insertPages() #### insertPages()
#### markVisible()
#### pop() #### pop()

View File

@ -12,16 +12,16 @@
string string
#### params
#### path #### path
string string
#### props
any
## Attributes ## Attributes
#### component #### component
@ -29,16 +29,16 @@ any
string string
#### params
#### path #### path
string string
#### props
any
---------------------------------------------- ----------------------------------------------

View File

@ -7,5 +7,5 @@ import { Component, Prop } from '@stencil/core';
export class Route { export class Route {
@Prop() path = ''; @Prop() path = '';
@Prop() component: string; @Prop() component: string;
@Prop() props: any = {}; @Prop() params: undefined;
} }

View File

@ -1,31 +1,30 @@
import { RouteChain } from '../utils/interfaces'; import { RouteChain } from '../utils/interfaces';
import { matchesIDs, matchesPath, routerPathToChain } from '../utils/matching'; import { matchesIDs, matchesPath, mergeParams, routerPathToChain } from '../utils/matching';
import { mockRouteElement } from './parser.spec'; import { parsePath } from '../utils/path';
import { mockElement } from '@stencil/core/dist/testing';
const CHAIN_1: RouteChain = [ const CHAIN_1: RouteChain = [
{ id: '2', path: ['to'], props: undefined }, { id: '2', path: ['to'], params: undefined },
{ id: '1', path: ['path'], props: undefined }, { id: '1', path: ['path'], params: undefined },
{ id: '3', path: ['segment'], props: undefined }, { id: '3', path: ['segment'], params: undefined },
{ id: '4', path: [''], props: undefined }, { id: '4', path: [''], params: undefined },
]; ];
const CHAIN_2: RouteChain = [ const CHAIN_2: RouteChain = [
{ id: '2', path: [''], props: undefined }, { id: '2', path: [''], params: undefined },
{ id: '1', path: [''], props: undefined }, { id: '1', path: [''], params: undefined },
{ id: '3', path: ['segment', 'to'], props: undefined }, { id: '3', path: ['segment', 'to'], params: undefined },
{ id: '4', path: [''], props: undefined }, { id: '4', path: [''], params: undefined },
{ id: '5', path: ['hola'], props: undefined }, { id: '5', path: ['hola'], params: undefined },
{ id: '6', path: [''], props: undefined }, { id: '6', path: [''], params: undefined },
{ id: '7', path: [''], props: undefined }, { id: '7', path: [''], params: undefined },
{ id: '8', path: ['adios', 'que', 'tal'], props: undefined }, { id: '8', path: ['adios', 'que', 'tal'], params: undefined },
]; ];
const CHAIN_3: RouteChain = [ const CHAIN_3: RouteChain = [
{ id: '2', path: ['this', 'to'], props: undefined }, { id: '2', path: ['this', 'to'], params: undefined },
{ id: '1', path: ['path'], props: undefined }, { id: '1', path: ['path'], params: undefined },
{ id: '3', path: ['segment', 'to', 'element'], props: undefined }, { id: '3', path: ['segment', 'to', 'element'], params: undefined },
{ id: '4', path: [''], props: undefined }, { id: '4', path: [''], params: undefined },
]; ];
@ -47,67 +46,85 @@ describe('matchesIDs', () => {
describe('matchesPath', () => { describe('matchesPath', () => {
it('should match simple path', () => { it('should match simple path', () => {
const chain: RouteChain = CHAIN_3; const chain: RouteChain = CHAIN_3;
expect(matchesPath(['this'], chain)).toBe(false); expect(matchesPath(['this'], chain)).toEqual(null);
expect(matchesPath(['this', 'to'], chain)).toBe(false); expect(matchesPath(['this', 'to'], chain)).toEqual(null);
expect(matchesPath(['this', 'to', 'path'], chain)).toBe(false); expect(matchesPath(['this', 'to', 'path'], chain)).toEqual(null);
expect(matchesPath(['this', 'to', 'path', 'segment'], chain)).toBe(false); expect(matchesPath(['this', 'to', 'path', 'segment'], chain)).toEqual(null);
expect(matchesPath(['this', 'to', 'path', 'segment', 'to'], chain)).toBe(false); expect(matchesPath(['this', 'to', 'path', 'segment', 'to'], chain)).toEqual(null);
expect(matchesPath(['this', 'to', 'path', 'segment', 'to', 'element'], chain)).toBe(true); expect(matchesPath(['this', 'to', 'path', 'segment', 'to', 'element'], chain)).toEqual(chain);
expect(matchesPath(['this', 'to', 'path', 'segment', 'to', 'element', 'more'], chain)).toBe(false); expect(matchesPath(['this', 'to', 'path', 'segment', 'to', 'element', 'more'], chain)).toEqual(null);
expect(matchesPath([], chain)).toBe(false); expect(matchesPath([], chain)).toEqual(null);
expect(matchesPath([''], chain)).toBe(false); expect(matchesPath([''], chain)).toEqual(null);
expect(matchesPath(['path'], chain)).toBe(false); expect(matchesPath(['path'], chain)).toEqual(null);
}); });
it('should match simple default route', () => { it('should match simple default route', () => {
const chain: RouteChain = CHAIN_2; const chain: RouteChain = CHAIN_2;
expect(matchesPath([''], chain)).toBe(false); expect(matchesPath([''], chain)).toEqual(null);
expect(matchesPath(['segment'], chain)).toBe(false); expect(matchesPath(['segment'], chain)).toEqual(null);
expect(matchesPath(['segment', 'to'], chain)).toBe(false); expect(matchesPath(['segment', 'to'], chain)).toEqual(null);
expect(matchesPath(['segment', 'to', 'hola'], chain)).toBe(false); expect(matchesPath(['segment', 'to', 'hola'], chain)).toEqual(null);
expect(matchesPath(['segment', 'to', 'hola', 'adios'], chain)).toBe(false); expect(matchesPath(['segment', 'to', 'hola', 'adios'], chain)).toEqual(null);
expect(matchesPath(['segment', 'to', 'hola', 'adios', 'que'], chain)).toBe(false); expect(matchesPath(['segment', 'to', 'hola', 'adios', 'que'], chain)).toEqual(null);
expect(matchesPath(['segment', 'to', 'hola', 'adios', 'que', 'tal'], chain)).toBe(true); expect(matchesPath(['segment', 'to', 'hola', 'adios', 'que', 'tal'], chain)).toEqual(chain);
expect(matchesPath(['segment', 'to', 'hola', 'adios', 'que', 'tal', 'more'], chain)).toEqual(chain);
expect(matchesPath(['to'], chain)).toBe(false); expect(matchesPath(['to'], chain)).toEqual(null);
expect(matchesPath(['path', 'to'], chain)).toBe(false); expect(matchesPath(['path', 'to'], chain)).toEqual(null);
}); });
it('should match simple route 2', () => { it('should match simple route 2', () => {
const chain: RouteChain = [{ id: '5', path: ['hola'], props: undefined }]; const chain: RouteChain = [{ id: '5', path: ['hola'], params: undefined }];
expect(matchesPath([''], chain)).toBe(false); expect(matchesPath([''], chain)).toEqual(null);
expect(matchesPath(['hola'], chain)).toBe(true); expect(matchesPath(['hola'], chain)).toEqual(chain);
expect(matchesPath(['hola', 'hola'], chain)).toBe(true); expect(matchesPath(['hola', 'hola'], chain)).toEqual(chain);
expect(matchesPath(['hola', 'adios'], chain)).toBe(true); expect(matchesPath(['hola', 'adios'], chain)).toEqual(chain);
}); });
it('should match simple route 3', () => { it('should match simple route 3', () => {
const chain: RouteChain = [{ id: '5', path: ['hola', 'adios'], props: undefined }]; const chain: RouteChain = [{ id: '5', path: ['hola', 'adios'], params: undefined }];
expect(matchesPath([''], chain)).toBe(false); expect(matchesPath([''], chain)).toEqual(null);
expect(matchesPath(['hola'], chain)).toBe(false); expect(matchesPath(['hola'], chain)).toEqual(null);
expect(matchesPath(['hola', 'hola'], chain)).toBe(false); expect(matchesPath(['hola', 'hola'], chain)).toEqual(null);
expect(matchesPath(['hola', 'adios'], chain)).toBe(true); expect(matchesPath(['hola', 'adios'], chain)).toEqual(chain);
expect(matchesPath(['hola', 'adios', 'bye'], chain)).toEqual(chain);
}); });
it('should match simple route 4', () => { it('should match simple route 4', () => {
const chain: RouteChain = [ const chain: RouteChain = [
{ id: '5', path: ['hola'], props: undefined }, { id: '5', path: ['hola'], params: undefined },
{ id: '5', path: ['adios'], props: undefined }]; { id: '5', path: ['adios'], params: undefined }];
expect(matchesPath([''], chain)).toBe(false); expect(matchesPath([''], chain)).toEqual(null);
expect(matchesPath(['hola'], chain)).toBe(false); expect(matchesPath(['hola'], chain)).toEqual(null);
expect(matchesPath(['hola', 'hola'], chain)).toBe(false); expect(matchesPath(['hola', 'hola'], chain)).toEqual(null);
expect(matchesPath(['hola', 'adios'], chain)).toBe(true); expect(matchesPath(['hola', 'adios'], chain)).toEqual(chain);
});
it('should match with parameters', () => {
const chain: RouteChain = [
{ id: '5', path: ['profile', ':name'], params: undefined },
{ id: '5', path: [''], params: undefined },
{ id: '5', path: ['image'], params: {size: 'lg'} },
{ id: '5', path: ['image', ':size', ':type'], params: {size: 'mg'} },
];
const matched = matchesPath(parsePath('/profile/manu/image/image/large/retina'), chain);
expect(matched).toEqual([
{ id: '5', path: ['profile', ':name'], params: {name: 'manu'} },
{ id: '5', path: [''], params: undefined },
{ id: '5', path: ['image'], params: {size: 'lg'} },
{ id: '5', path: ['image', ':size', ':type'], params: {size: 'large', type: 'retina'} },
]);
}); });
}); });
describe('routerPathToChain', () => { describe('routerPathToChain', () => {
it('should match the route with higher priority', () => { it('should match the route with higher priority', () => {
const chain3: RouteChain = [{ id: '5', path: ['hola'], props: undefined }]; const chain3: RouteChain = [{ id: '5', path: ['hola'], params: undefined }];
const chain4: RouteChain = [ const chain4: RouteChain = [
{ id: '5', path: ['hola'], props: undefined }, { id: '5', path: ['hola'], params: undefined },
{ id: '5', path: ['adios'], props: undefined }]; { id: '5', path: ['adios'], params: undefined }];
const routes: RouteChain[] = [ const routes: RouteChain[] = [
CHAIN_1, CHAIN_1,
@ -147,14 +164,14 @@ describe('routerPathToChain', () => {
it('should match the default route', () => { it('should match the default route', () => {
const chain1: RouteChain = [ const chain1: RouteChain = [
{ id: 'tabs', path: [''], props: undefined }, { id: 'tabs', path: [''], params: undefined },
{ id: 'tab1', path: [''], props: undefined }, { id: 'tab1', path: [''], params: undefined },
{ id: 'schedule', path: [''], props: undefined } { id: 'schedule', path: [''], params: undefined }
]; ];
const chain2: RouteChain = [ const chain2: RouteChain = [
{ id: 'tabs', path: [''], props: undefined }, { id: 'tabs', path: [''], params: undefined },
{ id: 'tab2', path: ['tab2'], props: undefined }, { id: 'tab2', path: ['tab2'], params: undefined },
{ id: 'page2', path: [''], props: undefined } { id: 'page2', path: [''], params: undefined }
]; ];
expect(routerPathToChain([''], [chain1])).toEqual({chain: chain1, matches: 3}); expect(routerPathToChain([''], [chain1])).toEqual({chain: chain1, matches: 3});
@ -162,20 +179,47 @@ describe('routerPathToChain', () => {
expect(routerPathToChain([''], [chain2])).toEqual({chain: null, matches: 0}); expect(routerPathToChain([''], [chain2])).toEqual({chain: null, matches: 0});
expect(routerPathToChain(['tab2'], [chain2])).toEqual({chain: chain2, matches: 3}); expect(routerPathToChain(['tab2'], [chain2])).toEqual({chain: chain2, matches: 3});
});
});
describe('mergeParams', () => {
it('should merge undefined', () => {
expect(mergeParams(undefined, undefined)).toBeUndefined();
expect(mergeParams(null, undefined)).toBeUndefined();
expect(mergeParams(undefined, null)).toBeUndefined();
expect(mergeParams(null, null)).toBeUndefined();
}); });
it('should merge undefined with params', () => {
const params = {data: '1'};
expect(mergeParams(undefined, params)).toEqual(params);
expect(mergeParams(null, params)).toEqual(params);
expect(mergeParams(params, undefined)).toEqual(params);
expect(mergeParams(params, null)).toEqual(params);
});
it('should merge params with params', () => {
const params1 = {data: '1', data3: 'hello'};
const params2 = {data: '2', data2: 'hola'};
expect(mergeParams(params1, params2)).toEqual({
data: '2',
data2: 'hola',
data3: 'hello'
});
expect(params1).toEqual({data: '1', data3: 'hello'});
expect(params2).toEqual({data: '2', data2: 'hola'});
});
}); });
// describe('matchRoute', () => { // describe('matchRoute', () => {
// it('should match simple route', () => { // it('should match simple route', () => {
// const path = ['path', 'to', 'component']; // const path = ['path', 'to', 'component'];
// const routes: RouteChain[] = [ // const routes: RouteChain[] = [
// [{ id: 2, path: ['to'], props: undefined }], // [{ id: 2, path: ['to'], params: undefined }],
// [{ id: 1, path: ['path'], props: undefined }], // [{ id: 1, path: ['path'], params: undefined }],
// [{ id: 3, path: ['segment'], props: undefined }], // [{ id: 3, path: ['segment'], params: undefined }],
// [{ id: 4, path: [''], props: undefined }], // [{ id: 4, path: [''], params: undefined }],
// ]; // ];
// const match = routerPathToChain(path, routes); // const match = routerPathToChain(path, routes);
// expect(match).toEqual({ id: 1, path: ['path'], children: [] }); // expect(match).toEqual({ id: 1, path: ['path'], children: [] });
@ -184,10 +228,10 @@ describe('routerPathToChain', () => {
// it('should match default route', () => { // it('should match default route', () => {
// const routes: RouteTree = [ // const routes: RouteTree = [
// { id: 2, path: ['to'], children: [], props: undefined }, // { id: 2, path: ['to'], children: [], params: undefined },
// { id: 1, path: ['path'], children: [], props: undefined }, // { id: 1, path: ['path'], children: [], params: undefined },
// { id: 3, path: ['segment'], children: [], props: undefined }, // { id: 3, path: ['segment'], children: [], params: undefined },
// { id: 4, path: [''], children: [], props: undefined }, // { id: 4, path: [''], children: [], params: undefined },
// ]; // ];
// const seg = new RouterSegments(['hola', 'path']); // const seg = new RouterSegments(['hola', 'path']);
// let match = matchRoute(seg, routes); // let match = matchRoute(seg, routes);
@ -204,10 +248,10 @@ describe('routerPathToChain', () => {
// it('should not match any route', () => { // it('should not match any route', () => {
// const routes: RouteTree = [ // const routes: RouteTree = [
// { id: 2, path: ['to', 'to', 'to'], children: [], props: undefined }, // { id: 2, path: ['to', 'to', 'to'], children: [], params: undefined },
// { id: 1, path: ['adam', 'manu'], children: [], props: undefined }, // { id: 1, path: ['adam', 'manu'], children: [], params: undefined },
// { id: 3, path: ['hola', 'adam'], children: [], props: undefined }, // { id: 3, path: ['hola', 'adam'], children: [], params: undefined },
// { id: 4, path: [''], children: [], props: undefined }, // { id: 4, path: [''], children: [], params: undefined },
// ]; // ];
// const seg = new RouterSegments(['hola', 'manu', 'adam']); // const seg = new RouterSegments(['hola', 'manu', 'adam']);
// const match = matchRoute(seg, routes); // const match = matchRoute(seg, routes);
@ -224,8 +268,8 @@ describe('routerPathToChain', () => {
// it('should not match any route (2)', () => { // it('should not match any route (2)', () => {
// const routes: RouteTree = [ // const routes: RouteTree = [
// { id: 1, path: ['adam', 'manu'], children: [], props: undefined }, // { id: 1, path: ['adam', 'manu'], children: [], params: undefined },
// { id: 3, path: ['hola', 'adam'], children: [], props: undefined }, // { id: 3, path: ['hola', 'adam'], children: [], params: undefined },
// ]; // ];
// const seg = new RouterSegments(['adam']); // const seg = new RouterSegments(['adam']);
// expect(matchRoute(seg, routes)).toBeNull(); // expect(matchRoute(seg, routes)).toBeNull();
@ -235,10 +279,10 @@ describe('routerPathToChain', () => {
// it ('should match multiple segments', () => { // it ('should match multiple segments', () => {
// const routes: RouteTree = [ // const routes: RouteTree = [
// { id: 1, path: ['adam', 'manu'], children: [], props: undefined }, // { id: 1, path: ['adam', 'manu'], children: [], params: undefined },
// { id: 2, path: ['manu', 'hello'], children: [], props: undefined }, // { id: 2, path: ['manu', 'hello'], children: [], params: undefined },
// { id: 3, path: ['hello'], children: [], props: undefined }, // { id: 3, path: ['hello'], children: [], params: undefined },
// { id: 4, path: [''], children: [], props: undefined }, // { id: 4, path: [''], children: [], params: undefined },
// ]; // ];
// const seg = new RouterSegments(['adam', 'manu', 'hello', 'manu', 'hello']); // const seg = new RouterSegments(['adam', 'manu', 'hello', 'manu', 'hello']);
// let match = matchRoute(seg, routes); // let match = matchRoute(seg, routes);
@ -259,9 +303,9 @@ describe('routerPathToChain', () => {
// it('should match long multi segments', () => { // it('should match long multi segments', () => {
// const routes: RouteTree = [ // const routes: RouteTree = [
// { id: 1, path: ['adam', 'manu', 'hello', 'menu', 'hello'], children: [], props: undefined }, // { id: 1, path: ['adam', 'manu', 'hello', 'menu', 'hello'], children: [], params: undefined },
// { id: 2, path: ['adam', 'manu', 'hello', 'menu'], children: [], props: undefined }, // { id: 2, path: ['adam', 'manu', 'hello', 'menu'], children: [], params: undefined },
// { id: 3, path: ['adam', 'manu'], children: [], props: undefined }, // { id: 3, path: ['adam', 'manu'], children: [], params: undefined },
// ]; // ];
// const seg = new RouterSegments(['adam', 'manu', 'hello', 'menu', 'hello']); // const seg = new RouterSegments(['adam', 'manu', 'hello', 'menu', 'hello']);
// const match = matchRoute(seg, routes); // const match = matchRoute(seg, routes);

View File

@ -11,18 +11,20 @@ export function writeNavState(root: HTMLElement, chain: RouteChain, index: numbe
return Promise.resolve(); return Promise.resolve();
} }
return node.componentOnReady() return node.componentOnReady()
.then(() => node.setRouteId(route.id, route.props, direction)) .then(() => node.setRouteId(route.id, route.params, direction))
.then(changed => { .then(changed => {
if (changed) { if (changed) {
direction = 0; direction = 0;
} }
const nextEl = node.getContentElement(); const nextEl = node.getContentElement();
if (nextEl) { const promise = (nextEl)
return writeNavState(nextEl, chain, index + 1, direction) ? writeNavState(nextEl, chain, index + 1, direction)
.then(() => node.markVisible()); : Promise.resolve();
} else {
return node.markVisible(); if (node.markVisible) {
return promise.then(() => node.markVisible());
} }
return promise;
}); });
} }

View File

@ -1,7 +1,7 @@
export interface NavOutlet { export interface NavOutlet {
setRouteId(id: any, data: any, direction: number): Promise<boolean>; setRouteId(id: any, data: any, direction: number): Promise<boolean>;
markVisible(): Promise<void>; markVisible?(): Promise<void>;
getRouteId(): string; getRouteId(): string;
getContentElement(): HTMLElement | null; getContentElement(): HTMLElement | null;
@ -17,7 +17,7 @@ export type NavOutletElement = NavOutlet & HTMLStencilElement;
export interface RouteEntry { export interface RouteEntry {
id: string; id: string;
path: string[]; path: string[];
props: any|undefined; params: any|undefined;
} }
export interface RouteNode extends RouteEntry { export interface RouteNode extends RouteEntry {

View File

@ -13,26 +13,61 @@ export function matchesIDs(ids: string[], chain: RouteChain): number {
} }
export function matchesPath(path: string[], chain: RouteChain): boolean { export function matchesPath(path: string[], chain: RouteChain): RouteChain | null {
const segments = new RouterSegments(path); const segments = new RouterSegments(path);
let matchesDefault = false; let matchesDefault = false;
let allparams: any[];
for (let i = 0; i < chain.length; i++) { for (let i = 0; i < chain.length; i++) {
const route = chain[i]; const path = chain[i].path;
if (route.path[0] !== '') { if (path[0] === '') {
for (const segment of route.path) { matchesDefault = true;
if (segments.next() !== segment) { } else {
return false; for (const segment of path) {
const data = segments.next();
// data param
if (segment[0] === ':') {
if (data === '') {
return null;
}
allparams = allparams || [];
const params = allparams[i] || (allparams[i] = {});
params[segment.slice(1)] = data;
} else if (data !== segment) {
return null;
} }
} }
matchesDefault = false; matchesDefault = false;
} else {
matchesDefault = true;
} }
} }
if (matchesDefault) { const matches = (matchesDefault)
return matchesDefault === segments.isDefault(); ? matchesDefault === segments.isDefault()
: true;
if (!matches) {
return null;
} }
return true; if (allparams) {
return chain.map((route, i) => ({
id: route.id,
path: route.path,
params: mergeParams(route.params, allparams[i])
}));
}
return chain;
}
export function mergeParams(a: any, b: any): any {
if (!a && b) {
return b;
} else if (a && !b) {
return a;
} else if (a && b) {
return {
...a,
...b
};
}
return undefined;
} }
@ -48,7 +83,7 @@ export function routerIDsToChain(ids: string[], chains: RouteChain[]): RouteMatc
} }
return { return {
chain: match, chain: match,
matches: maxMatches, matches: maxMatches
}; };
} }
@ -57,10 +92,11 @@ export function routerPathToChain(path: string[], chains: RouteChain[]): RouteMa
let match: RouteChain = null; let match: RouteChain = null;
let matches = 0; let matches = 0;
for (const chain of chains) { for (const chain of chains) {
if (matchesPath(path, chain)) { const matchedChain = matchesPath(path, chain);
if (chain.length > matches) { if (matchedChain !== null) {
matches = chain.length; if (matchedChain.length > matches) {
match = chain; matches = matchedChain.length;
match = matchedChain;
} }
} }
} }

View File

@ -8,7 +8,7 @@ export function readRoutes(root: Element): RouteTree {
.map(el => ({ .map(el => ({
path: parsePath(readProp(el, 'path')), path: parsePath(readProp(el, 'path')),
id: readProp(el, 'component'), id: readProp(el, 'component'),
props: readProp(el, 'props'), params: el.params,
children: readRoutes(el) children: readRoutes(el)
})); }));
} }
@ -36,7 +36,7 @@ function flattenNode(chain: RouteChain, routes: RouteChain[], node: RouteNode) {
s.push({ s.push({
id: node.id, id: node.id,
path: node.path, path: node.path,
props: node.props params: node.params
}); });
if (node.children.length === 0) { if (node.children.length === 0) {