diff --git a/packages/router/src/config.ts b/packages/router/src/config.ts index f1943e4f6e..fcc9c5af27 100644 --- a/packages/router/src/config.ts +++ b/packages/router/src/config.ts @@ -37,8 +37,15 @@ import {UrlSegment, UrlSegmentGroup} from './url_tree'; * - `resolve` is a map of DI tokens used to look up data resolvers. See `Resolve` for more * info. * - `runGuardsAndResolvers` defines when guards and resolvers will be run. By default they run only - * when the matrix parameters of the route change. When set to `paramsOrQueryParamsChange` they - * will also run when query params change. And when set to `always`, they will run every time. + * when the matrix parameters of the route change. Options include: + * - `paramsChange` (default) - Run guards and resolvers when path or matrix params change. This + * mode ignores query param changes. + * - `paramsOrQueryParamsChange` - Guards and resolvers will run when any parameters change. This + * includes path, matrix, and query params. + * - `pathParamsChange` Run guards and resolvers path or any path params change. This mode is + * useful if you want to ignore changes to all optional parameters such as query *and* matrix + * params. + * - `always` - Run guards and resolvers on every navigation. * - `children` is an array of child route definitions. * - `loadChildren` is a reference to lazy loaded child routes. See `LoadChildren` for more * info. @@ -359,7 +366,8 @@ export type QueryParamsHandling = 'merge' | 'preserve' | ''; * See `Routes` for more details. * @publicApi */ -export type RunGuardsAndResolvers = 'paramsChange' | 'paramsOrQueryParamsChange' | 'always'; +export type RunGuardsAndResolvers = + 'pathParamsChange' | 'paramsChange' | 'paramsOrQueryParamsChange' | 'always'; /** * See `Routes` for more details. diff --git a/packages/router/src/utils/preactivation.ts b/packages/router/src/utils/preactivation.ts index 3b3dd31d6a..e8324b8402 100644 --- a/packages/router/src/utils/preactivation.ts +++ b/packages/router/src/utils/preactivation.ts @@ -11,6 +11,7 @@ import {Injector} from '@angular/core'; import {LoadedRouterConfig, RunGuardsAndResolvers} from '../config'; import {ChildrenOutletContexts, OutletContext} from '../router_outlet_context'; import {ActivatedRouteSnapshot, RouterStateSnapshot, equalParamsAndUrlSegments} from '../router_state'; +import {equalPath} from '../url_tree'; import {forEach, shallowEqual} from '../utils/collection'; import {TreeNode, nodeChildrenAsMap} from '../utils/tree'; @@ -147,6 +148,9 @@ function shouldRunGuardsAndResolvers( curr: ActivatedRouteSnapshot, future: ActivatedRouteSnapshot, mode: RunGuardsAndResolvers | undefined): boolean { switch (mode) { + case 'pathParamsChange': + return !equalPath(curr.url, future.url); + case 'always': return true; diff --git a/packages/router/test/integration.spec.ts b/packages/router/test/integration.spec.ts index 32559d1fbf..4fc5d82993 100644 --- a/packages/router/test/integration.spec.ts +++ b/packages/router/test/integration.spec.ts @@ -2138,7 +2138,13 @@ describe('Integration', () => { canActivate: ['guard'], resolve: {data: 'resolver'} }, - {path: 'b', component: SimpleCmp, outlet: 'right'} + {path: 'b', component: SimpleCmp, outlet: 'right'}, { + path: 'c/:param', + runGuardsAndResolvers, + component: RouteCmp, + canActivate: ['guard'], + resolve: {data: 'resolver'} + } ]); router.navigateByUrl('/a'); @@ -2236,6 +2242,55 @@ describe('Integration', () => { expect(guardRunCount).toEqual(5); expect(recordedData).toEqual([{data: 0}, {data: 1}, {data: 2}, {data: 3}, {data: 4}]); }))); + + it('should not rerun guards and resolvers', fakeAsync(inject([Router], (router: Router) => { + const fixture = configureRouter(router, 'pathParamsChange'); + + const cmp: RouteCmp = fixture.debugElement.children[1].componentInstance; + const recordedData: any[] = []; + cmp.route.data.subscribe((data: any) => recordedData.push(data)); + + // First navigation has already run + expect(guardRunCount).toEqual(1); + expect(recordedData).toEqual([{data: 0}]); + + // Changing any optional params will not result in running guards or resolvers + router.navigateByUrl('/a;p=1'); + advance(fixture); + expect(guardRunCount).toEqual(1); + expect(recordedData).toEqual([{data: 0}]); + + router.navigateByUrl('/a;p=2'); + advance(fixture); + expect(guardRunCount).toEqual(1); + expect(recordedData).toEqual([{data: 0}]); + + router.navigateByUrl('/a;p=2?q=1'); + advance(fixture); + expect(guardRunCount).toEqual(1); + expect(recordedData).toEqual([{data: 0}]); + + router.navigateByUrl('/a;p=2(right:b)?q=1'); + advance(fixture); + expect(guardRunCount).toEqual(1); + expect(recordedData).toEqual([{data: 0}]); + + // Change to new route with path param should run guards and resolvers + router.navigateByUrl('/c/paramValue'); + advance(fixture); + + expect(guardRunCount).toEqual(2); + + // Modifying a path param should run guards and resolvers + router.navigateByUrl('/c/paramValueChanged'); + advance(fixture); + expect(guardRunCount).toEqual(3); + + // Adding optional params should not cause guards/resolvers to run + router.navigateByUrl('/c/paramValueChanged;p=1?q=2'); + advance(fixture); + expect(guardRunCount).toEqual(3); + }))); }); describe('should wait for parent to complete', () => { diff --git a/tools/public_api_guard/router/router.d.ts b/tools/public_api_guard/router/router.d.ts index c62e9f97e1..10ef4c557d 100644 --- a/tools/public_api_guard/router/router.d.ts +++ b/tools/public_api_guard/router/router.d.ts @@ -451,7 +451,7 @@ export declare class RoutesRecognized extends RouterEvent { toString(): string; } -export declare type RunGuardsAndResolvers = 'paramsChange' | 'paramsOrQueryParamsChange' | 'always'; +export declare type RunGuardsAndResolvers = 'pathParamsChange' | 'paramsChange' | 'paramsOrQueryParamsChange' | 'always'; export declare class Scroll { readonly anchor: string | null;