Skip to content

Commit 5efea2f

Browse files
voithosalxhub
authored andcommitted
feat(router): add "paramsInheritanceStrategy" router configuration option
Previously, the router would merge path and matrix params, as well as data/resolve, with special rules (only merging down when the route has an empty path, or is component-less). This change adds an extra option "paramsInheritanceStrategy" which, when set to 'always', makes child routes unconditionally inherit params from parent routes. Closes angular#20572.
1 parent 5f23a12 commit 5efea2f

File tree

10 files changed

+204
-48
lines changed

10 files changed

+204
-48
lines changed

packages/router/src/pre_activation.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import {reduce} from 'rxjs/operator/reduce';
2121
import {LoadedRouterConfig, ResolveData, RunGuardsAndResolvers} from './config';
2222
import {ActivationStart, ChildActivationStart, Event} from './events';
2323
import {ChildrenOutletContexts, OutletContext} from './router_outlet_context';
24-
import {ActivatedRouteSnapshot, RouterStateSnapshot, equalParamsAndUrlSegments, inheritedParamsDataResolve} from './router_state';
24+
import {ActivatedRouteSnapshot, ParamsInheritanceStrategy, RouterStateSnapshot, equalParamsAndUrlSegments, inheritedParamsDataResolve} from './router_state';
2525
import {andObservables, forEach, shallowEqual, wrapIntoObservable} from './utils/collection';
2626
import {TreeNode, nodeChildrenAsMap} from './utils/tree';
2727

@@ -63,11 +63,11 @@ export class PreActivation {
6363
(canDeactivate: boolean) => canDeactivate ? this.runCanActivateChecks() : of (false));
6464
}
6565

66-
resolveData(): Observable<any> {
66+
resolveData(paramsInheritanceStrategy: ParamsInheritanceStrategy): Observable<any> {
6767
if (!this.isActivating()) return of (null);
6868
const checks$ = from(this.canActivateChecks);
69-
const runningChecks$ =
70-
concatMap.call(checks$, (check: CanActivate) => this.runResolve(check.route));
69+
const runningChecks$ = concatMap.call(
70+
checks$, (check: CanActivate) => this.runResolve(check.route, paramsInheritanceStrategy));
7171
return reduce.call(runningChecks$, (_: any, __: any) => _);
7272
}
7373

@@ -306,11 +306,14 @@ export class PreActivation {
306306
return every.call(canDeactivate$, (result: any) => result === true);
307307
}
308308

309-
private runResolve(future: ActivatedRouteSnapshot): Observable<any> {
309+
private runResolve(
310+
future: ActivatedRouteSnapshot,
311+
paramsInheritanceStrategy: ParamsInheritanceStrategy): Observable<any> {
310312
const resolve = future._resolve;
311313
return map.call(this.resolveNode(resolve, future), (resolvedData: any): any => {
312314
future._resolvedData = resolvedData;
313-
future.data = {...future.data, ...inheritedParamsDataResolve(future).resolve};
315+
future.data = {...future.data,
316+
...inheritedParamsDataResolve(future, paramsInheritanceStrategy).resolve};
314317
return null;
315318
});
316319
}

packages/router/src/recognize.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {Observer} from 'rxjs/Observer';
1212
import {of } from 'rxjs/observable/of';
1313

1414
import {Data, ResolveData, Route, Routes} from './config';
15-
import {ActivatedRouteSnapshot, RouterStateSnapshot, inheritedParamsDataResolve} from './router_state';
15+
import {ActivatedRouteSnapshot, ParamsInheritanceStrategy, RouterStateSnapshot, inheritedParamsDataResolve} from './router_state';
1616
import {PRIMARY_OUTLET, defaultUrlMatcher} from './shared';
1717
import {UrlSegment, UrlSegmentGroup, UrlTree, mapChildrenIntoArray} from './url_tree';
1818
import {forEach, last} from './utils/collection';
@@ -21,15 +21,17 @@ import {TreeNode} from './utils/tree';
2121
class NoMatch {}
2222

2323
export function recognize(
24-
rootComponentType: Type<any>| null, config: Routes, urlTree: UrlTree,
25-
url: string): Observable<RouterStateSnapshot> {
26-
return new Recognizer(rootComponentType, config, urlTree, url).recognize();
24+
rootComponentType: Type<any>| null, config: Routes, urlTree: UrlTree, url: string,
25+
paramsInheritanceStrategy: ParamsInheritanceStrategy =
26+
'emptyOnly'): Observable<RouterStateSnapshot> {
27+
return new Recognizer(rootComponentType, config, urlTree, url, paramsInheritanceStrategy)
28+
.recognize();
2729
}
2830

2931
class Recognizer {
3032
constructor(
3133
private rootComponentType: Type<any>|null, private config: Routes, private urlTree: UrlTree,
32-
private url: string) {}
34+
private url: string, private paramsInheritanceStrategy: ParamsInheritanceStrategy) {}
3335

3436
recognize(): Observable<RouterStateSnapshot> {
3537
try {
@@ -55,7 +57,7 @@ class Recognizer {
5557
inheritParamsAndData(routeNode: TreeNode<ActivatedRouteSnapshot>): void {
5658
const route = routeNode.value;
5759

58-
const i = inheritedParamsDataResolve(route);
60+
const i = inheritedParamsDataResolve(route, this.paramsInheritanceStrategy);
5961
route.params = Object.freeze(i.params);
6062
route.data = Object.freeze(i.data);
6163

packages/router/src/router.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import {recognize} from './recognize';
2727
import {DefaultRouteReuseStrategy, DetachedRouteHandleInternal, RouteReuseStrategy} from './route_reuse_strategy';
2828
import {RouterConfigLoader} from './router_config_loader';
2929
import {ChildrenOutletContexts} from './router_outlet_context';
30-
import {ActivatedRoute, ActivatedRouteSnapshot, RouterState, RouterStateSnapshot, advanceActivatedRoute, createEmptyState} from './router_state';
30+
import {ActivatedRoute, ActivatedRouteSnapshot, RouterState, RouterStateSnapshot, advanceActivatedRoute, createEmptyState, inheritedParamsDataResolve} from './router_state';
3131
import {Params, isNavigationCancelingError} from './shared';
3232
import {DefaultUrlHandlingStrategy, UrlHandlingStrategy} from './url_handling_strategy';
3333
import {UrlSerializer, UrlTree, containsTree, createEmptyUrlTree} from './url_tree';
@@ -249,6 +249,16 @@ export class Router {
249249
*/
250250
onSameUrlNavigation: 'reload'|'ignore' = 'ignore';
251251

252+
/**
253+
* Defines how the router merges params, data and resolved data from parent to child
254+
* routes. Available options are:
255+
*
256+
* - `'emptyOnly'`, the default, only inherits parent params for path-less or component-less
257+
* routes.
258+
* - `'always'`, enables unconditional inheritance of parent params.
259+
*/
260+
paramsInheritanceStrategy: 'emptyOnly'|'always' = 'emptyOnly';
261+
252262
/**
253263
* Creates the router service.
254264
*/
@@ -611,7 +621,8 @@ export class Router {
611621
urlAndSnapshot$ = mergeMap.call(redirectsApplied$, (appliedUrl: UrlTree) => {
612622
return map.call(
613623
recognize(
614-
this.rootComponentType, this.config, appliedUrl, this.serializeUrl(appliedUrl)),
624+
this.rootComponentType, this.config, appliedUrl, this.serializeUrl(appliedUrl),
625+
this.paramsInheritanceStrategy),
615626
(snapshot: any) => {
616627

617628
(this.events as Subject<Event>)
@@ -667,7 +678,7 @@ export class Router {
667678
if (p.shouldActivate && preActivation.isActivating()) {
668679
this.triggerEvent(
669680
new ResolveStart(id, this.serializeUrl(url), p.appliedUrl, p.snapshot));
670-
return map.call(preActivation.resolveData(), () => {
681+
return map.call(preActivation.resolveData(this.paramsInheritanceStrategy), () => {
671682
this.triggerEvent(
672683
new ResolveEnd(id, this.serializeUrl(url), p.appliedUrl, p.snapshot));
673684
return p;

packages/router/src/router_module.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,16 @@ export interface ExtraOptions {
278278
* current URL. Default is 'ignore'.
279279
*/
280280
onSameUrlNavigation?: 'reload'|'ignore';
281+
282+
/**
283+
* Defines how the router merges params, data and resolved data from parent to child
284+
* routes. Available options are:
285+
*
286+
* - `'emptyOnly'`, the default, only inherits parent params for path-less or component-less
287+
* routes.
288+
* - `'always'`, enables unconditional inheritance of parent params.
289+
*/
290+
paramsInheritanceStrategy?: 'emptyOnly'|'always';
281291
}
282292

283293
export function setupRouter(
@@ -314,6 +324,10 @@ export function setupRouter(
314324
router.onSameUrlNavigation = opts.onSameUrlNavigation;
315325
}
316326

327+
if (opts.paramsInheritanceStrategy) {
328+
router.paramsInheritanceStrategy = opts.paramsInheritanceStrategy;
329+
}
330+
317331
return router;
318332
}
319333

packages/router/src/router_state.ts

Lines changed: 39 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {shallowEqual, shallowEqualArrays} from './utils/collection';
1818
import {Tree, TreeNode} from './utils/tree';
1919

2020

21+
2122
/**
2223
* @whatItDoes Represents the state of the router.
2324
*
@@ -174,36 +175,53 @@ export class ActivatedRoute {
174175
}
175176
}
176177

178+
/** @internal */
179+
export type ParamsInheritanceStrategy = 'emptyOnly' | 'always';
180+
177181
/** @internal */
178182
export type Inherited = {
179183
params: Params,
180184
data: Data,
181185
resolve: Data,
182186
};
183187

184-
/** @internal */
185-
export function inheritedParamsDataResolve(route: ActivatedRouteSnapshot): Inherited {
186-
const pathToRoot = route.pathFromRoot;
187-
188-
let inhertingStartingFrom = pathToRoot.length - 1;
189-
190-
while (inhertingStartingFrom >= 1) {
191-
const current = pathToRoot[inhertingStartingFrom];
192-
const parent = pathToRoot[inhertingStartingFrom - 1];
193-
// current route is an empty path => inherits its parent's params and data
194-
if (current.routeConfig && current.routeConfig.path === '') {
195-
inhertingStartingFrom--;
196-
197-
// parent is componentless => current route should inherit its params and data
198-
} else if (!parent.component) {
199-
inhertingStartingFrom--;
200-
201-
} else {
202-
break;
188+
/**
189+
* Returns the inherited params, data, and resolve for a given route.
190+
* By default, this only inherits values up to the nearest path-less or component-less route.
191+
* @internal
192+
*/
193+
export function inheritedParamsDataResolve(
194+
route: ActivatedRouteSnapshot,
195+
paramsInheritanceStrategy: ParamsInheritanceStrategy = 'emptyOnly'): Inherited {
196+
const pathFromRoot = route.pathFromRoot;
197+
198+
let inheritingStartingFrom = 0;
199+
if (paramsInheritanceStrategy !== 'always') {
200+
inheritingStartingFrom = pathFromRoot.length - 1;
201+
202+
while (inheritingStartingFrom >= 1) {
203+
const current = pathFromRoot[inheritingStartingFrom];
204+
const parent = pathFromRoot[inheritingStartingFrom - 1];
205+
// current route is an empty path => inherits its parent's params and data
206+
if (current.routeConfig && current.routeConfig.path === '') {
207+
inheritingStartingFrom--;
208+
209+
// parent is componentless => current route should inherit its params and data
210+
} else if (!parent.component) {
211+
inheritingStartingFrom--;
212+
213+
} else {
214+
break;
215+
}
203216
}
204217
}
205218

206-
return pathToRoot.slice(inhertingStartingFrom).reduce((res, curr) => {
219+
return flattenInherited(pathFromRoot.slice(inheritingStartingFrom));
220+
}
221+
222+
/** @internal */
223+
function flattenInherited(pathFromRoot: ActivatedRouteSnapshot[]): Inherited {
224+
return pathFromRoot.reduce((res, curr) => {
207225
const params = {...res.params, ...curr.params};
208226
const data = {...res.data, ...curr.data};
209227
const resolve = {...res.resolve, ...curr._resolvedData};
@@ -352,7 +370,7 @@ function setRouterState<U, T extends{_routerState: U}>(state: U, node: TreeNode<
352370
}
353371

354372
function serializeNode(node: TreeNode<ActivatedRouteSnapshot>): string {
355-
const c = node.children.length > 0 ? ` { ${node.children.map(serializeNode).join(", ")} } ` : '';
373+
const c = node.children.length > 0 ? ` { ${node.children.map(serializeNode).join(', ')} } ` : '';
356374
return `${node.value}${c}`;
357375
}
358376

packages/router/test/integration.spec.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3794,6 +3794,19 @@ describe('Integration', () => {
37943794
});
37953795
});
37963796

3797+
describe('Testing router options', () => {
3798+
describe('paramsInheritanceStrategy', () => {
3799+
beforeEach(() => {
3800+
TestBed.configureTestingModule(
3801+
{imports: [RouterTestingModule.withRoutes([], {paramsInheritanceStrategy: 'always'})]});
3802+
});
3803+
3804+
it('should configure the router', fakeAsync(inject([Router], (router: Router) => {
3805+
expect(router.paramsInheritanceStrategy).toEqual('always');
3806+
})));
3807+
});
3808+
});
3809+
37973810
function expectEvents(events: Event[], pairs: any[]) {
37983811
expect(events.length).toEqual(pairs.length);
37993812
for (let i = 0; i < events.length; ++i) {

packages/router/test/recognize.spec.ts

Lines changed: 56 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
import {Routes} from '../src/config';
1010
import {recognize} from '../src/recognize';
11-
import {ActivatedRouteSnapshot, RouterStateSnapshot} from '../src/router_state';
11+
import {ActivatedRouteSnapshot, ParamsInheritanceStrategy, RouterStateSnapshot, inheritedParamsDataResolve} from '../src/router_state';
1212
import {PRIMARY_OUTLET, Params} from '../src/shared';
1313
import {DefaultUrlSerializer, UrlTree} from '../src/url_tree';
1414

@@ -201,7 +201,7 @@ describe('recognize', () => {
201201
});
202202
});
203203

204-
it('should merge componentless route\'s data', () => {
204+
it('should inherit componentless route\'s data', () => {
205205
checkRecognize(
206206
[{
207207
path: 'a',
@@ -214,6 +214,34 @@ describe('recognize', () => {
214214
});
215215
});
216216

217+
it('should not inherit route\'s data if it has component', () => {
218+
checkRecognize(
219+
[{
220+
path: 'a',
221+
component: ComponentA,
222+
data: {one: 1},
223+
children: [{path: 'b', data: {two: 2}, component: ComponentB}]
224+
}],
225+
'a/b', (s: RouterStateSnapshot) => {
226+
const r: ActivatedRouteSnapshot = s.firstChild(<any>s.firstChild(s.root)) !;
227+
expect(r.data).toEqual({two: 2});
228+
});
229+
});
230+
231+
it('should inherit route\'s data if paramsInheritanceStrategy is \'always\'', () => {
232+
checkRecognize(
233+
[{
234+
path: 'a',
235+
component: ComponentA,
236+
data: {one: 1},
237+
children: [{path: 'b', data: {two: 2}, component: ComponentB}]
238+
}],
239+
'a/b', (s: RouterStateSnapshot) => {
240+
const r: ActivatedRouteSnapshot = s.firstChild(<any>s.firstChild(s.root)) !;
241+
expect(r.data).toEqual({one: 1, two: 2});
242+
}, 'always');
243+
});
244+
217245
it('should set resolved data', () => {
218246
checkRecognize(
219247
[{path: 'a', resolve: {one: 'some-token'}, component: ComponentA}], 'a',
@@ -307,7 +335,7 @@ describe('recognize', () => {
307335
});
308336
});
309337

310-
it('should match (non-termianl) when both primary and secondary and primary has a child',
338+
it('should match (non-terminal) when both primary and secondary and primary has a child',
311339
() => {
312340
const config = [{
313341
path: 'parent',
@@ -579,7 +607,7 @@ describe('recognize', () => {
579607
});
580608
});
581609

582-
it('should merge params until encounters a normal route', () => {
610+
it('should inherit params until encounters a normal route', () => {
583611
checkRecognize(
584612
[{
585613
path: 'p/:id',
@@ -606,6 +634,25 @@ describe('recognize', () => {
606634
checkActivatedRoute(c, 'c', {}, ComponentC);
607635
});
608636
});
637+
638+
it('should inherit all params if paramsInheritanceStrategy is \'always\'', () => {
639+
checkRecognize(
640+
[{
641+
path: 'p/:id',
642+
children: [{
643+
path: 'a/:name',
644+
children: [{
645+
path: 'b',
646+
component: ComponentB,
647+
children: [{path: 'c', component: ComponentC}]
648+
}]
649+
}]
650+
}],
651+
'p/11/a/victor/b/c', (s: RouterStateSnapshot) => {
652+
const c = s.firstChild(s.firstChild(s.firstChild(s.firstChild(s.root) !) !) !) !;
653+
checkActivatedRoute(c, 'c', {id: '11', name: 'victor'}, ComponentC);
654+
}, 'always');
655+
});
609656
});
610657

611658
describe('empty URL leftovers', () => {
@@ -722,8 +769,11 @@ describe('recognize', () => {
722769
});
723770
});
724771

725-
function checkRecognize(config: Routes, url: string, callback: any): void {
726-
recognize(RootComponent, config, tree(url), url).subscribe(callback, e => { throw e; });
772+
function checkRecognize(
773+
config: Routes, url: string, callback: any,
774+
paramsInheritanceStrategy?: ParamsInheritanceStrategy): void {
775+
recognize(RootComponent, config, tree(url), url, paramsInheritanceStrategy)
776+
.subscribe(callback, e => { throw e; });
727777
}
728778

729779
function checkActivatedRoute(

packages/router/test/router.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -498,7 +498,7 @@ function checkResolveData(
498498
future: RouterStateSnapshot, curr: RouterStateSnapshot, injector: any, check: any): void {
499499
const p = new PreActivation(future, curr, injector);
500500
p.initialize(new ChildrenOutletContexts());
501-
p.resolveData().subscribe(check, (e) => { throw e; });
501+
p.resolveData('emptyOnly').subscribe(check, (e) => { throw e; });
502502
}
503503

504504
function checkGuards(

0 commit comments

Comments
 (0)