feat(router): add router-level events for GuardsCheck and Resolve (#17601)

This commit is contained in:
Jason Aden 2017-07-01 10:30:17 -07:00 committed by GitHub
parent b479ed9407
commit 8a1a989a1c
7 changed files with 246 additions and 30 deletions

View File

@ -128,18 +128,114 @@ export class RouteConfigLoadEnd {
}
/**
* @whatItDoes Represents a router event.
* @whatItDoes Represents the start of the Guard phase of routing.
*
* @experimental
*/
export class GuardsCheckStart {
constructor(
/** @docsNotRequired */
public id: number,
/** @docsNotRequired */
public url: string,
/** @docsNotRequired */
public urlAfterRedirects: string,
/** @docsNotRequired */
public state: RouterStateSnapshot) {}
toString(): string {
return `GuardsCheckStart(id: ${this.id}, url: '${this.url}', urlAfterRedirects: '${this.urlAfterRedirects}', state: ${this.state})`;
}
}
/**
* @whatItDoes Represents the end of the Guard phase of routing.
*
* @experimental
*/
export class GuardsCheckEnd {
constructor(
/** @docsNotRequired */
public id: number,
/** @docsNotRequired */
public url: string,
/** @docsNotRequired */
public urlAfterRedirects: string,
/** @docsNotRequired */
public state: RouterStateSnapshot,
/** @docsNotRequired */
public shouldActivate: boolean) {}
toString(): string {
return `GuardsCheckEnd(id: ${this.id}, url: '${this.url}', urlAfterRedirects: '${this.urlAfterRedirects}', state: ${this.state}, shouldActivate: ${this.shouldActivate})`;
}
}
/**
* @whatItDoes Represents the start of the Resolve phase of routing. The timing of this
* event may change, thus it's experimental. In the current iteration it will run
* in the "resolve" phase whether there's things to resolve or not. In the future this
* behavior may change to only run when there are things to be resolved.
*
* @experimental
*/
export class ResolveStart {
constructor(
/** @docsNotRequired */
public id: number,
/** @docsNotRequired */
public url: string,
/** @docsNotRequired */
public urlAfterRedirects: string,
/** @docsNotRequired */
public state: RouterStateSnapshot) {}
toString(): string {
return `ResolveStart(id: ${this.id}, url: '${this.url}', urlAfterRedirects: '${this.urlAfterRedirects}', state: ${this.state})`;
}
}
/**
* @whatItDoes Represents the end of the Resolve phase of routing. See note on
* {@link ResolveStart} for use of this experimental API.
*
* @experimental
*/
export class ResolveEnd {
constructor(
/** @docsNotRequired */
public id: number,
/** @docsNotRequired */
public url: string,
/** @docsNotRequired */
public urlAfterRedirects: string,
/** @docsNotRequired */
public state: RouterStateSnapshot) {}
toString(): string {
return `ResolveEnd(id: ${this.id}, url: '${this.url}', urlAfterRedirects: '${this.urlAfterRedirects}', state: ${this.state})`;
}
}
/**
* @whatItDoes Represents a router event, allowing you to track the lifecycle of the router.
*
* The sequence of router events is:
*
* One of:
* - {@link NavigationStart},
* - {@link RouteConfigLoadStart},
* - {@link RouteConfigLoadEnd},
* - {@link RoutesRecognized},
* - {@link GuardsCheckStart},
* - {@link GuardsCheckEnd},
* - {@link ResolveStart},
* - {@link ResolveEnd},
* - {@link NavigationEnd},
* - {@link NavigationCancel},
* - {@link NavigationError},
* - {@link RoutesRecognized},
* - {@link RouteConfigLoadStart},
* - {@link RouteConfigLoadEnd}
* - {@link NavigationError}
*
* @stable
*/
export type Event = NavigationStart | NavigationEnd | NavigationCancel | NavigationError |
RoutesRecognized | RouteConfigLoadStart | RouteConfigLoadEnd;
RoutesRecognized | RouteConfigLoadStart | RouteConfigLoadEnd | GuardsCheckStart |
GuardsCheckEnd | ResolveStart | ResolveEnd;

View File

@ -11,7 +11,7 @@ export {Data, LoadChildren, LoadChildrenCallback, ResolveData, Route, Routes, Ru
export {RouterLink, RouterLinkWithHref} from './directives/router_link';
export {RouterLinkActive} from './directives/router_link_active';
export {RouterOutlet} from './directives/router_outlet';
export {Event, NavigationCancel, NavigationEnd, NavigationError, NavigationStart, RouteConfigLoadEnd, RouteConfigLoadStart, RoutesRecognized} from './events';
export {Event, GuardsCheckEnd, GuardsCheckStart, NavigationCancel, NavigationEnd, NavigationError, NavigationStart, ResolveEnd, ResolveStart, RouteConfigLoadEnd, RouteConfigLoadStart, RoutesRecognized} from './events';
export {CanActivate, CanActivateChild, CanDeactivate, CanLoad, Resolve} from './interfaces';
export {DetachedRouteHandle, RouteReuseStrategy} from './route_reuse_strategy';
export {NavigationExtras, Router} from './router';

View File

@ -25,7 +25,7 @@ import {applyRedirects} from './apply_redirects';
import {LoadedRouterConfig, QueryParamsHandling, ResolveData, Route, Routes, RunGuardsAndResolvers, validateConfig} from './config';
import {createRouterState} from './create_router_state';
import {createUrlTree} from './create_url_tree';
import {Event, NavigationCancel, NavigationEnd, NavigationError, NavigationStart, RouteConfigLoadEnd, RouteConfigLoadStart, RoutesRecognized} from './events';
import {Event, GuardsCheckEnd, GuardsCheckStart, NavigationCancel, NavigationEnd, NavigationError, NavigationStart, ResolveEnd, ResolveStart, RouteConfigLoadEnd, RouteConfigLoadStart, RoutesRecognized} from './events';
import {recognize} from './recognize';
import {DefaultRouteReuseStrategy, DetachedRouteHandleInternal, RouteReuseStrategy} from './route_reuse_strategy';
import {RouterConfigLoader} from './router_config_loader';
@ -639,16 +639,29 @@ export class Router {
({appliedUrl, snapshot}: {appliedUrl: string, snapshot: RouterStateSnapshot}) => {
if (this.navigationId !== id) return of (false);
this.triggerEvent(
new GuardsCheckStart(id, this.serializeUrl(url), appliedUrl, snapshot));
return map.call(preActivation.checkGuards(), (shouldActivate: boolean) => {
this.triggerEvent(new GuardsCheckEnd(
id, this.serializeUrl(url), appliedUrl, snapshot, shouldActivate));
return {appliedUrl: appliedUrl, snapshot: snapshot, shouldActivate: shouldActivate};
});
});
const preactivationResolveData$ = mergeMap.call(preactivationCheckGuards$, (p: any) => {
const preactivationResolveData$ = mergeMap.call(
preactivationCheckGuards$,
(p: {appliedUrl: string, snapshot: RouterStateSnapshot, shouldActivate: boolean}) => {
if (this.navigationId !== id) return of (false);
if (p.shouldActivate) {
return map.call(preActivation.resolveData(), () => p);
if (p.shouldActivate && preActivation.isActivating()) {
this.triggerEvent(
new ResolveStart(id, this.serializeUrl(url), p.appliedUrl, p.snapshot));
return map.call(preActivation.resolveData(), () => {
this.triggerEvent(
new ResolveEnd(id, this.serializeUrl(url), p.appliedUrl, p.snapshot));
return p;
});
} else {
return of (p);
}
@ -772,8 +785,12 @@ export class PreActivation {
this.traverseChildRoutes(futureRoot, currRoot, parentContexts, [futureRoot.value]);
}
// TODO(jasonaden): Refactor checkGuards and resolveData so they can collect the checks
// and guards before mapping into the observable. Likely remove the observable completely
// and make these pure functions so they are more predictable and don't rely on so much
// external state.
checkGuards(): Observable<boolean> {
if (this.canDeactivateChecks.length === 0 && this.canActivateChecks.length === 0) {
if (!this.isDeactivating() && !this.isActivating()) {
return of (true);
}
const canDeactivate$ = this.runCanDeactivateChecks();
@ -783,13 +800,17 @@ export class PreActivation {
}
resolveData(): Observable<any> {
if (this.canActivateChecks.length === 0) return of (null);
if (!this.isActivating()) return of (null);
const checks$ = from(this.canActivateChecks);
const runningChecks$ =
concatMap.call(checks$, (check: CanActivate) => this.runResolve(check.route));
return reduce.call(runningChecks$, (_: any, __: any) => _);
}
isDeactivating(): boolean { return this.canDeactivateChecks.length !== 0; }
isActivating(): boolean { return this.canActivateChecks.length !== 0; }
private traverseChildRoutes(
futureNode: TreeNode<ActivatedRouteSnapshot>, currNode: TreeNode<ActivatedRouteSnapshot>|null,
contexts: ChildrenOutletContexts|null, futurePath: ActivatedRouteSnapshot[]): void {

View File

@ -96,6 +96,9 @@ export class RouterPreloader implements OnDestroy {
return this.processRoutes(ngModule, this.router.config);
}
// TODO(jasonaden): This class relies on code external to the class to call setUpPreloading. If
// this hasn't been done, ngOnDestroy will fail as this.subscription will be undefined. This
// should be refactored.
ngOnDestroy(): void { this.subscription.unsubscribe(); }
private processRoutes(ngModule: NgModuleRef<any>, routes: Routes): Observable<void> {

View File

@ -82,8 +82,10 @@ describe('bootstrap', () => {
const router = res.injector.get(Router);
const data = router.routerState.snapshot.root.firstChild !.data;
expect(data['test']).toEqual('test-data');
expect(log).toEqual(
['TestModule', 'NavigationStart', 'RoutesRecognized', 'RootCmp', 'NavigationEnd']);
expect(log).toEqual([
'TestModule', 'NavigationStart', 'RoutesRecognized', 'GuardsCheckStart', 'GuardsCheckEnd',
'ResolveStart', 'ResolveEnd', 'RootCmp', 'NavigationEnd'
]);
done();
});
});
@ -116,8 +118,11 @@ describe('bootstrap', () => {
platformBrowserDynamic([]).bootstrapModule(TestModule).then(res => {
const router = res.injector.get(Router);
expect(router.routerState.snapshot.root.firstChild).toBeNull();
// NavigationEnd has not been emitted yet because bootstrap returned too early
expect(log).toEqual(['TestModule', 'RootCmp', 'NavigationStart', 'RoutesRecognized']);
// ResolveEnd has not been emitted yet because bootstrap returned too early
expect(log).toEqual([
'TestModule', 'RootCmp', 'NavigationStart', 'RoutesRecognized', 'GuardsCheckStart',
'GuardsCheckEnd', 'ResolveStart'
]);
router.events.subscribe((e) => {
if (e instanceof NavigationEnd) {

View File

@ -11,7 +11,7 @@ import {ChangeDetectionStrategy, Component, Injectable, NgModule, NgModuleFactor
import {ComponentFixture, TestBed, fakeAsync, inject, tick} from '@angular/core/testing';
import {By} from '@angular/platform-browser/src/dom/debug/by';
import {expect} from '@angular/platform-browser/testing/src/matchers';
import {ActivatedRoute, ActivatedRouteSnapshot, CanActivate, CanDeactivate, DetachedRouteHandle, Event, NavigationCancel, NavigationEnd, NavigationError, NavigationStart, PRIMARY_OUTLET, ParamMap, Params, PreloadAllModules, PreloadingStrategy, Resolve, RouteConfigLoadEnd, RouteConfigLoadStart, RouteReuseStrategy, Router, RouterModule, RouterPreloader, RouterStateSnapshot, RoutesRecognized, RunGuardsAndResolvers, UrlHandlingStrategy, UrlSegmentGroup, UrlTree} from '@angular/router';
import {ActivatedRoute, ActivatedRouteSnapshot, CanActivate, CanDeactivate, DetachedRouteHandle, Event, GuardsCheckEnd, GuardsCheckStart, NavigationCancel, NavigationEnd, NavigationError, NavigationStart, PRIMARY_OUTLET, ParamMap, Params, PreloadAllModules, PreloadingStrategy, Resolve, ResolveEnd, ResolveStart, RouteConfigLoadEnd, RouteConfigLoadStart, RouteReuseStrategy, Router, RouterModule, RouterPreloader, RouterStateSnapshot, RoutesRecognized, RunGuardsAndResolvers, UrlHandlingStrategy, UrlSegmentGroup, UrlTree} from '@angular/router';
import {Observable} from 'rxjs/Observable';
import {map} from 'rxjs/operator/map';
@ -422,9 +422,13 @@ describe('Integration', () => {
expectEvents(recordedEvents, [
[NavigationStart, '/team/22/user/victor'], [RoutesRecognized, '/team/22/user/victor'],
[GuardsCheckStart, '/team/22/user/victor'], [GuardsCheckEnd, '/team/22/user/victor'],
[ResolveStart, '/team/22/user/victor'], [ResolveEnd, '/team/22/user/victor'],
[NavigationEnd, '/team/22/user/victor'],
[NavigationStart, '/team/22/user/fedor'], [RoutesRecognized, '/team/22/user/fedor'],
[GuardsCheckStart, '/team/22/user/fedor'], [GuardsCheckEnd, '/team/22/user/fedor'],
[ResolveStart, '/team/22/user/fedor'], [ResolveEnd, '/team/22/user/fedor'],
[NavigationEnd, '/team/22/user/fedor']
]);
})));
@ -681,11 +685,14 @@ describe('Integration', () => {
expectEvents(recordedEvents, [
[NavigationStart, '/user/init'], [RoutesRecognized, '/user/init'],
[NavigationEnd, '/user/init'],
[GuardsCheckStart, '/user/init'], [GuardsCheckEnd, '/user/init'],
[ResolveStart, '/user/init'], [ResolveEnd, '/user/init'], [NavigationEnd, '/user/init'],
[NavigationStart, '/user/victor'], [NavigationCancel, '/user/victor'],
[NavigationStart, '/user/fedor'], [RoutesRecognized, '/user/fedor'],
[GuardsCheckStart, '/user/fedor'], [GuardsCheckEnd, '/user/fedor'],
[ResolveStart, '/user/fedor'], [ResolveEnd, '/user/fedor'],
[NavigationEnd, '/user/fedor']
]);
})));
@ -712,6 +719,8 @@ describe('Integration', () => {
[NavigationStart, '/invalid'], [NavigationError, '/invalid'],
[NavigationStart, '/user/fedor'], [RoutesRecognized, '/user/fedor'],
[GuardsCheckStart, '/user/fedor'], [GuardsCheckEnd, '/user/fedor'],
[ResolveStart, '/user/fedor'], [ResolveEnd, '/user/fedor'],
[NavigationEnd, '/user/fedor']
]);
})));
@ -965,6 +974,7 @@ describe('Integration', () => {
expectEvents(recordedEvents, [
[NavigationStart, '/simple'], [RoutesRecognized, '/simple'],
[GuardsCheckStart, '/simple'], [GuardsCheckEnd, '/simple'], [ResolveStart, '/simple'],
[NavigationError, '/simple']
]);
@ -1382,8 +1392,10 @@ describe('Integration', () => {
expect(location.path()).toEqual('/');
expectEvents(recordedEvents, [
[NavigationStart, '/team/22'], [RoutesRecognized, '/team/22'],
[GuardsCheckStart, '/team/22'], [GuardsCheckEnd, '/team/22'],
[NavigationCancel, '/team/22']
]);
expect((recordedEvents[3] as GuardsCheckEnd).shouldActivate).toBe(false);
})));
});
@ -2234,6 +2246,7 @@ describe('Integration', () => {
expectEvents(recordedEvents, [
[NavigationStart, '/lazyFalse/loaded'],
// [GuardsCheckStart, '/lazyFalse/loaded'],
[NavigationCancel, '/lazyFalse/loaded'],
]);
@ -2250,6 +2263,10 @@ describe('Integration', () => {
[RouteConfigLoadStart],
[RouteConfigLoadEnd],
[RoutesRecognized, '/lazyTrue/loaded'],
[GuardsCheckStart, '/lazyTrue/loaded'],
[GuardsCheckEnd, '/lazyTrue/loaded'],
[ResolveStart, '/lazyTrue/loaded'],
[ResolveEnd, '/lazyTrue/loaded'],
[NavigationEnd, '/lazyTrue/loaded'],
]);
})));
@ -2274,8 +2291,14 @@ describe('Integration', () => {
expect(location.path()).toEqual('/blank');
expectEvents(recordedEvents, [
[NavigationStart, '/lazyFalse/loaded'], [NavigationCancel, '/lazyFalse/loaded'],
[NavigationStart, '/blank'], [RoutesRecognized, '/blank'], [NavigationEnd, '/blank']
[NavigationStart, '/lazyFalse/loaded'],
// No GuardCheck events as `canLoad` is a special guard that's not actually part of the
// guard lifecycle.
[NavigationCancel, '/lazyFalse/loaded'],
[NavigationStart, '/blank'], [RoutesRecognized, '/blank'],
[GuardsCheckStart, '/blank'], [GuardsCheckEnd, '/blank'], [ResolveStart, '/blank'],
[ResolveEnd, '/blank'], [NavigationEnd, '/blank']
]);
})));
@ -3174,6 +3197,8 @@ describe('Integration', () => {
expect(location.path()).toEqual('/include/user/kate');
expectEvents(events, [
[NavigationStart, '/include/user/kate'], [RoutesRecognized, '/include/user/kate'],
[GuardsCheckStart, '/include/user/kate'], [GuardsCheckEnd, '/include/user/kate'],
[ResolveStart, '/include/user/kate'], [ResolveEnd, '/include/user/kate'],
[NavigationEnd, '/include/user/kate']
]);
expect(fixture.nativeElement).toHaveText('team [ user kate, right: ]');
@ -3186,8 +3211,10 @@ describe('Integration', () => {
expect(location.path()).toEqual('/exclude/one');
expect(Object.keys(router.routerState.root.children).length).toEqual(0);
expect(fixture.nativeElement).toHaveText('');
expectEvents(
events, [[NavigationStart, '/exclude/one'], [NavigationEnd, '/exclude/one']]);
expectEvents(events, [
[NavigationStart, '/exclude/one'], [GuardsCheckStart, '/exclude/one'],
[GuardsCheckEnd, '/exclude/one'], [NavigationEnd, '/exclude/one']
]);
events.splice(0);
// another unsupported URL
@ -3206,6 +3233,8 @@ describe('Integration', () => {
expectEvents(events, [
[NavigationStart, '/include/simple'], [RoutesRecognized, '/include/simple'],
[GuardsCheckStart, '/include/simple'], [GuardsCheckEnd, '/include/simple'],
[ResolveStart, '/include/simple'], [ResolveEnd, '/include/simple'],
[NavigationEnd, '/include/simple']
]);
})));
@ -3231,6 +3260,8 @@ describe('Integration', () => {
expect(location.path()).toEqual('/include/user/kate(aux:excluded)');
expectEvents(events, [
[NavigationStart, '/include/user/kate'], [RoutesRecognized, '/include/user/kate'],
[GuardsCheckStart, '/include/user/kate'], [GuardsCheckEnd, '/include/user/kate'],
[ResolveStart, '/include/user/kate'], [ResolveEnd, '/include/user/kate'],
[NavigationEnd, '/include/user/kate']
]);
events.splice(0);
@ -3245,6 +3276,8 @@ describe('Integration', () => {
expect(location.path()).toEqual('/include/simple(aux:excluded2)');
expectEvents(events, [
[NavigationStart, '/include/simple'], [RoutesRecognized, '/include/simple'],
[GuardsCheckStart, '/include/simple'], [GuardsCheckEnd, '/include/simple'],
[ResolveStart, '/include/simple'], [ResolveEnd, '/include/simple'],
[NavigationEnd, '/include/simple']
]);
})));

View File

@ -87,7 +87,7 @@ export declare class DefaultUrlSerializer implements UrlSerializer {
export declare type DetachedRouteHandle = {};
/** @stable */
export declare type Event = NavigationStart | NavigationEnd | NavigationCancel | NavigationError | RoutesRecognized | RouteConfigLoadStart | RouteConfigLoadEnd;
export declare type Event = NavigationStart | NavigationEnd | NavigationCancel | NavigationError | RoutesRecognized | RouteConfigLoadStart | RouteConfigLoadEnd | GuardsCheckStart | GuardsCheckEnd | ResolveStart | ResolveEnd;
/** @stable */
export interface ExtraOptions {
@ -98,6 +98,36 @@ export interface ExtraOptions {
useHash?: boolean;
}
/** @experimental */
export declare class GuardsCheckEnd {
id: number;
shouldActivate: boolean;
state: RouterStateSnapshot;
url: string;
urlAfterRedirects: string;
constructor(
id: number,
url: string,
urlAfterRedirects: string,
state: RouterStateSnapshot,
shouldActivate: boolean);
toString(): string;
}
/** @experimental */
export declare class GuardsCheckStart {
id: number;
state: RouterStateSnapshot;
url: string;
urlAfterRedirects: string;
constructor(
id: number,
url: string,
urlAfterRedirects: string,
state: RouterStateSnapshot);
toString(): string;
}
/** @stable */
export declare type LoadChildren = string | LoadChildrenCallback;
@ -215,6 +245,34 @@ export declare type ResolveData = {
[name: string]: any;
};
/** @experimental */
export declare class ResolveEnd {
id: number;
state: RouterStateSnapshot;
url: string;
urlAfterRedirects: string;
constructor(
id: number,
url: string,
urlAfterRedirects: string,
state: RouterStateSnapshot);
toString(): string;
}
/** @experimental */
export declare class ResolveStart {
id: number;
state: RouterStateSnapshot;
url: string;
urlAfterRedirects: string;
constructor(
id: number,
url: string,
urlAfterRedirects: string,
state: RouterStateSnapshot);
toString(): string;
}
/** @stable */
export interface Route {
canActivate?: any[];