diff --git a/modules/angular2/src/router/async_route_handler.ts b/modules/angular2/src/router/async_route_handler.ts index dbc5039038..40f3e1e2e2 100644 --- a/modules/angular2/src/router/async_route_handler.ts +++ b/modules/angular2/src/router/async_route_handler.ts @@ -6,7 +6,7 @@ export class AsyncRouteHandler implements RouteHandler { _resolvedComponent: Promise = null; componentType: Type; - constructor(private _loader: Function) {} + constructor(private _loader: Function, public data?: Object) {} resolveComponentType(): Promise { if (isPresent(this._resolvedComponent)) { diff --git a/modules/angular2/src/router/instruction.ts b/modules/angular2/src/router/instruction.ts index 5baf08866a..db8b85508f 100644 --- a/modules/angular2/src/router/instruction.ts +++ b/modules/angular2/src/router/instruction.ts @@ -18,7 +18,6 @@ export class RouteParams { get(param: string): string { return normalizeBlank(StringMapWrapper.get(this.params, param)); } } - /** * `Instruction` is a tree of `ComponentInstructions`, with all the information needed * to transition each component in the app to a given route, including all auxiliary routes. @@ -98,4 +97,6 @@ export class ComponentInstruction { get specificity() { return this._recognizer.specificity; } get terminal() { return this._recognizer.terminal; } + + routeData(): Object { return this._recognizer.handler.data; } } diff --git a/modules/angular2/src/router/route_config_decorator.ts b/modules/angular2/src/router/route_config_decorator.ts index ba518e6b7d..badeba28c0 100644 --- a/modules/angular2/src/router/route_config_decorator.ts +++ b/modules/angular2/src/router/route_config_decorator.ts @@ -2,6 +2,13 @@ import {RouteConfig as RouteConfigAnnotation, RouteDefinition} from './route_con import {makeDecorator} from 'angular2/src/util/decorators'; import {List} from 'angular2/src/facade/collection'; -export {Route, Redirect, AuxRoute, AsyncRoute, RouteDefinition} from './route_config_impl'; +export { + Route, + Redirect, + AuxRoute, + AsyncRoute, + RouteDefinition, + ROUTE_DATA +} from './route_config_impl'; export var RouteConfig: (configs: List) => ClassDecorator = makeDecorator(RouteConfigAnnotation); diff --git a/modules/angular2/src/router/route_config_impl.ts b/modules/angular2/src/router/route_config_impl.ts index f4e06664ff..e64d978740 100644 --- a/modules/angular2/src/router/route_config_impl.ts +++ b/modules/angular2/src/router/route_config_impl.ts @@ -1,7 +1,10 @@ -import {CONST, Type} from 'angular2/src/facade/lang'; +import {CONST, CONST_EXPR, Type} from 'angular2/src/facade/lang'; import {List} from 'angular2/src/facade/collection'; import {RouteDefinition} from './route_definition'; export {RouteDefinition} from './route_definition'; +import {OpaqueToken} from 'angular2/di'; + +export const ROUTE_DATA: OpaqueToken = CONST_EXPR(new OpaqueToken('routeData')); /** * You use the RouteConfig annotation to add routes to a component. @@ -10,6 +13,7 @@ export {RouteDefinition} from './route_definition'; * - `path` (required) * - `component`, `loader`, `redirectTo` (requires exactly one of these) * - `as` (optional) + * - `data` (optional) */ @CONST() export class RouteConfig { @@ -19,23 +23,29 @@ export class RouteConfig { @CONST() export class Route implements RouteDefinition { + data: any; path: string; component: Type; as: string; // added next two properties to work around https://github.com/Microsoft/TypeScript/issues/4107 loader: Function; redirectTo: string; - constructor({path, component, as}: {path: string, component: Type, as?: string}) { + constructor({path, component, as, data}: + {path: string, component: Type, as?: string, data?: any}) { this.path = path; this.component = component; this.as = as; this.loader = null; this.redirectTo = null; + this.data = data; } } + + @CONST() export class AuxRoute implements RouteDefinition { + data: any = null; path: string; component: Type; as: string; @@ -51,13 +61,15 @@ export class AuxRoute implements RouteDefinition { @CONST() export class AsyncRoute implements RouteDefinition { + data: any; path: string; loader: Function; as: string; - constructor({path, loader, as}: {path: string, loader: Function, as?: string}) { + constructor({path, loader, as, data}: {path: string, loader: Function, as?: string, data?: any}) { this.path = path; this.loader = loader; this.as = as; + this.data = data; } } @@ -68,6 +80,7 @@ export class Redirect implements RouteDefinition { as: string = null; // added next property to work around https://github.com/Microsoft/TypeScript/issues/4107 loader: Function = null; + data: any = null; constructor({path, redirectTo}: {path: string, redirectTo: string}) { this.path = path; this.redirectTo = redirectTo; diff --git a/modules/angular2/src/router/route_definition.ts b/modules/angular2/src/router/route_definition.ts index 1fa46ffd25..c6f90aae36 100644 --- a/modules/angular2/src/router/route_definition.ts +++ b/modules/angular2/src/router/route_definition.ts @@ -6,6 +6,7 @@ export interface RouteDefinition { loader?: Function; redirectTo?: string; as?: string; + data?: any; } export interface ComponentDefinition { diff --git a/modules/angular2/src/router/route_handler.ts b/modules/angular2/src/router/route_handler.ts index e0b7546df6..97c862f56a 100644 --- a/modules/angular2/src/router/route_handler.ts +++ b/modules/angular2/src/router/route_handler.ts @@ -4,4 +4,5 @@ import {Type} from 'angular2/src/facade/lang'; export interface RouteHandler { componentType: Type; resolveComponentType(): Promise; + data?: Object; } diff --git a/modules/angular2/src/router/route_recognizer.ts b/modules/angular2/src/router/route_recognizer.ts index e556a0c38c..9046921e4a 100644 --- a/modules/angular2/src/router/route_recognizer.ts +++ b/modules/angular2/src/router/route_recognizer.ts @@ -46,7 +46,7 @@ export class RouteRecognizer { var handler; if (config instanceof AuxRoute) { - handler = new SyncRouteHandler(config.component); + handler = new SyncRouteHandler(config.component, config.data); let path = config.path.startsWith('/') ? config.path.substring(1) : config.path; var recognizer = new PathRecognizer(config.path, handler); this.auxRoutes.set(path, recognizer); @@ -58,9 +58,9 @@ export class RouteRecognizer { } if (config instanceof Route) { - handler = new SyncRouteHandler(config.component); + handler = new SyncRouteHandler(config.component, config.data); } else if (config instanceof AsyncRoute) { - handler = new AsyncRouteHandler(config.loader); + handler = new AsyncRouteHandler(config.loader, config.data); } var recognizer = new PathRecognizer(config.path, handler); diff --git a/modules/angular2/src/router/router_outlet.ts b/modules/angular2/src/router/router_outlet.ts index 5966fb3cb0..0fc9af59dd 100644 --- a/modules/angular2/src/router/router_outlet.ts +++ b/modules/angular2/src/router/router_outlet.ts @@ -8,6 +8,7 @@ import {Injector, bind, Dependency, UNDEFINED} from 'angular2/di'; import * as routerMod from './router'; import {Instruction, ComponentInstruction, RouteParams} from './instruction'; +import {ROUTE_DATA} from './route_config_impl'; import * as hookMod from './lifecycle_annotations'; import {hasLifecycleHook} from './route_lifecycle_reflector'; @@ -77,8 +78,9 @@ export class RouterOutlet { this.childRouter = this._parentRouter.childRouter(componentType); var bindings = Injector.resolve([ - bind(RouteParams) - .toValue(new RouteParams(instruction.params)), + bind(ROUTE_DATA) + .toValue(instruction.routeData()), + bind(RouteParams).toValue(new RouteParams(instruction.params)), bind(routerMod.Router).toValue(this.childRouter) ]); return this._loader.loadNextToLocation(componentType, this._elementRef, bindings) diff --git a/modules/angular2/src/router/sync_route_handler.ts b/modules/angular2/src/router/sync_route_handler.ts index 598601c75c..a388657bf3 100644 --- a/modules/angular2/src/router/sync_route_handler.ts +++ b/modules/angular2/src/router/sync_route_handler.ts @@ -5,7 +5,7 @@ import {Type} from 'angular2/src/facade/lang'; export class SyncRouteHandler implements RouteHandler { _resolvedComponent: Promise = null; - constructor(public componentType: Type) { + constructor(public componentType: Type, public data?: Object) { this._resolvedComponent = PromiseWrapper.resolve(componentType); } diff --git a/modules/angular2/test/router/outlet_spec.ts b/modules/angular2/test/router/outlet_spec.ts index 00f3adfd59..5b9ea281f9 100644 --- a/modules/angular2/test/router/outlet_spec.ts +++ b/modules/angular2/test/router/outlet_spec.ts @@ -15,9 +15,9 @@ import { xit } from 'angular2/test_lib'; -import {Injector, bind} from 'angular2/di'; +import {Injector, Inject, bind} from 'angular2/di'; import {Component, View} from 'angular2/metadata'; -import {CONST, NumberWrapper, isPresent} from 'angular2/src/facade/lang'; +import {CONST, NumberWrapper, isPresent, Json} from 'angular2/src/facade/lang'; import { Promise, PromiseWrapper, @@ -28,7 +28,7 @@ import { import {RootRouter} from 'angular2/src/router/router'; import {Pipeline} from 'angular2/src/router/pipeline'; -import {Router, RouterOutlet, RouterLink, RouteParams} from 'angular2/router'; +import {Router, RouterOutlet, RouterLink, RouteParams, ROUTE_DATA} from 'angular2/router'; import { RouteConfig, Route, @@ -253,6 +253,91 @@ export function main() { }); })); + it('should inject RouteData into component', inject([AsyncTestCompleter], (async) => { + compile() + .then((_) => rtr.config([ + new Route({path: '/route-data', component: RouteDataCmp, data: {'isAdmin': true}}) + ])) + .then((_) => rtr.navigate('/route-data')) + .then((_) => { + rootTC.detectChanges(); + expect(rootTC.nativeElement).toHaveText(Json.stringify({'isAdmin': true})); + async.done(); + }); + })); + + it('should inject RouteData into component with AsyncRoute', + inject([AsyncTestCompleter], (async) => { + compile() + .then((_) => rtr.config([ + new AsyncRoute( + {path: '/route-data', loader: AsyncRouteDataCmp, data: {isAdmin: true}}) + ])) + .then((_) => rtr.navigate('/route-data')) + .then((_) => { + rootTC.detectChanges(); + expect(rootTC.nativeElement).toHaveText(Json.stringify({'isAdmin': true})); + async.done(); + }); + })); + + it('should inject nested RouteData into component', inject([AsyncTestCompleter], (async) => { + compile() + .then((_) => rtr.config([ + new Route({ + path: '/route-data-nested', + component: RouteDataCmp, + data: {'isAdmin': true, 'test': {'moreData': 'testing'}} + }) + ])) + .then((_) => rtr.navigate('/route-data-nested')) + .then((_) => { + rootTC.detectChanges(); + expect(rootTC.nativeElement) + .toHaveText(Json.stringify({'isAdmin': true, 'test': {'moreData': 'testing'}})); + async.done(); + }); + })); + + it('should inject null if the route has no data property', + inject([AsyncTestCompleter], (async) => { + compile() + .then((_) => rtr.config( + [new Route({path: '/route-data-default', component: RouteDataCmp})])) + .then((_) => rtr.navigate('/route-data-default')) + .then((_) => { + rootTC.detectChanges(); + expect(rootTC.nativeElement).toHaveText('null'); + async.done(); + }); + })); + + it('should allow an array as the route data', inject([AsyncTestCompleter], (async) => { + compile() + .then((_) => rtr.config([ + new Route({path: '/route-data-array', component: RouteDataCmp, data: [1, 2, 3]}) + ])) + .then((_) => rtr.navigate('/route-data-array')) + .then((_) => { + rootTC.detectChanges(); + expect(rootTC.nativeElement).toHaveText(Json.stringify([1, 2, 3])); + async.done(); + }); + })); + + it('should allow a string as the route data', inject([AsyncTestCompleter], (async) => { + compile() + .then((_) => rtr.config([ + new Route( + {path: '/route-data-string', component: RouteDataCmp, data: 'hello world'}) + ])) + .then((_) => rtr.navigate('/route-data-string')) + .then((_) => { + rootTC.detectChanges(); + expect(rootTC.nativeElement).toHaveText(Json.stringify('hello world')); + async.done(); + }); + })); describe('lifecycle hooks', () => { it('should call the onActivate hook', inject([AsyncTestCompleter], (async) => { @@ -633,6 +718,19 @@ class B { } +function AsyncRouteDataCmp() { + return PromiseWrapper.resolve(RouteDataCmp); +} + +@Component({selector: 'data-cmp'}) +@View({template: "{{myData}}"}) +class RouteDataCmp { + myData: string; + constructor(@Inject(ROUTE_DATA) data: any) { + this.myData = isPresent(data) ? Json.stringify(data) : 'null'; + } +} + @Component({selector: 'user-cmp'}) @View({template: "hello {{user}}"}) class UserCmp {