From fcfddbf79cfbdca45771bb31c0a2c1f55cff5801 Mon Sep 17 00:00:00 2001 From: vsavkin Date: Mon, 27 Jun 2016 20:10:36 -0700 Subject: [PATCH] feat(router): add pathMatch property to replace terminal --- .../@angular/router/src/apply_redirects.ts | 6 +++-- modules/@angular/router/src/config.ts | 12 ++++++++++ .../router/src/directives/router_outlet.ts | 2 +- modules/@angular/router/src/recognize.ts | 9 +++++--- .../router/test/apply_redirects.spec.ts | 10 ++++----- modules/@angular/router/test/config.spec.ts | 11 ++++++++-- .../@angular/router/test/recognize.spec.ts | 11 +++++----- modules/@angular/router/test/router.spec.ts | 22 +++++++++---------- tools/public_api_guard/router/index.d.ts | 12 ++-------- 9 files changed, 56 insertions(+), 39 deletions(-) diff --git a/modules/@angular/router/src/apply_redirects.ts b/modules/@angular/router/src/apply_redirects.ts index f1df9542a2..d0be8d2977 100644 --- a/modules/@angular/router/src/apply_redirects.ts +++ b/modules/@angular/router/src/apply_redirects.ts @@ -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; } diff --git a/modules/@angular/router/src/config.ts b/modules/@angular/router/src/config.ts index e415efee02..bf24129844 100644 --- a/modules/@angular/router/src/config.ts +++ b/modules/@angular/router/src/config.ts @@ -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}`); + } } \ No newline at end of file diff --git a/modules/@angular/router/src/directives/router_outlet.ts b/modules/@angular/router/src/directives/router_outlet.ts index 291750f903..8f52e964f4 100644 --- a/modules/@angular/router/src/directives/router_outlet.ts +++ b/modules/@angular/router/src/directives/router_outlet.ts @@ -53,7 +53,7 @@ export class RouterOutlet { const snapshot = activatedRoute._futureSnapshot; const component: any = snapshot._routeConfig.component; - let factory; + let factory: ComponentFactory; try { factory = typeof component === 'string' ? snapshot._resolvedComponentFactory : diff --git a/modules/@angular/router/src/recognize.ts b/modules/@angular/router/src/recognize.ts index bef0a9ca08..a3d1b2e38e 100644 --- a/modules/@angular/router/src/recognize.ts +++ b/modules/@angular/router/src/recognize.ts @@ -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; } diff --git a/modules/@angular/router/test/apply_redirects.spec.ts b/modules/@angular/router/test/apply_redirects.spec.ts index 59314345ee..f16ebc1c6d 100644 --- a/modules/@angular/router/test/apply_redirects.spec.ts +++ b/modules/@angular/router/test/apply_redirects.spec.ts @@ -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'} ] }]; diff --git a/modules/@angular/router/test/config.spec.ts b/modules/@angular/router/test/config.spec.ts index c694577e8a..7bd5d0d6eb 100644 --- a/modules/@angular/router/test/config.spec.ts +++ b/modules/@angular/router/test/config.spec.ts @@ -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([{path: '/a', componenta: '', redirectTo: 'b'}]); + validateConfig([{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([{path: '', redirectTo: 'b'}]); + }).toThrowError(/Invalid route configuration of route '{path: "", redirectTo: "b"}'/); + }); }); }); diff --git a/modules/@angular/router/test/recognize.spec.ts b/modules/@angular/router/test/recognize.spec.ts index d10f11536a..95e3a79448 100644 --- a/modules/@angular/router/test/recognize.spec.ts +++ b/modules/@angular/router/test/recognize.spec.ts @@ -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) => { diff --git a/modules/@angular/router/test/router.spec.ts b/modules/@angular/router/test/router.spec.ts index 2092c33588..e38ad02aba 100644 --- a/modules/@angular/router/test/router.spec.ts +++ b/modules/@angular/router/test/router.spec.ts @@ -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'] diff --git a/tools/public_api_guard/router/index.d.ts b/tools/public_api_guard/router/index.d.ts index 89dffb0969..acc823d2aa 100644 --- a/tools/public_api_guard/router/index.d.ts +++ b/tools/public_api_guard/router/index.d.ts @@ -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;