diff --git a/packages/router/src/operators/resolve_data.ts b/packages/router/src/operators/resolve_data.ts index a418a8d9ba..fe2f41a4d1 100644 --- a/packages/router/src/operators/resolve_data.ts +++ b/packages/router/src/operators/resolve_data.ts @@ -6,19 +6,81 @@ * found in the LICENSE file at https://angular.io/license */ -import {MonoTypeOperatorFunction, Observable} from 'rxjs'; +import {Injector} from '@angular/core'; +import {MonoTypeOperatorFunction, Observable, from, of } from 'rxjs'; +import {concatMap, last, map, mergeMap, reduce} from 'rxjs/operators'; +import {ResolveData} from '../config'; import {NavigationTransition} from '../router'; -import {switchTap} from './switch_tap'; +import {ChildrenOutletContexts} from '../router_outlet_context'; +import {ActivatedRouteSnapshot, RouterStateSnapshot, inheritedParamsDataResolve} from '../router_state'; +import {wrapIntoObservable} from '../utils/collection'; -export function resolveData(paramsInheritanceStrategy: 'emptyOnly' | 'always'): - MonoTypeOperatorFunction { +import {getAllRouteGuards, getToken} from './check_guards'; + +export function resolveData( + rootContexts: ChildrenOutletContexts, paramsInheritanceStrategy: 'emptyOnly' | 'always', + moduleInjector: Injector): MonoTypeOperatorFunction { return function(source: Observable) { - return source.pipe(switchTap(t => { - if (!t.preActivation) { - throw new Error('PreActivation required to resolve data'); + return source.pipe(mergeMap(t => { + const {targetSnapshot, currentSnapshot} = t; + const checks = getAllRouteGuards(targetSnapshot !, currentSnapshot, rootContexts); + + if (!checks.canActivateChecks.length) { + return of (t); } - return t.preActivation.resolveData(paramsInheritanceStrategy); + + return from(checks.canActivateChecks) + .pipe( + concatMap( + check => runResolve( + check.route, targetSnapshot !, paramsInheritanceStrategy, moduleInjector)), + reduce((_: any, __: any) => _), map(_ => t)); })); }; } + +function runResolve( + futureARS: ActivatedRouteSnapshot, futureRSS: RouterStateSnapshot, + paramsInheritanceStrategy: 'emptyOnly' | 'always', moduleInjector: Injector) { + const resolve = futureARS._resolve; + return resolveNode(resolve, futureARS, futureRSS, moduleInjector) + .pipe(map((resolvedData: any) => { + futureARS._resolvedData = resolvedData; + futureARS.data = { + ...futureARS.data, + ...inheritedParamsDataResolve(futureARS, paramsInheritanceStrategy).resolve}; + return null; + })); +} + +function resolveNode( + resolve: ResolveData, futureARS: ActivatedRouteSnapshot, futureRSS: RouterStateSnapshot, + moduleInjector: Injector): Observable { + const keys = Object.keys(resolve); + if (keys.length === 0) { + return of ({}); + } + if (keys.length === 1) { + const key = keys[0]; + return getResolver(resolve[key], futureARS, futureRSS, moduleInjector) + .pipe(map((value: any) => { return {[key]: value}; })); + } + const data: {[k: string]: any} = {}; + const runningResolvers$ = from(keys).pipe(mergeMap((key: string) => { + return getResolver(resolve[key], futureARS, futureRSS, moduleInjector) + .pipe(map((value: any) => { + data[key] = value; + return value; + })); + })); + return runningResolvers$.pipe(last(), map(() => data)); +} + +function getResolver( + injectionToken: any, futureARS: ActivatedRouteSnapshot, futureRSS: RouterStateSnapshot, + moduleInjector: Injector): Observable { + const resolver = getToken(injectionToken, futureARS, moduleInjector); + return resolver.resolve ? wrapIntoObservable(resolver.resolve(futureARS, futureRSS)) : + wrapIntoObservable(resolver(futureARS, futureRSS)); +} \ No newline at end of file diff --git a/packages/router/src/operators/setup_preactivation.ts b/packages/router/src/operators/setup_preactivation.ts deleted file mode 100644 index 2851f25f41..0000000000 --- a/packages/router/src/operators/setup_preactivation.ts +++ /dev/null @@ -1,26 +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 {Injector} from '@angular/core'; -import {Observable, OperatorFunction} from 'rxjs'; -import {map} from 'rxjs/operators'; - -import {Event} from '../events'; -import {PreActivation} from '../pre_activation'; -import {ChildrenOutletContexts} from '../router_outlet_context'; -import {RouterStateSnapshot} from '../router_state'; - -export const setupPreactivation = - (rootContexts: ChildrenOutletContexts, currentSnapshot: RouterStateSnapshot, - moduleInjector: Injector, forwardEvent?: (evt: Event) => void) => - map((snapshot: RouterStateSnapshot) => { - const preActivation = - new PreActivation(snapshot, currentSnapshot, moduleInjector, forwardEvent); - preActivation.initialize(rootContexts); - return preActivation; - }); diff --git a/packages/router/src/pre_activation.ts b/packages/router/src/pre_activation.ts deleted file mode 100644 index baa7bd4e2c..0000000000 --- a/packages/router/src/pre_activation.ts +++ /dev/null @@ -1,357 +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 {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; -} diff --git a/packages/router/src/router.ts b/packages/router/src/router.ts index 9214d09ad0..ba45e054da 100644 --- a/packages/router/src/router.ts +++ b/packages/router/src/router.ts @@ -17,11 +17,10 @@ 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} from './operators/check_guards'; +import {checkGuards, getAllRouteGuards} from './operators/check_guards'; import {recognize} from './operators/recognize'; import {resolveData} from './operators/resolve_data'; import {switchTap} from './operators/switch_tap'; -import {PreActivation} from './pre_activation'; import {DefaultRouteReuseStrategy, RouteReuseStrategy} from './route_reuse_strategy'; import {RouterConfigLoader} from './router_config_loader'; import {ChildrenOutletContexts} from './router_outlet_context'; @@ -167,9 +166,6 @@ function defaultMalformedUriErrorHandler( return urlSerializer.parse('/'); } -type NavStreamValue = - boolean | {appliedUrl: UrlTree, snapshot: RouterStateSnapshot, shouldActivate?: boolean}; - export type NavigationTransition = { id: number, currentUrlTree: UrlTree, @@ -188,7 +184,6 @@ export type NavigationTransition = { currentRouterState: RouterState, targetRouterState: RouterState | null, guardsResult: boolean | null, - preActivation: PreActivation | null }; /** @@ -358,7 +353,6 @@ export class Router { currentRouterState: this.routerState, targetRouterState: null, guardsResult: null, - preActivation: null }); this.navigations = this.setupNavigations(this.transitions); @@ -481,15 +475,10 @@ export class Router { t.targetSnapshot !); this.triggerEvent(guardsStart); }), - 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}; - }), - checkGuards(this.rootContexts, this.ngModule.injector, (evt: Event) => this.triggerEvent(evt)), + checkGuards( + this.rootContexts, this.ngModule.injector, + (evt: Event) => this.triggerEvent(evt)), tap(t => { const guardsEnd = new GuardsCheckEnd( @@ -512,7 +501,8 @@ export class Router { // --- RESOLVE --- switchTap(t => { - if (t.preActivation !.isActivating()) { + if (getAllRouteGuards(t.targetSnapshot !, t.currentSnapshot, this.rootContexts) + .canActivateChecks.length) { return of (t).pipe( tap(t => { const resolveStart = new ResolveStart( @@ -520,7 +510,9 @@ export class Router { this.serializeUrl(t.urlAfterRedirects), t.targetSnapshot !); this.triggerEvent(resolveStart); }), - resolveData(this.paramsInheritanceStrategy), // + resolveData( + this.rootContexts, this.paramsInheritanceStrategy, + this.ngModule.injector), // tap(t => { const resolveEnd = new ResolveEnd( t.id, this.serializeUrl(t.extractedUrl), diff --git a/packages/router/test/router.spec.ts b/packages/router/test/router.spec.ts index 51445f249d..2524811c62 100644 --- a/packages/router/test/router.spec.ts +++ b/packages/router/test/router.spec.ts @@ -13,8 +13,8 @@ import {of } from 'rxjs'; import {Routes} from '../src/config'; import {ChildActivationStart} from '../src/events'; import {checkGuards as checkGuardsOperator} from '../src/operators/check_guards'; -import {PreActivation} from '../src/pre_activation'; -import {Router} from '../src/router'; +import {resolveData as resolveDataOperator} from '../src/operators/resolve_data'; +import {NavigationTransition, Router} from '../src/router'; import {ChildrenOutletContexts} from '../src/router_outlet_context'; import {RouterStateSnapshot, createEmptyStateSnapshot} from '../src/router_state'; import {DefaultUrlSerializer} from '../src/url_tree'; @@ -538,15 +538,15 @@ describe('Router', () => { function checkResolveData( future: RouterStateSnapshot, curr: RouterStateSnapshot, injector: any, check: any): void { - const p = new PreActivation(future, curr, injector); - p.initialize(new ChildrenOutletContexts()); - p.resolveData('emptyOnly').subscribe(check, (e) => { throw e; }); + of ({ targetSnapshot: future, currentSnapshot: curr } as Partial) + .pipe(resolveDataOperator(new ChildrenOutletContexts(), 'emptyOnly', injector)) + .subscribe(check, (e) => { throw e; }); } function checkGuards( future: RouterStateSnapshot, curr: RouterStateSnapshot, injector: any, check: (result: boolean) => void): void { - of ({ targetSnapshot: future, currentSnapshot: curr } as any) + of ({ targetSnapshot: future, currentSnapshot: curr } as Partial) .pipe(checkGuardsOperator(new ChildrenOutletContexts(), injector)) .subscribe(t => check(!!t.guardsResult), (e) => { throw e; }); }