mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-11-10 00:27:41 +08:00
feat(router): wildcard redirects
This commit is contained in:
32
core/src/components.d.ts
vendored
32
core/src/components.d.ts
vendored
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
40
core/src/components/route-redirect/readme.md
Normal file
40
core/src/components/route-redirect/readme.md
Normal 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/)*
|
||||
23
core/src/components/route-redirect/route-redirect.tsx
Normal file
23
core/src/components/route-redirect/route-redirect.tsx
Normal 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();
|
||||
}
|
||||
}
|
||||
@ -17,11 +17,6 @@ string
|
||||
|
||||
|
||||
|
||||
#### redirectTo
|
||||
|
||||
string
|
||||
|
||||
|
||||
#### url
|
||||
|
||||
string
|
||||
@ -39,11 +34,6 @@ string
|
||||
|
||||
|
||||
|
||||
#### redirect-to
|
||||
|
||||
string
|
||||
|
||||
|
||||
#### url
|
||||
|
||||
string
|
||||
|
||||
@ -8,7 +8,6 @@ export class Route {
|
||||
|
||||
@Prop() url = '';
|
||||
@Prop() component: string;
|
||||
@Prop() redirectTo: string;
|
||||
@Prop() componentProps: {[key: string]: any};
|
||||
|
||||
@Event() ionRouteDataChanged: EventEmitter;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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'];
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -13,8 +13,8 @@ export interface RouterEventDetail {
|
||||
}
|
||||
|
||||
export interface RouteRedirect {
|
||||
path: string[];
|
||||
to: string[];
|
||||
from: string[];
|
||||
to: string[]|undefined;
|
||||
}
|
||||
|
||||
export interface RouteWrite {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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)
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user