From f2f1ec0117a827d88245554f9940f98ceca0dcc2 Mon Sep 17 00:00:00 2001 From: vsavkin Date: Mon, 27 Jun 2016 14:00:07 -0700 Subject: [PATCH] feat(router): implement data and resolve --- modules/@angular/router/index.ts | 4 +- modules/@angular/router/src/config.ts | 8 ++ .../router/src/create_router_state.ts | 3 +- modules/@angular/router/src/interfaces.ts | 4 + modules/@angular/router/src/recognize.ts | 88 +++++++++++++------ modules/@angular/router/src/router.ts | 72 ++++++++++++--- modules/@angular/router/src/router_state.ts | 46 ++++++++-- .../router/test/create_url_tree.spec.ts | 13 +-- .../@angular/router/test/recognize.spec.ts | 51 +++++++++++ modules/@angular/router/test/router.spec.ts | 69 ++++++++++++++- tools/public_api_guard/router/index.d.ts | 16 ++++ 11 files changed, 318 insertions(+), 56 deletions(-) diff --git a/modules/@angular/router/index.ts b/modules/@angular/router/index.ts index 910f6e4f32..25628fd3c7 100644 --- a/modules/@angular/router/index.ts +++ b/modules/@angular/router/index.ts @@ -11,8 +11,8 @@ import {RouterLinkActive} from './src/directives/router_link_active'; import {RouterOutlet} from './src/directives/router_outlet'; export {ExtraOptions} from './src/common_router_providers'; -export {Route, RouterConfig} from './src/config'; -export {CanActivate, CanDeactivate} from './src/interfaces'; +export {Data, ResolveData, Route, RouterConfig} from './src/config'; +export {CanActivate, CanDeactivate, Resolve} from './src/interfaces'; export {Event, NavigationCancel, NavigationEnd, NavigationError, NavigationStart, Router, RoutesRecognized} from './src/router'; export {RouterOutletMap} from './src/router_outlet_map'; export {provideRouter} from './src/router_providers'; diff --git a/modules/@angular/router/src/config.ts b/modules/@angular/router/src/config.ts index 3e906ee0a5..e415efee02 100644 --- a/modules/@angular/router/src/config.ts +++ b/modules/@angular/router/src/config.ts @@ -9,6 +9,12 @@ import {Type} from '@angular/core'; export type RouterConfig = Route[]; +export type Data = { + [name: string]: any +}; +export type ResolveData = { + [name: string]: any +}; export interface Route { path?: string; @@ -19,6 +25,8 @@ export interface Route { canDeactivate?: any[]; redirectTo?: string; children?: Route[]; + data?: Data; + resolve?: ResolveData; } export function validateConfig(config: RouterConfig): void { diff --git a/modules/@angular/router/src/create_router_state.ts b/modules/@angular/router/src/create_router_state.ts index dad66c1c63..c89c825a3b 100644 --- a/modules/@angular/router/src/create_router_state.ts +++ b/modules/@angular/router/src/create_router_state.ts @@ -48,7 +48,8 @@ function createOrReuseChildren( function createActivatedRoute(c: ActivatedRouteSnapshot) { return new ActivatedRoute( - new BehaviorSubject(c.url), new BehaviorSubject(c.params), c.outlet, c.component, c); + new BehaviorSubject(c.url), new BehaviorSubject(c.params), new BehaviorSubject(c.data), + c.outlet, c.component, c); } function equalRouteSnapshots(a: ActivatedRouteSnapshot, b: ActivatedRouteSnapshot): boolean { diff --git a/modules/@angular/router/src/interfaces.ts b/modules/@angular/router/src/interfaces.ts index f5ecdc06e7..bcd38b231d 100644 --- a/modules/@angular/router/src/interfaces.ts +++ b/modules/@angular/router/src/interfaces.ts @@ -23,4 +23,8 @@ export interface CanActivate { export interface CanDeactivate { canDeactivate(component: T, route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable|boolean; +} + +export interface Resolve { + resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable|any; } \ No newline at end of file diff --git a/modules/@angular/router/src/recognize.ts b/modules/@angular/router/src/recognize.ts index 9350da374b..bef0a9ca08 100644 --- a/modules/@angular/router/src/recognize.ts +++ b/modules/@angular/router/src/recognize.ts @@ -11,8 +11,8 @@ import {Observable} from 'rxjs/Observable'; import {Observer} from 'rxjs/Observer'; import {of } from 'rxjs/observable/of'; -import {Route, RouterConfig} from './config'; -import {ActivatedRouteSnapshot, RouterStateSnapshot} from './router_state'; +import {Data, ResolveData, Route, RouterConfig} from './config'; +import {ActivatedRouteSnapshot, InheritedResolve, RouterStateSnapshot} from './router_state'; import {PRIMARY_OUTLET, Params} from './shared'; import {UrlPathWithParams, UrlSegment, UrlTree, mapChildrenIntoArray} from './url_tree'; import {last, merge} from './utils/collection'; @@ -22,13 +22,31 @@ class NoMatch { constructor(public segment: UrlSegment = null) {} } +class InheritedFromParent { + constructor( + public parent: InheritedFromParent, public params: Params, public data: Data, + public resolve: InheritedResolve) {} + + get allParams(): Params { + return this.parent ? merge(this.parent.allParams, this.params) : this.params; + } + + get allData(): Data { return this.parent ? merge(this.parent.allData, this.data) : this.data; } + + static get empty(): InheritedFromParent { + return new InheritedFromParent(null, {}, {}, new InheritedResolve(null, {})); + } +} + export function recognize( rootComponentType: Type, config: RouterConfig, urlTree: UrlTree, url: string): Observable { try { - const children = processSegment(config, urlTree.root, {}, PRIMARY_OUTLET); + const children = + processSegment(config, urlTree.root, InheritedFromParent.empty, PRIMARY_OUTLET); const root = new ActivatedRouteSnapshot( - [], {}, PRIMARY_OUTLET, rootComponentType, null, urlTree.root, -1); + [], {}, {}, PRIMARY_OUTLET, rootComponentType, null, urlTree.root, -1, + InheritedResolve.empty); const rootNode = new TreeNode(root, children); return of (new RouterStateSnapshot(url, rootNode, urlTree.queryParams, urlTree.fragment)); } catch (e) { @@ -43,19 +61,21 @@ export function recognize( } } -function processSegment(config: Route[], segment: UrlSegment, extraParams: Params, outlet: string): - TreeNode[] { +function processSegment( + config: Route[], segment: UrlSegment, inherited: InheritedFromParent, + outlet: string): TreeNode[] { if (segment.pathsWithParams.length === 0 && segment.hasChildren()) { - return processSegmentChildren(config, segment, extraParams); + return processSegmentChildren(config, segment, inherited); } else { - return processPathsWithParams(config, segment, 0, segment.pathsWithParams, extraParams, outlet); + return processPathsWithParams(config, segment, 0, segment.pathsWithParams, inherited, outlet); } } function processSegmentChildren( - config: Route[], segment: UrlSegment, extraParams: Params): TreeNode[] { + config: Route[], segment: UrlSegment, + inherited: InheritedFromParent): TreeNode[] { const children = mapChildrenIntoArray( - segment, (child, childOutlet) => processSegment(config, child, extraParams, childOutlet)); + segment, (child, childOutlet) => processSegment(config, child, inherited, childOutlet)); checkOutletNameUniqueness(children); sortActivatedRouteSnapshots(children); return children; @@ -71,10 +91,10 @@ function sortActivatedRouteSnapshots(nodes: TreeNode[]): function processPathsWithParams( config: Route[], segment: UrlSegment, pathIndex: number, paths: UrlPathWithParams[], - extraParams: Params, outlet: string): TreeNode[] { + inherited: InheritedFromParent, outlet: string): TreeNode[] { for (let r of config) { try { - return processPathsWithParamsAgainstRoute(r, segment, pathIndex, paths, extraParams, outlet); + return processPathsWithParamsAgainstRoute(r, segment, pathIndex, paths, inherited, outlet); } catch (e) { if (!(e instanceof NoMatch)) throw e; } @@ -84,32 +104,39 @@ function processPathsWithParams( function processPathsWithParamsAgainstRoute( route: Route, rawSegment: UrlSegment, pathIndex: number, paths: UrlPathWithParams[], - parentExtraParams: Params, outlet: string): TreeNode[] { + inherited: InheritedFromParent, outlet: string): TreeNode[] { if (route.redirectTo) throw new NoMatch(); if ((route.outlet ? route.outlet : PRIMARY_OUTLET) !== outlet) throw new NoMatch(); + const newInheritedResolve = new InheritedResolve(inherited.resolve, getResolve(route)); + if (route.path === '**') { const params = paths.length > 0 ? last(paths).parameters : {}; const snapshot = new ActivatedRouteSnapshot( - paths, merge(parentExtraParams, params), outlet, route.component, route, - getSourceSegment(rawSegment), getPathIndexShift(rawSegment) - 1); + paths, merge(inherited.allParams, params), merge(inherited.allData, getData(route)), outlet, + route.component, route, getSourceSegment(rawSegment), getPathIndexShift(rawSegment) - 1, + newInheritedResolve); return [new TreeNode(snapshot, [])]; } - const {consumedPaths, parameters, extraParams, lastChild} = - match(rawSegment, route, paths, parentExtraParams); + const {consumedPaths, parameters, lastChild} = match(rawSegment, route, paths); const rawSlicedPath = paths.slice(lastChild); const childConfig = route.children ? route.children : []; + const newInherited = route.component ? + InheritedFromParent.empty : + new InheritedFromParent(inherited, parameters, getData(route), newInheritedResolve); const {segment, slicedPath} = split(rawSegment, consumedPaths, rawSlicedPath, childConfig); const snapshot = new ActivatedRouteSnapshot( - consumedPaths, parameters, outlet, route.component, route, getSourceSegment(rawSegment), - getPathIndexShift(rawSegment) + pathIndex + lastChild - 1); + consumedPaths, merge(inherited.allParams, parameters), + merge(inherited.allData, getData(route)), outlet, route.component, route, + getSourceSegment(rawSegment), getPathIndexShift(rawSegment) + pathIndex + lastChild - 1, + newInheritedResolve); if (slicedPath.length === 0 && segment.hasChildren()) { - const children = processSegmentChildren(childConfig, segment, extraParams); + const children = processSegmentChildren(childConfig, segment, newInherited); return [new TreeNode(snapshot, children)]; } else if (childConfig.length === 0 && slicedPath.length === 0) { @@ -117,18 +144,17 @@ function processPathsWithParamsAgainstRoute( } else { const children = processPathsWithParams( - childConfig, segment, pathIndex + lastChild, slicedPath, extraParams, PRIMARY_OUTLET); + childConfig, segment, pathIndex + lastChild, slicedPath, newInherited, PRIMARY_OUTLET); return [new TreeNode(snapshot, children)]; } } -function match( - segment: UrlSegment, route: Route, paths: UrlPathWithParams[], parentExtraParams: Params) { +function match(segment: UrlSegment, route: Route, paths: UrlPathWithParams[]) { if (route.path === '') { if (route.terminal && (segment.hasChildren() || paths.length > 0)) { throw new NoMatch(); } else { - return {consumedPaths: [], lastChild: 0, parameters: {}, extraParams: {}}; + return {consumedPaths: [], lastChild: 0, parameters: {}}; } } @@ -158,10 +184,8 @@ function match( throw new NoMatch(); } - const parameters = merge( - parentExtraParams, merge(posParameters, consumedPaths[consumedPaths.length - 1].parameters)); - const extraParams = route.component ? {} : parameters; - return {consumedPaths, lastChild: currentIndex, parameters, extraParams}; + const parameters = merge(posParameters, consumedPaths[consumedPaths.length - 1].parameters); + return {consumedPaths, lastChild: currentIndex, parameters}; } function checkOutletNameUniqueness(nodes: TreeNode[]): void { @@ -274,4 +298,12 @@ function emptyPathMatch(segment: UrlSegment, slicedPath: UrlPathWithParams[], r: function getOutlet(route: Route): string { return route.outlet ? route.outlet : PRIMARY_OUTLET; +} + +function getData(route: Route): Data { + return route.data ? route.data : {}; +} + +function getResolve(route: Route): ResolveData { + return route.resolve ? route.resolve : {}; } \ No newline at end of file diff --git a/modules/@angular/router/src/router.ts b/modules/@angular/router/src/router.ts index 1ed6a86ad5..f45ad73961 100644 --- a/modules/@angular/router/src/router.ts +++ b/modules/@angular/router/src/router.ts @@ -9,8 +9,10 @@ import 'rxjs/add/operator/map'; import 'rxjs/add/operator/mergeMap'; import 'rxjs/add/operator/mergeAll'; +import 'rxjs/add/operator/reduce'; import 'rxjs/add/operator/every'; import 'rxjs/add/observable/from'; +import 'rxjs/add/observable/forkJoin'; import {Location} from '@angular/common'; import {ComponentResolver, Injector, ReflectiveInjector, Type} from '@angular/core'; @@ -20,17 +22,17 @@ import {Subscription} from 'rxjs/Subscription'; import {of } from 'rxjs/observable/of'; import {applyRedirects} from './apply_redirects'; -import {RouterConfig, validateConfig} from './config'; +import {Data, ResolveData, RouterConfig, validateConfig} from './config'; import {createRouterState} from './create_router_state'; import {createUrlTree} from './create_url_tree'; import {RouterOutlet} from './directives/router_outlet'; import {recognize} from './recognize'; import {resolve} from './resolve'; import {RouterOutletMap} from './router_outlet_map'; -import {ActivatedRoute, ActivatedRouteSnapshot, RouterState, RouterStateSnapshot, advanceActivatedRoute, createEmptyState} from './router_state'; +import {ActivatedRoute, ActivatedRouteSnapshot, InheritedResolve, RouterState, RouterStateSnapshot, advanceActivatedRoute, createEmptyState} from './router_state'; import {PRIMARY_OUTLET, Params} from './shared'; import {UrlSerializer, UrlTree, createEmptyUrlTree} from './url_tree'; -import {forEach, shallowEqual} from './utils/collection'; +import {forEach, merge, shallowEqual} from './utils/collection'; import {TreeNode} from './utils/tree'; export interface NavigationExtras { @@ -277,6 +279,7 @@ export class Router { let updatedUrl: UrlTree; let state: RouterState; let navigationIsSuccessful: boolean; + let preActivation: PreActivation; applyRedirects(url, this.config) .mergeMap(u => { updatedUrl = u; @@ -296,11 +299,20 @@ export class Router { }) .map((newState: RouterState) => { state = newState; - + preActivation = + new PreActivation(state.snapshot, this.currentRouterState.snapshot, this.injector); + preActivation.traverse(this.outletMap); }) .mergeMap(_ => { - return new GuardChecks(state.snapshot, this.currentRouterState.snapshot, this.injector) - .check(this.outletMap); + return preActivation.checkGuards(); + + }) + .mergeMap(shouldActivate => { + if (shouldActivate) { + return preActivation.resolveData().map(() => shouldActivate); + } else { + return of (shouldActivate); + } }) .forEach((shouldActivate: boolean) => { @@ -345,18 +357,20 @@ class CanDeactivate { constructor(public component: Object, public route: ActivatedRouteSnapshot) {} } -class GuardChecks { +class PreActivation { private checks: Array = []; constructor( private future: RouterStateSnapshot, private curr: RouterStateSnapshot, private injector: Injector) {} - check(parentOutletMap: RouterOutletMap): Observable { + traverse(parentOutletMap: RouterOutletMap): void { const futureRoot = this.future._root; const currRoot = this.curr ? this.curr._root : null; this.traverseChildRoutes(futureRoot, currRoot, parentOutletMap); - if (this.checks.length === 0) return of (true); + } + checkGuards(): Observable { + if (this.checks.length === 0) return of (true); return Observable.from(this.checks) .map(s => { if (s instanceof CanActivate) { @@ -371,6 +385,19 @@ class GuardChecks { .every(result => result === true); } + resolveData(): Observable { + if (this.checks.length === 0) return of (null); + return Observable.from(this.checks) + .mergeMap(s => { + if (s instanceof CanActivate) { + return this.runResolve(s.route); + } else { + return of (null); + } + }) + .reduce((_, __) => _); + } + private traverseChildRoutes( futureNode: TreeNode, currNode: TreeNode, outletMap: RouterOutletMap): void { @@ -476,6 +503,32 @@ class GuardChecks { .mergeAll() .every(result => result === true); } + + private runResolve(future: ActivatedRouteSnapshot): Observable { + const resolve = future._resolve; + return this.resolveNode(resolve.current, future).map(resolvedData => { + resolve.resolvedData = resolvedData; + future.data = merge(future.data, resolve.flattenedResolvedData); + return null; + }); + } + + private resolveNode(resolve: ResolveData, future: ActivatedRouteSnapshot): Observable { + const resolvingObs: Observable[] = []; + const resolvedData: {[k: string]: any} = {}; + forEach(resolve, (v: any, k: string) => { + const resolver = this.injector.get(v); + const obs = resolver.resolve ? wrapIntoObservable(resolver.resolve(future, this.future)) : + wrapIntoObservable(resolver(future, this.future)); + resolvingObs.push(obs.map((_: any) => { resolvedData[k] = _; })); + }); + + if (resolvingObs.length > 0) { + return Observable.forkJoin(resolvingObs).map(r => resolvedData); + } else { + return of (resolvedData); + } + } } function wrapIntoObservable(value: T | Observable): Observable { @@ -492,7 +545,6 @@ class ActivateRoutes { activate(parentOutletMap: RouterOutletMap): void { const futureRoot = this.futureState._root; const currRoot = this.currState ? this.currState._root : null; - pushQueryParamsAndFragment(this.futureState); advanceActivatedRoute(this.futureState.root); this.activateChildRoutes(futureRoot, currRoot, parentOutletMap); diff --git a/modules/@angular/router/src/router_state.ts b/modules/@angular/router/src/router_state.ts index 8df82dcb93..7e276b356d 100644 --- a/modules/@angular/router/src/router_state.ts +++ b/modules/@angular/router/src/router_state.ts @@ -10,10 +10,10 @@ import {ComponentFactory, Type} from '@angular/core'; import {BehaviorSubject} from 'rxjs/BehaviorSubject'; import {Observable} from 'rxjs/Observable'; -import {Route} from './config'; +import {Data, ResolveData, Route} from './config'; import {PRIMARY_OUTLET, Params} from './shared'; import {UrlPathWithParams, UrlSegment, UrlTree} from './url_tree'; -import {shallowEqual, shallowEqualArrays} from './utils/collection'; +import {merge, shallowEqual, shallowEqualArrays} from './utils/collection'; import {Tree, TreeNode} from './utils/tree'; @@ -49,10 +49,11 @@ export function createEmptyState(urlTree: UrlTree, rootComponent: Type): RouterS const snapshot = createEmptyStateSnapshot(urlTree, rootComponent); const emptyUrl = new BehaviorSubject([new UrlPathWithParams('', {})]); const emptyParams = new BehaviorSubject({}); + const emptyData = new BehaviorSubject({}); const emptyQueryParams = new BehaviorSubject({}); const fragment = new BehaviorSubject(''); - const activated = - new ActivatedRoute(emptyUrl, emptyParams, PRIMARY_OUTLET, rootComponent, snapshot.root); + const activated = new ActivatedRoute( + emptyUrl, emptyParams, emptyData, PRIMARY_OUTLET, rootComponent, snapshot.root); activated.snapshot = snapshot.root; return new RouterState( new TreeNode(activated, []), emptyQueryParams, fragment, snapshot); @@ -60,10 +61,12 @@ export function createEmptyState(urlTree: UrlTree, rootComponent: Type): RouterS function createEmptyStateSnapshot(urlTree: UrlTree, rootComponent: Type): RouterStateSnapshot { const emptyParams = {}; + const emptyData = {}; const emptyQueryParams = {}; const fragment = ''; const activated = new ActivatedRouteSnapshot( - [], emptyParams, PRIMARY_OUTLET, rootComponent, null, urlTree.root, -1); + [], emptyParams, emptyData, PRIMARY_OUTLET, rootComponent, null, urlTree.root, -1, + InheritedResolve.empty); return new RouterStateSnapshot( '', new TreeNode(activated, []), emptyQueryParams, fragment); } @@ -93,7 +96,7 @@ export class ActivatedRoute { */ constructor( public url: Observable, public params: Observable, - public outlet: string, public component: Type|string, + public data: Observable, public outlet: string, public component: Type|string, futureSnapshot: ActivatedRouteSnapshot) { this._futureSnapshot = futureSnapshot; } @@ -103,6 +106,25 @@ export class ActivatedRoute { } } +export class InheritedResolve { + /** + * @internal + */ + resolvedData = {}; + + constructor(public parent: InheritedResolve, public current: ResolveData) {} + + /** + * @internal + */ + get flattenedResolvedData(): Data { + return this.parent ? merge(this.parent.flattenedResolvedData, this.resolvedData) : + this.resolvedData; + } + + static get empty(): InheritedResolve { return new InheritedResolve(null, {}); } +} + /** * Contains the information about a component loaded in an outlet at a particular moment in time. * @@ -131,16 +153,20 @@ export class ActivatedRouteSnapshot { /** @internal */ _lastPathIndex: number; + /** @internal */ + _resolve: InheritedResolve; + /** * @internal */ constructor( - public url: UrlPathWithParams[], public params: Params, public outlet: string, - public component: Type|string, routeConfig: Route, urlSegment: UrlSegment, - lastPathIndex: number) { + public url: UrlPathWithParams[], public params: Params, public data: Data, + public outlet: string, public component: Type|string, routeConfig: Route, + urlSegment: UrlSegment, lastPathIndex: number, resolve: InheritedResolve) { this._routeConfig = routeConfig; this._urlSegment = urlSegment; this._lastPathIndex = lastPathIndex; + this._resolve = resolve; } toString(): string { @@ -191,6 +217,7 @@ export function advanceActivatedRoute(route: ActivatedRoute): void { if (route.snapshot) { if (!shallowEqual(route.snapshot.params, route._futureSnapshot.params)) { (route.params).next(route._futureSnapshot.params); + (route.data).next(route._futureSnapshot.data); } if (!shallowEqualArrays(route.snapshot.url, route._futureSnapshot.url)) { (route.url).next(route._futureSnapshot.url); @@ -198,5 +225,6 @@ export function advanceActivatedRoute(route: ActivatedRoute): void { route.snapshot = route._futureSnapshot; } else { route.snapshot = route._futureSnapshot; + (route.data).next(route._futureSnapshot.data); } } \ No newline at end of file diff --git a/modules/@angular/router/test/create_url_tree.spec.ts b/modules/@angular/router/test/create_url_tree.spec.ts index cfcacd7e06..b1ef171672 100644 --- a/modules/@angular/router/test/create_url_tree.spec.ts +++ b/modules/@angular/router/test/create_url_tree.spec.ts @@ -167,10 +167,11 @@ describe('createUrlTree', () => { }); function createRoot(tree: UrlTree, commands: any[], queryParams?: Params, fragment?: string) { - const s = - new ActivatedRouteSnapshot([], {}, PRIMARY_OUTLET, 'someComponent', null, tree.root, -1); + const s = new ActivatedRouteSnapshot( + [], {}, {}, PRIMARY_OUTLET, 'someComponent', null, tree.root, -1, null); const a = new ActivatedRoute( - new BehaviorSubject(null), new BehaviorSubject(null), PRIMARY_OUTLET, 'someComponent', s); + new BehaviorSubject(null), new BehaviorSubject(null), new BehaviorSubject(null), + PRIMARY_OUTLET, 'someComponent', s); advanceActivatedRoute(a); return createUrlTree(a, tree, commands, queryParams, fragment); } @@ -182,9 +183,11 @@ function create( expect(segment).toBeDefined(); } const s = new ActivatedRouteSnapshot( - [], {}, PRIMARY_OUTLET, 'someComponent', null, segment, startIndex); + [], {}, {}, PRIMARY_OUTLET, 'someComponent', null, segment, startIndex, + null); const a = new ActivatedRoute( - new BehaviorSubject(null), new BehaviorSubject(null), PRIMARY_OUTLET, 'someComponent', s); + new BehaviorSubject(null), new BehaviorSubject(null), new BehaviorSubject(null), + PRIMARY_OUTLET, 'someComponent', s); advanceActivatedRoute(a); return createUrlTree(a, tree, commands, queryParams, fragment); } \ No newline at end of file diff --git a/modules/@angular/router/test/recognize.spec.ts b/modules/@angular/router/test/recognize.spec.ts index 8bd5a61e10..d10f11536a 100644 --- a/modules/@angular/router/test/recognize.spec.ts +++ b/modules/@angular/router/test/recognize.spec.ts @@ -154,6 +154,57 @@ describe('recognize', () => { }); }); + describe('data', () => { + it('should set static data', () => { + checkRecognize( + [{path: 'a', data: {one: 1}, component: ComponentA}], 'a', (s: RouterStateSnapshot) => { + const r: ActivatedRouteSnapshot = s.firstChild(s.root); + expect(r.data).toEqual({one: 1}); + }); + }); + + it('should merge componentless route\'s data', () => { + checkRecognize( + [{ + path: 'a', + data: {one: 1}, + children: [{path: 'b', data: {two: 2}, component: ComponentB}] + }], + 'a/b', (s: RouterStateSnapshot) => { + const r: ActivatedRouteSnapshot = s.firstChild(s.firstChild(s.root)); + expect(r.data).toEqual({one: 1, two: 2}); + }); + }); + + it('should set resolved data', () => { + checkRecognize( + [{path: 'a', resolve: {one: 'some-token'}, component: ComponentA}], 'a', + (s: RouterStateSnapshot) => { + const r: ActivatedRouteSnapshot = s.firstChild(s.root); + expect(r._resolve.current).toEqual({one: 'some-token'}); + }); + }); + + it('should reuse componentless route\'s resolve', () => { + checkRecognize( + [{ + path: 'a', + resolve: {one: 'one'}, + children: [ + {path: '', resolve: {two: 'two'}, component: ComponentB}, + {path: '', resolve: {three: 'three'}, component: ComponentC, outlet: 'aux'} + ] + }], + 'a', (s: RouterStateSnapshot) => { + const a: ActivatedRouteSnapshot = s.firstChild(s.root); + const c: ActivatedRouteSnapshot[] = s.children(a); + + expect(c[0]._resolve.parent).toBe(a._resolve); + expect(c[1]._resolve.parent).toBe(a._resolve); + }); + }); + }); + describe('empty path', () => { describe('root', () => { it('should work', () => { diff --git a/modules/@angular/router/test/router.spec.ts b/modules/@angular/router/test/router.spec.ts index 1a559232b2..db05b87f40 100644 --- a/modules/@angular/router/test/router.spec.ts +++ b/modules/@angular/router/test/router.spec.ts @@ -11,7 +11,7 @@ import {expect} from '@angular/platform-browser/testing/matchers'; import {Observable} from 'rxjs/Observable'; import {of } from 'rxjs/observable/of'; -import {ActivatedRoute, ActivatedRouteSnapshot, CanActivate, CanDeactivate, DefaultUrlSerializer, Event, NavigationCancel, NavigationEnd, NavigationError, NavigationStart, Params, ROUTER_DIRECTIVES, Router, RouterConfig, RouterOutletMap, RouterStateSnapshot, RoutesRecognized, UrlSerializer} from '../index'; +import {ActivatedRoute, ActivatedRouteSnapshot, CanActivate, CanDeactivate, DefaultUrlSerializer, Event, NavigationCancel, NavigationEnd, NavigationError, NavigationStart, Params, ROUTER_DIRECTIVES, Resolve, Router, RouterConfig, RouterOutletMap, RouterStateSnapshot, RoutesRecognized, UrlSerializer} from '../index'; describe('Integration', () => { @@ -433,6 +433,68 @@ describe('Integration', () => { .toHaveText('primary {simple} right {user victor}'); }))); + describe('data', () => { + class ResolveSix implements Resolve { + resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): number { return 6; } + } + + beforeEachProviders( + () => + [{provide: 'resolveTwo', useValue: (a: any, b: any) => 2}, + {provide: 'resolveFour', useValue: (a: any, b: any) => 4}, + {provide: 'resolveSix', useClass: ResolveSix}]); + + it('should provide resolved data', + fakeAsync(inject( + [Router, TestComponentBuilder, Location], + (router: Router, tcb: TestComponentBuilder, location: Location) => { + const fixture = tcb.createFakeAsync(RootCmpWithTwoOutlets); + advance(fixture); + + router.resetConfig([{ + path: 'parent/:id', + data: {one: 1}, + resolve: {two: 'resolveTwo'}, + children: [ + {path: '', data: {three: 3}, resolve: {four: 'resolveFour'}, component: RouteCmp}, + { + path: '', + data: {five: 5}, + resolve: {six: 'resolveSix'}, + component: RouteCmp, + outlet: 'right' + } + ] + }]); + + router.navigateByUrl('/parent/1'); + advance(fixture); + + const primaryCmp = fixture.debugElement.children[1].componentInstance; + const rightCmp = fixture.debugElement.children[3].componentInstance; + + expect(primaryCmp.route.snapshot.data).toEqual({one: 1, two: 2, three: 3, four: 4}); + expect(rightCmp.route.snapshot.data).toEqual({one: 1, two: 2, five: 5, six: 6}); + + let primaryRecorded: any[] = []; + primaryCmp.route.data.forEach((rec: any) => primaryRecorded.push(rec)); + + let rightRecorded: any[] = []; + rightCmp.route.data.forEach((rec: any) => rightRecorded.push(rec)); + + router.navigateByUrl('/parent/2'); + advance(fixture); + + expect(primaryRecorded).toEqual([ + {one: 1, three: 3, two: 2, four: 4}, {one: 1, three: 3, two: 2, four: 4} + ]); + expect(rightRecorded).toEqual([ + {one: 1, five: 5, two: 2, six: 6}, {one: 1, five: 5, two: 2, six: 6} + ]); + }))); + }); + + describe('router links', () => { it('should support string router links', fakeAsync( @@ -1120,6 +1182,11 @@ class QueryParamsAndFragmentCmp { } } +@Component({selector: 'route-cmp', template: `route`, directives: ROUTER_DIRECTIVES}) +class RouteCmp { + constructor(public route: ActivatedRoute) {} +} + @Component({ selector: 'root-cmp', template: ``, diff --git a/tools/public_api_guard/router/index.d.ts b/tools/public_api_guard/router/index.d.ts index 8ad2de9701..89dffb0969 100644 --- a/tools/public_api_guard/router/index.d.ts +++ b/tools/public_api_guard/router/index.d.ts @@ -1,5 +1,6 @@ export declare class ActivatedRoute { component: Type | string; + data: Observable; outlet: string; params: Observable; snapshot: ActivatedRouteSnapshot; @@ -9,6 +10,7 @@ export declare class ActivatedRoute { export declare class ActivatedRouteSnapshot { component: Type | string; + data: Data; outlet: string; params: Params; url: UrlPathWithParams[]; @@ -23,6 +25,10 @@ export interface CanDeactivate { canDeactivate(component: T, route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable | boolean; } +export declare type Data = { + [name: string]: any; +}; + export declare class DefaultUrlSerializer implements UrlSerializer { parse(url: string): UrlTree; serialize(tree: UrlTree): string; @@ -72,14 +78,24 @@ export declare const PRIMARY_OUTLET: string; export declare function provideRouter(config: RouterConfig, opts?: ExtraOptions): any[]; +export interface Resolve { + resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable | any; +} + +export declare type ResolveData = { + [name: string]: any; +}; + export interface Route { canActivate?: any[]; canDeactivate?: any[]; children?: Route[]; component?: Type | string; + data?: Data; outlet?: string; path?: string; redirectTo?: string; + resolve?: ResolveData; terminal?: boolean; }