diff --git a/modules/@angular/router/src/config.ts b/modules/@angular/router/src/config.ts index 4dd00be40b..9f41da34cf 100644 --- a/modules/@angular/router/src/config.ts +++ b/modules/@angular/router/src/config.ts @@ -8,9 +8,12 @@ import {NgModuleFactory, Type} from '@angular/core'; import {Observable} from 'rxjs/Observable'; + +import {ActivatedRouteSnapshot, RouterStateSnapshot} from './router_state'; import {PRIMARY_OUTLET} from './shared'; import {UrlSegment, UrlSegmentGroup} from './url_tree'; + /** * @whatItDoes Represents router configuration. * @@ -35,6 +38,9 @@ import {UrlSegment, UrlSegmentGroup} from './url_tree'; * - `data` is additional data provided to the component via `ActivatedRoute`. * - `resolve` is a map of DI tokens used to look up data resolvers. See {@link Resolve} for more * info. + * - `runGuardsAndResolvers` defines when guards and resovlers 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. * - `children` is an array of child route definitions. * - `loadChildren` is a reference to lazy loaded child routes. See {@link LoadChildren} for more * info. @@ -327,6 +333,13 @@ export type LoadChildren = string | LoadChildrenCallback; */ export type QueryParamsHandling = 'merge' | 'preserve' | ''; +/** + * @whatItDoes The type of `runGuardsAndResolvers`. + * See {@link Routes} for more details. + * @experimental + */ +export type RunGuardsAndResolvers = 'paramsChange' | 'paramsOrQueryParamsChange' | 'always'; + /** * See {@link Routes} for more details. * @stable @@ -346,6 +359,7 @@ export interface Route { resolve?: ResolveData; children?: Routes; loadChildren?: LoadChildren; + runGuardsAndResolvers?: RunGuardsAndResolvers; } export function validateConfig(config: Routes, parentPath: string = ''): void { @@ -362,8 +376,8 @@ function validateNode(route: Route, fullPath: string): void { throw new Error(` Invalid configuration of route '${fullPath}': Encountered undefined route. The reason might be an extra comma. - - Example: + + Example: const routes: Routes = [ { path: '', redirectTo: '/dashboard', pathMatch: 'full' }, { path: 'dashboard', component: DashboardComponent },, << two commas diff --git a/modules/@angular/router/src/index.ts b/modules/@angular/router/src/index.ts index 7fd902aea9..d1bd73dd12 100644 --- a/modules/@angular/router/src/index.ts +++ b/modules/@angular/router/src/index.ts @@ -7,7 +7,7 @@ */ -export {Data, LoadChildren, LoadChildrenCallback, ResolveData, Route, Routes} from './config'; +export {Data, LoadChildren, LoadChildrenCallback, ResolveData, Route, Routes, RunGuardsAndResolvers} from './config'; export {RouterLink, RouterLinkWithHref} from './directives/router_link'; export {RouterLinkActive} from './directives/router_link_active'; export {RouterOutlet} from './directives/router_outlet'; diff --git a/modules/@angular/router/src/router.ts b/modules/@angular/router/src/router.ts index 2dc04bc31c..a0bf1fd8d0 100644 --- a/modules/@angular/router/src/router.ts +++ b/modules/@angular/router/src/router.ts @@ -22,7 +22,7 @@ import {mergeMap} from 'rxjs/operator/mergeMap'; import {reduce} from 'rxjs/operator/reduce'; import {applyRedirects} from './apply_redirects'; -import {QueryParamsHandling, ResolveData, Route, Routes, validateConfig} from './config'; +import {QueryParamsHandling, ResolveData, Route, Routes, RunGuardsAndResolvers, validateConfig} from './config'; import {createRouterState} from './create_router_state'; import {createUrlTree} from './create_url_tree'; import {RouterOutlet} from './directives/router_outlet'; @@ -35,7 +35,7 @@ import {ActivatedRoute, ActivatedRouteSnapshot, RouterState, RouterStateSnapshot import {PRIMARY_OUTLET, Params, isNavigationCancelingError} from './shared'; import {DefaultUrlHandlingStrategy, UrlHandlingStrategy} from './url_handling_strategy'; import {UrlSerializer, UrlTree, containsTree, createEmptyUrlTree} from './url_tree'; -import {andObservables, forEach, merge, waitForMap, wrapIntoObservable} from './utils/collection'; +import {andObservables, forEach, merge, shallowEqual, waitForMap, wrapIntoObservable} from './utils/collection'; import {TreeNode} from './utils/tree'; declare let Zone: any; @@ -180,7 +180,6 @@ type NavigationParams = { source: NavigationSource, }; - /** * Does not detach any subtrees. Reuses routes as long as their route config is the same. */ @@ -799,7 +798,8 @@ export class PreActivation { // reusing the node if (curr && future._routeConfig === curr._routeConfig) { - if (!equalParamsAndUrlSegments(future, curr)) { + if (this.shouldRunGuardsAndResolvers( + curr, future, future._routeConfig.runGuardsAndResolvers)) { this.checks.push(new CanDeactivate(outlet.component, curr), new CanActivate(futurePath)); } else { // we need to set the data @@ -833,6 +833,23 @@ export class PreActivation { } } + private shouldRunGuardsAndResolvers( + curr: ActivatedRouteSnapshot, future: ActivatedRouteSnapshot, + mode: RunGuardsAndResolvers): boolean { + switch (mode) { + case 'always': + return true; + + case 'paramsOrQueryParamsChange': + return !equalParamsAndUrlSegments(curr, future) || + !shallowEqual(curr.queryParams, future.queryParams); + + case 'paramsChange': + default: + return !equalParamsAndUrlSegments(curr, future); + } + } + private deactiveRouteAndItsChildren( route: TreeNode, outlet: RouterOutlet): void { const prevChildren: {[key: string]: any} = nodeChildrenAsMap(route); diff --git a/modules/@angular/router/test/integration.spec.ts b/modules/@angular/router/test/integration.spec.ts index 6d28640331..46b93332c9 100644 --- a/modules/@angular/router/test/integration.spec.ts +++ b/modules/@angular/router/test/integration.spec.ts @@ -16,7 +16,7 @@ import {map} from 'rxjs/operator/map'; import {ActivatedRoute, ActivatedRouteSnapshot, CanActivate, CanDeactivate, DetachedRouteHandle, Event, NavigationCancel, NavigationEnd, NavigationError, NavigationStart, PRIMARY_OUTLET, Params, PreloadAllModules, PreloadingStrategy, Resolve, RouteConfigLoadEnd, RouteConfigLoadStart, RouteReuseStrategy, Router, RouterModule, RouterStateSnapshot, RoutesRecognized, UrlHandlingStrategy, UrlSegmentGroup, UrlTree} from '../index'; import {RouterPreloader} from '../src/router_preloader'; -import {forEach} from '../src/utils/collection'; +import {forEach, shallowEqual} from '../src/utils/collection'; import {RouterTestingModule, SpyNgModuleFactoryLoader} from '../testing'; describe('Integration', () => { @@ -1527,6 +1527,125 @@ describe('Integration', () => { expect(fixture.nativeElement).toHaveText('simple'); }))); }); + + describe('runGuardsAndResolvers', () => { + let count = 0; + + beforeEach(() => { + count = 0; + TestBed.configureTestingModule({ + providers: [{ + provide: 'loggingCanActivate', + useValue: (a: any, b: any) => { + count++; + return true; + } + }] + }); + }); + + it('should rerun guards when params change', + fakeAsync(inject([Router, Location], (router: Router, location: Location) => { + const fixture = createRoot(router, RootCmpWithTwoOutlets); + + router.resetConfig([ + { + path: 'a', + runGuardsAndResolvers: 'paramsChange', + component: SimpleCmp, + canActivate: ['loggingCanActivate'] + }, + {path: 'b', component: SimpleCmp, outlet: 'right'} + ]); + + router.navigateByUrl('/a'); + advance(fixture); + expect(count).toEqual(1); + + router.navigateByUrl('/a;p=1'); + advance(fixture); + expect(count).toEqual(2); + + router.navigateByUrl('/a;p=2'); + advance(fixture); + expect(count).toEqual(3); + + router.navigateByUrl('/a;p=2?q=1'); + advance(fixture); + expect(count).toEqual(3); + }))); + + it('should rerun guards when query params change', + fakeAsync(inject([Router, Location], (router: Router, location: Location) => { + const fixture = createRoot(router, RootCmpWithTwoOutlets); + + router.resetConfig([ + { + path: 'a', + runGuardsAndResolvers: 'paramsOrQueryParamsChange', + component: SimpleCmp, + canActivate: ['loggingCanActivate'] + }, + {path: 'b', component: SimpleCmp, outlet: 'right'} + ]); + + router.navigateByUrl('/a'); + advance(fixture); + expect(count).toEqual(1); + + router.navigateByUrl('/a;p=1'); + advance(fixture); + expect(count).toEqual(2); + + router.navigateByUrl('/a;p=2'); + advance(fixture); + expect(count).toEqual(3); + + router.navigateByUrl('/a;p=2?q=1'); + advance(fixture); + expect(count).toEqual(4); + + router.navigateByUrl('/a;p=2(right:b)?q=1'); + advance(fixture); + expect(count).toEqual(4); + }))); + + it('should always rerun guards', + fakeAsync(inject([Router, Location], (router: Router, location: Location) => { + const fixture = createRoot(router, RootCmpWithTwoOutlets); + + router.resetConfig([ + { + path: 'a', + runGuardsAndResolvers: 'always', + component: SimpleCmp, + canActivate: ['loggingCanActivate'] + }, + {path: 'b', component: SimpleCmp, outlet: 'right'} + ]); + + router.navigateByUrl('/a'); + advance(fixture); + expect(count).toEqual(1); + + router.navigateByUrl('/a;p=1'); + advance(fixture); + expect(count).toEqual(2); + + router.navigateByUrl('/a;p=2'); + advance(fixture); + expect(count).toEqual(3); + + router.navigateByUrl('/a;p=2?q=1'); + advance(fixture); + expect(count).toEqual(4); + + router.navigateByUrl('/a;p=2(right:b)?q=1'); + advance(fixture); + expect(count).toEqual(5); + }))); + }); + }); describe('CanDeactivate', () => { diff --git a/modules/@angular/router/test/router.spec.ts b/modules/@angular/router/test/router.spec.ts index bdd2ac2725..1ceb58f210 100644 --- a/modules/@angular/router/test/router.spec.ts +++ b/modules/@angular/router/test/router.spec.ts @@ -115,6 +115,6 @@ function checkResolveData( function createActivatedRouteSnapshot(cmp: string, extra: any = {}): ActivatedRouteSnapshot { return new ActivatedRouteSnapshot( - [], {}, null, null, null, null, cmp, null, null, -1, + [], {}, null, null, null, null, cmp, {}, null, -1, extra.resolve); } \ No newline at end of file diff --git a/tools/public_api_guard/router/typings/router.d.ts b/tools/public_api_guard/router/typings/router.d.ts index 244b1f1f26..87b6a1a921 100644 --- a/tools/public_api_guard/router/typings/router.d.ts +++ b/tools/public_api_guard/router/typings/router.d.ts @@ -197,6 +197,7 @@ export interface Route { pathMatch?: string; redirectTo?: string; resolve?: ResolveData; + runGuardsAndResolvers?: RunGuardsAndResolvers; } /** @experimental */ @@ -376,6 +377,9 @@ export declare class RoutesRecognized { toString(): string; } +/** @experimental */ +export declare type RunGuardsAndResolvers = 'paramsChange' | 'paramsOrQueryParamsChange' | 'always'; + /** @experimental */ export declare abstract class UrlHandlingStrategy { abstract extract(url: UrlTree): UrlTree;