diff --git a/packages/router/src/pre_activation.ts b/packages/router/src/pre_activation.ts index bad6aa8dbf..00c68936bd 100644 --- a/packages/router/src/pre_activation.ts +++ b/packages/router/src/pre_activation.ts @@ -21,7 +21,7 @@ import {reduce} from 'rxjs/operator/reduce'; import {LoadedRouterConfig, ResolveData, RunGuardsAndResolvers} from './config'; import {ActivationStart, ChildActivationStart, Event} from './events'; import {ChildrenOutletContexts, OutletContext} from './router_outlet_context'; -import {ActivatedRouteSnapshot, RouterStateSnapshot, equalParamsAndUrlSegments, inheritedParamsDataResolve} from './router_state'; +import {ActivatedRouteSnapshot, ParamsInheritanceStrategy, RouterStateSnapshot, equalParamsAndUrlSegments, inheritedParamsDataResolve} from './router_state'; import {andObservables, forEach, shallowEqual, wrapIntoObservable} from './utils/collection'; import {TreeNode, nodeChildrenAsMap} from './utils/tree'; @@ -63,11 +63,11 @@ export class PreActivation { (canDeactivate: boolean) => canDeactivate ? this.runCanActivateChecks() : of (false)); } - resolveData(): Observable { + resolveData(paramsInheritanceStrategy: ParamsInheritanceStrategy): Observable { if (!this.isActivating()) return of (null); const checks$ = from(this.canActivateChecks); - const runningChecks$ = - concatMap.call(checks$, (check: CanActivate) => this.runResolve(check.route)); + const runningChecks$ = concatMap.call( + checks$, (check: CanActivate) => this.runResolve(check.route, paramsInheritanceStrategy)); return reduce.call(runningChecks$, (_: any, __: any) => _); } @@ -306,11 +306,14 @@ export class PreActivation { return every.call(canDeactivate$, (result: any) => result === true); } - private runResolve(future: ActivatedRouteSnapshot): Observable { + private runResolve( + future: ActivatedRouteSnapshot, + paramsInheritanceStrategy: ParamsInheritanceStrategy): Observable { const resolve = future._resolve; return map.call(this.resolveNode(resolve, future), (resolvedData: any): any => { future._resolvedData = resolvedData; - future.data = {...future.data, ...inheritedParamsDataResolve(future).resolve}; + future.data = {...future.data, + ...inheritedParamsDataResolve(future, paramsInheritanceStrategy).resolve}; return null; }); } diff --git a/packages/router/src/recognize.ts b/packages/router/src/recognize.ts index 90200debf7..344ea01dd6 100644 --- a/packages/router/src/recognize.ts +++ b/packages/router/src/recognize.ts @@ -12,7 +12,7 @@ import {Observer} from 'rxjs/Observer'; import {of } from 'rxjs/observable/of'; import {Data, ResolveData, Route, Routes} from './config'; -import {ActivatedRouteSnapshot, RouterStateSnapshot, inheritedParamsDataResolve} from './router_state'; +import {ActivatedRouteSnapshot, ParamsInheritanceStrategy, RouterStateSnapshot, inheritedParamsDataResolve} from './router_state'; import {PRIMARY_OUTLET, defaultUrlMatcher} from './shared'; import {UrlSegment, UrlSegmentGroup, UrlTree, mapChildrenIntoArray} from './url_tree'; import {forEach, last} from './utils/collection'; @@ -21,15 +21,17 @@ import {TreeNode} from './utils/tree'; class NoMatch {} export function recognize( - rootComponentType: Type| null, config: Routes, urlTree: UrlTree, - url: string): Observable { - return new Recognizer(rootComponentType, config, urlTree, url).recognize(); + rootComponentType: Type| null, config: Routes, urlTree: UrlTree, url: string, + paramsInheritanceStrategy: ParamsInheritanceStrategy = + 'emptyOnly'): Observable { + return new Recognizer(rootComponentType, config, urlTree, url, paramsInheritanceStrategy) + .recognize(); } class Recognizer { constructor( private rootComponentType: Type|null, private config: Routes, private urlTree: UrlTree, - private url: string) {} + private url: string, private paramsInheritanceStrategy: ParamsInheritanceStrategy) {} recognize(): Observable { try { @@ -55,7 +57,7 @@ class Recognizer { inheritParamsAndData(routeNode: TreeNode): void { const route = routeNode.value; - const i = inheritedParamsDataResolve(route); + const i = inheritedParamsDataResolve(route, this.paramsInheritanceStrategy); route.params = Object.freeze(i.params); route.data = Object.freeze(i.data); diff --git a/packages/router/src/router.ts b/packages/router/src/router.ts index 53aed24a0b..4c1f278c3c 100644 --- a/packages/router/src/router.ts +++ b/packages/router/src/router.ts @@ -27,7 +27,7 @@ import {recognize} from './recognize'; import {DefaultRouteReuseStrategy, DetachedRouteHandleInternal, RouteReuseStrategy} from './route_reuse_strategy'; import {RouterConfigLoader} from './router_config_loader'; import {ChildrenOutletContexts} from './router_outlet_context'; -import {ActivatedRoute, ActivatedRouteSnapshot, RouterState, RouterStateSnapshot, advanceActivatedRoute, createEmptyState} from './router_state'; +import {ActivatedRoute, ActivatedRouteSnapshot, RouterState, RouterStateSnapshot, advanceActivatedRoute, createEmptyState, inheritedParamsDataResolve} from './router_state'; import {Params, isNavigationCancelingError} from './shared'; import {DefaultUrlHandlingStrategy, UrlHandlingStrategy} from './url_handling_strategy'; import {UrlSerializer, UrlTree, containsTree, createEmptyUrlTree} from './url_tree'; @@ -249,6 +249,16 @@ export class Router { */ onSameUrlNavigation: 'reload'|'ignore' = 'ignore'; + /** + * Defines how the router merges params, data and resolved data from parent to child + * routes. Available options are: + * + * - `'emptyOnly'`, the default, only inherits parent params for path-less or component-less + * routes. + * - `'always'`, enables unconditional inheritance of parent params. + */ + paramsInheritanceStrategy: 'emptyOnly'|'always' = 'emptyOnly'; + /** * Creates the router service. */ @@ -611,7 +621,8 @@ export class Router { urlAndSnapshot$ = mergeMap.call(redirectsApplied$, (appliedUrl: UrlTree) => { return map.call( recognize( - this.rootComponentType, this.config, appliedUrl, this.serializeUrl(appliedUrl)), + this.rootComponentType, this.config, appliedUrl, this.serializeUrl(appliedUrl), + this.paramsInheritanceStrategy), (snapshot: any) => { (this.events as Subject) @@ -667,7 +678,7 @@ export class Router { if (p.shouldActivate && preActivation.isActivating()) { this.triggerEvent( new ResolveStart(id, this.serializeUrl(url), p.appliedUrl, p.snapshot)); - return map.call(preActivation.resolveData(), () => { + return map.call(preActivation.resolveData(this.paramsInheritanceStrategy), () => { this.triggerEvent( new ResolveEnd(id, this.serializeUrl(url), p.appliedUrl, p.snapshot)); return p; diff --git a/packages/router/src/router_module.ts b/packages/router/src/router_module.ts index fcfb4c8c27..fe6ebf7d09 100644 --- a/packages/router/src/router_module.ts +++ b/packages/router/src/router_module.ts @@ -278,6 +278,16 @@ export interface ExtraOptions { * current URL. Default is 'ignore'. */ onSameUrlNavigation?: 'reload'|'ignore'; + + /** + * Defines how the router merges params, data and resolved data from parent to child + * routes. Available options are: + * + * - `'emptyOnly'`, the default, only inherits parent params for path-less or component-less + * routes. + * - `'always'`, enables unconditional inheritance of parent params. + */ + paramsInheritanceStrategy?: 'emptyOnly'|'always'; } export function setupRouter( @@ -314,6 +324,10 @@ export function setupRouter( router.onSameUrlNavigation = opts.onSameUrlNavigation; } + if (opts.paramsInheritanceStrategy) { + router.paramsInheritanceStrategy = opts.paramsInheritanceStrategy; + } + return router; } diff --git a/packages/router/src/router_state.ts b/packages/router/src/router_state.ts index b017d59f3a..a10c896fb4 100644 --- a/packages/router/src/router_state.ts +++ b/packages/router/src/router_state.ts @@ -18,6 +18,7 @@ import {shallowEqual, shallowEqualArrays} from './utils/collection'; import {Tree, TreeNode} from './utils/tree'; + /** * @whatItDoes Represents the state of the router. * @@ -174,6 +175,9 @@ export class ActivatedRoute { } } +/** @internal */ +export type ParamsInheritanceStrategy = 'emptyOnly' | 'always'; + /** @internal */ export type Inherited = { params: Params, @@ -181,29 +185,43 @@ export type Inherited = { resolve: Data, }; -/** @internal */ -export function inheritedParamsDataResolve(route: ActivatedRouteSnapshot): Inherited { - const pathToRoot = route.pathFromRoot; +/** + * Returns the inherited params, data, and resolve for a given route. + * By default, this only inherits values up to the nearest path-less or component-less route. + * @internal + */ +export function inheritedParamsDataResolve( + route: ActivatedRouteSnapshot, + paramsInheritanceStrategy: ParamsInheritanceStrategy = 'emptyOnly'): Inherited { + const pathFromRoot = route.pathFromRoot; - let inhertingStartingFrom = pathToRoot.length - 1; + let inheritingStartingFrom = 0; + if (paramsInheritanceStrategy !== 'always') { + inheritingStartingFrom = pathFromRoot.length - 1; - while (inhertingStartingFrom >= 1) { - const current = pathToRoot[inhertingStartingFrom]; - const parent = pathToRoot[inhertingStartingFrom - 1]; - // current route is an empty path => inherits its parent's params and data - if (current.routeConfig && current.routeConfig.path === '') { - inhertingStartingFrom--; + while (inheritingStartingFrom >= 1) { + const current = pathFromRoot[inheritingStartingFrom]; + const parent = pathFromRoot[inheritingStartingFrom - 1]; + // current route is an empty path => inherits its parent's params and data + if (current.routeConfig && current.routeConfig.path === '') { + inheritingStartingFrom--; - // parent is componentless => current route should inherit its params and data - } else if (!parent.component) { - inhertingStartingFrom--; + // parent is componentless => current route should inherit its params and data + } else if (!parent.component) { + inheritingStartingFrom--; - } else { - break; + } else { + break; + } } } - return pathToRoot.slice(inhertingStartingFrom).reduce((res, curr) => { + return flattenInherited(pathFromRoot.slice(inheritingStartingFrom)); +} + +/** @internal */ +function flattenInherited(pathFromRoot: ActivatedRouteSnapshot[]): Inherited { + return pathFromRoot.reduce((res, curr) => { const params = {...res.params, ...curr.params}; const data = {...res.data, ...curr.data}; const resolve = {...res.resolve, ...curr._resolvedData}; @@ -352,7 +370,7 @@ function setRouterState(state: U, node: TreeNode< } function serializeNode(node: TreeNode): string { - const c = node.children.length > 0 ? ` { ${node.children.map(serializeNode).join(", ")} } ` : ''; + const c = node.children.length > 0 ? ` { ${node.children.map(serializeNode).join(', ')} } ` : ''; return `${node.value}${c}`; } diff --git a/packages/router/test/integration.spec.ts b/packages/router/test/integration.spec.ts index 1fdcd7f9eb..71c8429414 100644 --- a/packages/router/test/integration.spec.ts +++ b/packages/router/test/integration.spec.ts @@ -3794,6 +3794,19 @@ describe('Integration', () => { }); }); +describe('Testing router options', () => { + describe('paramsInheritanceStrategy', () => { + beforeEach(() => { + TestBed.configureTestingModule( + {imports: [RouterTestingModule.withRoutes([], {paramsInheritanceStrategy: 'always'})]}); + }); + + it('should configure the router', fakeAsync(inject([Router], (router: Router) => { + expect(router.paramsInheritanceStrategy).toEqual('always'); + }))); + }); +}); + function expectEvents(events: Event[], pairs: any[]) { expect(events.length).toEqual(pairs.length); for (let i = 0; i < events.length; ++i) { diff --git a/packages/router/test/recognize.spec.ts b/packages/router/test/recognize.spec.ts index 8da65f3f3c..edab240b93 100644 --- a/packages/router/test/recognize.spec.ts +++ b/packages/router/test/recognize.spec.ts @@ -8,7 +8,7 @@ import {Routes} from '../src/config'; import {recognize} from '../src/recognize'; -import {ActivatedRouteSnapshot, RouterStateSnapshot} from '../src/router_state'; +import {ActivatedRouteSnapshot, ParamsInheritanceStrategy, RouterStateSnapshot, inheritedParamsDataResolve} from '../src/router_state'; import {PRIMARY_OUTLET, Params} from '../src/shared'; import {DefaultUrlSerializer, UrlTree} from '../src/url_tree'; @@ -201,7 +201,7 @@ describe('recognize', () => { }); }); - it('should merge componentless route\'s data', () => { + it('should inherit componentless route\'s data', () => { checkRecognize( [{ path: 'a', @@ -214,6 +214,34 @@ describe('recognize', () => { }); }); + it('should not inherit route\'s data if it has component', () => { + checkRecognize( + [{ + path: 'a', + component: ComponentA, + data: {one: 1}, + children: [{path: 'b', data: {two: 2}, component: ComponentB}] + }], + 'a/b', (s: RouterStateSnapshot) => { + const r: ActivatedRouteSnapshot = s.firstChild(s.firstChild(s.root)) !; + expect(r.data).toEqual({two: 2}); + }); + }); + + it('should inherit route\'s data if paramsInheritanceStrategy is \'always\'', () => { + checkRecognize( + [{ + path: 'a', + component: ComponentA, + data: {one: 1}, + children: [{path: 'b', data: {two: 2}, component: ComponentB}] + }], + 'a/b', (s: RouterStateSnapshot) => { + const r: ActivatedRouteSnapshot = s.firstChild(s.firstChild(s.root)) !; + expect(r.data).toEqual({one: 1, two: 2}); + }, 'always'); + }); + it('should set resolved data', () => { checkRecognize( [{path: 'a', resolve: {one: 'some-token'}, component: ComponentA}], 'a', @@ -307,7 +335,7 @@ describe('recognize', () => { }); }); - it('should match (non-termianl) when both primary and secondary and primary has a child', + it('should match (non-terminal) when both primary and secondary and primary has a child', () => { const config = [{ path: 'parent', @@ -579,7 +607,7 @@ describe('recognize', () => { }); }); - it('should merge params until encounters a normal route', () => { + it('should inherit params until encounters a normal route', () => { checkRecognize( [{ path: 'p/:id', @@ -606,6 +634,25 @@ describe('recognize', () => { checkActivatedRoute(c, 'c', {}, ComponentC); }); }); + + it('should inherit all params if paramsInheritanceStrategy is \'always\'', () => { + checkRecognize( + [{ + path: 'p/:id', + children: [{ + path: 'a/:name', + children: [{ + path: 'b', + component: ComponentB, + children: [{path: 'c', component: ComponentC}] + }] + }] + }], + 'p/11/a/victor/b/c', (s: RouterStateSnapshot) => { + const c = s.firstChild(s.firstChild(s.firstChild(s.firstChild(s.root) !) !) !) !; + checkActivatedRoute(c, 'c', {id: '11', name: 'victor'}, ComponentC); + }, 'always'); + }); }); describe('empty URL leftovers', () => { @@ -722,8 +769,11 @@ describe('recognize', () => { }); }); -function checkRecognize(config: Routes, url: string, callback: any): void { - recognize(RootComponent, config, tree(url), url).subscribe(callback, e => { throw e; }); +function checkRecognize( + config: Routes, url: string, callback: any, + paramsInheritanceStrategy?: ParamsInheritanceStrategy): void { + recognize(RootComponent, config, tree(url), url, paramsInheritanceStrategy) + .subscribe(callback, e => { throw e; }); } function checkActivatedRoute( diff --git a/packages/router/test/router.spec.ts b/packages/router/test/router.spec.ts index 8a6787fd34..2a3ed2ea2a 100644 --- a/packages/router/test/router.spec.ts +++ b/packages/router/test/router.spec.ts @@ -498,7 +498,7 @@ function checkResolveData( future: RouterStateSnapshot, curr: RouterStateSnapshot, injector: any, check: any): void { const p = new PreActivation(future, curr, injector); p.initialize(new ChildrenOutletContexts()); - p.resolveData().subscribe(check, (e) => { throw e; }); + p.resolveData('emptyOnly').subscribe(check, (e) => { throw e; }); } function checkGuards( diff --git a/packages/router/testing/src/router_testing_module.ts b/packages/router/testing/src/router_testing_module.ts index 650502d11c..09a1ed9e5f 100644 --- a/packages/router/testing/src/router_testing_module.ts +++ b/packages/router/testing/src/router_testing_module.ts @@ -9,7 +9,7 @@ import {Location, LocationStrategy} from '@angular/common'; import {MockLocationStrategy, SpyLocation} from '@angular/common/testing'; import {Compiler, Injectable, Injector, ModuleWithProviders, NgModule, NgModuleFactory, NgModuleFactoryLoader, Optional} from '@angular/core'; -import {ChildrenOutletContexts, NoPreloading, PreloadingStrategy, ROUTES, Route, Router, RouterModule, Routes, UrlHandlingStrategy, UrlSerializer, provideRoutes, ɵROUTER_PROVIDERS as ROUTER_PROVIDERS, ɵflatten as flatten} from '@angular/router'; +import {ChildrenOutletContexts, ExtraOptions, NoPreloading, PreloadingStrategy, ROUTER_CONFIGURATION, ROUTES, Route, Router, RouterModule, Routes, UrlHandlingStrategy, UrlSerializer, provideRoutes, ɵROUTER_PROVIDERS as ROUTER_PROVIDERS, ɵflatten as flatten} from '@angular/router'; @@ -76,6 +76,13 @@ export class SpyNgModuleFactoryLoader implements NgModuleFactoryLoader { } } +function isUrlHandlingStrategy(opts: ExtraOptions | UrlHandlingStrategy): + opts is UrlHandlingStrategy { + // This property check is needed because UrlHandlingStrategy is an interface and doesn't exist at + // runtime. + return 'shouldProcessUrl' in opts; +} + /** * Router setup factory function used for testing. * @@ -84,9 +91,39 @@ export class SpyNgModuleFactoryLoader implements NgModuleFactoryLoader { export function setupTestingRouter( urlSerializer: UrlSerializer, contexts: ChildrenOutletContexts, location: Location, loader: NgModuleFactoryLoader, compiler: Compiler, injector: Injector, routes: Route[][], - urlHandlingStrategy?: UrlHandlingStrategy) { + opts?: ExtraOptions, urlHandlingStrategy?: UrlHandlingStrategy): Router; + +/** + * Router setup factory function used for testing. + * + * @deprecated As of v5.2. The 2nd-to-last argument should be `ExtraOptions`, not + * `UrlHandlingStrategy` + */ +export function setupTestingRouter( + urlSerializer: UrlSerializer, contexts: ChildrenOutletContexts, location: Location, + loader: NgModuleFactoryLoader, compiler: Compiler, injector: Injector, routes: Route[][], + urlHandlingStrategy?: UrlHandlingStrategy): Router; + +/** + * Router setup factory function used for testing. + * + * @stable + */ +export function setupTestingRouter( + urlSerializer: UrlSerializer, contexts: ChildrenOutletContexts, location: Location, + loader: NgModuleFactoryLoader, compiler: Compiler, injector: Injector, routes: Route[][], + opts?: ExtraOptions | UrlHandlingStrategy, urlHandlingStrategy?: UrlHandlingStrategy) { const router = new Router( null !, urlSerializer, contexts, location, injector, loader, compiler, flatten(routes)); + // Handle deprecated argument ordering. + if (opts) { + if (isUrlHandlingStrategy(opts)) { + router.urlHandlingStrategy = opts; + } else if (opts.paramsInheritanceStrategy) { + router.paramsInheritanceStrategy = opts.paramsInheritanceStrategy; + } + } + if (urlHandlingStrategy) { router.urlHandlingStrategy = urlHandlingStrategy; } @@ -128,14 +165,20 @@ export function setupTestingRouter( useFactory: setupTestingRouter, deps: [ UrlSerializer, ChildrenOutletContexts, Location, NgModuleFactoryLoader, Compiler, Injector, - ROUTES, [UrlHandlingStrategy, new Optional()] + ROUTES, ROUTER_CONFIGURATION, [UrlHandlingStrategy, new Optional()] ] }, {provide: PreloadingStrategy, useExisting: NoPreloading}, provideRoutes([]) ] }) export class RouterTestingModule { - static withRoutes(routes: Routes): ModuleWithProviders { - return {ngModule: RouterTestingModule, providers: [provideRoutes(routes)]}; + static withRoutes(routes: Routes, config?: ExtraOptions): ModuleWithProviders { + return { + ngModule: RouterTestingModule, + providers: [ + provideRoutes(routes), + {provide: ROUTER_CONFIGURATION, useValue: config ? config : {}}, + ] + }; } } diff --git a/tools/public_api_guard/router/router.d.ts b/tools/public_api_guard/router/router.d.ts index a1d5d899c8..f9f0524226 100644 --- a/tools/public_api_guard/router/router.d.ts +++ b/tools/public_api_guard/router/router.d.ts @@ -127,6 +127,7 @@ export interface ExtraOptions { errorHandler?: ErrorHandler; initialNavigation?: InitialNavigation; onSameUrlNavigation?: 'reload' | 'ignore'; + paramsInheritanceStrategy?: 'emptyOnly' | 'always'; preloadingStrategy?: any; useHash?: boolean; } @@ -329,6 +330,7 @@ export declare class Router { readonly events: Observable; navigated: boolean; onSameUrlNavigation: 'reload' | 'ignore'; + paramsInheritanceStrategy: 'emptyOnly' | 'always'; routeReuseStrategy: RouteReuseStrategy; readonly routerState: RouterState; readonly url: string;