feat(router): add an option to rerun guards and resolvers when query changes
Closes #14514 Closes #14567
This commit is contained in:
parent
fbe4b76f2d
commit
c2e0f71a78
|
@ -8,9 +8,12 @@
|
||||||
|
|
||||||
import {NgModuleFactory, Type} from '@angular/core';
|
import {NgModuleFactory, Type} from '@angular/core';
|
||||||
import {Observable} from 'rxjs/Observable';
|
import {Observable} from 'rxjs/Observable';
|
||||||
|
|
||||||
|
import {ActivatedRouteSnapshot, RouterStateSnapshot} from './router_state';
|
||||||
import {PRIMARY_OUTLET} from './shared';
|
import {PRIMARY_OUTLET} from './shared';
|
||||||
import {UrlSegment, UrlSegmentGroup} from './url_tree';
|
import {UrlSegment, UrlSegmentGroup} from './url_tree';
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @whatItDoes Represents router configuration.
|
* @whatItDoes Represents router configuration.
|
||||||
*
|
*
|
||||||
|
@ -35,6 +38,9 @@ import {UrlSegment, UrlSegmentGroup} from './url_tree';
|
||||||
* - `data` is additional data provided to the component via `ActivatedRoute`.
|
* - `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
|
* - `resolve` is a map of DI tokens used to look up data resolvers. See {@link Resolve} for more
|
||||||
* info.
|
* 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.
|
* - `children` is an array of child route definitions.
|
||||||
* - `loadChildren` is a reference to lazy loaded child routes. See {@link LoadChildren} for more
|
* - `loadChildren` is a reference to lazy loaded child routes. See {@link LoadChildren} for more
|
||||||
* info.
|
* info.
|
||||||
|
@ -327,6 +333,13 @@ export type LoadChildren = string | LoadChildrenCallback;
|
||||||
*/
|
*/
|
||||||
export type QueryParamsHandling = 'merge' | 'preserve' | '';
|
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.
|
* See {@link Routes} for more details.
|
||||||
* @stable
|
* @stable
|
||||||
|
@ -346,6 +359,7 @@ export interface Route {
|
||||||
resolve?: ResolveData;
|
resolve?: ResolveData;
|
||||||
children?: Routes;
|
children?: Routes;
|
||||||
loadChildren?: LoadChildren;
|
loadChildren?: LoadChildren;
|
||||||
|
runGuardsAndResolvers?: RunGuardsAndResolvers;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function validateConfig(config: Routes, parentPath: string = ''): void {
|
export function validateConfig(config: Routes, parentPath: string = ''): void {
|
||||||
|
@ -362,8 +376,8 @@ function validateNode(route: Route, fullPath: string): void {
|
||||||
throw new Error(`
|
throw new Error(`
|
||||||
Invalid configuration of route '${fullPath}': Encountered undefined route.
|
Invalid configuration of route '${fullPath}': Encountered undefined route.
|
||||||
The reason might be an extra comma.
|
The reason might be an extra comma.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
{ path: '', redirectTo: '/dashboard', pathMatch: 'full' },
|
{ path: '', redirectTo: '/dashboard', pathMatch: 'full' },
|
||||||
{ path: 'dashboard', component: DashboardComponent },, << two commas
|
{ path: 'dashboard', component: DashboardComponent },, << two commas
|
||||||
|
|
|
@ -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 {RouterLink, RouterLinkWithHref} from './directives/router_link';
|
||||||
export {RouterLinkActive} from './directives/router_link_active';
|
export {RouterLinkActive} from './directives/router_link_active';
|
||||||
export {RouterOutlet} from './directives/router_outlet';
|
export {RouterOutlet} from './directives/router_outlet';
|
||||||
|
|
|
@ -22,7 +22,7 @@ import {mergeMap} from 'rxjs/operator/mergeMap';
|
||||||
import {reduce} from 'rxjs/operator/reduce';
|
import {reduce} from 'rxjs/operator/reduce';
|
||||||
|
|
||||||
import {applyRedirects} from './apply_redirects';
|
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 {createRouterState} from './create_router_state';
|
||||||
import {createUrlTree} from './create_url_tree';
|
import {createUrlTree} from './create_url_tree';
|
||||||
import {RouterOutlet} from './directives/router_outlet';
|
import {RouterOutlet} from './directives/router_outlet';
|
||||||
|
@ -35,7 +35,7 @@ import {ActivatedRoute, ActivatedRouteSnapshot, RouterState, RouterStateSnapshot
|
||||||
import {PRIMARY_OUTLET, Params, isNavigationCancelingError} from './shared';
|
import {PRIMARY_OUTLET, Params, isNavigationCancelingError} from './shared';
|
||||||
import {DefaultUrlHandlingStrategy, UrlHandlingStrategy} from './url_handling_strategy';
|
import {DefaultUrlHandlingStrategy, UrlHandlingStrategy} from './url_handling_strategy';
|
||||||
import {UrlSerializer, UrlTree, containsTree, createEmptyUrlTree} from './url_tree';
|
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';
|
import {TreeNode} from './utils/tree';
|
||||||
|
|
||||||
declare let Zone: any;
|
declare let Zone: any;
|
||||||
|
@ -180,7 +180,6 @@ type NavigationParams = {
|
||||||
source: NavigationSource,
|
source: NavigationSource,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Does not detach any subtrees. Reuses routes as long as their route config is the same.
|
* 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
|
// reusing the node
|
||||||
if (curr && future._routeConfig === curr._routeConfig) {
|
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));
|
this.checks.push(new CanDeactivate(outlet.component, curr), new CanActivate(futurePath));
|
||||||
} else {
|
} else {
|
||||||
// we need to set the data
|
// 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(
|
private deactiveRouteAndItsChildren(
|
||||||
route: TreeNode<ActivatedRouteSnapshot>, outlet: RouterOutlet): void {
|
route: TreeNode<ActivatedRouteSnapshot>, outlet: RouterOutlet): void {
|
||||||
const prevChildren: {[key: string]: any} = nodeChildrenAsMap(route);
|
const prevChildren: {[key: string]: any} = nodeChildrenAsMap(route);
|
||||||
|
|
|
@ -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 {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 {RouterPreloader} from '../src/router_preloader';
|
||||||
import {forEach} from '../src/utils/collection';
|
import {forEach, shallowEqual} from '../src/utils/collection';
|
||||||
import {RouterTestingModule, SpyNgModuleFactoryLoader} from '../testing';
|
import {RouterTestingModule, SpyNgModuleFactoryLoader} from '../testing';
|
||||||
|
|
||||||
describe('Integration', () => {
|
describe('Integration', () => {
|
||||||
|
@ -1527,6 +1527,125 @@ describe('Integration', () => {
|
||||||
expect(fixture.nativeElement).toHaveText('simple');
|
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', () => {
|
describe('CanDeactivate', () => {
|
||||||
|
|
|
@ -115,6 +115,6 @@ function checkResolveData(
|
||||||
|
|
||||||
function createActivatedRouteSnapshot(cmp: string, extra: any = {}): ActivatedRouteSnapshot {
|
function createActivatedRouteSnapshot(cmp: string, extra: any = {}): ActivatedRouteSnapshot {
|
||||||
return new 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);
|
extra.resolve);
|
||||||
}
|
}
|
|
@ -197,6 +197,7 @@ export interface Route {
|
||||||
pathMatch?: string;
|
pathMatch?: string;
|
||||||
redirectTo?: string;
|
redirectTo?: string;
|
||||||
resolve?: ResolveData;
|
resolve?: ResolveData;
|
||||||
|
runGuardsAndResolvers?: RunGuardsAndResolvers;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @experimental */
|
/** @experimental */
|
||||||
|
@ -376,6 +377,9 @@ export declare class RoutesRecognized {
|
||||||
toString(): string;
|
toString(): string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @experimental */
|
||||||
|
export declare type RunGuardsAndResolvers = 'paramsChange' | 'paramsOrQueryParamsChange' | 'always';
|
||||||
|
|
||||||
/** @experimental */
|
/** @experimental */
|
||||||
export declare abstract class UrlHandlingStrategy {
|
export declare abstract class UrlHandlingStrategy {
|
||||||
abstract extract(url: UrlTree): UrlTree;
|
abstract extract(url: UrlTree): UrlTree;
|
||||||
|
|
Loading…
Reference in New Issue