Skip to content

Commit

Permalink
feat(router): add an option to rerun guards and resolvers when query …
Browse files Browse the repository at this point in the history
…changes (#14624)

Closes #14514
Closes #14567
  • Loading branch information
vicb authored and IgorMinar committed Feb 24, 2017
1 parent be85103 commit 41da599
Show file tree
Hide file tree
Showing 6 changed files with 163 additions and 9 deletions.
18 changes: 16 additions & 2 deletions modules/@angular/router/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand All @@ -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.
Expand Down Expand Up @@ -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
Expand All @@ -346,6 +359,7 @@ export interface Route {
resolve?: ResolveData;
children?: Routes;
loadChildren?: LoadChildren;
runGuardsAndResolvers?: RunGuardsAndResolvers;
}

export function validateConfig(config: Routes, parentPath: string = ''): void {
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion modules/@angular/router/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
25 changes: 21 additions & 4 deletions modules/@angular/router/src/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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;
Expand Down Expand Up @@ -180,7 +180,6 @@ type NavigationParams = {
source: NavigationSource,
};


/**
* Does not detach any subtrees. Reuses routes as long as their route config is the same.
*/
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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<ActivatedRouteSnapshot>, outlet: RouterOutlet): void {
const prevChildren: {[key: string]: any} = nodeChildrenAsMap(route);
Expand Down
121 changes: 120 additions & 1 deletion modules/@angular/router/test/integration.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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', () => {
Expand Down
2 changes: 1 addition & 1 deletion modules/@angular/router/test/router.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,6 @@ function checkResolveData(

function createActivatedRouteSnapshot(cmp: string, extra: any = {}): ActivatedRouteSnapshot {
return new ActivatedRouteSnapshot(
<any>[], {}, <any>null, <any>null, <any>null, <any>null, <any>cmp, <any>null, <any>null, -1,
<any>[], {}, <any>null, <any>null, <any>null, <any>null, <any>cmp, <any>{}, <any>null, -1,
extra.resolve);
}
4 changes: 4 additions & 0 deletions tools/public_api_guard/router/typings/router.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,7 @@ export interface Route {
pathMatch?: string;
redirectTo?: string;
resolve?: ResolveData;
runGuardsAndResolvers?: RunGuardsAndResolvers;
}

/** @experimental */
Expand Down Expand Up @@ -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;
Expand Down

0 comments on commit 41da599

Please sign in to comment.