From f29e3f4ea43a8ed37f255c0652e722d9af695f61 Mon Sep 17 00:00:00 2001 From: "Manu Mtz.-Almeida" Date: Wed, 7 Mar 2018 23:03:39 +0100 Subject: [PATCH] feat(router): adds parameters --- packages/core/src/components.d.ts | 2 +- packages/core/src/components/nav/nav.tsx | 5 - packages/core/src/components/nav/readme.md | 3 - packages/core/src/components/route/readme.md | 20 +- packages/core/src/components/route/route.tsx | 2 +- .../components/router/test/matching.spec.tsx | 212 +++++++++++------- .../core/src/components/router/utils/dom.ts | 14 +- .../src/components/router/utils/interfaces.ts | 4 +- .../src/components/router/utils/matching.ts | 68 ++++-- .../src/components/router/utils/parser.ts | 4 +- 10 files changed, 204 insertions(+), 130 deletions(-) diff --git a/packages/core/src/components.d.ts b/packages/core/src/components.d.ts index 105eb77dcd..64ef531289 100644 --- a/packages/core/src/components.d.ts +++ b/packages/core/src/components.d.ts @@ -2600,8 +2600,8 @@ declare global { namespace JSXElements { export interface IonRouteAttributes extends HTMLAttributes { component?: string; + params?: undefined; path?: string; - props?: any; } } } diff --git a/packages/core/src/components/nav/nav.tsx b/packages/core/src/components/nav/nav.tsx index c01eaed855..bae73fff57 100644 --- a/packages/core/src/components/nav/nav.tsx +++ b/packages/core/src/components/nav/nav.tsx @@ -223,11 +223,6 @@ export class NavControllerBase implements NavOutlet { return null; } - @Method() - markVisible() { - return Promise.resolve(); - } - @Method() getContentElement(): HTMLElement { const active = this.getActive(); diff --git a/packages/core/src/components/nav/readme.md b/packages/core/src/components/nav/readme.md index 764ff09032..e93d32fb64 100644 --- a/packages/core/src/components/nav/readme.md +++ b/packages/core/src/components/nav/readme.md @@ -71,9 +71,6 @@ Return a view controller #### insertPages() -#### markVisible() - - #### pop() diff --git a/packages/core/src/components/route/readme.md b/packages/core/src/components/route/readme.md index aec9f098b8..185af3066f 100644 --- a/packages/core/src/components/route/readme.md +++ b/packages/core/src/components/route/readme.md @@ -12,16 +12,16 @@ string +#### params + + + + #### path string -#### props - -any - - ## Attributes #### component @@ -29,16 +29,16 @@ any string +#### params + + + + #### path string -#### props - -any - - ---------------------------------------------- diff --git a/packages/core/src/components/route/route.tsx b/packages/core/src/components/route/route.tsx index fdabf5c6b5..c24afd074b 100644 --- a/packages/core/src/components/route/route.tsx +++ b/packages/core/src/components/route/route.tsx @@ -7,5 +7,5 @@ import { Component, Prop } from '@stencil/core'; export class Route { @Prop() path = ''; @Prop() component: string; - @Prop() props: any = {}; + @Prop() params: undefined; } diff --git a/packages/core/src/components/router/test/matching.spec.tsx b/packages/core/src/components/router/test/matching.spec.tsx index 73efc5aca5..535a856b3d 100644 --- a/packages/core/src/components/router/test/matching.spec.tsx +++ b/packages/core/src/components/router/test/matching.spec.tsx @@ -1,31 +1,30 @@ import { RouteChain } from '../utils/interfaces'; -import { matchesIDs, matchesPath, routerPathToChain } from '../utils/matching'; -import { mockRouteElement } from './parser.spec'; -import { mockElement } from '@stencil/core/dist/testing'; +import { matchesIDs, matchesPath, mergeParams, routerPathToChain } from '../utils/matching'; +import { parsePath } from '../utils/path'; 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 }, + { id: '2', path: ['to'], params: undefined }, + { id: '1', path: ['path'], params: undefined }, + { id: '3', path: ['segment'], params: undefined }, + { id: '4', path: [''], params: 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 }, + { id: '2', path: [''], params: undefined }, + { id: '1', path: [''], params: undefined }, + { id: '3', path: ['segment', 'to'], params: undefined }, + { id: '4', path: [''], params: undefined }, + { id: '5', path: ['hola'], params: undefined }, + { id: '6', path: [''], params: undefined }, + { id: '7', path: [''], params: undefined }, + { id: '8', path: ['adios', 'que', 'tal'], params: 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 }, + { id: '2', path: ['this', 'to'], params: undefined }, + { id: '1', path: ['path'], params: undefined }, + { id: '3', path: ['segment', 'to', 'element'], params: undefined }, + { id: '4', path: [''], params: undefined }, ]; @@ -47,67 +46,85 @@ describe('matchesIDs', () => { 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(['this'], chain)).toEqual(null); + expect(matchesPath(['this', 'to'], chain)).toEqual(null); + expect(matchesPath(['this', 'to', 'path'], chain)).toEqual(null); + expect(matchesPath(['this', 'to', 'path', 'segment'], chain)).toEqual(null); + expect(matchesPath(['this', 'to', 'path', 'segment', 'to'], chain)).toEqual(null); + expect(matchesPath(['this', 'to', 'path', 'segment', 'to', 'element'], chain)).toEqual(chain); + expect(matchesPath(['this', 'to', 'path', 'segment', 'to', 'element', 'more'], chain)).toEqual(null); - expect(matchesPath([], chain)).toBe(false); - expect(matchesPath([''], chain)).toBe(false); - expect(matchesPath(['path'], chain)).toBe(false); + expect(matchesPath([], chain)).toEqual(null); + expect(matchesPath([''], chain)).toEqual(null); + expect(matchesPath(['path'], chain)).toEqual(null); }); 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([''], chain)).toEqual(null); + expect(matchesPath(['segment'], chain)).toEqual(null); + expect(matchesPath(['segment', 'to'], chain)).toEqual(null); + expect(matchesPath(['segment', 'to', 'hola'], chain)).toEqual(null); + expect(matchesPath(['segment', 'to', 'hola', 'adios'], chain)).toEqual(null); + expect(matchesPath(['segment', 'to', 'hola', 'adios', 'que'], chain)).toEqual(null); + 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(['path', 'to'], chain)).toBe(false); + expect(matchesPath(['to'], chain)).toEqual(null); + expect(matchesPath(['path', 'to'], chain)).toEqual(null); }); 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); + const chain: RouteChain = [{ id: '5', path: ['hola'], params: undefined }]; + expect(matchesPath([''], chain)).toEqual(null); + expect(matchesPath(['hola'], chain)).toEqual(chain); + expect(matchesPath(['hola', 'hola'], chain)).toEqual(chain); + expect(matchesPath(['hola', 'adios'], chain)).toEqual(chain); }); 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); + const chain: RouteChain = [{ id: '5', path: ['hola', 'adios'], params: undefined }]; + expect(matchesPath([''], chain)).toEqual(null); + expect(matchesPath(['hola'], chain)).toEqual(null); + expect(matchesPath(['hola', 'hola'], chain)).toEqual(null); + expect(matchesPath(['hola', 'adios'], chain)).toEqual(chain); + expect(matchesPath(['hola', 'adios', 'bye'], chain)).toEqual(chain); }); it('should match simple route 4', () => { const chain: RouteChain = [ - { id: '5', path: ['hola'], props: undefined }, - { id: '5', path: ['adios'], props: undefined }]; + { id: '5', path: ['hola'], params: undefined }, + { id: '5', path: ['adios'], params: 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); + expect(matchesPath([''], chain)).toEqual(null); + expect(matchesPath(['hola'], chain)).toEqual(null); + expect(matchesPath(['hola', 'hola'], chain)).toEqual(null); + 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', () => { 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 = [ - { id: '5', path: ['hola'], props: undefined }, - { id: '5', path: ['adios'], props: undefined }]; + { id: '5', path: ['hola'], params: undefined }, + { id: '5', path: ['adios'], params: undefined }]; const routes: RouteChain[] = [ CHAIN_1, @@ -147,14 +164,14 @@ describe('routerPathToChain', () => { it('should match the default route', () => { const chain1: RouteChain = [ - { id: 'tabs', path: [''], props: undefined }, - { id: 'tab1', path: [''], props: undefined }, - { id: 'schedule', path: [''], props: undefined } + { id: 'tabs', path: [''], params: undefined }, + { id: 'tab1', path: [''], params: undefined }, + { id: 'schedule', path: [''], params: undefined } ]; const chain2: RouteChain = [ - { id: 'tabs', path: [''], props: undefined }, - { id: 'tab2', path: ['tab2'], props: undefined }, - { id: 'page2', path: [''], props: undefined } + { id: 'tabs', path: [''], params: undefined }, + { id: 'tab2', path: ['tab2'], params: undefined }, + { id: 'page2', path: [''], params: undefined } ]; expect(routerPathToChain([''], [chain1])).toEqual({chain: chain1, matches: 3}); @@ -162,20 +179,47 @@ describe('routerPathToChain', () => { expect(routerPathToChain([''], [chain2])).toEqual({chain: null, matches: 0}); 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', () => { // 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 }], +// [{ id: 2, path: ['to'], params: undefined }], +// [{ id: 1, path: ['path'], params: undefined }], +// [{ id: 3, path: ['segment'], params: undefined }], +// [{ id: 4, path: [''], params: undefined }], // ]; // const match = routerPathToChain(path, routes); // expect(match).toEqual({ id: 1, path: ['path'], children: [] }); @@ -184,10 +228,10 @@ describe('routerPathToChain', () => { // 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 }, +// { id: 2, path: ['to'], children: [], params: undefined }, +// { id: 1, path: ['path'], children: [], params: undefined }, +// { id: 3, path: ['segment'], children: [], params: undefined }, +// { id: 4, path: [''], children: [], params: undefined }, // ]; // const seg = new RouterSegments(['hola', 'path']); // let match = matchRoute(seg, routes); @@ -204,10 +248,10 @@ describe('routerPathToChain', () => { // 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 }, +// { id: 2, path: ['to', 'to', 'to'], children: [], params: undefined }, +// { id: 1, path: ['adam', 'manu'], children: [], params: undefined }, +// { id: 3, path: ['hola', 'adam'], children: [], params: undefined }, +// { id: 4, path: [''], children: [], params: undefined }, // ]; // const seg = new RouterSegments(['hola', 'manu', 'adam']); // const match = matchRoute(seg, routes); @@ -224,8 +268,8 @@ describe('routerPathToChain', () => { // 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 }, +// { id: 1, path: ['adam', 'manu'], children: [], params: undefined }, +// { id: 3, path: ['hola', 'adam'], children: [], params: undefined }, // ]; // const seg = new RouterSegments(['adam']); // expect(matchRoute(seg, routes)).toBeNull(); @@ -235,10 +279,10 @@ describe('routerPathToChain', () => { // 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 }, +// { id: 1, path: ['adam', 'manu'], children: [], params: undefined }, +// { id: 2, path: ['manu', 'hello'], children: [], params: undefined }, +// { id: 3, path: ['hello'], children: [], params: undefined }, +// { id: 4, path: [''], children: [], params: undefined }, // ]; // const seg = new RouterSegments(['adam', 'manu', 'hello', 'manu', 'hello']); // let match = matchRoute(seg, routes); @@ -259,9 +303,9 @@ describe('routerPathToChain', () => { // 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 }, +// { id: 1, path: ['adam', 'manu', 'hello', 'menu', 'hello'], children: [], params: undefined }, +// { id: 2, path: ['adam', 'manu', 'hello', 'menu'], children: [], params: undefined }, +// { id: 3, path: ['adam', 'manu'], children: [], params: undefined }, // ]; // const seg = new RouterSegments(['adam', 'manu', 'hello', 'menu', 'hello']); // const match = matchRoute(seg, routes); diff --git a/packages/core/src/components/router/utils/dom.ts b/packages/core/src/components/router/utils/dom.ts index 32f42bdff2..53a085f316 100644 --- a/packages/core/src/components/router/utils/dom.ts +++ b/packages/core/src/components/router/utils/dom.ts @@ -11,18 +11,20 @@ export function writeNavState(root: HTMLElement, chain: RouteChain, index: numbe return Promise.resolve(); } return node.componentOnReady() - .then(() => node.setRouteId(route.id, route.props, direction)) + .then(() => node.setRouteId(route.id, route.params, direction)) .then(changed => { if (changed) { direction = 0; } const nextEl = node.getContentElement(); - if (nextEl) { - return writeNavState(nextEl, chain, index + 1, direction) - .then(() => node.markVisible()); - } else { - return node.markVisible(); + const promise = (nextEl) + ? writeNavState(nextEl, chain, index + 1, direction) + : Promise.resolve(); + + if (node.markVisible) { + return promise.then(() => node.markVisible()); } + return promise; }); } diff --git a/packages/core/src/components/router/utils/interfaces.ts b/packages/core/src/components/router/utils/interfaces.ts index a53d676113..abc294ad3c 100644 --- a/packages/core/src/components/router/utils/interfaces.ts +++ b/packages/core/src/components/router/utils/interfaces.ts @@ -1,7 +1,7 @@ export interface NavOutlet { setRouteId(id: any, data: any, direction: number): Promise; - markVisible(): Promise; + markVisible?(): Promise; getRouteId(): string; getContentElement(): HTMLElement | null; @@ -17,7 +17,7 @@ export type NavOutletElement = NavOutlet & HTMLStencilElement; export interface RouteEntry { id: string; path: string[]; - props: any|undefined; + params: any|undefined; } export interface RouteNode extends RouteEntry { diff --git a/packages/core/src/components/router/utils/matching.ts b/packages/core/src/components/router/utils/matching.ts index a188aba7d7..da028b288e 100644 --- a/packages/core/src/components/router/utils/matching.ts +++ b/packages/core/src/components/router/utils/matching.ts @@ -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); let matchesDefault = false; + let allparams: any[]; 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; + const path = chain[i].path; + if (path[0] === '') { + matchesDefault = true; + } else { + 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; - } else { - matchesDefault = true; } } - if (matchesDefault) { - return matchesDefault === segments.isDefault(); + const matches = (matchesDefault) + ? 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 { chain: match, - matches: maxMatches, + matches: maxMatches }; } @@ -57,10 +92,11 @@ export function routerPathToChain(path: string[], chains: RouteChain[]): RouteMa 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; + const matchedChain = matchesPath(path, chain); + if (matchedChain !== null) { + if (matchedChain.length > matches) { + matches = matchedChain.length; + match = matchedChain; } } } diff --git a/packages/core/src/components/router/utils/parser.ts b/packages/core/src/components/router/utils/parser.ts index 9ab783c143..33b31e3a4b 100644 --- a/packages/core/src/components/router/utils/parser.ts +++ b/packages/core/src/components/router/utils/parser.ts @@ -8,7 +8,7 @@ export function readRoutes(root: Element): RouteTree { .map(el => ({ path: parsePath(readProp(el, 'path')), id: readProp(el, 'component'), - props: readProp(el, 'props'), + params: el.params, children: readRoutes(el) })); } @@ -36,7 +36,7 @@ function flattenNode(chain: RouteChain, routes: RouteChain[], node: RouteNode) { s.push({ id: node.id, path: node.path, - props: node.props + params: node.params }); if (node.children.length === 0) {