feat(router): wildcard redirects

This commit is contained in:
Manu Mtz.-Almeida
2018-03-15 21:48:29 +01:00
parent bb3f406ffc
commit 2bdf4add5a
11 changed files with 230 additions and 50 deletions

View File

@ -2576,6 +2576,37 @@ declare global {
}
import {
RouteRedirect as IonRouteRedirect
} from './components/route-redirect/route-redirect';
declare global {
interface HTMLIonRouteRedirectElement extends IonRouteRedirect, HTMLStencilElement {
}
var HTMLIonRouteRedirectElement: {
prototype: HTMLIonRouteRedirectElement;
new (): HTMLIonRouteRedirectElement;
};
interface HTMLElementTagNameMap {
"ion-route-redirect": HTMLIonRouteRedirectElement;
}
interface ElementTagNameMap {
"ion-route-redirect": HTMLIonRouteRedirectElement;
}
namespace JSX {
interface IntrinsicElements {
"ion-route-redirect": JSXElements.IonRouteRedirectAttributes;
}
}
namespace JSXElements {
export interface IonRouteRedirectAttributes extends HTMLAttributes {
from?: string;
to?: string;
}
}
}
import {
Route as IonRoute
} from './components/route/route';
@ -2602,7 +2633,6 @@ declare global {
export interface IonRouteAttributes extends HTMLAttributes {
component?: string;
componentProps?: {[key: string]: any};
redirectTo?: string;
url?: string;
}
}

View File

@ -0,0 +1,40 @@
# ion-route
<!-- Auto Generated Below -->
## Properties
#### from
string
#### to
string
## Attributes
#### from
string
#### to
string
## Events
#### ionRouteRedirectChanged
----------------------------------------------
*Built with [StencilJS](https://stenciljs.com/)*

View File

@ -0,0 +1,23 @@
import { Component, Event, Prop } from '@stencil/core';
import { EventEmitter } from 'ionicons/dist/types/stencil.core';
@Component({
tag: 'ion-route-redirect'
})
export class RouteRedirect {
@Prop() from = '';
@Prop() to: string;
@Event() ionRouteRedirectChanged: EventEmitter;
componentDidLoad() {
this.ionRouteRedirectChanged.emit();
}
componentDidUnload() {
this.ionRouteRedirectChanged.emit();
}
componentDidUpdate() {
this.ionRouteRedirectChanged.emit();
}
}

View File

@ -17,11 +17,6 @@ string
#### redirectTo
string
#### url
string
@ -39,11 +34,6 @@ string
#### redirect-to
string
#### url
string

View File

@ -8,7 +8,6 @@ export class Route {
@Prop() url = '';
@Prop() component: string;
@Prop() redirectTo: string;
@Prop() componentProps: {[key: string]: any};
@Event() ionRouteDataChanged: EventEmitter;

View File

@ -33,17 +33,25 @@ export class Router {
componentDidLoad() {
this.init = true;
this.onRouteChanged();
this.onRedirectChanged();
this.onRoutesChanged();
}
@Listen('ionRouteRedirectChanged')
protected onRedirectChanged() {
if (!this.init) {
return;
}
this.redirects = readRedirects(this.el);
}
@Listen('ionRouteDataChanged')
protected onRouteChanged() {
protected onRoutesChanged() {
if (!this.init) {
return;
}
const tree = readRoutes(this.el);
this.routes = flattenRouterTree(tree);
this.redirects = readRedirects(this.el);
if (Build.isDev) {
printRoutes(this.routes);
@ -60,6 +68,7 @@ export class Router {
});
}
@Listen('window:popstate')
protected onPopState() {
if (window.history.state === null) {
@ -110,7 +119,7 @@ export class Router {
let redirectFrom: string[] = null;
if (redirect) {
this.setPath(redirect.to, true);
redirectFrom = redirect.path;
redirectFrom = redirect.from;
path = redirect.to;
}
const direction = window.history.state >= this.state ? 1 : -1;

View File

@ -1,5 +1,5 @@
import { RouteChain } from '../utils/interfaces';
import { RouterSegments, matchesIDs, matchesPath, mergeParams, routerPathToChain } from '../utils/matching';
import { RouterSegments, matchesIDs, matchesPath, matchesRedirect, mergeParams, routerPathToChain } from '../utils/matching';
import { parsePath } from '../utils/path';
const CHAIN_1: RouteChain = [
@ -250,6 +250,49 @@ describe('RouterSegments', () => {
});
});
describe('matchesRedirect', () => {
it('should match empty redirect', () => {
expect(matchesRedirect([''], {from: [''], to: ['']})).toBeTruthy();
expect(matchesRedirect([''], {from: ['*'], to: ['']})).toBeTruthy();
expect(matchesRedirect([''], {from: ['hola'], to: ['']})).toBeFalsy();
expect(matchesRedirect([''], {from: ['hola', '*'], to: ['']})).toBeFalsy();
});
it('should match simple segment redirect', () => {
expect(matchesRedirect(['workouts'], {from: ['workouts'], to: ['']})).toBeTruthy();
expect(matchesRedirect(['workouts'], {from: ['*'], to: ['']})).toBeTruthy();
expect(matchesRedirect(['workouts', 'hola'], {from: ['workouts', '*'], to: ['']})).toBeTruthy();
expect(matchesRedirect(['workouts', 'hola'], {from: ['workouts', 'hola'], to: ['']})).toBeTruthy();
expect(matchesRedirect(['workouts'], {from: ['workouts', '*'], to: ['']})).toBeFalsy();
expect(matchesRedirect(['workouts', 'hola'], {from: ['workouts'], to: ['']})).toBeFalsy();
expect(matchesRedirect(['workouts', 'hola'], {from: ['workouts', 'adios'], to: ['']})).toBeFalsy();
});
it('should match long route', () => {
expect(matchesRedirect(['workouts', 'path', 'to'], {from: ['*'], to: ['']})).toBeTruthy();
expect(matchesRedirect(['workouts', 'path', 'to'], {from: ['workouts', '*'], to: ['']})).toBeTruthy();
expect(matchesRedirect(['workouts', 'path', 'to'], {from: ['workouts', 'path', '*'], to: ['']})).toBeTruthy();
expect(matchesRedirect(['workouts', 'path', 'to'], {from: ['workouts', 'path', 'to'], to: ['']})).toBeTruthy();
expect(matchesRedirect(['workouts', 'path', 'to'], {from: ['login'], to: ['']})).toBeFalsy();
expect(matchesRedirect(['workouts', 'path', 'to'], {from: ['login', '*'], to: ['']})).toBeFalsy();
expect(matchesRedirect(['workouts', 'path', 'to'], {from: ['workouts'], to: ['']})).toBeFalsy();
expect(matchesRedirect(['workouts', 'path', 'to'], {from: ['workouts', 'path'], to: ['']})).toBeFalsy();
expect(matchesRedirect(['workouts', 'path', 'to'], {from: ['workouts', 'path', 'to', '*'], to: ['']})).toBeFalsy();
});
it('should not match undefined "to"', () => {
expect(matchesRedirect(['workouts', 'path', 'to'], {from: ['*'], to: undefined})).toBeFalsy();
expect(matchesRedirect(['workouts', 'path', 'to'], {from: ['workouts', '*'], to: undefined})).toBeFalsy();
expect(matchesRedirect(['workouts', 'path', 'to'], {from: ['workouts', 'path', '*'], to: undefined})).toBeFalsy();
expect(matchesRedirect(['workouts', 'path', 'to'], {from: ['workouts', 'path', 'to'], to: undefined})).toBeFalsy();
});
});
// describe('matchRoute', () => {
// it('should match simple route', () => {
// const path = ['path', 'to', 'component'];

View File

@ -1,6 +1,6 @@
import { mockElement } from '@stencil/core/testing';
import { flattenRouterTree, readRoutes } from '../utils/parser';
import { RouteTree } from '../utils/interfaces';
import { flattenRouterTree, readRedirects, readRoutes } from '../utils/parser';
import { RouteRedirect, RouteTree } from '../utils/interfaces';
describe('readRoutes', () => {
it('should read URL', () => {
@ -20,12 +20,12 @@ describe('readRoutes', () => {
r4.appendChild(r6);
const expected: RouteTree = [
{ path: [''], id: 'main-page', children: [], params: undefined },
{ path: ['one-page'], id: 'one-page', children: [], params: undefined },
{ path: ['secondpage'], id: 'second-page', params: undefined, children: [
{ path: ['5', 'hola'], id: '4', params: undefined, children: [
{ path: ['path', 'to', 'five'], id: '5', children: [], params: undefined },
{ path: ['path', 'to', 'five2'], id: '6', children: [], params: undefined }
{ path: [''], id: 'main-page', children: [], params: {router: root} },
{ path: ['one-page'], id: 'one-page', children: [], params: {router: root} },
{ path: ['secondpage'], id: 'second-page', params: {router: root}, children: [
{ path: ['5', 'hola'], id: '4', params: {router: root}, children: [
{ path: ['path', 'to', 'five'], id: '5', children: [], params: {router: root} },
{ path: ['path', 'to', 'five2'], id: '6', children: [], params: {router: root} }
] }
] }
];
@ -33,6 +33,33 @@ describe('readRoutes', () => {
});
});
describe('readRedirects', () => {
it('should read redirects', () => {
const root = mockElement('div');
const r1 = mockRedirectElement('/', undefined);
const r2 = mockRedirectElement(undefined, '/workout');
const r3 = mockRedirectElement('*', null);
const r4 = mockRedirectElement('/workout/*', '');
const r5 = mockRedirectElement('path/hey', '/path/to//login');
root.appendChild(r1);
root.appendChild(r2);
root.appendChild(r3);
root.appendChild(r4);
root.appendChild(r5);
const expected: RouteRedirect[] = [
{from: [''], to: undefined},
{from: [''], to: ['workout']},
{from: ['*'], to: undefined},
{from: ['workout', '*'], to: ['']},
{from: ['path', 'hey'], to: ['path', 'to', 'login']}
];
expect(readRedirects(root)).toEqual(expected);
});
});
describe('flattenRouterTree', () => {
it('should process routes', () => {
const entries: RouteTree = [
@ -55,12 +82,21 @@ describe('flattenRouterTree', () => {
});
});
export function mockRouteElement(path: string, component: string) {
const el = mockElement('ion-route');
el.setAttribute('url', path);
(el as any).component = component;
return el;
}
export function mockRedirectElement(from: string|undefined, to: string|undefined) {
const el = mockElement('ion-route-redirect');
if (from != null) {
el.setAttribute('from', from);
}
if (to != null) {
el.setAttribute('to', to);
}
return el;
}

View File

@ -13,8 +13,8 @@ export interface RouterEventDetail {
}
export interface RouteRedirect {
path: string[];
to: string[];
from: string[];
to: string[]|undefined;
}
export interface RouteWrite {

View File

@ -2,17 +2,25 @@ import { RouteChain, RouteID, RouteRedirect } from './interfaces';
export function matchesRedirect(input: string[], route: RouteRedirect): boolean {
const {path} = route;
if (path.length !== input.length) {
const {from, to} = route;
if (to === undefined) {
return false;
}
for (let i = 0; i < path.length; i++) {
if (path[i] !== input[i]) {
if (from.length > input.length) {
return false;
}
for (let i = 0; i < from.length; i++) {
const expected = from[i];
if (expected === '*') {
return true;
}
if (expected !== input[i]) {
return false;
}
}
return true;
return from.length === input.length;
}
export function routeRedirect(path: string[], routes: RouteRedirect[]): RouteRedirect|null {

View File

@ -1,30 +1,32 @@
import { RouteChain, RouteNode, RouteRedirect, RouteTree } from './interfaces';
import { parsePath } from './path';
import { mergeParams } from './matching';
export function readRedirects(root: Element): RouteRedirect[] {
return (Array.from(root.children) as HTMLIonRouteElement[])
.filter(el => el.tagName === 'ION-ROUTE' && el.redirectTo)
.map(el => {
if (el.component) {
throw new Error('Can\'t mix the component and redirectTo attribute in the same ion-route');
}
return {
path: parsePath(readProp(el, 'url')),
to: parsePath(readProp(el, 'redirectTo'))
};
});
return (Array.from(root.children) as HTMLIonRouteRedirectElement[])
.filter(el => el.tagName === 'ION-ROUTE-REDIRECT')
.map(el => {
const to = readProp(el, 'to');
return {
from: parsePath(readProp(el, 'from')),
to: to == null ? undefined : parsePath(to),
};
});
}
export function readRoutes(root: Element): RouteTree {
return (Array.from(root.children) as HTMLIonRouteElement[])
export function readRoutes(root: Element, node = root): RouteTree {
const commonParams = {
router: root
};
return (Array.from(node.children) as HTMLIonRouteElement[])
.filter(el => el.tagName === 'ION-ROUTE' && el.component)
.map(el => {
return {
path: parsePath(readProp(el, 'url')),
id: readProp(el, 'component').toLowerCase(),
params: el.componentProps,
children: readRoutes(el)
params: mergeParams(commonParams, el.componentProps),
children: readRoutes(root, el)
};
});
}