/** * @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 {Injector} from '@angular/core'; import {Observable, from, of } from 'rxjs'; import {concatMap, every, first, last, map, mergeMap, reduce} from 'rxjs/operators'; import {LoadedRouterConfig, ResolveData, RunGuardsAndResolvers} from './config'; import {ActivationStart, ChildActivationStart, Event} from './events'; import {ChildrenOutletContexts, OutletContext} from './router_outlet_context'; import {ActivatedRouteSnapshot, RouterStateSnapshot, equalParamsAndUrlSegments, inheritedParamsDataResolve} from './router_state'; import {andObservables, forEach, shallowEqual, wrapIntoObservable} from './utils/collection'; import {TreeNode, nodeChildrenAsMap} from './utils/tree'; class CanActivate { readonly route: ActivatedRouteSnapshot; constructor(public path: ActivatedRouteSnapshot[]) { this.route = this.path[this.path.length - 1]; } } class CanDeactivate { constructor(public component: Object|null, public route: ActivatedRouteSnapshot) {} } /** * This class bundles the actions involved in preactivation of a route. */ export class PreActivation { private canActivateChecks: CanActivate[] = []; private canDeactivateChecks: CanDeactivate[] = []; constructor( private future: RouterStateSnapshot, private curr: RouterStateSnapshot, private moduleInjector: Injector, private forwardEvent?: (evt: Event) => void) {} initialize(parentContexts: ChildrenOutletContexts): void { const futureRoot = this.future._root; const currRoot = this.curr ? this.curr._root : null; this.setupChildRouteGuards(futureRoot, currRoot, parentContexts, [futureRoot.value]); } checkGuards(): Observable { if (!this.isDeactivating() && !this.isActivating()) { return of (true); } const canDeactivate$ = this.runCanDeactivateChecks(); return canDeactivate$.pipe(mergeMap( (canDeactivate: boolean) => canDeactivate ? this.runCanActivateChecks() : of (false))); } resolveData(paramsInheritanceStrategy: 'emptyOnly'|'always'): Observable { if (!this.isActivating()) return of (null); return from(this.canActivateChecks) .pipe( concatMap( (check: CanActivate) => this.runResolve(check.route, paramsInheritanceStrategy)), reduce((_: any, __: any) => _)); } isDeactivating(): boolean { return this.canDeactivateChecks.length !== 0; } isActivating(): boolean { return this.canActivateChecks.length !== 0; } /** * Iterates over child routes and calls recursive `setupRouteGuards` to get `this` instance in * proper state to run `checkGuards()` method. */ private setupChildRouteGuards( futureNode: TreeNode, currNode: TreeNode|null, contexts: ChildrenOutletContexts|null, futurePath: ActivatedRouteSnapshot[]): void { const prevChildren = nodeChildrenAsMap(currNode); // Process the children of the future route futureNode.children.forEach(c => { this.setupRouteGuards( c, prevChildren[c.value.outlet], contexts, futurePath.concat([c.value])); delete prevChildren[c.value.outlet]; }); // Process any children left from the current route (not active for the future route) forEach( prevChildren, (v: TreeNode, k: string) => this.deactivateRouteAndItsChildren(v, contexts !.getContext(k))); } /** * Iterates over child routes and calls recursive `setupRouteGuards` to get `this` instance in * proper state to run `checkGuards()` method. */ private setupRouteGuards( futureNode: TreeNode, currNode: TreeNode, parentContexts: ChildrenOutletContexts|null, futurePath: ActivatedRouteSnapshot[]): void { const future = futureNode.value; const curr = currNode ? currNode.value : null; const context = parentContexts ? parentContexts.getContext(futureNode.value.outlet) : null; // reusing the node if (curr && future.routeConfig === curr.routeConfig) { const shouldRunGuardsAndResolvers = this.shouldRunGuardsAndResolvers( curr, future, future.routeConfig !.runGuardsAndResolvers); if (shouldRunGuardsAndResolvers) { this.canActivateChecks.push(new CanActivate(futurePath)); } else { // we need to set the data future.data = curr.data; future._resolvedData = curr._resolvedData; } // If we have a component, we need to go through an outlet. if (future.component) { this.setupChildRouteGuards( futureNode, currNode, context ? context.children : null, futurePath); // if we have a componentless route, we recurse but keep the same outlet map. } else { this.setupChildRouteGuards(futureNode, currNode, parentContexts, futurePath); } if (shouldRunGuardsAndResolvers) { const outlet = context !.outlet !; this.canDeactivateChecks.push(new CanDeactivate(outlet.component, curr)); } } else { if (curr) { this.deactivateRouteAndItsChildren(currNode, context); } this.canActivateChecks.push(new CanActivate(futurePath)); // If we have a component, we need to go through an outlet. if (future.component) { this.setupChildRouteGuards(futureNode, null, context ? context.children : null, futurePath); // if we have a componentless route, we recurse but keep the same outlet map. } else { this.setupChildRouteGuards(futureNode, null, parentContexts, futurePath); } } } private shouldRunGuardsAndResolvers( curr: ActivatedRouteSnapshot, future: ActivatedRouteSnapshot, mode: RunGuardsAndResolvers|undefined): boolean { switch (mode) { case 'always': return true; case 'paramsOrQueryParamsChange': return !equalParamsAndUrlSegments(curr, future) || !shallowEqual(curr.queryParams, future.queryParams); case 'paramsChange': default: return !equalParamsAndUrlSegments(curr, future); } } private deactivateRouteAndItsChildren( route: TreeNode, context: OutletContext|null): void { const children = nodeChildrenAsMap(route); const r = route.value; forEach(children, (node: TreeNode, childName: string) => { if (!r.component) { this.deactivateRouteAndItsChildren(node, context); } else if (context) { this.deactivateRouteAndItsChildren(node, context.children.getContext(childName)); } else { this.deactivateRouteAndItsChildren(node, null); } }); if (!r.component) { this.canDeactivateChecks.push(new CanDeactivate(null, r)); } else if (context && context.outlet && context.outlet.isActivated) { this.canDeactivateChecks.push(new CanDeactivate(context.outlet.component, r)); } else { this.canDeactivateChecks.push(new CanDeactivate(null, r)); } } private runCanDeactivateChecks(): Observable { return from(this.canDeactivateChecks) .pipe( mergeMap((check: CanDeactivate) => this.runCanDeactivate(check.component, check.route)), every((result: boolean) => result === true)); } private runCanActivateChecks(): Observable { return from(this.canActivateChecks) .pipe( concatMap((check: CanActivate) => andObservables(from([ this.fireChildActivationStart(check.route.parent), this.fireActivationStart(check.route), this.runCanActivateChild(check.path), this.runCanActivate(check.route) ]))), every((result: boolean) => result === true)); // this.fireChildActivationStart(check.path), } /** * This should fire off `ActivationStart` events for each route being activated at this * level. * In other words, if you're activating `a` and `b` below, `path` will contain the * `ActivatedRouteSnapshot`s for both and we will fire `ActivationStart` for both. Always * return * `true` so checks continue to run. */ private fireActivationStart(snapshot: ActivatedRouteSnapshot|null): Observable { if (snapshot !== null && this.forwardEvent) { this.forwardEvent(new ActivationStart(snapshot)); } return of (true); } /** * This should fire off `ChildActivationStart` events for each route being activated at this * level. * In other words, if you're activating `a` and `b` below, `path` will contain the * `ActivatedRouteSnapshot`s for both and we will fire `ChildActivationStart` for both. Always * return * `true` so checks continue to run. */ private fireChildActivationStart(snapshot: ActivatedRouteSnapshot|null): Observable { if (snapshot !== null && this.forwardEvent) { this.forwardEvent(new ChildActivationStart(snapshot)); } return of (true); } private runCanActivate(future: ActivatedRouteSnapshot): Observable { const canActivate = future.routeConfig ? future.routeConfig.canActivate : null; if (!canActivate || canActivate.length === 0) return of (true); const obs = from(canActivate).pipe(map((c: any) => { const guard = this.getToken(c, future); let observable: Observable; if (guard.canActivate) { observable = wrapIntoObservable(guard.canActivate(future, this.future)); } else { observable = wrapIntoObservable(guard(future, this.future)); } return observable.pipe(first()); })); return andObservables(obs); } private runCanActivateChild(path: ActivatedRouteSnapshot[]): Observable { const future = path[path.length - 1]; const canActivateChildGuards = path.slice(0, path.length - 1) .reverse() .map(p => this.extractCanActivateChild(p)) .filter(_ => _ !== null); return andObservables(from(canActivateChildGuards).pipe(map((d: any) => { const obs = from(d.guards).pipe(map((c: any) => { const guard = this.getToken(c, d.node); let observable: Observable; if (guard.canActivateChild) { observable = wrapIntoObservable(guard.canActivateChild(future, this.future)); } else { observable = wrapIntoObservable(guard(future, this.future)); } return observable.pipe(first()); })); return andObservables(obs); }))); } private extractCanActivateChild(p: ActivatedRouteSnapshot): {node: ActivatedRouteSnapshot, guards: any[]}|null { const canActivateChild = p.routeConfig ? p.routeConfig.canActivateChild : null; if (!canActivateChild || canActivateChild.length === 0) return null; return {node: p, guards: canActivateChild}; } private runCanDeactivate(component: Object|null, curr: ActivatedRouteSnapshot): Observable { const canDeactivate = curr && curr.routeConfig ? curr.routeConfig.canDeactivate : null; if (!canDeactivate || canDeactivate.length === 0) return of (true); const canDeactivate$ = from(canDeactivate).pipe(mergeMap((c: any) => { const guard = this.getToken(c, curr); let observable: Observable; if (guard.canDeactivate) { observable = wrapIntoObservable(guard.canDeactivate(component, curr, this.curr, this.future)); } else { observable = wrapIntoObservable(guard(component, curr, this.curr, this.future)); } return observable.pipe(first()); })); return canDeactivate$.pipe(every((result: any) => result === true)); } private runResolve( future: ActivatedRouteSnapshot, paramsInheritanceStrategy: 'emptyOnly'|'always'): Observable { const resolve = future._resolve; return this.resolveNode(resolve, future).pipe(map((resolvedData: any): any => { future._resolvedData = resolvedData; future.data = {...future.data, ...inheritedParamsDataResolve(future, paramsInheritanceStrategy).resolve}; return null; })); } private resolveNode(resolve: ResolveData, future: ActivatedRouteSnapshot): Observable { const keys = Object.keys(resolve); if (keys.length === 0) { return of ({}); } if (keys.length === 1) { const key = keys[0]; return this.getResolver(resolve[key], future).pipe(map((value: any) => { return {[key]: value}; })); } const data: {[k: string]: any} = {}; const runningResolvers$ = from(keys).pipe(mergeMap((key: string) => { return this.getResolver(resolve[key], future).pipe(map((value: any) => { data[key] = value; return value; })); })); return runningResolvers$.pipe(last(), map(() => data)); } private getResolver(injectionToken: any, future: ActivatedRouteSnapshot): Observable { const resolver = this.getToken(injectionToken, future); return resolver.resolve ? wrapIntoObservable(resolver.resolve(future, this.future)) : wrapIntoObservable(resolver(future, this.future)); } private getToken(token: any, snapshot: ActivatedRouteSnapshot): any { const config = closestLoadedConfig(snapshot); const injector = config ? config.module.injector : this.moduleInjector; return injector.get(token); } } function closestLoadedConfig(snapshot: ActivatedRouteSnapshot): LoadedRouterConfig|null { if (!snapshot) return null; for (let s = snapshot.parent; s; s = s.parent) { const route = s.routeConfig; if (route && route._loadedConfig) return route._loadedConfig; } return null; }