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:
parent
9acd04c192
commit
9523991a9b
|
@ -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';
|
||||||
|
|
|
@ -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 {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})))));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 => {
|
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})));
|
||||||
}));
|
}));
|
||||||
|
|
|
@ -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})))));
|
||||||
};
|
};
|
||||||
|
|
|
@ -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);
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>) {
|
const preActivation =
|
||||||
return source.pipe(map(snapshot => {
|
new PreActivation(snapshot, currentSnapshot, moduleInjector, forwardEvent);
|
||||||
const preActivation =
|
preActivation.initialize(rootContexts);
|
||||||
new PreActivation(snapshot, currentSnapshot, moduleInjector, forwardEvent);
|
return preActivation;
|
||||||
preActivation.initialize(rootContexts);
|
});
|
||||||
return preActivation;
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
|
@ -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]);
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
}
|
|
@ -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,199 +365,265 @@ 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 =
|
||||||
return of (t).pipe(
|
(this.onSameUrlNavigation === 'reload' ? true : urlTransition) &&
|
||||||
// Update URL if in `eager` update mode
|
this.urlHandlingStrategy.shouldProcessUrl(t.rawUrl);
|
||||||
tap(t => this.urlUpdateStrategy === 'eager' && !t.extras.skipLocationChange &&
|
|
||||||
this.setBrowserUrl(t.rawUrl, !!t.extras.replaceUrl, t.id)),
|
if (processCurrentUrl) {
|
||||||
// Fire NavigationStart event
|
return of (t).pipe(
|
||||||
mergeMap(t => {
|
// Update URL if in `eager` update mode
|
||||||
const transition = this.transitions.getValue();
|
tap(t => this.urlUpdateStrategy === 'eager' && !t.extras.skipLocationChange &&
|
||||||
(this.events as Subject<Event>).next(new NavigationStart(
|
this.setBrowserUrl(t.rawUrl, !!t.extras.replaceUrl, t.id)),
|
||||||
t.id, this.serializeUrl(t.extractedUrl), t.source, t.state));
|
// Fire NavigationStart event
|
||||||
if (transition !== this.transitions.getValue()) {
|
switchMap(t => {
|
||||||
return EMPTY;
|
const transition = this.transitions.getValue();
|
||||||
|
eventsSubject.next(new NavigationStart(
|
||||||
|
t.id, this.serializeUrl(t.extractedUrl), t.source, t.state));
|
||||||
|
if (transition !== this.transitions.getValue()) {
|
||||||
|
return EMPTY;
|
||||||
|
}
|
||||||
|
return [t];
|
||||||
|
}),
|
||||||
|
|
||||||
|
// This delay is required to match old behavior that forced navigation to
|
||||||
|
// always be async
|
||||||
|
switchMap(t => Promise.resolve(t)),
|
||||||
|
|
||||||
|
// ApplyRedirects
|
||||||
|
applyRedirects(
|
||||||
|
this.ngModule.injector, this.configLoader, this.urlSerializer,
|
||||||
|
this.config),
|
||||||
|
// Recognize
|
||||||
|
recognize(
|
||||||
|
this.rootComponentType, this.config, (url) => this.serializeUrl(url),
|
||||||
|
this.paramsInheritanceStrategy),
|
||||||
|
|
||||||
|
// Fire RoutesRecognized
|
||||||
|
tap(t => {
|
||||||
|
const routesRecognized = new RoutesRecognized(
|
||||||
|
t.id, this.serializeUrl(t.extractedUrl),
|
||||||
|
this.serializeUrl(t.urlAfterRedirects), t.targetSnapshot !);
|
||||||
|
eventsSubject.next(routesRecognized);
|
||||||
|
}), );
|
||||||
|
} else {
|
||||||
|
const processPreviousUrl = urlTransition && this.rawUrlTree &&
|
||||||
|
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 ({
|
||||||
|
...t,
|
||||||
|
targetSnapshot,
|
||||||
|
urlAfterRedirects: extractedUrl,
|
||||||
|
extras: {...extras, skipLocationChange: false, replaceUrl: false},
|
||||||
|
});
|
||||||
|
} 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;
|
||||||
|
t.resolve(null);
|
||||||
|
return EMPTY;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return [t];
|
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// This delay is required to match old behavior that forced navigation to
|
// Before Preactivation
|
||||||
// always be async
|
switchTap(t => {
|
||||||
mergeMap(t => Promise.resolve(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,
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
|
||||||
// ApplyRedirects
|
// --- GUARDS ---
|
||||||
applyRedirects(
|
tap(t => {
|
||||||
this.ngModule.injector, this.configLoader, this.urlSerializer,this.config),
|
const guardsStart = new GuardsCheckStart(
|
||||||
// Recognize
|
t.id, this.serializeUrl(t.extractedUrl), this.serializeUrl(t.urlAfterRedirects),
|
||||||
recognize(
|
t.targetSnapshot !);
|
||||||
this.rootComponentType, this.config, (url) => this.serializeUrl(url),
|
this.triggerEvent(guardsStart);
|
||||||
this.paramsInheritanceStrategy),
|
}),
|
||||||
|
map(t => {
|
||||||
|
const preActivation = new PreActivation(
|
||||||
|
t.targetSnapshot !, t.currentSnapshot, this.ngModule.injector,
|
||||||
|
(evt: Event) => this.triggerEvent(evt));
|
||||||
|
preActivation.initialize(this.rootContexts);
|
||||||
|
return {...t, preActivation};
|
||||||
|
}),
|
||||||
|
|
||||||
// Fire RoutesRecognized
|
checkGuards(),
|
||||||
tap(t => (this.events as Subject<Event>).next(new RoutesRecognized(
|
|
||||||
t.id, this.serializeUrl(t.extractedUrl), this.serializeUrl(t.urlAfterRedirects),
|
|
||||||
t.targetSnapshot !)))
|
|
||||||
);
|
|
||||||
} else if (
|
|
||||||
urlTransition && this.rawUrlTree &&
|
|
||||||
this.urlHandlingStrategy.shouldProcessUrl(this.rawUrlTree)) {
|
|
||||||
(this.events as Subject<Event>).next(new NavigationStart(
|
|
||||||
t.id, this.serializeUrl(t.extractedUrl), t.source, t.state));
|
|
||||||
|
|
||||||
return of ({
|
tap(t => {
|
||||||
...t,
|
const guardsEnd = new GuardsCheckEnd(
|
||||||
urlAfterRedirects: t.extractedUrl,
|
t.id, this.serializeUrl(t.extractedUrl), this.serializeUrl(t.urlAfterRedirects),
|
||||||
extras: {...t.extras, skipLocationChange: false, replaceUrl: false},
|
t.targetSnapshot !, !!t.guardsResult);
|
||||||
targetSnapshot: createEmptyState(t.extractedUrl, this.rootComponentType).snapshot
|
this.triggerEvent(guardsEnd);
|
||||||
});
|
}),
|
||||||
} else {
|
|
||||||
this.rawUrlTree = t.rawUrl;
|
|
||||||
t.resolve(null);
|
|
||||||
return EMPTY;
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
|
|
||||||
// Before Preactivation
|
filter(t => {
|
||||||
beforePreactivation(this.hooks.beforePreactivation),
|
if (!t.guardsResult) {
|
||||||
|
this.resetUrlToCurrentUrlTree();
|
||||||
|
const navCancel =
|
||||||
|
new NavigationCancel(t.id, this.serializeUrl(t.extractedUrl), '');
|
||||||
|
eventsSubject.next(navCancel);
|
||||||
|
t.resolve(false);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}),
|
||||||
|
|
||||||
// --- GUARDS ---
|
// --- RESOLVE ---
|
||||||
tap(t => this.triggerEvent(new GuardsCheckStart(
|
switchTap(t => {
|
||||||
t.id, this.serializeUrl(t.extractedUrl),
|
if (t.preActivation !.isActivating()) {
|
||||||
this.serializeUrl(t.urlAfterRedirects), t.targetSnapshot !))),
|
return of (t).pipe(
|
||||||
map(t => {
|
tap(t => {
|
||||||
const preActivation = new PreActivation(
|
const resolveStart = new ResolveStart(
|
||||||
t.targetSnapshot !, t.currentSnapshot, this.ngModule.injector,
|
t.id, this.serializeUrl(t.extractedUrl),
|
||||||
(evt: Event) => this.triggerEvent(evt));
|
this.serializeUrl(t.urlAfterRedirects), t.targetSnapshot !);
|
||||||
preActivation.initialize(this.rootContexts);
|
this.triggerEvent(resolveStart);
|
||||||
return {...t, preActivation};
|
}),
|
||||||
}),
|
resolveData(this.paramsInheritanceStrategy), //
|
||||||
|
tap(t => {
|
||||||
|
const resolveEnd = new ResolveEnd(
|
||||||
|
t.id, this.serializeUrl(t.extractedUrl),
|
||||||
|
this.serializeUrl(t.urlAfterRedirects), t.targetSnapshot !);
|
||||||
|
this.triggerEvent(resolveEnd);
|
||||||
|
}), );
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}),
|
||||||
|
|
||||||
checkGuards(),
|
// --- AFTER PREACTIVATION ---
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
|
||||||
tap(t => this.triggerEvent(new GuardsCheckEnd(
|
map(t => {
|
||||||
t.id, this.serializeUrl(t.extractedUrl),
|
const targetRouterState = createRouterState(
|
||||||
this.serializeUrl(t.urlAfterRedirects), t.targetSnapshot !,
|
this.routeReuseStrategy, t.targetSnapshot !, t.currentRouterState);
|
||||||
!!t.guardsResult))),
|
return ({...t, targetRouterState});
|
||||||
|
}),
|
||||||
|
|
||||||
mergeMap(t => {
|
/* Once here, we are about to activate syncronously. The assumption is this will
|
||||||
if (!t.guardsResult) {
|
succeed, and user code may read from the Router service. Therefore before
|
||||||
this.resetUrlToCurrentUrlTree();
|
activation, we need to update router properties storing the current URL and the
|
||||||
(this.events as Subject<Event>)
|
RouterState, as well as updated the browser URL. All this should happen *before*
|
||||||
.next(new NavigationCancel(t.id, this.serializeUrl(t.extractedUrl), ''));
|
activating. */
|
||||||
t.resolve(false);
|
tap(t => {
|
||||||
return EMPTY;
|
this.currentUrlTree = t.urlAfterRedirects;
|
||||||
}
|
this.rawUrlTree = this.urlHandlingStrategy.merge(this.currentUrlTree, t.rawUrl);
|
||||||
return of(t);
|
|
||||||
}),
|
|
||||||
|
|
||||||
// --- RESOLVE ---
|
(this as{routerState: RouterState}).routerState = t.targetRouterState !;
|
||||||
mergeMap(t => {
|
|
||||||
if (t.preActivation !.isActivating()) {
|
|
||||||
return of (t).pipe(
|
|
||||||
tap(t => this.triggerEvent(new ResolveStart(
|
|
||||||
t.id, this.serializeUrl(t.extractedUrl),
|
|
||||||
this.serializeUrl(t.urlAfterRedirects), t.targetSnapshot !))),
|
|
||||||
resolveData(this.paramsInheritanceStrategy),
|
|
||||||
tap(t => this.triggerEvent(new ResolveEnd(
|
|
||||||
t.id, this.serializeUrl(t.extractedUrl),
|
|
||||||
this.serializeUrl(t.urlAfterRedirects), t.targetSnapshot !))), );
|
|
||||||
}
|
|
||||||
return of (t);
|
|
||||||
}),
|
|
||||||
|
|
||||||
// --- AFTER PREACTIVATION ---
|
if (this.urlUpdateStrategy === 'deferred' && !t.extras.skipLocationChange) {
|
||||||
afterPreactivation(this.hooks.afterPreactivation), map(t => {
|
this.setBrowserUrl(this.rawUrlTree, !!t.extras.replaceUrl, t.id);
|
||||||
const targetRouterState = createRouterState(
|
}
|
||||||
this.routeReuseStrategy, t.targetSnapshot !, t.currentRouterState);
|
}),
|
||||||
return ({...t, targetRouterState});
|
|
||||||
}),
|
|
||||||
|
|
||||||
// Side effects of resetting Router instance
|
activateRoutes(
|
||||||
afterPreactivation(this.hooks.afterPreactivation), map(t => {
|
this.rootContexts, this.routeReuseStrategy,
|
||||||
const targetRouterState = createRouterState(
|
(evt: Event) => this.triggerEvent(evt)),
|
||||||
this.routeReuseStrategy, t.targetSnapshot !, t.currentRouterState);
|
|
||||||
return ({...t, targetRouterState});
|
|
||||||
}),
|
|
||||||
|
|
||||||
// Side effects of resetting Router instance
|
tap({next() { completed = true; }, complete() { completed = true; }}),
|
||||||
tap(t => {
|
finalize(() => {
|
||||||
this.currentUrlTree = t.urlAfterRedirects;
|
/* When the navigation stream finishes either through error or success, we set the
|
||||||
this.rawUrlTree = this.urlHandlingStrategy.merge(this.currentUrlTree, t.rawUrl);
|
* `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
|
||||||
(this as{routerState: RouterState}).routerState = t.targetRouterState !;
|
* 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 (this.urlUpdateStrategy === 'deferred' && !t.extras.skipLocationChange) {
|
if (!completed && !errored) {
|
||||||
this.setBrowserUrl(this.rawUrlTree, !!t.extras.replaceUrl, t.id);
|
// 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
|
||||||
activate(
|
// to determine whether or not to handle a given popstate event or to leave it
|
||||||
this.rootContexts, this.routeReuseStrategy,
|
// to the Angualr router.
|
||||||
(evt: Event) => this.triggerEvent(evt)),
|
this.resetUrlToCurrentUrlTree();
|
||||||
|
const navCancel = new NavigationCancel(
|
||||||
tap({next: () => completed = true, complete: () => completed = true}),
|
t.id, this.serializeUrl(t.extractedUrl),
|
||||||
finalize(() => {
|
`Navigation ID ${t.id} is not equal to the current navigation id ${this.navigationId}`);
|
||||||
if (!completed && !errored) {
|
eventsSubject.next(navCancel);
|
||||||
(this.events as Subject<Event>).next(new NavigationCancel(
|
t.resolve(false);
|
||||||
t.id, this.serializeUrl(t.extractedUrl),
|
}
|
||||||
`Navigation ID ${t.id} is not equal to the current navigation id ${this.navigationId}`));
|
}),
|
||||||
t.resolve(false);
|
catchError((e) => {
|
||||||
}
|
errored = true;
|
||||||
}),
|
/* This error type is issued during Redirect, and is handled as a cancellation
|
||||||
catchError((e) => {
|
* rather than an error. */
|
||||||
errored = true;
|
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);
|
const navCancel =
|
||||||
(this.events as Subject<Event>).next(
|
new NavigationCancel(t.id, this.serializeUrl(t.extractedUrl), e.message);
|
||||||
new NavigationCancel(t.id, this.serializeUrl(t.extractedUrl), e.message));
|
eventsSubject.next(navCancel);
|
||||||
} else {
|
/* All other errors should reset to the router's internal URL reference to the
|
||||||
this.resetStateAndUrl(t.currentRouterState, t.currentUrlTree, t.rawUrl);
|
* pre-error state. */
|
||||||
(this.events as Subject<Event>).next(new NavigationError(
|
} else {
|
||||||
t.id, this.serializeUrl(t.extractedUrl), e));
|
this.resetStateAndUrl(t.currentRouterState, t.currentUrlTree, t.rawUrl);
|
||||||
try {
|
const navError = new NavigationError(t.id, this.serializeUrl(t.extractedUrl), e);
|
||||||
t.resolve(this.errorHandler(e));
|
eventsSubject.next(navError);
|
||||||
} catch (ee) {
|
try {
|
||||||
t.reject(ee);
|
t.resolve(this.errorHandler(e));
|
||||||
}
|
} catch (ee) {
|
||||||
}
|
t.reject(ee);
|
||||||
return EMPTY;
|
}
|
||||||
}), );
|
}
|
||||||
})) as any as Observable<NavigationTransition>;
|
return EMPTY;
|
||||||
|
}), );
|
||||||
function activate(
|
// TODO(jasonaden): remove cast once g3 is on updated TypeScript
|
||||||
rootContexts: ChildrenOutletContexts, routeReuseStrategy: RouteReuseStrategy,
|
})) as any as Observable<NavigationTransition>;
|
||||||
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];
|
||||||
|
|
Loading…
Reference in New Issue