Skip to content

Commit

Permalink
feat(router): wildcard redirects
Browse files Browse the repository at this point in the history
  • Loading branch information
manucorporat committed Mar 15, 2018
1 parent bb3f406 commit 2bdf4ad
Show file tree
Hide file tree
Showing 11 changed files with 230 additions and 50 deletions.
32 changes: 31 additions & 1 deletion core/src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -2602,7 +2633,6 @@ declare global {
export interface IonRouteAttributes extends HTMLAttributes {
component?: string;
componentProps?: {[key: string]: any};
redirectTo?: string;
url?: string;
}
}
Expand Down
40 changes: 40 additions & 0 deletions core/src/components/route-redirect/readme.md
Original file line number Diff line number Diff line change
@@ -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/)*
23 changes: 23 additions & 0 deletions core/src/components/route-redirect/route-redirect.tsx
Original file line number Diff line number Diff line change
@@ -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();
}
}
10 changes: 0 additions & 10 deletions core/src/components/route/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,6 @@ string



#### redirectTo

string


#### url

string
Expand All @@ -39,11 +34,6 @@ string



#### redirect-to

string


#### url

string
Expand Down
1 change: 0 additions & 1 deletion core/src/components/route/route.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ export class Route {

@Prop() url = '';
@Prop() component: string;
@Prop() redirectTo: string;
@Prop() componentProps: {[key: string]: any};

@Event() ionRouteDataChanged: EventEmitter;
Expand Down
17 changes: 13 additions & 4 deletions core/src/components/router/router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -60,6 +68,7 @@ export class Router {
});
}


@Listen('window:popstate')
protected onPopState() {
if (window.history.state === null) {
Expand Down Expand Up @@ -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;
Expand Down
45 changes: 44 additions & 1 deletion core/src/components/router/test/matching.spec.tsx
Original file line number Diff line number Diff line change
@@ -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 = [
Expand Down Expand Up @@ -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'];
Expand Down
58 changes: 47 additions & 11 deletions core/src/components/router/test/parser.spec.tsx
Original file line number Diff line number Diff line change
@@ -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', () => {
Expand All @@ -20,19 +20,46 @@ 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} }
] }
] }
];
expect(readRoutes(root)).toEqual(expected);
});
});

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 = [
Expand All @@ -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;
}

4 changes: 2 additions & 2 deletions core/src/components/router/utils/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ export interface RouterEventDetail {
}

export interface RouteRedirect {
path: string[];
to: string[];
from: string[];
to: string[]|undefined;
}

export interface RouteWrite {
Expand Down
18 changes: 13 additions & 5 deletions core/src/components/router/utils/matching.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading

0 comments on commit 2bdf4ad

Please sign in to comment.