/** * @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 => 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, currNode: TreeNode|null, contexts: ChildrenOutletContexts): void { const children: {[outletName: string]: TreeNode} = 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, childName: string) => { this.deactivateRouteAndItsChildren(v, contexts); }); } private deactivateRoutes( futureNode: TreeNode, currNode: TreeNode, 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, parentContexts: ChildrenOutletContexts): void { if (this.routeReuseStrategy.shouldDetach(route.value.snapshot)) { this.detachAndStoreRouteSubtree(route, parentContexts); } else { this.deactivateRouteAndOutlet(route, parentContexts); } } private detachAndStoreRouteSubtree( route: TreeNode, 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, 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, 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); this.forwardEvent(new ActivationEnd(c.value.snapshot)); }); if (futureNode.children.length) { this.forwardEvent(new ChildActivationEnd(futureNode.value.snapshot)); } } private activateRoutes( futureNode: TreeNode, currNode: TreeNode, 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 = (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): 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; }