diff --git a/packages/router/src/events.ts b/packages/router/src/events.ts index d4c7610c77..fed7286aa7 100644 --- a/packages/router/src/events.ts +++ b/packages/router/src/events.ts @@ -7,7 +7,8 @@ */ import {Route} from './config'; -import {RouterStateSnapshot} from './router_state'; +import {ActivatedRouteSnapshot, RouterStateSnapshot} from './router_state'; + /** * @whatItDoes Base for events the Router goes through, as opposed to events tied to a specific @@ -264,8 +265,11 @@ export class RouteConfigLoadEnd { export class ChildActivationStart { constructor( /** @docsNotRequired */ - public route: Route) {} - toString(): string { return `ChildActivationStart(path: '${this.route.path}')`; } + public snapshot: ActivatedRouteSnapshot) {} + toString(): string { + const path = this.snapshot.routeConfig && this.snapshot.routeConfig.path || ''; + return `ChildActivationStart(path: '${path}')`; + } } /** @@ -277,8 +281,11 @@ export class ChildActivationStart { export class ChildActivationEnd { constructor( /** @docsNotRequired */ - public route: Route) {} - toString(): string { return `ChildActivationEnd(path: '${this.route.path}')`; } + public snapshot: ActivatedRouteSnapshot) {} + toString(): string { + const path = this.snapshot.routeConfig && this.snapshot.routeConfig.path || ''; + return `ChildActivationEnd(path: '${path}')`; + } } /** diff --git a/packages/router/src/pre_activation.ts b/packages/router/src/pre_activation.ts index e5cea48475..69a942d425 100644 --- a/packages/router/src/pre_activation.ts +++ b/packages/router/src/pre_activation.ts @@ -202,8 +202,8 @@ export class PreActivation { const checks$ = from(this.canActivateChecks); const runningChecks$ = concatMap.call( checks$, (check: CanActivate) => andObservables(from([ - this.fireChildActivationStart(check.path), this.runCanActivateChild(check.path), - this.runCanActivate(check.route) + this.fireChildActivationStart(check.route.parent), + this.runCanActivateChild(check.path), this.runCanActivate(check.route) ]))); return every.call(runningChecks$, (result: boolean) => result === true); // this.fireChildActivationStart(check.path), @@ -217,16 +217,11 @@ export class PreActivation { * return * `true` so checks continue to run. */ - private fireChildActivationStart(path: ActivatedRouteSnapshot[]): Observable { - if (!this.forwardEvent) return of (true); - const childActivations = path.slice(0, path.length - 1).reverse().filter(_ => _ !== null); - - return andObservables(map.call(from(childActivations), (snapshot: ActivatedRouteSnapshot) => { - if (this.forwardEvent && snapshot._routeConfig) { - this.forwardEvent(new ChildActivationStart(snapshot._routeConfig)); - } - return of (true); - })); + private fireChildActivationStart(snapshot: ActivatedRouteSnapshot|null): Observable { + if (snapshot !== null && this.forwardEvent) { + this.forwardEvent(new ChildActivationStart(snapshot)); + } + return of (true); } private runCanActivate(future: ActivatedRouteSnapshot): Observable { const canActivate = future._routeConfig ? future._routeConfig.canActivate : null; diff --git a/packages/router/src/router.ts b/packages/router/src/router.ts index 48f1eaf6b4..5a63f911d6 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, RouterEvent, RoutesRecognized} from './events'; +import {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'; @@ -864,8 +864,8 @@ class ActivateRoutes { const children: {[outlet: string]: any} = nodeChildrenAsMap(currNode); futureNode.children.forEach( c => { this.activateRoutes(c, children[c.value.outlet], contexts); }); - if (futureNode.children.length && futureNode.value.routeConfig) { - this.forwardEvent(new ChildActivationEnd(futureNode.value.routeConfig)); + 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 1277345b87..8812dea19e 100644 --- a/packages/router/test/bootstrap.spec.ts +++ b/packages/router/test/bootstrap.spec.ts @@ -83,8 +83,9 @@ describe('bootstrap', () => { const data = router.routerState.snapshot.root.firstChild !.data; expect(data['test']).toEqual('test-data'); expect(log).toEqual([ - 'TestModule', 'NavigationStart', 'RoutesRecognized', 'GuardsCheckStart', 'GuardsCheckEnd', - 'ResolveStart', 'ResolveEnd', 'RootCmp', 'NavigationEnd' + 'TestModule', 'NavigationStart', 'RoutesRecognized', 'GuardsCheckStart', + 'ChildActivationStart', 'GuardsCheckEnd', 'ResolveStart', 'ResolveEnd', 'RootCmp', + 'ChildActivationEnd', 'NavigationEnd' ]); done(); }); @@ -121,7 +122,7 @@ describe('bootstrap', () => { // ResolveEnd has not been emitted yet because bootstrap returned too early expect(log).toEqual([ 'TestModule', 'RootCmp', 'NavigationStart', 'RoutesRecognized', 'GuardsCheckStart', - 'GuardsCheckEnd', 'ResolveStart' + 'ChildActivationStart', 'GuardsCheckEnd', 'ResolveStart' ]); router.events.subscribe((e) => { diff --git a/packages/router/test/integration.spec.ts b/packages/router/test/integration.spec.ts index b4da6e9953..fa47782a43 100644 --- a/packages/router/test/integration.spec.ts +++ b/packages/router/test/integration.spec.ts @@ -705,15 +705,18 @@ describe('Integration', () => { expect(user.recordedParams).toEqual([{name: 'init'}, {name: 'fedor'}]); expectEvents(recordedEvents, [ - [NavigationStart, '/user/init'], [RoutesRecognized, '/user/init'], - [GuardsCheckStart, '/user/init'], [GuardsCheckEnd, '/user/init'], - [ResolveStart, '/user/init'], [ResolveEnd, '/user/init'], [NavigationEnd, '/user/init'], + [NavigationStart, '/user/init'], [RoutesRecognized, '/user/init'], + [GuardsCheckStart, '/user/init'], [ChildActivationStart], + [GuardsCheckEnd, '/user/init'], [ResolveStart, '/user/init'], + [ResolveEnd, '/user/init'], [ChildActivationEnd], + [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'], + [NavigationStart, '/user/fedor'], [RoutesRecognized, '/user/fedor'], + [GuardsCheckStart, '/user/fedor'], [ChildActivationStart], + [GuardsCheckEnd, '/user/fedor'], [ResolveStart, '/user/fedor'], + [ResolveEnd, '/user/fedor'], [ChildActivationEnd], [NavigationEnd, '/user/fedor'] ]); }))); @@ -740,8 +743,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'], + [GuardsCheckStart, '/user/fedor'], [ChildActivationStart], [GuardsCheckEnd, '/user/fedor'], + [ResolveStart, '/user/fedor'], [ResolveEnd, '/user/fedor'], [ChildActivationEnd], [NavigationEnd, '/user/fedor'] ]); }))); @@ -1463,10 +1466,10 @@ describe('Integration', () => { expect(location.path()).toEqual('/'); expectEvents(recordedEvents, [ [NavigationStart, '/team/22'], [RoutesRecognized, '/team/22'], - [GuardsCheckStart, '/team/22'], [GuardsCheckEnd, '/team/22'], + [GuardsCheckStart, '/team/22'], [ChildActivationStart], [GuardsCheckEnd, '/team/22'], [NavigationCancel, '/team/22'] ]); - expect((recordedEvents[3] as GuardsCheckEnd).shouldActivate).toBe(false); + expect((recordedEvents[4] as GuardsCheckEnd).shouldActivate).toBe(false); }))); }); @@ -2389,10 +2392,12 @@ describe('Integration', () => { [RoutesRecognized, '/lazyTrue/loaded'], [GuardsCheckStart, '/lazyTrue/loaded'], [ChildActivationStart], + [ChildActivationStart], [GuardsCheckEnd, '/lazyTrue/loaded'], [ResolveStart, '/lazyTrue/loaded'], [ResolveEnd, '/lazyTrue/loaded'], [ChildActivationEnd], + [ChildActivationEnd], [NavigationEnd, '/lazyTrue/loaded'], ]); }))); @@ -2423,8 +2428,9 @@ describe('Integration', () => { [NavigationCancel, '/lazyFalse/loaded'], [NavigationStart, '/blank'], [RoutesRecognized, '/blank'], - [GuardsCheckStart, '/blank'], [GuardsCheckEnd, '/blank'], [ResolveStart, '/blank'], - [ResolveEnd, '/blank'], [NavigationEnd, '/blank'] + [GuardsCheckStart, '/blank'], [ChildActivationStart], [GuardsCheckEnd, '/blank'], + [ResolveStart, '/blank'], [ResolveEnd, '/blank'], [ChildActivationEnd], + [NavigationEnd, '/blank'] ]); }))); diff --git a/packages/router/test/router.spec.ts b/packages/router/test/router.spec.ts index be1e469c6d..9d3b5339d0 100644 --- a/packages/router/test/router.spec.ts +++ b/packages/router/test/router.spec.ts @@ -10,6 +10,7 @@ import {Location} from '@angular/common'; import {TestBed, inject} from '@angular/core/testing'; import {ResolveData} from '../src/config'; +import {ChildActivationStart} from '../src/events'; import {PreActivation} from '../src/pre_activation'; import {Router} from '../src/router'; import {ChildrenOutletContexts} from '../src/router_outlet_context'; @@ -60,6 +61,7 @@ describe('Router', () => { const inj = {get: (token: any) => () => `${token}_value`}; let empty: RouterStateSnapshot; let logger: Logger; + let events: any[]; const CA_CHILD = 'canActivate_child'; const CA_CHILD_FALSE = 'canActivate_child_false'; @@ -88,8 +90,144 @@ describe('Router', () => { beforeEach(inject([Logger], (_logger: Logger) => { empty = createEmptyStateSnapshot(serializer.parse('/'), null !); logger = _logger; + events = []; })); + describe('ChildActivation', () => { + it('should run', () => { + /** + * R --> R (ChildActivationStart) + * \ + * child + */ + let result = false; + const childSnapshot = + createActivatedRouteSnapshot({component: 'child', routeConfig: {path: 'child'}}); + const futureState = new RouterStateSnapshot( + 'url', new TreeNode(empty.root, [new TreeNode(childSnapshot, [])])); + + const p = new PreActivation(futureState, empty, TestBed, (evt) => { events.push(evt); }); + p.initalize(new ChildrenOutletContexts()); + p.checkGuards().subscribe((x) => result = x, (e) => { throw e; }); + expect(result).toBe(true); + expect(events.length).toEqual(1); + }); + + it('should run from top to bottom', () => { + /** + * R --> R (ChildActivationStart) + * \ + * child (ChildActivationStart) + * \ + * grandchild (ChildActivationStart) + * \ + * great grandchild + */ + let result = false; + const childSnapshot = + createActivatedRouteSnapshot({component: 'child', routeConfig: {path: 'child'}}); + const grandchildSnapshot = createActivatedRouteSnapshot( + {component: 'grandchild', routeConfig: {path: 'grandchild'}}); + const greatGrandchildSnapshot = createActivatedRouteSnapshot( + {component: 'great-grandchild', routeConfig: {path: 'great-grandchild'}}); + const futureState = new RouterStateSnapshot( + 'url', + new TreeNode( + empty.root, [new TreeNode(childSnapshot, [ + new TreeNode(grandchildSnapshot, [new TreeNode(greatGrandchildSnapshot, [])]) + ])])); + + const p = new PreActivation(futureState, empty, TestBed, (evt) => { events.push(evt); }); + p.initalize(new ChildrenOutletContexts()); + p.checkGuards().subscribe((x) => result = x, (e) => { throw e; }); + + expect(result).toBe(true); + expect(events.length).toEqual(3); + 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'); + }); + + it('should not run for unchanged routes', () => { + /** + * R --> R + * / \ + * child child (ChildActivationStart) + * \ + * grandchild + */ + let result = false; + const childSnapshot = + createActivatedRouteSnapshot({component: 'child', routeConfig: {path: 'child'}}); + const grandchildSnapshot = createActivatedRouteSnapshot( + {component: 'grandchild', routeConfig: {path: 'grandchild'}}); + const currentState = new RouterStateSnapshot( + 'url', new TreeNode(empty.root, [new TreeNode(childSnapshot, [])])); + const futureState = new RouterStateSnapshot( + 'url', + new TreeNode( + empty.root, [new TreeNode(childSnapshot, [new TreeNode(grandchildSnapshot, [])])])); + + const p = + new PreActivation(futureState, currentState, TestBed, (evt) => { events.push(evt); }); + p.initalize(new ChildrenOutletContexts()); + p.checkGuards().subscribe((x) => result = x, (e) => { throw e; }); + + expect(result).toBe(true); + expect(events.length).toEqual(1); + expect(events[0].snapshot).not.toBe(events[0].snapshot.root); + expect(events[0].snapshot.routeConfig.path).toBe('child'); + }); + + it('should skip multiple unchanged routes but fire for all changed routes', () => { + /** + * R --> R + * / \ + * child child + * / \ + * grandchild grandchild (ChildActivationStart) + * \ + * greatgrandchild (ChildActivationStart) + * \ + * great-greatgrandchild + */ + let result = false; + const childSnapshot = + createActivatedRouteSnapshot({component: 'child', routeConfig: {path: 'child'}}); + const grandchildSnapshot = createActivatedRouteSnapshot( + {component: 'grandchild', routeConfig: {path: 'grandchild'}}); + const greatGrandchildSnapshot = createActivatedRouteSnapshot( + {component: 'greatgrandchild', routeConfig: {path: 'greatgrandchild'}}); + const greatGreatGrandchildSnapshot = createActivatedRouteSnapshot( + {component: 'great-greatgrandchild', routeConfig: {path: 'great-greatgrandchild'}}); + const currentState = new RouterStateSnapshot( + 'url', + new TreeNode( + empty.root, [new TreeNode(childSnapshot, [new TreeNode(grandchildSnapshot, [])])])); + const futureState = new RouterStateSnapshot( + 'url', + new TreeNode( + empty.root, + [new TreeNode( + childSnapshot, [new TreeNode(grandchildSnapshot, [ + new TreeNode( + greatGrandchildSnapshot, [new TreeNode(greatGreatGrandchildSnapshot, [])]) + ])])])); + + const p = + new PreActivation(futureState, currentState, TestBed, (evt) => { events.push(evt); }); + p.initalize(new ChildrenOutletContexts()); + p.checkGuards().subscribe((x) => result = x, (e) => { throw e; }); + + expect(result).toBe(true); + expect(events.length).toEqual(2); + 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'); + }); + }); + describe('guards', () => { it('should run CanActivate checks', () => { /** diff --git a/tools/public_api_guard/router/index.d.ts b/tools/public_api_guard/router/index.d.ts index 92002f68d2..6eadbcd603 100644 --- a/tools/public_api_guard/router/index.d.ts +++ b/tools/public_api_guard/router/index.d.ts @@ -61,17 +61,17 @@ export interface CanLoad { /** @experimental */ export declare class ChildActivationEnd { - route: Route; + snapshot: ActivatedRouteSnapshot; constructor( - route: Route); + snapshot: ActivatedRouteSnapshot); toString(): string; } /** @experimental */ export declare class ChildActivationStart { - route: Route; + snapshot: ActivatedRouteSnapshot; constructor( - route: Route); + snapshot: ActivatedRouteSnapshot); toString(): string; }