feat(router): add pathMatch property to replace terminal

This commit is contained in:
vsavkin 2016-06-27 20:10:36 -07:00
parent dc64e90ab9
commit fcfddbf79c
9 changed files with 56 additions and 39 deletions

View File

@ -149,7 +149,8 @@ function match(segment: UrlSegment, route: Route, paths: UrlPathWithParams[]): {
positionalParamSegments: {[k: string]: UrlPathWithParams} positionalParamSegments: {[k: string]: UrlPathWithParams}
} { } {
if (route.path === '') { if (route.path === '') {
if (route.terminal && (segment.hasChildren() || paths.length > 0)) { if ((route.terminal || route.pathMatch === 'full') &&
(segment.hasChildren() || paths.length > 0)) {
throw new NoMatch(); throw new NoMatch();
} else { } else {
return {consumedPaths: [], lastChild: 0, positionalParamSegments: {}}; return {consumedPaths: [], lastChild: 0, positionalParamSegments: {}};
@ -286,7 +287,8 @@ function containsEmptyPathRedirects(
function emptyPathRedirect( function emptyPathRedirect(
segment: UrlSegment, slicedPath: UrlPathWithParams[], r: Route): boolean { segment: UrlSegment, slicedPath: UrlPathWithParams[], r: Route): boolean {
if ((segment.hasChildren() || slicedPath.length > 0) && r.terminal) return false; if ((segment.hasChildren() || slicedPath.length > 0) && (r.terminal || r.pathMatch === 'full'))
return false;
return r.path === '' && r.redirectTo !== undefined; return r.path === '' && r.redirectTo !== undefined;
} }

View File

@ -18,7 +18,12 @@ export type ResolveData = {
export interface Route { export interface Route {
path?: string; path?: string;
/**
* @deprecated - use `pathMatch` instead
*/
terminal?: boolean; terminal?: boolean;
pathMatch?: 'full'|'prefix';
component?: Type|string; component?: Type|string;
outlet?: string; outlet?: string;
canActivate?: any[]; canActivate?: any[];
@ -53,4 +58,11 @@ function validateNode(route: Route): void {
throw new Error( throw new Error(
`Invalid route configuration of route '${route.path}': path cannot start with a slash`); `Invalid route configuration of route '${route.path}': path cannot start with a slash`);
} }
if (route.path === '' && route.redirectTo !== undefined &&
(route.terminal === undefined && route.pathMatch === undefined)) {
const exp =
`The default value of 'pathMatch' is 'prefix', but often the intent is to use 'full'.`;
throw new Error(
`Invalid route configuration of route '{path: "${route.path}", redirectTo: "${route.redirectTo}"}': please provide 'pathMatch'. ${exp}`);
}
} }

View File

@ -53,7 +53,7 @@ export class RouterOutlet {
const snapshot = activatedRoute._futureSnapshot; const snapshot = activatedRoute._futureSnapshot;
const component: any = <any>snapshot._routeConfig.component; const component: any = <any>snapshot._routeConfig.component;
let factory; let factory: ComponentFactory<any>;
try { try {
factory = typeof component === 'string' ? factory = typeof component === 'string' ?
snapshot._resolvedComponentFactory : snapshot._resolvedComponentFactory :

View File

@ -151,7 +151,8 @@ function processPathsWithParamsAgainstRoute(
function match(segment: UrlSegment, route: Route, paths: UrlPathWithParams[]) { function match(segment: UrlSegment, route: Route, paths: UrlPathWithParams[]) {
if (route.path === '') { if (route.path === '') {
if (route.terminal && (segment.hasChildren() || paths.length > 0)) { if ((route.terminal || route.pathMatch === 'full') &&
(segment.hasChildren() || paths.length > 0)) {
throw new NoMatch(); throw new NoMatch();
} else { } else {
return {consumedPaths: [], lastChild: 0, parameters: {}}; return {consumedPaths: [], lastChild: 0, parameters: {}};
@ -180,7 +181,8 @@ function match(segment: UrlSegment, route: Route, paths: UrlPathWithParams[]) {
currentIndex++; currentIndex++;
} }
if (route.terminal && (segment.hasChildren() || currentIndex < paths.length)) { if ((route.terminal || route.pathMatch === 'full') &&
(segment.hasChildren() || currentIndex < paths.length)) {
throw new NoMatch(); throw new NoMatch();
} }
@ -292,7 +294,8 @@ function containsEmptyPathMatches(
} }
function emptyPathMatch(segment: UrlSegment, slicedPath: UrlPathWithParams[], r: Route): boolean { function emptyPathMatch(segment: UrlSegment, slicedPath: UrlPathWithParams[], r: Route): boolean {
if ((segment.hasChildren() || slicedPath.length > 0) && r.terminal) return false; if ((segment.hasChildren() || slicedPath.length > 0) && (r.terminal || r.pathMatch === 'full'))
return false;
return r.path === '' && r.redirectTo === undefined; return r.path === '' && r.redirectTo === undefined;
} }

View File

@ -160,7 +160,7 @@ describe('applyRedirects', () => {
}); });
it('should redirect empty path route only when terminal', () => { it('should redirect empty path route only when terminal', () => {
const config = [ const config: RouterConfig = [
{ {
path: 'a', path: 'a',
component: ComponentA, component: ComponentA,
@ -168,7 +168,7 @@ describe('applyRedirects', () => {
{path: 'b', component: ComponentB}, {path: 'b', component: ComponentB},
] ]
}, },
{path: '', redirectTo: 'a', terminal: true} {path: '', redirectTo: 'a', pathMatch: 'full'}
]; ];
applyRedirects(tree('b'), config) applyRedirects(tree('b'), config)
@ -220,7 +220,7 @@ describe('applyRedirects', () => {
children: [ children: [
{path: 'b', component: ComponentB}, {path: 'b', component: ComponentB},
{path: 'c', component: ComponentC, outlet: 'aux'}, {path: 'c', component: ComponentC, outlet: 'aux'},
{path: '', terminal: true, redirectTo: 'c', outlet: 'aux'} {path: '', pathMatch: 'full', redirectTo: 'c', outlet: 'aux'}
] ]
}], }],
'a/b', (t: UrlTree) => { compareTrees(t, tree('a/b')); }); 'a/b', (t: UrlTree) => { compareTrees(t, tree('a/b')); });
@ -287,7 +287,7 @@ describe('applyRedirects', () => {
}); });
it('should not create a new child (terminal)', () => { it('should not create a new child (terminal)', () => {
const config = [{ const config: RouterConfig = [{
path: 'a', path: 'a',
children: [ children: [
{path: 'b', component: ComponentB, children: [{path: 'd', component: ComponentB}]}, {path: 'b', component: ComponentB, children: [{path: 'd', component: ComponentB}]},
@ -297,7 +297,7 @@ describe('applyRedirects', () => {
outlet: 'aux', outlet: 'aux',
children: [{path: 'e', component: ComponentC}] children: [{path: 'e', component: ComponentC}]
}, },
{path: '', terminal: true, redirectTo: 'c', outlet: 'aux'} {path: '', pathMatch: 'full', redirectTo: 'c', outlet: 'aux'}
] ]
}]; }];

View File

@ -3,7 +3,7 @@ import {validateConfig} from '../src/config';
describe('config', () => { describe('config', () => {
describe('validateConfig', () => { describe('validateConfig', () => {
it('should not throw when no errors', () => { it('should not throw when no errors', () => {
validateConfig([{path: '', redirectTo: 'b'}, {path: 'b', component: ComponentA}]); validateConfig([{path: 'a', redirectTo: 'b'}, {path: 'b', component: ComponentA}]);
}); });
it('should throw when redirectTo and children are used together', () => { it('should throw when redirectTo and children are used together', () => {
@ -35,9 +35,16 @@ describe('config', () => {
it('should throw when path starts with a slash', () => { it('should throw when path starts with a slash', () => {
expect(() => { expect(() => {
validateConfig([<any>{path: '/a', componenta: '', redirectTo: 'b'}]); validateConfig([<any>{path: '/a', redirectTo: 'b'}]);
}).toThrowError(`Invalid route configuration of route '/a': path cannot start with a slash`); }).toThrowError(`Invalid route configuration of route '/a': path cannot start with a slash`);
}); });
it('should throw when emptyPath is used with redirectTo without explicitly providing matching',
() => {
expect(() => {
validateConfig([<any>{path: '', redirectTo: 'b'}]);
}).toThrowError(/Invalid route configuration of route '{path: "", redirectTo: "b"}'/);
});
}); });
}); });

View File

@ -215,7 +215,8 @@ describe('recognize', () => {
it('should match when terminal', () => { it('should match when terminal', () => {
checkRecognize( checkRecognize(
[{path: '', terminal: true, component: ComponentA}], '', (s: RouterStateSnapshot) => { [{path: '', pathMatch: 'full', component: ComponentA}], '',
(s: RouterStateSnapshot) => {
checkActivatedRoute(s.firstChild(s.root), '', {}, ComponentA); checkActivatedRoute(s.firstChild(s.root), '', {}, ComponentA);
}); });
}); });
@ -224,7 +225,7 @@ describe('recognize', () => {
recognize( recognize(
RootComponent, [{ RootComponent, [{
path: '', path: '',
terminal: true, pathMatch: 'full',
component: ComponentA, component: ComponentA,
children: [{path: 'b', component: ComponentB}] children: [{path: 'b', component: ComponentB}]
}], }],
@ -290,7 +291,7 @@ describe('recognize', () => {
component: ComponentA, component: ComponentA,
children: [ children: [
{path: 'b', component: ComponentB}, {path: 'b', component: ComponentB},
{path: '', terminal: true, component: ComponentC, outlet: 'aux'} {path: '', pathMatch: 'full', component: ComponentC, outlet: 'aux'}
] ]
}], }],
'a/b', (s: RouterStateSnapshot) => { 'a/b', (s: RouterStateSnapshot) => {
@ -359,8 +360,8 @@ describe('recognize', () => {
path: 'a', path: 'a',
component: ComponentA, component: ComponentA,
children: [ children: [
{path: '', terminal: true, component: ComponentB}, {path: '', pathMatch: 'full', component: ComponentB},
{path: '', terminal: true, component: ComponentC, outlet: 'aux'}, {path: '', pathMatch: 'full', component: ComponentC, outlet: 'aux'},
] ]
}], }],
'a', (s: RouterStateSnapshot) => { 'a', (s: RouterStateSnapshot) => {

View File

@ -37,7 +37,7 @@ describe('Integration', () => {
]; ];
}); });
fit('should navigate with a provided config', it('should navigate with a provided config',
fakeAsync(inject( fakeAsync(inject(
[Router, TestComponentBuilder, Location], [Router, TestComponentBuilder, Location],
(router: Router, tcb: TestComponentBuilder, location: Location) => { (router: Router, tcb: TestComponentBuilder, location: Location) => {
@ -262,7 +262,7 @@ describe('Integration', () => {
const fixture = createRoot(tcb, router, RootCmp); const fixture = createRoot(tcb, router, RootCmp);
router.resetConfig([ router.resetConfig([
{path: '', terminal: true, component: SimpleCmp}, {path: '', pathMatch: 'full', component: SimpleCmp},
{path: 'user/:name', component: UserCmp} {path: 'user/:name', component: UserCmp}
]); ]);
@ -830,7 +830,7 @@ describe('Integration', () => {
path: 'team/:id', path: 'team/:id',
component: TeamCmp, component: TeamCmp,
children: [ children: [
{path: '', terminal: true, component: SimpleCmp}, { {path: '', pathMatch: 'full', component: SimpleCmp}, {
path: 'user/:name', path: 'user/:name',
component: UserCmp, component: UserCmp,
canDeactivate: ['CanDeactivateUser'] canDeactivate: ['CanDeactivateUser']

View File

@ -87,17 +87,9 @@ export declare type ResolveData = {
}; };
export interface Route { export interface Route {
canActivate?: any[];
canDeactivate?: any[];
children?: Route[];
component?: Type | string;
data?: Data;
outlet?: string;
path?: string; path?: string;
redirectTo?: string; pathMatch?:
resolve?: ResolveData; /** @deprecated */ terminal?: boolean;
terminal?: boolean;
}
export declare class Router { export declare class Router {
events: Observable<Event>; events: Observable<Event>;