angular/packages/router/src/operators/activate_routes.ts
Jason Aden 9523991a9b refactor(router): cleanup to navigation stream for readability and documentation (#25740)
* Pull out `activateRoutes` into new operator
* Add `asyncTap` operator
* Use `asyncTap` operator for router hooks and remove corresponding abstracted operators
* Clean up formatting
* Minor performance improvements

PR Close #25740
2018-09-27 14:02:58 -07:00

213 lines
8.6 KiB
TypeScript

/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {MonoTypeOperatorFunction} from 'rxjs';
import {map} from 'rxjs/operators';
import {LoadedRouterConfig} from '../config';
import {ActivationEnd, ChildActivationEnd, Event} from '../events';
import {DetachedRouteHandleInternal, RouteReuseStrategy} from '../route_reuse_strategy';
import {NavigationTransition} from '../router';
import {ChildrenOutletContexts} from '../router_outlet_context';
import {ActivatedRoute, ActivatedRouteSnapshot, RouterState, advanceActivatedRoute} from '../router_state';
import {forEach} from '../utils/collection';
import {TreeNode, nodeChildrenAsMap} from '../utils/tree';
export const activateRoutes =
(rootContexts: ChildrenOutletContexts, routeReuseStrategy: RouteReuseStrategy,
forwardEvent: (evt: Event) => void): MonoTypeOperatorFunction<NavigationTransition> =>
map(t => {
new ActivateRoutes(
routeReuseStrategy, t.targetRouterState !, t.currentRouterState, forwardEvent)
.activate(rootContexts);
return t;
});
export class ActivateRoutes {
constructor(
private routeReuseStrategy: RouteReuseStrategy, private futureState: RouterState,
private currState: RouterState, private forwardEvent: (evt: Event) => void) {}
activate(parentContexts: ChildrenOutletContexts): void {
const futureRoot = this.futureState._root;
const currRoot = this.currState ? this.currState._root : null;
this.deactivateChildRoutes(futureRoot, currRoot, parentContexts);
advanceActivatedRoute(this.futureState.root);
this.activateChildRoutes(futureRoot, currRoot, parentContexts);
}
// De-activate the child route that are not re-used for the future state
private deactivateChildRoutes(
futureNode: TreeNode<ActivatedRoute>, currNode: TreeNode<ActivatedRoute>|null,
contexts: ChildrenOutletContexts): void {
const children: {[outletName: string]: TreeNode<ActivatedRoute>} = nodeChildrenAsMap(currNode);
// Recurse on the routes active in the future state to de-activate deeper children
futureNode.children.forEach(futureChild => {
const childOutletName = futureChild.value.outlet;
this.deactivateRoutes(futureChild, children[childOutletName], contexts);
delete children[childOutletName];
});
// De-activate the routes that will not be re-used
forEach(children, (v: TreeNode<ActivatedRoute>, childName: string) => {
this.deactivateRouteAndItsChildren(v, contexts);
});
}
private deactivateRoutes(
futureNode: TreeNode<ActivatedRoute>, currNode: TreeNode<ActivatedRoute>,
parentContext: ChildrenOutletContexts): void {
const future = futureNode.value;
const curr = currNode ? currNode.value : null;
if (future === curr) {
// Reusing the node, check to see if the children need to be de-activated
if (future.component) {
// If we have a normal route, we need to go through an outlet.
const context = parentContext.getContext(future.outlet);
if (context) {
this.deactivateChildRoutes(futureNode, currNode, context.children);
}
} else {
// if we have a componentless route, we recurse but keep the same outlet map.
this.deactivateChildRoutes(futureNode, currNode, parentContext);
}
} else {
if (curr) {
// Deactivate the current route which will not be re-used
this.deactivateRouteAndItsChildren(currNode, parentContext);
}
}
}
private deactivateRouteAndItsChildren(
route: TreeNode<ActivatedRoute>, parentContexts: ChildrenOutletContexts): void {
if (this.routeReuseStrategy.shouldDetach(route.value.snapshot)) {
this.detachAndStoreRouteSubtree(route, parentContexts);
} else {
this.deactivateRouteAndOutlet(route, parentContexts);
}
}
private detachAndStoreRouteSubtree(
route: TreeNode<ActivatedRoute>, parentContexts: ChildrenOutletContexts): void {
const context = parentContexts.getContext(route.value.outlet);
if (context && context.outlet) {
const componentRef = context.outlet.detach();
const contexts = context.children.onOutletDeactivated();
this.routeReuseStrategy.store(route.value.snapshot, {componentRef, route, contexts});
}
}
private deactivateRouteAndOutlet(
route: TreeNode<ActivatedRoute>, parentContexts: ChildrenOutletContexts): void {
const context = parentContexts.getContext(route.value.outlet);
if (context) {
const children: {[outletName: string]: any} = nodeChildrenAsMap(route);
const contexts = route.value.component ? context.children : parentContexts;
forEach(children, (v: any, k: string) => this.deactivateRouteAndItsChildren(v, contexts));
if (context.outlet) {
// Destroy the component
context.outlet.deactivate();
// Destroy the contexts for all the outlets that were in the component
context.children.onOutletDeactivated();
}
}
}
private activateChildRoutes(
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);
this.forwardEvent(new ActivationEnd(c.value.snapshot));
});
if (futureNode.children.length) {
this.forwardEvent(new ChildActivationEnd(futureNode.value.snapshot));
}
}
private activateRoutes(
futureNode: TreeNode<ActivatedRoute>, currNode: TreeNode<ActivatedRoute>,
parentContexts: ChildrenOutletContexts): void {
const future = futureNode.value;
const curr = currNode ? currNode.value : null;
advanceActivatedRoute(future);
// reusing the node
if (future === curr) {
if (future.component) {
// If we have a normal route, we need to go through an outlet.
const context = parentContexts.getOrCreateContext(future.outlet);
this.activateChildRoutes(futureNode, currNode, context.children);
} else {
// if we have a componentless route, we recurse but keep the same outlet map.
this.activateChildRoutes(futureNode, currNode, parentContexts);
}
} else {
if (future.component) {
// if we have a normal route, we need to place the component into the outlet and recurse.
const context = parentContexts.getOrCreateContext(future.outlet);
if (this.routeReuseStrategy.shouldAttach(future.snapshot)) {
const stored =
(<DetachedRouteHandleInternal>this.routeReuseStrategy.retrieve(future.snapshot));
this.routeReuseStrategy.store(future.snapshot, null);
context.children.onOutletReAttached(stored.contexts);
context.attachRef = stored.componentRef;
context.route = stored.route.value;
if (context.outlet) {
// Attach right away when the outlet has already been instantiated
// Otherwise attach from `RouterOutlet.ngOnInit` when it is instantiated
context.outlet.attach(stored.componentRef, stored.route.value);
}
advanceActivatedRouteNodeAndItsChildren(stored.route);
} else {
const config = parentLoadedConfig(future.snapshot);
const cmpFactoryResolver = config ? config.module.componentFactoryResolver : null;
context.attachRef = null;
context.route = future;
context.resolver = cmpFactoryResolver;
if (context.outlet) {
// Activate the outlet when it has already been instantiated
// Otherwise it will get activated from its `ngOnInit` when instantiated
context.outlet.activateWith(future, cmpFactoryResolver);
}
this.activateChildRoutes(futureNode, null, context.children);
}
} else {
// if we have a componentless route, we recurse but keep the same outlet map.
this.activateChildRoutes(futureNode, null, parentContexts);
}
}
}
}
function advanceActivatedRouteNodeAndItsChildren(node: TreeNode<ActivatedRoute>): void {
advanceActivatedRoute(node.value);
node.children.forEach(advanceActivatedRouteNodeAndItsChildren);
}
function parentLoadedConfig(snapshot: ActivatedRouteSnapshot): LoadedRouterConfig|null {
for (let s = snapshot.parent; s; s = s.parent) {
const route = s.routeConfig;
if (route && route._loadedConfig) return route._loadedConfig;
if (route && route.component) return null;
}
return null;
}