fix(router): adjust ChildActivation events to only fire when the child is actually changing (#19043)

* The problem was with the `fireChildActivationStart` function. It was taking a `path` param, which was an
array of `ActivatedRouteSnapshot`s. The function was being fired for each piece of the route that was being
activated. This resulted in far too many `ChildActivationStart` events being fired, and being fired on routes
that weren't actually getting activated. This change fires the event only for those routes that are actually
being activated.

fixes #18942

PR Close #19043
This commit is contained in:
Jason Aden 2017-09-04 13:00:59 -07:00 committed by Miško Hevery
parent dce36751f5
commit 66f0ab0371
7 changed files with 186 additions and 39 deletions

View File

@ -7,7 +7,8 @@
*/ */
import {Route} from './config'; 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 * @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 { export class ChildActivationStart {
constructor( constructor(
/** @docsNotRequired */ /** @docsNotRequired */
public route: Route) {} public snapshot: ActivatedRouteSnapshot) {}
toString(): string { return `ChildActivationStart(path: '${this.route.path}')`; } 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 { export class ChildActivationEnd {
constructor( constructor(
/** @docsNotRequired */ /** @docsNotRequired */
public route: Route) {} public snapshot: ActivatedRouteSnapshot) {}
toString(): string { return `ChildActivationEnd(path: '${this.route.path}')`; } toString(): string {
const path = this.snapshot.routeConfig && this.snapshot.routeConfig.path || '';
return `ChildActivationEnd(path: '${path}')`;
}
} }
/** /**

View File

@ -202,8 +202,8 @@ export class PreActivation {
const checks$ = from(this.canActivateChecks); const checks$ = from(this.canActivateChecks);
const runningChecks$ = concatMap.call( const runningChecks$ = concatMap.call(
checks$, (check: CanActivate) => andObservables(from([ checks$, (check: CanActivate) => andObservables(from([
this.fireChildActivationStart(check.path), this.runCanActivateChild(check.path), this.fireChildActivationStart(check.route.parent),
this.runCanActivate(check.route) this.runCanActivateChild(check.path), this.runCanActivate(check.route)
]))); ])));
return every.call(runningChecks$, (result: boolean) => result === true); return every.call(runningChecks$, (result: boolean) => result === true);
// this.fireChildActivationStart(check.path), // this.fireChildActivationStart(check.path),
@ -217,16 +217,11 @@ export class PreActivation {
* return * return
* `true` so checks continue to run. * `true` so checks continue to run.
*/ */
private fireChildActivationStart(path: ActivatedRouteSnapshot[]): Observable<boolean> { private fireChildActivationStart(snapshot: ActivatedRouteSnapshot|null): Observable<boolean> {
if (!this.forwardEvent) return of (true); if (snapshot !== null && this.forwardEvent) {
const childActivations = path.slice(0, path.length - 1).reverse().filter(_ => _ !== null); this.forwardEvent(new ChildActivationStart(snapshot));
}
return andObservables(map.call(from(childActivations), (snapshot: ActivatedRouteSnapshot) => { return of (true);
if (this.forwardEvent && snapshot._routeConfig) {
this.forwardEvent(new ChildActivationStart(snapshot._routeConfig));
}
return of (true);
}));
} }
private runCanActivate(future: ActivatedRouteSnapshot): Observable<boolean> { private runCanActivate(future: ActivatedRouteSnapshot): Observable<boolean> {
const canActivate = future._routeConfig ? future._routeConfig.canActivate : null; const canActivate = future._routeConfig ? future._routeConfig.canActivate : null;

View File

@ -21,7 +21,7 @@ import {applyRedirects} from './apply_redirects';
import {LoadedRouterConfig, QueryParamsHandling, Route, Routes, validateConfig} from './config'; import {LoadedRouterConfig, QueryParamsHandling, Route, Routes, 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 {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 {PreActivation} from './pre_activation';
import {recognize} from './recognize'; import {recognize} from './recognize';
import {DefaultRouteReuseStrategy, DetachedRouteHandleInternal, RouteReuseStrategy} from './route_reuse_strategy'; import {DefaultRouteReuseStrategy, DetachedRouteHandleInternal, RouteReuseStrategy} from './route_reuse_strategy';
@ -864,8 +864,8 @@ class ActivateRoutes {
const children: {[outlet: string]: any} = nodeChildrenAsMap(currNode); const children: {[outlet: string]: any} = nodeChildrenAsMap(currNode);
futureNode.children.forEach( futureNode.children.forEach(
c => { this.activateRoutes(c, children[c.value.outlet], contexts); }); c => { this.activateRoutes(c, children[c.value.outlet], contexts); });
if (futureNode.children.length && futureNode.value.routeConfig) { if (futureNode.children.length) {
this.forwardEvent(new ChildActivationEnd(futureNode.value.routeConfig)); this.forwardEvent(new ChildActivationEnd(futureNode.value.snapshot));
} }
} }

View File

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

View File

@ -705,15 +705,18 @@ describe('Integration', () => {
expect(user.recordedParams).toEqual([{name: 'init'}, {name: 'fedor'}]); expect(user.recordedParams).toEqual([{name: 'init'}, {name: 'fedor'}]);
expectEvents(recordedEvents, [ expectEvents(recordedEvents, [
[NavigationStart, '/user/init'], [RoutesRecognized, '/user/init'], [NavigationStart, '/user/init'], [RoutesRecognized, '/user/init'],
[GuardsCheckStart, '/user/init'], [GuardsCheckEnd, '/user/init'], [GuardsCheckStart, '/user/init'], [ChildActivationStart],
[ResolveStart, '/user/init'], [ResolveEnd, '/user/init'], [NavigationEnd, '/user/init'], [GuardsCheckEnd, '/user/init'], [ResolveStart, '/user/init'],
[ResolveEnd, '/user/init'], [ChildActivationEnd],
[NavigationEnd, '/user/init'],
[NavigationStart, '/user/victor'], [NavigationCancel, '/user/victor'], [NavigationStart, '/user/victor'], [NavigationCancel, '/user/victor'],
[NavigationStart, '/user/fedor'], [RoutesRecognized, '/user/fedor'], [NavigationStart, '/user/fedor'], [RoutesRecognized, '/user/fedor'],
[GuardsCheckStart, '/user/fedor'], [GuardsCheckEnd, '/user/fedor'], [GuardsCheckStart, '/user/fedor'], [ChildActivationStart],
[ResolveStart, '/user/fedor'], [ResolveEnd, '/user/fedor'], [GuardsCheckEnd, '/user/fedor'], [ResolveStart, '/user/fedor'],
[ResolveEnd, '/user/fedor'], [ChildActivationEnd],
[NavigationEnd, '/user/fedor'] [NavigationEnd, '/user/fedor']
]); ]);
}))); })));
@ -740,8 +743,8 @@ describe('Integration', () => {
[NavigationStart, '/invalid'], [NavigationError, '/invalid'], [NavigationStart, '/invalid'], [NavigationError, '/invalid'],
[NavigationStart, '/user/fedor'], [RoutesRecognized, '/user/fedor'], [NavigationStart, '/user/fedor'], [RoutesRecognized, '/user/fedor'],
[GuardsCheckStart, '/user/fedor'], [GuardsCheckEnd, '/user/fedor'], [GuardsCheckStart, '/user/fedor'], [ChildActivationStart], [GuardsCheckEnd, '/user/fedor'],
[ResolveStart, '/user/fedor'], [ResolveEnd, '/user/fedor'], [ResolveStart, '/user/fedor'], [ResolveEnd, '/user/fedor'], [ChildActivationEnd],
[NavigationEnd, '/user/fedor'] [NavigationEnd, '/user/fedor']
]); ]);
}))); })));
@ -1463,10 +1466,10 @@ describe('Integration', () => {
expect(location.path()).toEqual('/'); expect(location.path()).toEqual('/');
expectEvents(recordedEvents, [ expectEvents(recordedEvents, [
[NavigationStart, '/team/22'], [RoutesRecognized, '/team/22'], [NavigationStart, '/team/22'], [RoutesRecognized, '/team/22'],
[GuardsCheckStart, '/team/22'], [GuardsCheckEnd, '/team/22'], [GuardsCheckStart, '/team/22'], [ChildActivationStart], [GuardsCheckEnd, '/team/22'],
[NavigationCancel, '/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'], [RoutesRecognized, '/lazyTrue/loaded'],
[GuardsCheckStart, '/lazyTrue/loaded'], [GuardsCheckStart, '/lazyTrue/loaded'],
[ChildActivationStart], [ChildActivationStart],
[ChildActivationStart],
[GuardsCheckEnd, '/lazyTrue/loaded'], [GuardsCheckEnd, '/lazyTrue/loaded'],
[ResolveStart, '/lazyTrue/loaded'], [ResolveStart, '/lazyTrue/loaded'],
[ResolveEnd, '/lazyTrue/loaded'], [ResolveEnd, '/lazyTrue/loaded'],
[ChildActivationEnd], [ChildActivationEnd],
[ChildActivationEnd],
[NavigationEnd, '/lazyTrue/loaded'], [NavigationEnd, '/lazyTrue/loaded'],
]); ]);
}))); })));
@ -2423,8 +2428,9 @@ describe('Integration', () => {
[NavigationCancel, '/lazyFalse/loaded'], [NavigationCancel, '/lazyFalse/loaded'],
[NavigationStart, '/blank'], [RoutesRecognized, '/blank'], [NavigationStart, '/blank'], [RoutesRecognized, '/blank'],
[GuardsCheckStart, '/blank'], [GuardsCheckEnd, '/blank'], [ResolveStart, '/blank'], [GuardsCheckStart, '/blank'], [ChildActivationStart], [GuardsCheckEnd, '/blank'],
[ResolveEnd, '/blank'], [NavigationEnd, '/blank'] [ResolveStart, '/blank'], [ResolveEnd, '/blank'], [ChildActivationEnd],
[NavigationEnd, '/blank']
]); ]);
}))); })));

View File

@ -10,6 +10,7 @@ import {Location} from '@angular/common';
import {TestBed, inject} from '@angular/core/testing'; import {TestBed, inject} from '@angular/core/testing';
import {ResolveData} from '../src/config'; import {ResolveData} from '../src/config';
import {ChildActivationStart} from '../src/events';
import {PreActivation} from '../src/pre_activation'; import {PreActivation} from '../src/pre_activation';
import {Router} from '../src/router'; import {Router} from '../src/router';
import {ChildrenOutletContexts} from '../src/router_outlet_context'; import {ChildrenOutletContexts} from '../src/router_outlet_context';
@ -60,6 +61,7 @@ describe('Router', () => {
const inj = {get: (token: any) => () => `${token}_value`}; const inj = {get: (token: any) => () => `${token}_value`};
let empty: RouterStateSnapshot; let empty: RouterStateSnapshot;
let logger: Logger; let logger: Logger;
let events: any[];
const CA_CHILD = 'canActivate_child'; const CA_CHILD = 'canActivate_child';
const CA_CHILD_FALSE = 'canActivate_child_false'; const CA_CHILD_FALSE = 'canActivate_child_false';
@ -88,8 +90,144 @@ describe('Router', () => {
beforeEach(inject([Logger], (_logger: Logger) => { beforeEach(inject([Logger], (_logger: Logger) => {
empty = createEmptyStateSnapshot(serializer.parse('/'), null !); empty = createEmptyStateSnapshot(serializer.parse('/'), null !);
logger = _logger; 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', () => { describe('guards', () => {
it('should run CanActivate checks', () => { it('should run CanActivate checks', () => {
/** /**

View File

@ -61,17 +61,17 @@ export interface CanLoad {
/** @experimental */ /** @experimental */
export declare class ChildActivationEnd { export declare class ChildActivationEnd {
route: Route; snapshot: ActivatedRouteSnapshot;
constructor( constructor(
route: Route); snapshot: ActivatedRouteSnapshot);
toString(): string; toString(): string;
} }
/** @experimental */ /** @experimental */
export declare class ChildActivationStart { export declare class ChildActivationStart {
route: Route; snapshot: ActivatedRouteSnapshot;
constructor( constructor(
route: Route); snapshot: ActivatedRouteSnapshot);
toString(): string; toString(): string;
} }