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 {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}')`;
}
}
/**

View File

@ -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<boolean> {
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<boolean> {
if (snapshot !== null && this.forwardEvent) {
this.forwardEvent(new ChildActivationStart(snapshot));
}
return of (true);
}
private runCanActivate(future: ActivatedRouteSnapshot): Observable<boolean> {
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 {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));
}
}

View File

@ -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) => {

View File

@ -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']
]);
})));

View File

@ -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', () => {
/**

View File

@ -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;
}