Skip to content

Commit fcfddbf

Browse files
committed
feat(router): add pathMatch property to replace terminal
1 parent dc64e90 commit fcfddbf

9 files changed

Lines changed: 56 additions & 39 deletions

File tree

modules/@angular/router/src/apply_redirects.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,8 @@ function match(segment: UrlSegment, route: Route, paths: UrlPathWithParams[]): {
149149
positionalParamSegments: {[k: string]: UrlPathWithParams}
150150
} {
151151
if (route.path === '') {
152-
if (route.terminal && (segment.hasChildren() || paths.length > 0)) {
152+
if ((route.terminal || route.pathMatch === 'full') &&
153+
(segment.hasChildren() || paths.length > 0)) {
153154
throw new NoMatch();
154155
} else {
155156
return {consumedPaths: [], lastChild: 0, positionalParamSegments: {}};
@@ -286,7 +287,8 @@ function containsEmptyPathRedirects(
286287

287288
function emptyPathRedirect(
288289
segment: UrlSegment, slicedPath: UrlPathWithParams[], r: Route): boolean {
289-
if ((segment.hasChildren() || slicedPath.length > 0) && r.terminal) return false;
290+
if ((segment.hasChildren() || slicedPath.length > 0) && (r.terminal || r.pathMatch === 'full'))
291+
return false;
290292
return r.path === '' && r.redirectTo !== undefined;
291293
}
292294

modules/@angular/router/src/config.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,12 @@ export type ResolveData = {
1818

1919
export interface Route {
2020
path?: string;
21+
22+
/**
23+
* @deprecated - use `pathMatch` instead
24+
*/
2125
terminal?: boolean;
26+
pathMatch?: 'full'|'prefix';
2227
component?: Type|string;
2328
outlet?: string;
2429
canActivate?: any[];
@@ -53,4 +58,11 @@ function validateNode(route: Route): void {
5358
throw new Error(
5459
`Invalid route configuration of route '${route.path}': path cannot start with a slash`);
5560
}
61+
if (route.path === '' && route.redirectTo !== undefined &&
62+
(route.terminal === undefined && route.pathMatch === undefined)) {
63+
const exp =
64+
`The default value of 'pathMatch' is 'prefix', but often the intent is to use 'full'.`;
65+
throw new Error(
66+
`Invalid route configuration of route '{path: "${route.path}", redirectTo: "${route.redirectTo}"}': please provide 'pathMatch'. ${exp}`);
67+
}
5668
}

modules/@angular/router/src/directives/router_outlet.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ export class RouterOutlet {
5353
const snapshot = activatedRoute._futureSnapshot;
5454
const component: any = <any>snapshot._routeConfig.component;
5555

56-
let factory;
56+
let factory: ComponentFactory<any>;
5757
try {
5858
factory = typeof component === 'string' ?
5959
snapshot._resolvedComponentFactory :

modules/@angular/router/src/recognize.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,8 @@ function processPathsWithParamsAgainstRoute(
151151

152152
function match(segment: UrlSegment, route: Route, paths: UrlPathWithParams[]) {
153153
if (route.path === '') {
154-
if (route.terminal && (segment.hasChildren() || paths.length > 0)) {
154+
if ((route.terminal || route.pathMatch === 'full') &&
155+
(segment.hasChildren() || paths.length > 0)) {
155156
throw new NoMatch();
156157
} else {
157158
return {consumedPaths: [], lastChild: 0, parameters: {}};
@@ -180,7 +181,8 @@ function match(segment: UrlSegment, route: Route, paths: UrlPathWithParams[]) {
180181
currentIndex++;
181182
}
182183

183-
if (route.terminal && (segment.hasChildren() || currentIndex < paths.length)) {
184+
if ((route.terminal || route.pathMatch === 'full') &&
185+
(segment.hasChildren() || currentIndex < paths.length)) {
184186
throw new NoMatch();
185187
}
186188

@@ -292,7 +294,8 @@ function containsEmptyPathMatches(
292294
}
293295

294296
function emptyPathMatch(segment: UrlSegment, slicedPath: UrlPathWithParams[], r: Route): boolean {
295-
if ((segment.hasChildren() || slicedPath.length > 0) && r.terminal) return false;
297+
if ((segment.hasChildren() || slicedPath.length > 0) && (r.terminal || r.pathMatch === 'full'))
298+
return false;
296299
return r.path === '' && r.redirectTo === undefined;
297300
}
298301

modules/@angular/router/test/apply_redirects.spec.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -160,15 +160,15 @@ describe('applyRedirects', () => {
160160
});
161161

162162
it('should redirect empty path route only when terminal', () => {
163-
const config = [
163+
const config: RouterConfig = [
164164
{
165165
path: 'a',
166166
component: ComponentA,
167167
children: [
168168
{path: 'b', component: ComponentB},
169169
]
170170
},
171-
{path: '', redirectTo: 'a', terminal: true}
171+
{path: '', redirectTo: 'a', pathMatch: 'full'}
172172
];
173173

174174
applyRedirects(tree('b'), config)
@@ -220,7 +220,7 @@ describe('applyRedirects', () => {
220220
children: [
221221
{path: 'b', component: ComponentB},
222222
{path: 'c', component: ComponentC, outlet: 'aux'},
223-
{path: '', terminal: true, redirectTo: 'c', outlet: 'aux'}
223+
{path: '', pathMatch: 'full', redirectTo: 'c', outlet: 'aux'}
224224
]
225225
}],
226226
'a/b', (t: UrlTree) => { compareTrees(t, tree('a/b')); });
@@ -287,7 +287,7 @@ describe('applyRedirects', () => {
287287
});
288288

289289
it('should not create a new child (terminal)', () => {
290-
const config = [{
290+
const config: RouterConfig = [{
291291
path: 'a',
292292
children: [
293293
{path: 'b', component: ComponentB, children: [{path: 'd', component: ComponentB}]},
@@ -297,7 +297,7 @@ describe('applyRedirects', () => {
297297
outlet: 'aux',
298298
children: [{path: 'e', component: ComponentC}]
299299
},
300-
{path: '', terminal: true, redirectTo: 'c', outlet: 'aux'}
300+
{path: '', pathMatch: 'full', redirectTo: 'c', outlet: 'aux'}
301301
]
302302
}];
303303

modules/@angular/router/test/config.spec.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import {validateConfig} from '../src/config';
33
describe('config', () => {
44
describe('validateConfig', () => {
55
it('should not throw when no errors', () => {
6-
validateConfig([{path: '', redirectTo: 'b'}, {path: 'b', component: ComponentA}]);
6+
validateConfig([{path: 'a', redirectTo: 'b'}, {path: 'b', component: ComponentA}]);
77
});
88

99
it('should throw when redirectTo and children are used together', () => {
@@ -35,9 +35,16 @@ describe('config', () => {
3535

3636
it('should throw when path starts with a slash', () => {
3737
expect(() => {
38-
validateConfig([<any>{path: '/a', componenta: '', redirectTo: 'b'}]);
38+
validateConfig([<any>{path: '/a', redirectTo: 'b'}]);
3939
}).toThrowError(`Invalid route configuration of route '/a': path cannot start with a slash`);
4040
});
41+
42+
it('should throw when emptyPath is used with redirectTo without explicitly providing matching',
43+
() => {
44+
expect(() => {
45+
validateConfig([<any>{path: '', redirectTo: 'b'}]);
46+
}).toThrowError(/Invalid route configuration of route '{path: "", redirectTo: "b"}'/);
47+
});
4148
});
4249
});
4350

modules/@angular/router/test/recognize.spec.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,8 @@ describe('recognize', () => {
215215

216216
it('should match when terminal', () => {
217217
checkRecognize(
218-
[{path: '', terminal: true, component: ComponentA}], '', (s: RouterStateSnapshot) => {
218+
[{path: '', pathMatch: 'full', component: ComponentA}], '',
219+
(s: RouterStateSnapshot) => {
219220
checkActivatedRoute(s.firstChild(s.root), '', {}, ComponentA);
220221
});
221222
});
@@ -224,7 +225,7 @@ describe('recognize', () => {
224225
recognize(
225226
RootComponent, [{
226227
path: '',
227-
terminal: true,
228+
pathMatch: 'full',
228229
component: ComponentA,
229230
children: [{path: 'b', component: ComponentB}]
230231
}],
@@ -290,7 +291,7 @@ describe('recognize', () => {
290291
component: ComponentA,
291292
children: [
292293
{path: 'b', component: ComponentB},
293-
{path: '', terminal: true, component: ComponentC, outlet: 'aux'}
294+
{path: '', pathMatch: 'full', component: ComponentC, outlet: 'aux'}
294295
]
295296
}],
296297
'a/b', (s: RouterStateSnapshot) => {
@@ -359,8 +360,8 @@ describe('recognize', () => {
359360
path: 'a',
360361
component: ComponentA,
361362
children: [
362-
{path: '', terminal: true, component: ComponentB},
363-
{path: '', terminal: true, component: ComponentC, outlet: 'aux'},
363+
{path: '', pathMatch: 'full', component: ComponentB},
364+
{path: '', pathMatch: 'full', component: ComponentC, outlet: 'aux'},
364365
]
365366
}],
366367
'a', (s: RouterStateSnapshot) => {

modules/@angular/router/test/router.spec.ts

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -37,17 +37,17 @@ describe('Integration', () => {
3737
];
3838
});
3939

40-
fit('should navigate with a provided config',
41-
fakeAsync(inject(
42-
[Router, TestComponentBuilder, Location],
43-
(router: Router, tcb: TestComponentBuilder, location: Location) => {
44-
const fixture = createRoot(tcb, router, RootCmp);
40+
it('should navigate with a provided config',
41+
fakeAsync(inject(
42+
[Router, TestComponentBuilder, Location],
43+
(router: Router, tcb: TestComponentBuilder, location: Location) => {
44+
const fixture = createRoot(tcb, router, RootCmp);
4545

46-
router.navigateByUrl('/simple');
47-
advance(fixture);
46+
router.navigateByUrl('/simple');
47+
advance(fixture);
4848

49-
expect(location.path()).toEqual('/simple');
50-
})));
49+
expect(location.path()).toEqual('/simple');
50+
})));
5151

5252

5353
it('should update location when navigating',
@@ -262,7 +262,7 @@ describe('Integration', () => {
262262
const fixture = createRoot(tcb, router, RootCmp);
263263

264264
router.resetConfig([
265-
{path: '', terminal: true, component: SimpleCmp},
265+
{path: '', pathMatch: 'full', component: SimpleCmp},
266266
{path: 'user/:name', component: UserCmp}
267267
]);
268268

@@ -830,7 +830,7 @@ describe('Integration', () => {
830830
path: 'team/:id',
831831
component: TeamCmp,
832832
children: [
833-
{path: '', terminal: true, component: SimpleCmp}, {
833+
{path: '', pathMatch: 'full', component: SimpleCmp}, {
834834
path: 'user/:name',
835835
component: UserCmp,
836836
canDeactivate: ['CanDeactivateUser']

tools/public_api_guard/router/index.d.ts

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -87,17 +87,9 @@ export declare type ResolveData = {
8787
};
8888

8989
export interface Route {
90-
canActivate?: any[];
91-
canDeactivate?: any[];
92-
children?: Route[];
93-
component?: Type | string;
94-
data?: Data;
95-
outlet?: string;
9690
path?: string;
97-
redirectTo?: string;
98-
resolve?: ResolveData;
99-
terminal?: boolean;
100-
}
91+
pathMatch?:
92+
/** @deprecated */ terminal?: boolean;
10193

10294
export declare class Router {
10395
events: Observable<Event>;

0 commit comments

Comments
 (0)