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
This commit is contained in:

committed by
Alex Rickabaugh

parent
9acd04c192
commit
9523991a9b
213
packages/router/src/operators/activate_routes.ts
Normal file
213
packages/router/src/operators/activate_routes.ts
Normal file
@ -0,0 +1,213 @@
|
||||
/**
|
||||
* @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;
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
/**
|
||||
* @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, mergeMap} from 'rxjs/operators';
|
||||
|
||||
import {NavigationTransition, RouterHook} from '../router';
|
||||
|
||||
export function afterPreactivation(hook: RouterHook):
|
||||
MonoTypeOperatorFunction<NavigationTransition> {
|
||||
return function(source) {
|
||||
return source.pipe(mergeMap(t => hook(t.targetSnapshot !, {
|
||||
navigationId: t.id,
|
||||
appliedUrlTree: t.extractedUrl,
|
||||
rawUrlTree: t.rawUrl,
|
||||
skipLocationChange: !!t.extras.skipLocationChange,
|
||||
replaceUrl: !!t.extras.replaceUrl,
|
||||
}).pipe(map(() => t))));
|
||||
};
|
||||
}
|
@ -8,7 +8,7 @@
|
||||
|
||||
import {Injector} from '@angular/core';
|
||||
import {MonoTypeOperatorFunction, Observable} from 'rxjs';
|
||||
import {flatMap, map} from 'rxjs/operators';
|
||||
import {map, switchMap} from 'rxjs/operators';
|
||||
|
||||
import {applyRedirects as applyRedirectsFn} from '../apply_redirects';
|
||||
import {Routes} from '../config';
|
||||
@ -20,8 +20,8 @@ export function applyRedirects(
|
||||
moduleInjector: Injector, configLoader: RouterConfigLoader, urlSerializer: UrlSerializer,
|
||||
config: Routes): MonoTypeOperatorFunction<NavigationTransition> {
|
||||
return function(source: Observable<NavigationTransition>) {
|
||||
return source.pipe(flatMap(
|
||||
return source.pipe(switchMap(
|
||||
t => applyRedirectsFn(moduleInjector, configLoader, urlSerializer, t.extractedUrl, config)
|
||||
.pipe(map(url => ({...t, urlAfterRedirects: url})))));
|
||||
.pipe(map(urlAfterRedirects => ({...t, urlAfterRedirects})))));
|
||||
};
|
||||
}
|
||||
|
@ -1,25 +0,0 @@
|
||||
/**
|
||||
* @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, mergeMap} from 'rxjs/operators';
|
||||
|
||||
import {NavigationTransition, RouterHook} from '../router';
|
||||
|
||||
export function beforePreactivation(hook: RouterHook):
|
||||
MonoTypeOperatorFunction<NavigationTransition> {
|
||||
return function(source) {
|
||||
return source.pipe(mergeMap(t => hook(t.targetSnapshot !, {
|
||||
navigationId: t.id,
|
||||
appliedUrlTree: t.extractedUrl,
|
||||
rawUrlTree: t.rawUrl,
|
||||
skipLocationChange: !!t.extras.skipLocationChange,
|
||||
replaceUrl: !!t.extras.replaceUrl,
|
||||
}).pipe(map(() => t))));
|
||||
};
|
||||
}
|
@ -16,7 +16,7 @@ export function checkGuards(): MonoTypeOperatorFunction<NavigationTransition> {
|
||||
|
||||
return source.pipe(mergeMap(t => {
|
||||
if (!t.preActivation) {
|
||||
throw 'Initialized PreActivation required to check guards';
|
||||
throw new Error('PreActivation required to check guards');
|
||||
}
|
||||
return t.preActivation.checkGuards().pipe(map(guardsResult => ({...t, guardsResult})));
|
||||
}));
|
||||
|
@ -22,7 +22,7 @@ export function recognize(
|
||||
return function(source: Observable<NavigationTransition>) {
|
||||
return source.pipe(mergeMap(
|
||||
t => recognizeFn(
|
||||
rootComponentType, config, t.urlAfterRedirects, serializer(t.extractedUrl),
|
||||
rootComponentType, config, t.urlAfterRedirects, serializer(t.urlAfterRedirects),
|
||||
paramsInheritanceStrategy)
|
||||
.pipe(map(targetSnapshot => ({...t, targetSnapshot})))));
|
||||
};
|
||||
|
@ -6,19 +6,19 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {MonoTypeOperatorFunction, Observable, OperatorFunction, from, of } from 'rxjs';
|
||||
import {map, mergeMap} from 'rxjs/operators';
|
||||
import {MonoTypeOperatorFunction, Observable} from 'rxjs';
|
||||
|
||||
import {NavigationTransition} from '../router';
|
||||
import {switchTap} from './switch_tap';
|
||||
|
||||
export function resolveData(paramsInheritanceStrategy: 'emptyOnly' | 'always'):
|
||||
MonoTypeOperatorFunction<NavigationTransition> {
|
||||
return function(source: Observable<NavigationTransition>) {
|
||||
return source.pipe(mergeMap(t => {
|
||||
return source.pipe(switchTap(t => {
|
||||
if (!t.preActivation) {
|
||||
throw 'Initialized PreActivation required to check guards';
|
||||
throw new Error('PreActivation required to resolve data');
|
||||
}
|
||||
return t.preActivation.resolveData(paramsInheritanceStrategy).pipe(map(_ => t));
|
||||
return t.preActivation.resolveData(paramsInheritanceStrategy);
|
||||
}));
|
||||
};
|
||||
}
|
||||
|
@ -15,16 +15,12 @@ import {PreActivation} from '../pre_activation';
|
||||
import {ChildrenOutletContexts} from '../router_outlet_context';
|
||||
import {RouterStateSnapshot} from '../router_state';
|
||||
|
||||
export function setupPreactivation(
|
||||
rootContexts: ChildrenOutletContexts, currentSnapshot: RouterStateSnapshot,
|
||||
moduleInjector: Injector,
|
||||
forwardEvent?: (evt: Event) => void): OperatorFunction<RouterStateSnapshot, PreActivation> {
|
||||
return function(source: Observable<RouterStateSnapshot>) {
|
||||
return source.pipe(map(snapshot => {
|
||||
const preActivation =
|
||||
new PreActivation(snapshot, currentSnapshot, moduleInjector, forwardEvent);
|
||||
preActivation.initialize(rootContexts);
|
||||
return preActivation;
|
||||
}));
|
||||
};
|
||||
}
|
||||
export const setupPreactivation =
|
||||
(rootContexts: ChildrenOutletContexts, currentSnapshot: RouterStateSnapshot,
|
||||
moduleInjector: Injector, forwardEvent?: (evt: Event) => void) =>
|
||||
map((snapshot: RouterStateSnapshot) => {
|
||||
const preActivation =
|
||||
new PreActivation(snapshot, currentSnapshot, moduleInjector, forwardEvent);
|
||||
preActivation.initialize(rootContexts);
|
||||
return preActivation;
|
||||
});
|
||||
|
29
packages/router/src/operators/switch_tap.ts
Normal file
29
packages/router/src/operators/switch_tap.ts
Normal file
@ -0,0 +1,29 @@
|
||||
/**
|
||||
* @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, ObservableInput, from} from 'rxjs';
|
||||
import {map, switchMap} from 'rxjs/operators';
|
||||
|
||||
/**
|
||||
* Perform a side effect through a switchMap for every emission on the source Observable,
|
||||
* but return an Observable that is identical to the source. It's essentially the same as
|
||||
* the `tap` operator, but if the side effectful `next` function returns an ObservableInput,
|
||||
* it will wait before continuing with the original value.
|
||||
*/
|
||||
export function switchTap<T>(next: (x: T) => void|ObservableInput<any>):
|
||||
MonoTypeOperatorFunction<T> {
|
||||
return function(source) {
|
||||
return source.pipe(switchMap(v => {
|
||||
const nextResult = next(v);
|
||||
if (nextResult) {
|
||||
return from(nextResult).pipe(map(() => v));
|
||||
}
|
||||
return from([v]);
|
||||
}));
|
||||
};
|
||||
}
|
Reference in New Issue
Block a user