feat(router): add ActivationStart/End events

This commit is contained in:
Jason Aden 2017-09-06 11:00:32 -07:00 committed by Matias Niemelä
parent 4a0466e574
commit 8f7915022c
8 changed files with 169 additions and 40 deletions

View File

@ -288,6 +288,38 @@ export class ChildActivationEnd {
}
}
/**
* @whatItDoes Represents the start of end of the Resolve phase of routing. See note on
* {@link ActivationEnd} for use of this experimental API.
*
* @experimental
*/
export class ActivationStart {
constructor(
/** @docsNotRequired */
public snapshot: ActivatedRouteSnapshot) {}
toString(): string {
const path = this.snapshot.routeConfig && this.snapshot.routeConfig.path || '';
return `ChildActivationStart(path: '${path}')`;
}
}
/**
* @whatItDoes Represents the start of end of the Resolve phase of routing. See note on
* {@link ActivationStart} for use of this experimental API.
*
* @experimental
*/
export class ActivationEnd {
constructor(
/** @docsNotRequired */
public snapshot: ActivatedRouteSnapshot) {}
toString(): string {
const path = this.snapshot.routeConfig && this.snapshot.routeConfig.path || '';
return `ChildActivationEnd(path: '${path}')`;
}
}
/**
* @whatItDoes Represents a router event, allowing you to track the lifecycle of the router.
*
@ -310,4 +342,5 @@ export class ChildActivationEnd {
* @stable
*/
export type Event = RouterEvent | RouteConfigLoadStart | RouteConfigLoadEnd | ChildActivationStart |
ChildActivationEnd;
ChildActivationEnd | ActivationStart | ActivationEnd;
;

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 {ChildActivationEnd, ChildActivationStart, Event, GuardsCheckEnd, GuardsCheckStart, NavigationCancel, NavigationEnd, NavigationError, NavigationStart, ResolveEnd, ResolveStart, RouteConfigLoadEnd, RouteConfigLoadStart, RouterEvent, RoutesRecognized} from './events';
export {ActivationEnd, ActivationStart, ChildActivationEnd, ChildActivationStart, Event, GuardsCheckEnd, GuardsCheckStart, NavigationCancel, NavigationEnd, NavigationError, NavigationStart, ResolveEnd, ResolveStart, RouteConfigLoadEnd, RouteConfigLoadStart, RouterEvent, 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

@ -19,7 +19,7 @@ import {mergeMap} from 'rxjs/operator/mergeMap';
import {reduce} from 'rxjs/operator/reduce';
import {LoadedRouterConfig, ResolveData, RunGuardsAndResolvers} from './config';
import {ChildActivationStart, Event} from './events';
import {ActivationStart, ChildActivationStart, Event} from './events';
import {ChildrenOutletContexts, OutletContext} from './router_outlet_context';
import {ActivatedRouteSnapshot, RouterStateSnapshot, equalParamsAndUrlSegments, inheritedParamsDataResolve} from './router_state';
import {andObservables, forEach, shallowEqual, wrapIntoObservable} from './utils/collection';
@ -201,14 +201,30 @@ export class PreActivation {
private runCanActivateChecks(): Observable<boolean> {
const checks$ = from(this.canActivateChecks);
const runningChecks$ = concatMap.call(
checks$, (check: CanActivate) => andObservables(from([
this.fireChildActivationStart(check.route.parent),
this.runCanActivateChild(check.path), this.runCanActivate(check.route)
])));
checks$,
(check: CanActivate) => andObservables(from([
this.fireChildActivationStart(check.route.parent), this.fireActivationStart(check.route),
this.runCanActivateChild(check.path), this.runCanActivate(check.route)
])));
return every.call(runningChecks$, (result: boolean) => result === true);
// this.fireChildActivationStart(check.path),
}
/**
* This should fire off `ChildActivationStart` events for each route being activated at this
* level.
* In other words, if you're activating `a` and `b` below, `path` will contain the
* `ActivatedRouteSnapshot`s for both and we will fire `ChildActivationStart` for both. Always
* return
* `true` so checks continue to run.
*/
private fireActivationStart(snapshot: ActivatedRouteSnapshot|null): Observable<boolean> {
if (snapshot !== null && this.forwardEvent) {
this.forwardEvent(new ActivationStart(snapshot));
}
return of (true);
}
/**
* This should fire off `ChildActivationStart` events for each route being activated at this
* level.
@ -223,6 +239,7 @@ export class PreActivation {
}
return of (true);
}
private runCanActivate(future: ActivatedRouteSnapshot): Observable<boolean> {
const canActivate = future._routeConfig ? future._routeConfig.canActivate : null;
if (!canActivate || canActivate.length === 0) return of (true);

View File

@ -21,7 +21,7 @@ import {applyRedirects} from './apply_redirects';
import {LoadedRouterConfig, QueryParamsHandling, Route, Routes, validateConfig} from './config';
import {createRouterState} from './create_router_state';
import {createUrlTree} from './create_url_tree';
import {ChildActivationEnd, Event, GuardsCheckEnd, GuardsCheckStart, NavigationCancel, NavigationEnd, NavigationError, NavigationStart, ResolveEnd, ResolveStart, RouteConfigLoadEnd, RouteConfigLoadStart, RoutesRecognized} from './events';
import {ActivationEnd, ChildActivationEnd, Event, GuardsCheckEnd, GuardsCheckStart, NavigationCancel, NavigationEnd, NavigationError, NavigationStart, ResolveEnd, ResolveStart, RouteConfigLoadEnd, RouteConfigLoadStart, RoutesRecognized} from './events';
import {PreActivation} from './pre_activation';
import {recognize} from './recognize';
import {DefaultRouteReuseStrategy, DetachedRouteHandleInternal, RouteReuseStrategy} from './route_reuse_strategy';
@ -862,8 +862,10 @@ class ActivateRoutes {
futureNode: TreeNode<ActivatedRoute>, currNode: TreeNode<ActivatedRoute>|null,
contexts: ChildrenOutletContexts): void {
const children: {[outlet: string]: any} = nodeChildrenAsMap(currNode);
futureNode.children.forEach(
c => { this.activateRoutes(c, children[c.value.outlet], contexts); });
futureNode.children.forEach(c => {
this.activateRoutes(c, children[c.value.outlet], contexts);
this.forwardEvent(new ActivationEnd(c.value.snapshot));
});
if (futureNode.children.length) {
this.forwardEvent(new ChildActivationEnd(futureNode.value.snapshot));
}

View File

@ -84,8 +84,8 @@ describe('bootstrap', () => {
expect(data['test']).toEqual('test-data');
expect(log).toEqual([
'TestModule', 'NavigationStart', 'RoutesRecognized', 'GuardsCheckStart',
'ChildActivationStart', 'GuardsCheckEnd', 'ResolveStart', 'ResolveEnd', 'RootCmp',
'ChildActivationEnd', 'NavigationEnd'
'ChildActivationStart', 'ActivationStart', 'GuardsCheckEnd', 'ResolveStart', 'ResolveEnd',
'RootCmp', 'ActivationEnd', 'ChildActivationEnd', 'NavigationEnd'
]);
done();
});
@ -122,7 +122,7 @@ describe('bootstrap', () => {
// ResolveEnd has not been emitted yet because bootstrap returned too early
expect(log).toEqual([
'TestModule', 'RootCmp', 'NavigationStart', 'RoutesRecognized', 'GuardsCheckStart',
'ChildActivationStart', 'GuardsCheckEnd', 'ResolveStart'
'ChildActivationStart', 'ActivationStart', 'GuardsCheckEnd', 'ResolveStart'
]);
router.events.subscribe((e) => {

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, ChildActivationEnd, ChildActivationStart, DetachedRouteHandle, Event, GuardsCheckEnd, GuardsCheckStart, NavigationCancel, NavigationEnd, NavigationError, NavigationStart, PRIMARY_OUTLET, ParamMap, Params, PreloadAllModules, PreloadingStrategy, Resolve, ResolveEnd, ResolveStart, RouteConfigLoadEnd, RouteConfigLoadStart, RouteReuseStrategy, Router, RouterEvent, RouterModule, RouterPreloader, RouterStateSnapshot, RoutesRecognized, RunGuardsAndResolvers, UrlHandlingStrategy, UrlSegmentGroup, UrlTree} from '@angular/router';
import {ActivatedRoute, ActivatedRouteSnapshot, ActivationEnd, ActivationStart, CanActivate, CanDeactivate, ChildActivationEnd, ChildActivationStart, DetachedRouteHandle, Event, GuardsCheckEnd, GuardsCheckStart, NavigationCancel, NavigationEnd, NavigationError, NavigationStart, PRIMARY_OUTLET, ParamMap, Params, PreloadAllModules, PreloadingStrategy, Resolve, ResolveEnd, ResolveStart, RouteConfigLoadEnd, RouteConfigLoadStart, RouteReuseStrategy, Router, RouterEvent, RouterModule, RouterPreloader, RouterStateSnapshot, RoutesRecognized, RunGuardsAndResolvers, UrlHandlingStrategy, UrlSegmentGroup, UrlTree} from '@angular/router';
import {Observable} from 'rxjs/Observable';
import {Observer} from 'rxjs/Observer';
import {of } from 'rxjs/observable/of';
@ -705,18 +705,31 @@ describe('Integration', () => {
expect(user.recordedParams).toEqual([{name: 'init'}, {name: 'fedor'}]);
expectEvents(recordedEvents, [
[NavigationStart, '/user/init'], [RoutesRecognized, '/user/init'],
[GuardsCheckStart, '/user/init'], [ChildActivationStart],
[GuardsCheckEnd, '/user/init'], [ResolveStart, '/user/init'],
[ResolveEnd, '/user/init'], [ChildActivationEnd],
[NavigationStart, '/user/init'],
[RoutesRecognized, '/user/init'],
[GuardsCheckStart, '/user/init'],
[ChildActivationStart],
[ActivationStart],
[GuardsCheckEnd, '/user/init'],
[ResolveStart, '/user/init'],
[ResolveEnd, '/user/init'],
[ActivationEnd],
[ChildActivationEnd],
[NavigationEnd, '/user/init'],
[NavigationStart, '/user/victor'], [NavigationCancel, '/user/victor'],
[NavigationStart, '/user/victor'],
[NavigationCancel, '/user/victor'],
[NavigationStart, '/user/fedor'], [RoutesRecognized, '/user/fedor'],
[GuardsCheckStart, '/user/fedor'], [ChildActivationStart],
[GuardsCheckEnd, '/user/fedor'], [ResolveStart, '/user/fedor'],
[ResolveEnd, '/user/fedor'], [ChildActivationEnd],
[NavigationStart, '/user/fedor'],
[RoutesRecognized, '/user/fedor'],
[GuardsCheckStart, '/user/fedor'],
[ChildActivationStart],
[ActivationStart],
[GuardsCheckEnd, '/user/fedor'],
[ResolveStart, '/user/fedor'],
[ResolveEnd, '/user/fedor'],
[ActivationEnd],
[ChildActivationEnd],
[NavigationEnd, '/user/fedor']
]);
})));
@ -743,8 +756,9 @@ describe('Integration', () => {
[NavigationStart, '/invalid'], [NavigationError, '/invalid'],
[NavigationStart, '/user/fedor'], [RoutesRecognized, '/user/fedor'],
[GuardsCheckStart, '/user/fedor'], [ChildActivationStart], [GuardsCheckEnd, '/user/fedor'],
[ResolveStart, '/user/fedor'], [ResolveEnd, '/user/fedor'], [ChildActivationEnd],
[GuardsCheckStart, '/user/fedor'], [ChildActivationStart], [ActivationStart],
[GuardsCheckEnd, '/user/fedor'], [ResolveStart, '/user/fedor'],
[ResolveEnd, '/user/fedor'], [ActivationEnd], [ChildActivationEnd],
[NavigationEnd, '/user/fedor']
]);
})));
@ -925,6 +939,8 @@ describe('Integration', () => {
expect(cmp.path.length).toEqual(2);
})));
describe('data', () => {
class ResolveSix implements Resolve<number> {
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): number { return 6; }
@ -1465,11 +1481,15 @@ describe('Integration', () => {
expect(location.path()).toEqual('/');
expectEvents(recordedEvents, [
[NavigationStart, '/team/22'], [RoutesRecognized, '/team/22'],
[GuardsCheckStart, '/team/22'], [ChildActivationStart], [GuardsCheckEnd, '/team/22'],
[NavigationCancel, '/team/22']
[NavigationStart, '/team/22'],
[RoutesRecognized, '/team/22'],
[GuardsCheckStart, '/team/22'],
[ChildActivationStart],
[ActivationStart],
[GuardsCheckEnd, '/team/22'],
[NavigationCancel, '/team/22'],
]);
expect((recordedEvents[4] as GuardsCheckEnd).shouldActivate).toBe(false);
expect((recordedEvents[5] as GuardsCheckEnd).shouldActivate).toBe(false);
})));
});
@ -2392,11 +2412,15 @@ describe('Integration', () => {
[RoutesRecognized, '/lazyTrue/loaded'],
[GuardsCheckStart, '/lazyTrue/loaded'],
[ChildActivationStart],
[ActivationStart],
[ChildActivationStart],
[ActivationStart],
[GuardsCheckEnd, '/lazyTrue/loaded'],
[ResolveStart, '/lazyTrue/loaded'],
[ResolveEnd, '/lazyTrue/loaded'],
[ActivationEnd],
[ChildActivationEnd],
[ActivationEnd],
[ChildActivationEnd],
[NavigationEnd, '/lazyTrue/loaded'],
]);
@ -2428,9 +2452,9 @@ describe('Integration', () => {
[NavigationCancel, '/lazyFalse/loaded'],
[NavigationStart, '/blank'], [RoutesRecognized, '/blank'],
[GuardsCheckStart, '/blank'], [ChildActivationStart], [GuardsCheckEnd, '/blank'],
[ResolveStart, '/blank'], [ResolveEnd, '/blank'], [ChildActivationEnd],
[NavigationEnd, '/blank']
[GuardsCheckStart, '/blank'], [ChildActivationStart], [ActivationStart],
[GuardsCheckEnd, '/blank'], [ResolveStart, '/blank'], [ResolveEnd, '/blank'],
[ActivationEnd], [ChildActivationEnd], [NavigationEnd, '/blank']
]);
})));
@ -2562,6 +2586,40 @@ describe('Integration', () => {
});
});
describe('route events', () => {
it('should fire matching (Child)ActivationStart/End events',
fakeAsync(inject([Router], (router: Router) => {
const fixture = createRoot(router, RootCmp);
router.resetConfig([{path: 'user/:name', component: UserCmp}]);
const recordedEvents: any[] = [];
router.events.forEach(e => recordedEvents.push(e));
router.navigateByUrl('/user/fedor');
advance(fixture);
expect(fixture.nativeElement).toHaveText('user fedor');
expect(recordedEvents[3] instanceof ChildActivationStart).toBe(true);
expect(recordedEvents[3].snapshot).toBe(recordedEvents[9].snapshot.root);
expect(recordedEvents[9] instanceof ChildActivationEnd).toBe(true);
expect(recordedEvents[9].snapshot).toBe(recordedEvents[9].snapshot.root);
expect(recordedEvents[4] instanceof ActivationStart).toBe(true);
expect(recordedEvents[4].snapshot.routeConfig.path).toBe('user/:name');
expect(recordedEvents[8] instanceof ActivationEnd).toBe(true);
expect(recordedEvents[8].snapshot.routeConfig.path).toBe('user/:name');
expectEvents(recordedEvents, [
[NavigationStart, '/user/fedor'], [RoutesRecognized, '/user/fedor'],
[GuardsCheckStart, '/user/fedor'], [ChildActivationStart], [ActivationStart],
[GuardsCheckEnd, '/user/fedor'], [ResolveStart, '/user/fedor'],
[ResolveEnd, '/user/fedor'], [ActivationEnd], [ChildActivationEnd],
[NavigationEnd, '/user/fedor']
]);
})));
});
describe('routerActiveLink', () => {
it('should set the class when the link is active (a tag)',
fakeAsync(inject([Router, Location], (router: Router, location: Location) => {

View File

@ -110,7 +110,9 @@ describe('Router', () => {
p.initalize(new ChildrenOutletContexts());
p.checkGuards().subscribe((x) => result = x, (e) => { throw e; });
expect(result).toBe(true);
expect(events.length).toEqual(1);
expect(events.length).toEqual(2);
expect(events[0].snapshot).toBe(events[0].snapshot.root);
expect(events[1].snapshot.routeConfig.path).toBe('child');
});
it('should run from top to bottom', () => {
@ -142,10 +144,11 @@ describe('Router', () => {
p.checkGuards().subscribe((x) => result = x, (e) => { throw e; });
expect(result).toBe(true);
expect(events.length).toEqual(3);
expect(events.length).toEqual(6);
expect(events[0].snapshot).toBe(events[0].snapshot.root);
expect(events[1].snapshot.routeConfig.path).toBe('child');
expect(events[2].snapshot.routeConfig.path).toBe('grandchild');
expect(events[2].snapshot.routeConfig.path).toBe('child');
expect(events[4].snapshot.routeConfig.path).toBe('grandchild');
expect(events[5].snapshot.routeConfig.path).toBe('great-grandchild');
});
it('should not run for unchanged routes', () => {
@ -174,7 +177,7 @@ describe('Router', () => {
p.checkGuards().subscribe((x) => result = x, (e) => { throw e; });
expect(result).toBe(true);
expect(events.length).toEqual(1);
expect(events.length).toEqual(2);
expect(events[0].snapshot).not.toBe(events[0].snapshot.root);
expect(events[0].snapshot.routeConfig.path).toBe('child');
});
@ -220,11 +223,11 @@ describe('Router', () => {
p.checkGuards().subscribe((x) => result = x, (e) => { throw e; });
expect(result).toBe(true);
expect(events.length).toEqual(2);
expect(events.length).toEqual(4);
expect(events[0] instanceof ChildActivationStart).toBe(true);
expect(events[0].snapshot).not.toBe(events[0].snapshot.root);
expect(events[0].snapshot.routeConfig.path).toBe('grandchild');
expect(events[1].snapshot.routeConfig.path).toBe('greatgrandchild');
expect(events[2].snapshot.routeConfig.path).toBe('greatgrandchild');
});
});

View File

@ -39,6 +39,22 @@ export declare class ActivatedRouteSnapshot {
toString(): string;
}
/** @experimental */
export declare class ActivationEnd {
snapshot: ActivatedRouteSnapshot;
constructor(
snapshot: ActivatedRouteSnapshot);
toString(): string;
}
/** @experimental */
export declare class ActivationStart {
snapshot: ActivatedRouteSnapshot;
constructor(
snapshot: ActivatedRouteSnapshot);
toString(): string;
}
/** @stable */
export interface CanActivate {
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean;
@ -103,7 +119,7 @@ export declare class DefaultUrlSerializer implements UrlSerializer {
export declare type DetachedRouteHandle = {};
/** @stable */
export declare type Event = RouterEvent | RouteConfigLoadStart | RouteConfigLoadEnd | ChildActivationStart | ChildActivationEnd;
export declare type Event = RouterEvent | RouteConfigLoadStart | RouteConfigLoadEnd | ChildActivationStart | ChildActivationEnd | ActivationStart | ActivationEnd;
/** @stable */
export interface ExtraOptions {