Skip to content

Commit

Permalink
feat(router): user metadata in route configs
Browse files Browse the repository at this point in the history
Provide the ability to attach custom data onto a route and retrieve
that data as an injectable (RouteData) inside the component.

Closes #2777

Closes #3541
  • Loading branch information
Daniel Rasmuson authored and btford committed Aug 18, 2015
1 parent 1f54e64 commit ed81cb9
Show file tree
Hide file tree
Showing 10 changed files with 138 additions and 15 deletions.
2 changes: 1 addition & 1 deletion modules/angular2/src/router/async_route_handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export class AsyncRouteHandler implements RouteHandler {
_resolvedComponent: Promise<any> = null;
componentType: Type;

constructor(private _loader: Function) {}
constructor(private _loader: Function, public data?: Object) {}

resolveComponentType(): Promise<any> {
if (isPresent(this._resolvedComponent)) {
Expand Down
3 changes: 2 additions & 1 deletion modules/angular2/src/router/instruction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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; }
}
9 changes: 8 additions & 1 deletion modules/angular2/src/router/route_config_decorator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<RouteDefinition>) => ClassDecorator =
makeDecorator(RouteConfigAnnotation);
19 changes: 16 additions & 3 deletions modules/angular2/src/router/route_config_impl.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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 {
Expand All @@ -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;
Expand All @@ -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;
}
}

Expand All @@ -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;
Expand Down
1 change: 1 addition & 0 deletions modules/angular2/src/router/route_definition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export interface RouteDefinition {
loader?: Function;
redirectTo?: string;
as?: string;
data?: any;
}

export interface ComponentDefinition {
Expand Down
1 change: 1 addition & 0 deletions modules/angular2/src/router/route_handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ import {Type} from 'angular2/src/facade/lang';
export interface RouteHandler {
componentType: Type;
resolveComponentType(): Promise<any>;
data?: Object;
}
6 changes: 3 additions & 3 deletions modules/angular2/src/router/route_recognizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);

Expand Down
6 changes: 4 additions & 2 deletions modules/angular2/src/router/router_outlet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion modules/angular2/src/router/sync_route_handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {Type} from 'angular2/src/facade/lang';
export class SyncRouteHandler implements RouteHandler {
_resolvedComponent: Promise<any> = null;

constructor(public componentType: Type) {
constructor(public componentType: Type, public data?: Object) {
this._resolvedComponent = PromiseWrapper.resolve(componentType);
}

Expand Down
104 changes: 101 additions & 3 deletions modules/angular2/test/router/outlet_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -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 {
Expand Down

0 comments on commit ed81cb9

Please sign in to comment.