feat(router): add pathMatch property to replace terminal
This commit is contained in:
parent
dc64e90ab9
commit
fcfddbf79c
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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}`);
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -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 :
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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'}
|
||||||
]
|
]
|
||||||
}];
|
}];
|
||||||
|
|
||||||
|
|
|
@ -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"}'/);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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) => {
|
||||||
|
|
|
@ -37,17 +37,17 @@ 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) => {
|
||||||
const fixture = createRoot(tcb, router, RootCmp);
|
const fixture = createRoot(tcb, router, RootCmp);
|
||||||
|
|
||||||
router.navigateByUrl('/simple');
|
router.navigateByUrl('/simple');
|
||||||
advance(fixture);
|
advance(fixture);
|
||||||
|
|
||||||
expect(location.path()).toEqual('/simple');
|
expect(location.path()).toEqual('/simple');
|
||||||
})));
|
})));
|
||||||
|
|
||||||
|
|
||||||
it('should update location when navigating',
|
it('should update location when navigating',
|
||||||
|
@ -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']
|
||||||
|
|
|
@ -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>;
|
||||||
|
|
Loading…
Reference in New Issue