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}
} {
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();
} else {
return {consumedPaths: [], lastChild: 0, positionalParamSegments: {}};
@ -286,7 +287,8 @@ function containsEmptyPathRedirects(
function emptyPathRedirect(
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;
}

View File

@ -18,7 +18,12 @@ export type ResolveData = {
export interface Route {
path?: string;
/**
* @deprecated - use `pathMatch` instead
*/
terminal?: boolean;
pathMatch?: 'full'|'prefix';
component?: Type|string;
outlet?: string;
canActivate?: any[];
@ -53,4 +58,11 @@ function validateNode(route: Route): void {
throw new Error(
`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 component: any = <any>snapshot._routeConfig.component;
let factory;
let factory: ComponentFactory<any>;
try {
factory = typeof component === 'string' ?
snapshot._resolvedComponentFactory :

View File

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

View File

@ -160,7 +160,7 @@ describe('applyRedirects', () => {
});
it('should redirect empty path route only when terminal', () => {
const config = [
const config: RouterConfig = [
{
path: 'a',
component: ComponentA,
@ -168,7 +168,7 @@ describe('applyRedirects', () => {
{path: 'b', component: ComponentB},
]
},
{path: '', redirectTo: 'a', terminal: true}
{path: '', redirectTo: 'a', pathMatch: 'full'}
];
applyRedirects(tree('b'), config)
@ -220,7 +220,7 @@ describe('applyRedirects', () => {
children: [
{path: 'b', component: ComponentB},
{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')); });
@ -287,7 +287,7 @@ describe('applyRedirects', () => {
});
it('should not create a new child (terminal)', () => {
const config = [{
const config: RouterConfig = [{
path: 'a',
children: [
{path: 'b', component: ComponentB, children: [{path: 'd', component: ComponentB}]},
@ -297,7 +297,7 @@ describe('applyRedirects', () => {
outlet: 'aux',
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('validateConfig', () => {
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', () => {
@ -35,9 +35,16 @@ describe('config', () => {
it('should throw when path starts with a slash', () => {
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`);
});
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', () => {
checkRecognize(
[{path: '', terminal: true, component: ComponentA}], '', (s: RouterStateSnapshot) => {
[{path: '', pathMatch: 'full', component: ComponentA}], '',
(s: RouterStateSnapshot) => {
checkActivatedRoute(s.firstChild(s.root), '', {}, ComponentA);
});
});
@ -224,7 +225,7 @@ describe('recognize', () => {
recognize(
RootComponent, [{
path: '',
terminal: true,
pathMatch: 'full',
component: ComponentA,
children: [{path: 'b', component: ComponentB}]
}],
@ -290,7 +291,7 @@ describe('recognize', () => {
component: ComponentA,
children: [
{path: 'b', component: ComponentB},
{path: '', terminal: true, component: ComponentC, outlet: 'aux'}
{path: '', pathMatch: 'full', component: ComponentC, outlet: 'aux'}
]
}],
'a/b', (s: RouterStateSnapshot) => {
@ -359,8 +360,8 @@ describe('recognize', () => {
path: 'a',
component: ComponentA,
children: [
{path: '', terminal: true, component: ComponentB},
{path: '', terminal: true, component: ComponentC, outlet: 'aux'},
{path: '', pathMatch: 'full', component: ComponentB},
{path: '', pathMatch: 'full', component: ComponentC, outlet: 'aux'},
]
}],
'a', (s: RouterStateSnapshot) => {

View File

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

View File

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