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:
Jason Aden 2018-09-17 14:37:30 -07:00 committed by Alex Rickabaugh
parent 9acd04c192
commit 9523991a9b
11 changed files with 510 additions and 443 deletions

View File

@ -14,7 +14,7 @@ export {RouterOutlet} from './directives/router_outlet';
export {ActivationEnd, ActivationStart, ChildActivationEnd, ChildActivationStart, Event, GuardsCheckEnd, GuardsCheckStart, NavigationCancel, NavigationEnd, NavigationError, NavigationStart, ResolveEnd, ResolveStart, RouteConfigLoadEnd, RouteConfigLoadStart, RouterEvent, RoutesRecognized, Scroll} from './events'; export {ActivationEnd, ActivationStart, ChildActivationEnd, ChildActivationStart, Event, GuardsCheckEnd, GuardsCheckStart, NavigationCancel, NavigationEnd, NavigationError, NavigationStart, ResolveEnd, ResolveStart, RouteConfigLoadEnd, RouteConfigLoadStart, RouterEvent, RoutesRecognized, Scroll} from './events';
export {CanActivate, CanActivateChild, CanDeactivate, CanLoad, Resolve} from './interfaces'; export {CanActivate, CanActivateChild, CanDeactivate, CanLoad, Resolve} from './interfaces';
export {DetachedRouteHandle, RouteReuseStrategy} from './route_reuse_strategy'; export {DetachedRouteHandle, RouteReuseStrategy} from './route_reuse_strategy';
export {NavigationExtras, NavigationTransition, Router} from './router'; export {NavigationExtras, Router} from './router';
export {ROUTES} from './router_config_loader'; export {ROUTES} from './router_config_loader';
export {ExtraOptions, ROUTER_CONFIGURATION, ROUTER_INITIALIZER, RouterModule, provideRoutes} from './router_module'; export {ExtraOptions, ROUTER_CONFIGURATION, ROUTER_INITIALIZER, RouterModule, provideRoutes} from './router_module';
export {ChildrenOutletContexts, OutletContext} from './router_outlet_context'; export {ChildrenOutletContexts, OutletContext} from './router_outlet_context';

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

View File

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

View File

@ -8,7 +8,7 @@
import {Injector} from '@angular/core'; import {Injector} from '@angular/core';
import {MonoTypeOperatorFunction, Observable} from 'rxjs'; 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 {applyRedirects as applyRedirectsFn} from '../apply_redirects';
import {Routes} from '../config'; import {Routes} from '../config';
@ -20,8 +20,8 @@ export function applyRedirects(
moduleInjector: Injector, configLoader: RouterConfigLoader, urlSerializer: UrlSerializer, moduleInjector: Injector, configLoader: RouterConfigLoader, urlSerializer: UrlSerializer,
config: Routes): MonoTypeOperatorFunction<NavigationTransition> { config: Routes): MonoTypeOperatorFunction<NavigationTransition> {
return function(source: Observable<NavigationTransition>) { return function(source: Observable<NavigationTransition>) {
return source.pipe(flatMap( return source.pipe(switchMap(
t => applyRedirectsFn(moduleInjector, configLoader, urlSerializer, t.extractedUrl, config) t => applyRedirectsFn(moduleInjector, configLoader, urlSerializer, t.extractedUrl, config)
.pipe(map(url => ({...t, urlAfterRedirects: url}))))); .pipe(map(urlAfterRedirects => ({...t, urlAfterRedirects})))));
}; };
} }

View File

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

View File

@ -16,7 +16,7 @@ export function checkGuards(): MonoTypeOperatorFunction<NavigationTransition> {
return source.pipe(mergeMap(t => { return source.pipe(mergeMap(t => {
if (!t.preActivation) { 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}))); return t.preActivation.checkGuards().pipe(map(guardsResult => ({...t, guardsResult})));
})); }));

View File

@ -22,7 +22,7 @@ export function recognize(
return function(source: Observable<NavigationTransition>) { return function(source: Observable<NavigationTransition>) {
return source.pipe(mergeMap( return source.pipe(mergeMap(
t => recognizeFn( t => recognizeFn(
rootComponentType, config, t.urlAfterRedirects, serializer(t.extractedUrl), rootComponentType, config, t.urlAfterRedirects, serializer(t.urlAfterRedirects),
paramsInheritanceStrategy) paramsInheritanceStrategy)
.pipe(map(targetSnapshot => ({...t, targetSnapshot}))))); .pipe(map(targetSnapshot => ({...t, targetSnapshot})))));
}; };

View File

@ -6,19 +6,19 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {MonoTypeOperatorFunction, Observable, OperatorFunction, from, of } from 'rxjs'; import {MonoTypeOperatorFunction, Observable} from 'rxjs';
import {map, mergeMap} from 'rxjs/operators';
import {NavigationTransition} from '../router'; import {NavigationTransition} from '../router';
import {switchTap} from './switch_tap';
export function resolveData(paramsInheritanceStrategy: 'emptyOnly' | 'always'): export function resolveData(paramsInheritanceStrategy: 'emptyOnly' | 'always'):
MonoTypeOperatorFunction<NavigationTransition> { MonoTypeOperatorFunction<NavigationTransition> {
return function(source: Observable<NavigationTransition>) { return function(source: Observable<NavigationTransition>) {
return source.pipe(mergeMap(t => { return source.pipe(switchTap(t => {
if (!t.preActivation) { 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);
})); }));
}; };
} }

View File

@ -15,16 +15,12 @@ import {PreActivation} from '../pre_activation';
import {ChildrenOutletContexts} from '../router_outlet_context'; import {ChildrenOutletContexts} from '../router_outlet_context';
import {RouterStateSnapshot} from '../router_state'; import {RouterStateSnapshot} from '../router_state';
export function setupPreactivation( export const setupPreactivation =
rootContexts: ChildrenOutletContexts, currentSnapshot: RouterStateSnapshot, (rootContexts: ChildrenOutletContexts, currentSnapshot: RouterStateSnapshot,
moduleInjector: Injector, moduleInjector: Injector, forwardEvent?: (evt: Event) => void) =>
forwardEvent?: (evt: Event) => void): OperatorFunction<RouterStateSnapshot, PreActivation> { map((snapshot: RouterStateSnapshot) => {
return function(source: Observable<RouterStateSnapshot>) {
return source.pipe(map(snapshot => {
const preActivation = const preActivation =
new PreActivation(snapshot, currentSnapshot, moduleInjector, forwardEvent); new PreActivation(snapshot, currentSnapshot, moduleInjector, forwardEvent);
preActivation.initialize(rootContexts); preActivation.initialize(rootContexts);
return preActivation; return preActivation;
})); });
};
}

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

View File

@ -8,30 +8,27 @@
import {Location} from '@angular/common'; import {Location} from '@angular/common';
import {Compiler, Injector, NgModuleFactoryLoader, NgModuleRef, NgZone, Type, isDevMode, ɵConsole as Console} from '@angular/core'; import {Compiler, Injector, NgModuleFactoryLoader, NgModuleRef, NgZone, Type, isDevMode, ɵConsole as Console} from '@angular/core';
import {BehaviorSubject, EMPTY, MonoTypeOperatorFunction, Observable, Subject, Subscription, of } from 'rxjs'; import {BehaviorSubject, EMPTY, Observable, Subject, Subscription, of } from 'rxjs';
import {catchError, filter, finalize, map, mergeMap, switchMap, tap} from 'rxjs/operators'; import {catchError, filter, finalize, map, switchMap, tap} from 'rxjs/operators';
import {LoadedRouterConfig, QueryParamsHandling, Route, Routes, standardizeConfig, validateConfig} from './config'; import {QueryParamsHandling, Route, Routes, standardizeConfig, 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 {ActivationEnd, ChildActivationEnd, Event, GuardsCheckEnd, GuardsCheckStart, NavigationCancel, NavigationEnd, NavigationError, NavigationStart, NavigationTrigger, ResolveEnd, ResolveStart, RouteConfigLoadEnd, RouteConfigLoadStart, RoutesRecognized} from './events'; import {Event, GuardsCheckEnd, GuardsCheckStart, NavigationCancel, NavigationEnd, NavigationError, NavigationStart, NavigationTrigger, ResolveEnd, ResolveStart, RouteConfigLoadEnd, RouteConfigLoadStart, RoutesRecognized} from './events';
import {afterPreactivation} from './operators/after_preactivation'; import {activateRoutes} from './operators/activate_routes';
import {applyRedirects} from './operators/apply_redirects'; import {applyRedirects} from './operators/apply_redirects';
import {beforePreactivation} from './operators/before_preactivation';
import {checkGuards} from './operators/check_guards'; import {checkGuards} from './operators/check_guards';
import {recognize} from './operators/recognize'; import {recognize} from './operators/recognize';
import {resolveData} from './operators/resolve_data'; import {resolveData} from './operators/resolve_data';
import {switchTap} from './operators/switch_tap';
import {PreActivation} from './pre_activation'; import {PreActivation} from './pre_activation';
import {DefaultRouteReuseStrategy, DetachedRouteHandleInternal, RouteReuseStrategy} from './route_reuse_strategy'; import {DefaultRouteReuseStrategy, RouteReuseStrategy} from './route_reuse_strategy';
import {RouterConfigLoader} from './router_config_loader'; import {RouterConfigLoader} from './router_config_loader';
import {ChildrenOutletContexts} from './router_outlet_context'; import {ChildrenOutletContexts} from './router_outlet_context';
import {ActivatedRoute, ActivatedRouteSnapshot, RouterState, RouterStateSnapshot, advanceActivatedRoute, createEmptyState} from './router_state'; import {ActivatedRoute, RouterState, RouterStateSnapshot, createEmptyState} from './router_state';
import {Params, isNavigationCancelingError} from './shared'; import {Params, isNavigationCancelingError} from './shared';
import {DefaultUrlHandlingStrategy, UrlHandlingStrategy} from './url_handling_strategy'; import {DefaultUrlHandlingStrategy, UrlHandlingStrategy} from './url_handling_strategy';
import {UrlSerializer, UrlTree, containsTree, createEmptyUrlTree} from './url_tree'; import {UrlSerializer, UrlTree, containsTree, createEmptyUrlTree} from './url_tree';
import {forEach} from './utils/collection';
import {TreeNode, nodeChildrenAsMap} from './utils/tree';
/** /**
@ -232,6 +229,7 @@ function defaultRouterHook(snapshot: RouterStateSnapshot, runExtras: {
export class Router { export class Router {
private currentUrlTree: UrlTree; private currentUrlTree: UrlTree;
private rawUrlTree: UrlTree; private rawUrlTree: UrlTree;
private readonly transitions: BehaviorSubject<NavigationTransition>;
private navigations: Observable<NavigationTransition>; private navigations: Observable<NavigationTransition>;
// TODO(issue/24571): remove '!'. // TODO(issue/24571): remove '!'.
@ -242,7 +240,6 @@ export class Router {
private console: Console; private console: Console;
private isNgZoneEnabled: boolean = false; private isNgZoneEnabled: boolean = false;
public readonly transitions: BehaviorSubject<NavigationTransition>;
public readonly events: Observable<Event> = new Subject<Event>(); public readonly events: Observable<Event> = new Subject<Event>();
public readonly routerState: RouterState; public readonly routerState: RouterState;
@ -368,34 +365,38 @@ export class Router {
this.processNavigations(); this.processNavigations();
} }
// clang-format off
private setupNavigations(transitions: Observable<NavigationTransition>): private setupNavigations(transitions: Observable<NavigationTransition>):
Observable<NavigationTransition> { Observable<NavigationTransition> {
const eventsSubject = (this.events as Subject<Event>);
return transitions.pipe( return transitions.pipe(
filter(t => t.id !== 0), mergeMap(t => Promise.resolve(t)), filter(t => t.id !== 0),
// Extract URL // Extract URL
map(t => ({ map(t => ({
...t, ...t, extractedUrl: this.urlHandlingStrategy.extract(t.rawUrl)
extractedUrl: this.urlHandlingStrategy.extract(t.rawUrl)} as NavigationTransition)), } as NavigationTransition)),
// Using switchMap so we cancel executing navigations when a new one comes in // Using switchMap so we cancel executing navigations when a new one comes in
switchMap(t => { switchMap(t => {
let completed = false; let completed = false;
let errored = false; let errored = false;
return of (t).pipe(mergeMap(t => { return of (t).pipe(
const urlTransition = !this.navigated || switchMap(t => {
t.extractedUrl.toString() !== this.currentUrlTree.toString(); const urlTransition =
if ((this.onSameUrlNavigation === 'reload' ? true : urlTransition) && !this.navigated || t.extractedUrl.toString() !== this.currentUrlTree.toString();
this.urlHandlingStrategy.shouldProcessUrl(t.rawUrl)) { const processCurrentUrl =
(this.onSameUrlNavigation === 'reload' ? true : urlTransition) &&
this.urlHandlingStrategy.shouldProcessUrl(t.rawUrl);
if (processCurrentUrl) {
return of (t).pipe( return of (t).pipe(
// Update URL if in `eager` update mode // Update URL if in `eager` update mode
tap(t => this.urlUpdateStrategy === 'eager' && !t.extras.skipLocationChange && tap(t => this.urlUpdateStrategy === 'eager' && !t.extras.skipLocationChange &&
this.setBrowserUrl(t.rawUrl, !!t.extras.replaceUrl, t.id)), this.setBrowserUrl(t.rawUrl, !!t.extras.replaceUrl, t.id)),
// Fire NavigationStart event // Fire NavigationStart event
mergeMap(t => { switchMap(t => {
const transition = this.transitions.getValue(); const transition = this.transitions.getValue();
(this.events as Subject<Event>).next(new NavigationStart( eventsSubject.next(new NavigationStart(
t.id, this.serializeUrl(t.extractedUrl), t.source, t.state)); t.id, this.serializeUrl(t.extractedUrl), t.source, t.state));
if (transition !== this.transitions.getValue()) { if (transition !== this.transitions.getValue()) {
return EMPTY; return EMPTY;
@ -405,47 +406,81 @@ export class Router {
// This delay is required to match old behavior that forced navigation to // This delay is required to match old behavior that forced navigation to
// always be async // always be async
mergeMap(t => Promise.resolve(t)), switchMap(t => Promise.resolve(t)),
// ApplyRedirects // ApplyRedirects
applyRedirects( applyRedirects(
this.ngModule.injector, this.configLoader, this.urlSerializer,this.config), this.ngModule.injector, this.configLoader, this.urlSerializer,
this.config),
// Recognize // Recognize
recognize( recognize(
this.rootComponentType, this.config, (url) => this.serializeUrl(url), this.rootComponentType, this.config, (url) => this.serializeUrl(url),
this.paramsInheritanceStrategy), this.paramsInheritanceStrategy),
// Fire RoutesRecognized // Fire RoutesRecognized
tap(t => (this.events as Subject<Event>).next(new RoutesRecognized( tap(t => {
t.id, this.serializeUrl(t.extractedUrl), this.serializeUrl(t.urlAfterRedirects), const routesRecognized = new RoutesRecognized(
t.targetSnapshot !))) t.id, this.serializeUrl(t.extractedUrl),
); this.serializeUrl(t.urlAfterRedirects), t.targetSnapshot !);
} else if ( eventsSubject.next(routesRecognized);
urlTransition && this.rawUrlTree && }), );
this.urlHandlingStrategy.shouldProcessUrl(this.rawUrlTree)) { } else {
(this.events as Subject<Event>).next(new NavigationStart( const processPreviousUrl = urlTransition && this.rawUrlTree &&
t.id, this.serializeUrl(t.extractedUrl), t.source, t.state)); this.urlHandlingStrategy.shouldProcessUrl(this.rawUrlTree);
/* When the current URL shouldn't be processed, but the previous one was, we
* handle this "error condition" by navigating to the previously successful URL,
* but leaving the URL intact.*/
if (processPreviousUrl) {
const {id, extractedUrl, source, state, extras} = t;
const navStart =
new NavigationStart(id, this.serializeUrl(extractedUrl), source, state);
eventsSubject.next(navStart);
const targetSnapshot =
createEmptyState(extractedUrl, this.rootComponentType).snapshot;
return of ({ return of ({
...t, ...t,
urlAfterRedirects: t.extractedUrl, targetSnapshot,
extras: {...t.extras, skipLocationChange: false, replaceUrl: false}, urlAfterRedirects: extractedUrl,
targetSnapshot: createEmptyState(t.extractedUrl, this.rootComponentType).snapshot extras: {...extras, skipLocationChange: false, replaceUrl: false},
}); });
} else { } else {
/* When neither the current or previous URL can be processed, do nothing other
* than update router's internal reference to the current "settled" URL. This
* way the next navigation will be coming from the current URL in the browser.
*/
this.rawUrlTree = t.rawUrl; this.rawUrlTree = t.rawUrl;
t.resolve(null); t.resolve(null);
return EMPTY; return EMPTY;
} }
}
}), }),
// Before Preactivation // Before Preactivation
beforePreactivation(this.hooks.beforePreactivation), switchTap(t => {
const {
targetSnapshot,
id: navigationId,
extractedUrl: appliedUrlTree,
rawUrl: rawUrlTree,
extras: {skipLocationChange, replaceUrl}
} = t;
return this.hooks.beforePreactivation(targetSnapshot !, {
navigationId,
appliedUrlTree,
rawUrlTree,
skipLocationChange: !!skipLocationChange,
replaceUrl: !!replaceUrl,
});
}),
// --- GUARDS --- // --- GUARDS ---
tap(t => this.triggerEvent(new GuardsCheckStart( tap(t => {
t.id, this.serializeUrl(t.extractedUrl), const guardsStart = new GuardsCheckStart(
this.serializeUrl(t.urlAfterRedirects), t.targetSnapshot !))), t.id, this.serializeUrl(t.extractedUrl), this.serializeUrl(t.urlAfterRedirects),
t.targetSnapshot !);
this.triggerEvent(guardsStart);
}),
map(t => { map(t => {
const preActivation = new PreActivation( const preActivation = new PreActivation(
t.targetSnapshot !, t.currentSnapshot, this.ngModule.injector, t.targetSnapshot !, t.currentSnapshot, this.ngModule.injector,
@ -456,52 +491,75 @@ export class Router {
checkGuards(), checkGuards(),
tap(t => this.triggerEvent(new GuardsCheckEnd( tap(t => {
t.id, this.serializeUrl(t.extractedUrl), const guardsEnd = new GuardsCheckEnd(
this.serializeUrl(t.urlAfterRedirects), t.targetSnapshot !, t.id, this.serializeUrl(t.extractedUrl), this.serializeUrl(t.urlAfterRedirects),
!!t.guardsResult))), t.targetSnapshot !, !!t.guardsResult);
this.triggerEvent(guardsEnd);
}),
mergeMap(t => { filter(t => {
if (!t.guardsResult) { if (!t.guardsResult) {
this.resetUrlToCurrentUrlTree(); this.resetUrlToCurrentUrlTree();
(this.events as Subject<Event>) const navCancel =
.next(new NavigationCancel(t.id, this.serializeUrl(t.extractedUrl), '')); new NavigationCancel(t.id, this.serializeUrl(t.extractedUrl), '');
eventsSubject.next(navCancel);
t.resolve(false); t.resolve(false);
return EMPTY; return false;
} }
return of(t); return true;
}), }),
// --- RESOLVE --- // --- RESOLVE ---
mergeMap(t => { switchTap(t => {
if (t.preActivation !.isActivating()) { if (t.preActivation !.isActivating()) {
return of (t).pipe( return of (t).pipe(
tap(t => this.triggerEvent(new ResolveStart( tap(t => {
const resolveStart = new ResolveStart(
t.id, this.serializeUrl(t.extractedUrl), t.id, this.serializeUrl(t.extractedUrl),
this.serializeUrl(t.urlAfterRedirects), t.targetSnapshot !))), this.serializeUrl(t.urlAfterRedirects), t.targetSnapshot !);
resolveData(this.paramsInheritanceStrategy), this.triggerEvent(resolveStart);
tap(t => this.triggerEvent(new ResolveEnd( }),
resolveData(this.paramsInheritanceStrategy), //
tap(t => {
const resolveEnd = new ResolveEnd(
t.id, this.serializeUrl(t.extractedUrl), t.id, this.serializeUrl(t.extractedUrl),
this.serializeUrl(t.urlAfterRedirects), t.targetSnapshot !))), ); this.serializeUrl(t.urlAfterRedirects), t.targetSnapshot !);
this.triggerEvent(resolveEnd);
}), );
} }
return of (t); return undefined;
}), }),
// --- AFTER PREACTIVATION --- // --- AFTER PREACTIVATION ---
afterPreactivation(this.hooks.afterPreactivation), map(t => { switchTap(t => {
const {
targetSnapshot,
id: navigationId,
extractedUrl: appliedUrlTree,
rawUrl: rawUrlTree,
extras: {skipLocationChange, replaceUrl}
} = t;
return this.hooks.afterPreactivation(targetSnapshot !, {
navigationId,
appliedUrlTree,
rawUrlTree,
skipLocationChange: !!skipLocationChange,
replaceUrl: !!replaceUrl,
});
}),
map(t => {
const targetRouterState = createRouterState( const targetRouterState = createRouterState(
this.routeReuseStrategy, t.targetSnapshot !, t.currentRouterState); this.routeReuseStrategy, t.targetSnapshot !, t.currentRouterState);
return ({...t, targetRouterState}); return ({...t, targetRouterState});
}), }),
// Side effects of resetting Router instance /* Once here, we are about to activate syncronously. The assumption is this will
afterPreactivation(this.hooks.afterPreactivation), map(t => { succeed, and user code may read from the Router service. Therefore before
const targetRouterState = createRouterState( activation, we need to update router properties storing the current URL and the
this.routeReuseStrategy, t.targetSnapshot !, t.currentRouterState); RouterState, as well as updated the browser URL. All this should happen *before*
return ({...t, targetRouterState}); activating. */
}),
// Side effects of resetting Router instance
tap(t => { tap(t => {
this.currentUrlTree = t.urlAfterRedirects; this.currentUrlTree = t.urlAfterRedirects;
this.rawUrlTree = this.urlHandlingStrategy.merge(this.currentUrlTree, t.rawUrl); this.rawUrlTree = this.urlHandlingStrategy.merge(this.currentUrlTree, t.rawUrl);
@ -513,30 +571,48 @@ export class Router {
} }
}), }),
activate( activateRoutes(
this.rootContexts, this.routeReuseStrategy, this.rootContexts, this.routeReuseStrategy,
(evt: Event) => this.triggerEvent(evt)), (evt: Event) => this.triggerEvent(evt)),
tap({next: () => completed = true, complete: () => completed = true}), tap({next() { completed = true; }, complete() { completed = true; }}),
finalize(() => { finalize(() => {
/* When the navigation stream finishes either through error or success, we set the
* `completed` or `errored` flag. However, there are some situations where we could
* get here without either of those being set. For instance, a redirect during
* NavigationStart. Therefore, this is a catch-all to make sure the NavigationCancel
* event is fired when a navigation gets cancelled but not caught by other means. */
if (!completed && !errored) { if (!completed && !errored) {
(this.events as Subject<Event>).next(new NavigationCancel( // Must reset to current URL tree here to ensure history.state is set. On a fresh
// page load, if a new navigation comes in before a successful navigation
// completes, there will be nothing in history.state.navigationId. This can cause
// sync problems with AngularJS sync code which looks for a value here in order
// to determine whether or not to handle a given popstate event or to leave it
// to the Angualr router.
this.resetUrlToCurrentUrlTree();
const navCancel = new NavigationCancel(
t.id, this.serializeUrl(t.extractedUrl), t.id, this.serializeUrl(t.extractedUrl),
`Navigation ID ${t.id} is not equal to the current navigation id ${this.navigationId}`)); `Navigation ID ${t.id} is not equal to the current navigation id ${this.navigationId}`);
eventsSubject.next(navCancel);
t.resolve(false); t.resolve(false);
} }
}), }),
catchError((e) => { catchError((e) => {
errored = true; errored = true;
/* This error type is issued during Redirect, and is handled as a cancellation
* rather than an error. */
if (isNavigationCancelingError(e)) { if (isNavigationCancelingError(e)) {
this.navigated = true; this.navigated = true;
this.resetStateAndUrl(t.currentRouterState, t.currentUrlTree, t.rawUrl); this.resetStateAndUrl(t.currentRouterState, t.currentUrlTree, t.rawUrl);
(this.events as Subject<Event>).next( const navCancel =
new NavigationCancel(t.id, this.serializeUrl(t.extractedUrl), e.message)); new NavigationCancel(t.id, this.serializeUrl(t.extractedUrl), e.message);
eventsSubject.next(navCancel);
/* All other errors should reset to the router's internal URL reference to the
* pre-error state. */
} else { } else {
this.resetStateAndUrl(t.currentRouterState, t.currentUrlTree, t.rawUrl); this.resetStateAndUrl(t.currentRouterState, t.currentUrlTree, t.rawUrl);
(this.events as Subject<Event>).next(new NavigationError( const navError = new NavigationError(t.id, this.serializeUrl(t.extractedUrl), e);
t.id, this.serializeUrl(t.extractedUrl), e)); eventsSubject.next(navError);
try { try {
t.resolve(this.errorHandler(e)); t.resolve(this.errorHandler(e));
} catch (ee) { } catch (ee) {
@ -545,22 +621,9 @@ export class Router {
} }
return EMPTY; return EMPTY;
}), ); }), );
// TODO(jasonaden): remove cast once g3 is on updated TypeScript
})) as any as Observable<NavigationTransition>; })) as any as Observable<NavigationTransition>;
function activate(
rootContexts: ChildrenOutletContexts, routeReuseStrategy: RouteReuseStrategy,
forwardEvent: (evt: Event) => void): MonoTypeOperatorFunction<NavigationTransition> {
return function(source: Observable<NavigationTransition>) {
return source.pipe(map(t => {
new ActivateRoutes(
routeReuseStrategy, t.targetRouterState !, t.currentRouterState, forwardEvent)
.activate(rootContexts);
return t;
}));
};
} }
}
// clang-format on
/** /**
* @internal * @internal
@ -832,7 +895,7 @@ export class Router {
t.id, this.serializeUrl(t.extractedUrl), this.serializeUrl(this.currentUrlTree))); t.id, this.serializeUrl(t.extractedUrl), this.serializeUrl(this.currentUrlTree)));
t.resolve(true); t.resolve(true);
}, },
e => { throw 'never get here!'; }); e => { this.console.warn(`Unhandled Navigation Error: `); });
} }
private scheduleNavigation( private scheduleNavigation(
@ -908,190 +971,6 @@ export class Router {
} }
} }
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;
}
function validateCommands(commands: string[]): void { function validateCommands(commands: string[]): void {
for (let i = 0; i < commands.length; i++) { for (let i = 0; i < commands.length; i++) {
const cmd = commands[i]; const cmd = commands[i];