diff --git a/packages/router/src/operators/check_guards.ts b/packages/router/src/operators/check_guards.ts index 55594be79c..370189966d 100644 --- a/packages/router/src/operators/check_guards.ts +++ b/packages/router/src/operators/check_guards.ts @@ -7,20 +7,18 @@ */ import {Injector} from '@angular/core'; -import {Observable, MonoTypeOperatorFunction, from, of } from 'rxjs'; +import {MonoTypeOperatorFunction, Observable, from, of } from 'rxjs'; import {concatMap, every, first, map, mergeMap} from 'rxjs/operators'; -import {LoadedRouterConfig, RunGuardsAndResolvers} from '../config'; import {ActivationStart, ChildActivationStart, Event} from '../events'; -import {ChildrenOutletContexts, OutletContext} from '../router_outlet_context'; -import {ActivatedRouteSnapshot, RouterStateSnapshot, equalParamsAndUrlSegments} from '../router_state'; -import {andObservables, forEach, shallowEqual, wrapIntoObservable} from '../utils/collection'; -import {TreeNode, nodeChildrenAsMap} from '../utils/tree'; -import { NavigationTransition } from '../router'; +import {NavigationTransition} from '../router'; +import {ChildrenOutletContexts} from '../router_outlet_context'; +import {ActivatedRouteSnapshot, RouterStateSnapshot} from '../router_state'; +import {andObservables, wrapIntoObservable} from '../utils/collection'; +import {CanActivate, CanDeactivate, Checks, getAllRouteGuards, getCanActivateChild, getToken} from '../utils/preactivation'; export function checkGuards( - rootContexts: ChildrenOutletContexts, - moduleInjector: Injector, + rootContexts: ChildrenOutletContexts, moduleInjector: Injector, forwardEvent?: (evt: Event) => void): MonoTypeOperatorFunction { return function(source: Observable) { @@ -33,12 +31,12 @@ export function checkGuards( return runCanDeactivateChecks(checks, targetSnapshot !, currentSnapshot, moduleInjector) .pipe( - mergeMap((canDeactivate: boolean) => { - return canDeactivate ? - runCanActivateChecks(targetSnapshot !, checks, moduleInjector, forwardEvent) : - of (false); - }), - map(guardsResult => ({...t, guardsResult}))); + mergeMap((canDeactivate: boolean) => { + return canDeactivate ? + runCanActivateChecks(targetSnapshot !, checks, moduleInjector, forwardEvent) : + of (false); + }), + map(guardsResult => ({...t, guardsResult}))); })); }; } @@ -127,7 +125,7 @@ function runCanActivateChild( const canActivateChildGuards = path.slice(0, path.length - 1) .reverse() - .map(p => extractCanActivateChild(p)) + .map(p => getCanActivateChild(p)) .filter(_ => _ !== null); return andObservables(from(canActivateChildGuards).pipe(map((d: any) => { @@ -145,31 +143,6 @@ function runCanActivateChild( }))); } -function 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}; -} - -export function getToken( - token: any, snapshot: ActivatedRouteSnapshot, moduleInjector: Injector): any { - const config = closestLoadedConfig(snapshot); - const injector = config ? config.module.injector : 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; -} - function runCanDeactivate( component: Object | null, currARS: ActivatedRouteSnapshot, currRSS: RouterStateSnapshot, futureRSS: RouterStateSnapshot, moduleInjector: Injector): Observable { @@ -187,158 +160,3 @@ function runCanDeactivate( })); return canDeactivate$.pipe(every((result: any) => result === true)); } - -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) {} -} - -export function getAllRouteGuards( - future: RouterStateSnapshot, curr: RouterStateSnapshot, - parentContexts: ChildrenOutletContexts) { - const futureRoot = future._root; - const currRoot = curr ? curr._root : null; - - return getChildRouteGuards(futureRoot, currRoot, parentContexts, [futureRoot.value]); -} - -declare type Checks = { - canDeactivateChecks: CanDeactivate[], - canActivateChecks: CanActivate[], -}; - -function getChildRouteGuards( - futureNode: TreeNode, currNode: TreeNode| null, - contexts: ChildrenOutletContexts | null, futurePath: ActivatedRouteSnapshot[], - checks: Checks = { - canDeactivateChecks: [], - canActivateChecks: [] - }): Checks { - const prevChildren = nodeChildrenAsMap(currNode); - - // Process the children of the future route - futureNode.children.forEach(c => { - getRouteGuards(c, prevChildren[c.value.outlet], contexts, futurePath.concat([c.value]), checks); - 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) => - deactivateRouteAndItsChildren(v, contexts !.getContext(k), checks)); - - return checks; -} - -function getRouteGuards( - futureNode: TreeNode, currNode: TreeNode, - parentContexts: ChildrenOutletContexts | null, futurePath: ActivatedRouteSnapshot[], - checks: Checks = { - canDeactivateChecks: [], - canActivateChecks: [] - }): Checks { - 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 shouldRun = - shouldRunGuardsAndResolvers(curr, future, future.routeConfig !.runGuardsAndResolvers); - if (shouldRun) { - checks.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) { - getChildRouteGuards( - futureNode, currNode, context ? context.children : null, futurePath, checks); - - // if we have a componentless route, we recurse but keep the same outlet map. - } else { - getChildRouteGuards(futureNode, currNode, parentContexts, futurePath, checks); - } - - if (shouldRun) { - const outlet = context !.outlet !; - checks.canDeactivateChecks.push(new CanDeactivate(outlet.component, curr)); - } - } else { - if (curr) { - deactivateRouteAndItsChildren(currNode, context, checks); - } - - checks.canActivateChecks.push(new CanActivate(futurePath)); - // If we have a component, we need to go through an outlet. - if (future.component) { - getChildRouteGuards(futureNode, null, context ? context.children : null, futurePath, checks); - - // if we have a componentless route, we recurse but keep the same outlet map. - } else { - getChildRouteGuards(futureNode, null, parentContexts, futurePath, checks); - } - } - - return checks; -} - -function 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); - } -} - -function deactivateRouteAndItsChildren( - route: TreeNode, context: OutletContext | null, checks: Checks): void { - const children = nodeChildrenAsMap(route); - const r = route.value; - - forEach(children, (node: TreeNode, childName: string) => { - if (!r.component) { - deactivateRouteAndItsChildren(node, context, checks); - } else if (context) { - deactivateRouteAndItsChildren(node, context.children.getContext(childName), checks); - } else { - deactivateRouteAndItsChildren(node, null, checks); - } - }); - - if (!r.component) { - checks.canDeactivateChecks.push(new CanDeactivate(null, r)); - } else if (context && context.outlet && context.outlet.isActivated) { - checks.canDeactivateChecks.push(new CanDeactivate(context.outlet.component, r)); - } else { - checks.canDeactivateChecks.push(new CanDeactivate(null, r)); - } -} - -// function checkGuards2(future: RouterStateSnapshot, curr: RouterStateSnapshot): -// Observable { -// if (!this.isDeactivating() && !this.isActivating()) { -// return of (true); -// } -// const canDeactivate$ = this.runCanDeactivateChecks(); -// return canDeactivate$.pipe(mergeMap( -// (canDeactivate: boolean) => canDeactivate ? this.runCanActivateChecks() : of (false))); -// } \ No newline at end of file diff --git a/packages/router/src/operators/resolve_data.ts b/packages/router/src/operators/resolve_data.ts index fe2f41a4d1..ff633d755f 100644 --- a/packages/router/src/operators/resolve_data.ts +++ b/packages/router/src/operators/resolve_data.ts @@ -16,7 +16,7 @@ import {ChildrenOutletContexts} from '../router_outlet_context'; import {ActivatedRouteSnapshot, RouterStateSnapshot, inheritedParamsDataResolve} from '../router_state'; import {wrapIntoObservable} from '../utils/collection'; -import {getAllRouteGuards, getToken} from './check_guards'; +import {getAllRouteGuards, getToken} from '../utils/preactivation'; export function resolveData( rootContexts: ChildrenOutletContexts, paramsInheritanceStrategy: 'emptyOnly' | 'always', diff --git a/packages/router/src/router.ts b/packages/router/src/router.ts index ba45e054da..211813b5dd 100644 --- a/packages/router/src/router.ts +++ b/packages/router/src/router.ts @@ -17,7 +17,7 @@ import {createUrlTree} from './create_url_tree'; import {Event, GuardsCheckEnd, GuardsCheckStart, NavigationCancel, NavigationEnd, NavigationError, NavigationStart, NavigationTrigger, ResolveEnd, ResolveStart, RouteConfigLoadEnd, RouteConfigLoadStart, RoutesRecognized} from './events'; import {activateRoutes} from './operators/activate_routes'; import {applyRedirects} from './operators/apply_redirects'; -import {checkGuards, getAllRouteGuards} from './operators/check_guards'; +import {checkGuards} from './operators/check_guards'; import {recognize} from './operators/recognize'; import {resolveData} from './operators/resolve_data'; import {switchTap} from './operators/switch_tap'; @@ -28,6 +28,8 @@ import {ActivatedRoute, RouterState, RouterStateSnapshot, createEmptyState} from import {Params, isNavigationCancelingError} from './shared'; import {DefaultUrlHandlingStrategy, UrlHandlingStrategy} from './url_handling_strategy'; import {UrlSerializer, UrlTree, containsTree, createEmptyUrlTree} from './url_tree'; +import {getAllRouteGuards} from './utils/preactivation'; + /** diff --git a/packages/router/src/utils/preactivation.ts b/packages/router/src/utils/preactivation.ts new file mode 100644 index 0000000000..2f56a177aa --- /dev/null +++ b/packages/router/src/utils/preactivation.ts @@ -0,0 +1,185 @@ +/** + * @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 {LoadedRouterConfig, RunGuardsAndResolvers} from '../config'; +import {ChildrenOutletContexts, OutletContext} from '../router_outlet_context'; +import {ActivatedRouteSnapshot, RouterStateSnapshot, equalParamsAndUrlSegments} from '../router_state'; +import {forEach, shallowEqual} from '../utils/collection'; +import {TreeNode, nodeChildrenAsMap} from '../utils/tree'; + +export class CanActivate { + readonly route: ActivatedRouteSnapshot; + constructor(public path: ActivatedRouteSnapshot[]) { + this.route = this.path[this.path.length - 1]; + } +} + +export class CanDeactivate { + constructor(public component: Object|null, public route: ActivatedRouteSnapshot) {} +} + +export declare type Checks = { + canDeactivateChecks: CanDeactivate[], + canActivateChecks: CanActivate[], +}; + +export function getAllRouteGuards( + future: RouterStateSnapshot, curr: RouterStateSnapshot, + parentContexts: ChildrenOutletContexts) { + const futureRoot = future._root; + const currRoot = curr ? curr._root : null; + + return getChildRouteGuards(futureRoot, currRoot, parentContexts, [futureRoot.value]); +} + +export function getCanActivateChild(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}; +} + +export function getToken( + token: any, snapshot: ActivatedRouteSnapshot, moduleInjector: Injector): any { + const config = getClosestLoadedConfig(snapshot); + const injector = config ? config.module.injector : moduleInjector; + return injector.get(token); +} + +function getClosestLoadedConfig(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; +} + +function getChildRouteGuards( + futureNode: TreeNode, currNode: TreeNode| null, + contexts: ChildrenOutletContexts | null, futurePath: ActivatedRouteSnapshot[], + checks: Checks = { + canDeactivateChecks: [], + canActivateChecks: [] + }): Checks { + const prevChildren = nodeChildrenAsMap(currNode); + + // Process the children of the future route + futureNode.children.forEach(c => { + getRouteGuards(c, prevChildren[c.value.outlet], contexts, futurePath.concat([c.value]), checks); + 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) => + deactivateRouteAndItsChildren(v, contexts !.getContext(k), checks)); + + return checks; +} + +function getRouteGuards( + futureNode: TreeNode, currNode: TreeNode, + parentContexts: ChildrenOutletContexts | null, futurePath: ActivatedRouteSnapshot[], + checks: Checks = { + canDeactivateChecks: [], + canActivateChecks: [] + }): Checks { + 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 shouldRun = + shouldRunGuardsAndResolvers(curr, future, future.routeConfig !.runGuardsAndResolvers); + if (shouldRun) { + checks.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) { + getChildRouteGuards( + futureNode, currNode, context ? context.children : null, futurePath, checks); + + // if we have a componentless route, we recurse but keep the same outlet map. + } else { + getChildRouteGuards(futureNode, currNode, parentContexts, futurePath, checks); + } + + if (shouldRun) { + const outlet = context !.outlet !; + checks.canDeactivateChecks.push(new CanDeactivate(outlet.component, curr)); + } + } else { + if (curr) { + deactivateRouteAndItsChildren(currNode, context, checks); + } + + checks.canActivateChecks.push(new CanActivate(futurePath)); + // If we have a component, we need to go through an outlet. + if (future.component) { + getChildRouteGuards(futureNode, null, context ? context.children : null, futurePath, checks); + + // if we have a componentless route, we recurse but keep the same outlet map. + } else { + getChildRouteGuards(futureNode, null, parentContexts, futurePath, checks); + } + } + + return checks; +} + +function 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); + } +} + +function deactivateRouteAndItsChildren( + route: TreeNode, context: OutletContext | null, checks: Checks): void { + const children = nodeChildrenAsMap(route); + const r = route.value; + + forEach(children, (node: TreeNode, childName: string) => { + if (!r.component) { + deactivateRouteAndItsChildren(node, context, checks); + } else if (context) { + deactivateRouteAndItsChildren(node, context.children.getContext(childName), checks); + } else { + deactivateRouteAndItsChildren(node, null, checks); + } + }); + + if (!r.component) { + checks.canDeactivateChecks.push(new CanDeactivate(null, r)); + } else if (context && context.outlet && context.outlet.isActivated) { + checks.canDeactivateChecks.push(new CanDeactivate(context.outlet.component, r)); + } else { + checks.canDeactivateChecks.push(new CanDeactivate(null, r)); + } +}