From 8f7915022c61b86194bebce9ce6591c7ced40551 Mon Sep 17 00:00:00 2001 From: Jason Aden Date: Wed, 6 Sep 2017 11:00:32 -0700 Subject: [PATCH] feat(router): add ActivationStart/End events --- packages/router/src/events.ts | 35 ++++++++- packages/router/src/index.ts | 2 +- packages/router/src/pre_activation.ts | 27 +++++-- packages/router/src/router.ts | 8 +- packages/router/test/bootstrap.spec.ts | 6 +- packages/router/test/integration.spec.ts | 98 +++++++++++++++++++----- packages/router/test/router.spec.ts | 17 ++-- tools/public_api_guard/router/index.d.ts | 18 ++++- 8 files changed, 170 insertions(+), 41 deletions(-) diff --git a/packages/router/src/events.ts b/packages/router/src/events.ts index fed7286aa734a..e9ca1b1a2a7de 100644 --- a/packages/router/src/events.ts +++ b/packages/router/src/events.ts @@ -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; +; diff --git a/packages/router/src/index.ts b/packages/router/src/index.ts index f658c9a4e8891..73377dc09ae12 100644 --- a/packages/router/src/index.ts +++ b/packages/router/src/index.ts @@ -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'; diff --git a/packages/router/src/pre_activation.ts b/packages/router/src/pre_activation.ts index 69a942d4255eb..64151dfb73f9b 100644 --- a/packages/router/src/pre_activation.ts +++ b/packages/router/src/pre_activation.ts @@ -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 { 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 { + 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 { const canActivate = future._routeConfig ? future._routeConfig.canActivate : null; if (!canActivate || canActivate.length === 0) return of (true); diff --git a/packages/router/src/router.ts b/packages/router/src/router.ts index 5a63f911d69f6..4a5b8426eff82 100644 --- a/packages/router/src/router.ts +++ b/packages/router/src/router.ts @@ -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, currNode: TreeNode|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)); } diff --git a/packages/router/test/bootstrap.spec.ts b/packages/router/test/bootstrap.spec.ts index 8812dea19ef1f..0cc8d863017bf 100644 --- a/packages/router/test/bootstrap.spec.ts +++ b/packages/router/test/bootstrap.spec.ts @@ -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) => { diff --git a/packages/router/test/integration.spec.ts b/packages/router/test/integration.spec.ts index fa47782a43293..b8ecaa03a30ca 100644 --- a/packages/router/test/integration.spec.ts +++ b/packages/router/test/integration.spec.ts @@ -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/fedor'], [RoutesRecognized, '/user/fedor'], - [GuardsCheckStart, '/user/fedor'], [ChildActivationStart], - [GuardsCheckEnd, '/user/fedor'], [ResolveStart, '/user/fedor'], - [ResolveEnd, '/user/fedor'], [ChildActivationEnd], + [NavigationStart, '/user/victor'], + [NavigationCancel, '/user/victor'], + + [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 { 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) => { diff --git a/packages/router/test/router.spec.ts b/packages/router/test/router.spec.ts index 9d3b5339d03c7..deef48e1fb471 100644 --- a/packages/router/test/router.spec.ts +++ b/packages/router/test/router.spec.ts @@ -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'); }); }); diff --git a/tools/public_api_guard/router/index.d.ts b/tools/public_api_guard/router/index.d.ts index 6eadbcd603709..7dd99781b40b9 100644 --- a/tools/public_api_guard/router/index.d.ts +++ b/tools/public_api_guard/router/index.d.ts @@ -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 | Promise | 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 {