X Tutup
Skip to content

Commit ed81cb9

Browse files
danrasmusonbtford
authored andcommitted
feat(router): user metadata in route configs
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
1 parent 1f54e64 commit ed81cb9

File tree

10 files changed

+138
-15
lines changed

10 files changed

+138
-15
lines changed

modules/angular2/src/router/async_route_handler.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ export class AsyncRouteHandler implements RouteHandler {
66
_resolvedComponent: Promise<any> = null;
77
componentType: Type;
88

9-
constructor(private _loader: Function) {}
9+
constructor(private _loader: Function, public data?: Object) {}
1010

1111
resolveComponentType(): Promise<any> {
1212
if (isPresent(this._resolvedComponent)) {

modules/angular2/src/router/instruction.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ export class RouteParams {
1818
get(param: string): string { return normalizeBlank(StringMapWrapper.get(this.params, param)); }
1919
}
2020

21-
2221
/**
2322
* `Instruction` is a tree of `ComponentInstructions`, with all the information needed
2423
* to transition each component in the app to a given route, including all auxiliary routes.
@@ -98,4 +97,6 @@ export class ComponentInstruction {
9897
get specificity() { return this._recognizer.specificity; }
9998

10099
get terminal() { return this._recognizer.terminal; }
100+
101+
routeData(): Object { return this._recognizer.handler.data; }
101102
}

modules/angular2/src/router/route_config_decorator.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,13 @@ import {RouteConfig as RouteConfigAnnotation, RouteDefinition} from './route_con
22
import {makeDecorator} from 'angular2/src/util/decorators';
33
import {List} from 'angular2/src/facade/collection';
44

5-
export {Route, Redirect, AuxRoute, AsyncRoute, RouteDefinition} from './route_config_impl';
5+
export {
6+
Route,
7+
Redirect,
8+
AuxRoute,
9+
AsyncRoute,
10+
RouteDefinition,
11+
ROUTE_DATA
12+
} from './route_config_impl';
613
export var RouteConfig: (configs: List<RouteDefinition>) => ClassDecorator =
714
makeDecorator(RouteConfigAnnotation);

modules/angular2/src/router/route_config_impl.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1-
import {CONST, Type} from 'angular2/src/facade/lang';
1+
import {CONST, CONST_EXPR, Type} from 'angular2/src/facade/lang';
22
import {List} from 'angular2/src/facade/collection';
33
import {RouteDefinition} from './route_definition';
44
export {RouteDefinition} from './route_definition';
5+
import {OpaqueToken} from 'angular2/di';
6+
7+
export const ROUTE_DATA: OpaqueToken = CONST_EXPR(new OpaqueToken('routeData'));
58

69
/**
710
* You use the RouteConfig annotation to add routes to a component.
@@ -10,6 +13,7 @@ export {RouteDefinition} from './route_definition';
1013
* - `path` (required)
1114
* - `component`, `loader`, `redirectTo` (requires exactly one of these)
1215
* - `as` (optional)
16+
* - `data` (optional)
1317
*/
1418
@CONST()
1519
export class RouteConfig {
@@ -19,23 +23,29 @@ export class RouteConfig {
1923

2024
@CONST()
2125
export class Route implements RouteDefinition {
26+
data: any;
2227
path: string;
2328
component: Type;
2429
as: string;
2530
// added next two properties to work around https://github.com/Microsoft/TypeScript/issues/4107
2631
loader: Function;
2732
redirectTo: string;
28-
constructor({path, component, as}: {path: string, component: Type, as?: string}) {
33+
constructor({path, component, as, data}:
34+
{path: string, component: Type, as?: string, data?: any}) {
2935
this.path = path;
3036
this.component = component;
3137
this.as = as;
3238
this.loader = null;
3339
this.redirectTo = null;
40+
this.data = data;
3441
}
3542
}
3643

44+
45+
3746
@CONST()
3847
export class AuxRoute implements RouteDefinition {
48+
data: any = null;
3949
path: string;
4050
component: Type;
4151
as: string;
@@ -51,13 +61,15 @@ export class AuxRoute implements RouteDefinition {
5161

5262
@CONST()
5363
export class AsyncRoute implements RouteDefinition {
64+
data: any;
5465
path: string;
5566
loader: Function;
5667
as: string;
57-
constructor({path, loader, as}: {path: string, loader: Function, as?: string}) {
68+
constructor({path, loader, as, data}: {path: string, loader: Function, as?: string, data?: any}) {
5869
this.path = path;
5970
this.loader = loader;
6071
this.as = as;
72+
this.data = data;
6173
}
6274
}
6375

@@ -68,6 +80,7 @@ export class Redirect implements RouteDefinition {
6880
as: string = null;
6981
// added next property to work around https://github.com/Microsoft/TypeScript/issues/4107
7082
loader: Function = null;
83+
data: any = null;
7184
constructor({path, redirectTo}: {path: string, redirectTo: string}) {
7285
this.path = path;
7386
this.redirectTo = redirectTo;

modules/angular2/src/router/route_definition.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export interface RouteDefinition {
66
loader?: Function;
77
redirectTo?: string;
88
as?: string;
9+
data?: any;
910
}
1011

1112
export interface ComponentDefinition {

modules/angular2/src/router/route_handler.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@ import {Type} from 'angular2/src/facade/lang';
44
export interface RouteHandler {
55
componentType: Type;
66
resolveComponentType(): Promise<any>;
7+
data?: Object;
78
}

modules/angular2/src/router/route_recognizer.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ export class RouteRecognizer {
4646
var handler;
4747

4848
if (config instanceof AuxRoute) {
49-
handler = new SyncRouteHandler(config.component);
49+
handler = new SyncRouteHandler(config.component, config.data);
5050
let path = config.path.startsWith('/') ? config.path.substring(1) : config.path;
5151
var recognizer = new PathRecognizer(config.path, handler);
5252
this.auxRoutes.set(path, recognizer);
@@ -58,9 +58,9 @@ export class RouteRecognizer {
5858
}
5959

6060
if (config instanceof Route) {
61-
handler = new SyncRouteHandler(config.component);
61+
handler = new SyncRouteHandler(config.component, config.data);
6262
} else if (config instanceof AsyncRoute) {
63-
handler = new AsyncRouteHandler(config.loader);
63+
handler = new AsyncRouteHandler(config.loader, config.data);
6464
}
6565
var recognizer = new PathRecognizer(config.path, handler);
6666

modules/angular2/src/router/router_outlet.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {Injector, bind, Dependency, UNDEFINED} from 'angular2/di';
88

99
import * as routerMod from './router';
1010
import {Instruction, ComponentInstruction, RouteParams} from './instruction';
11+
import {ROUTE_DATA} from './route_config_impl';
1112
import * as hookMod from './lifecycle_annotations';
1213
import {hasLifecycleHook} from './route_lifecycle_reflector';
1314

@@ -77,8 +78,9 @@ export class RouterOutlet {
7778
this.childRouter = this._parentRouter.childRouter(componentType);
7879

7980
var bindings = Injector.resolve([
80-
bind(RouteParams)
81-
.toValue(new RouteParams(instruction.params)),
81+
bind(ROUTE_DATA)
82+
.toValue(instruction.routeData()),
83+
bind(RouteParams).toValue(new RouteParams(instruction.params)),
8284
bind(routerMod.Router).toValue(this.childRouter)
8385
]);
8486
return this._loader.loadNextToLocation(componentType, this._elementRef, bindings)

modules/angular2/src/router/sync_route_handler.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {Type} from 'angular2/src/facade/lang';
55
export class SyncRouteHandler implements RouteHandler {
66
_resolvedComponent: Promise<any> = null;
77

8-
constructor(public componentType: Type) {
8+
constructor(public componentType: Type, public data?: Object) {
99
this._resolvedComponent = PromiseWrapper.resolve(componentType);
1010
}
1111

modules/angular2/test/router/outlet_spec.ts

Lines changed: 101 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@ import {
1515
xit
1616
} from 'angular2/test_lib';
1717

18-
import {Injector, bind} from 'angular2/di';
18+
import {Injector, Inject, bind} from 'angular2/di';
1919
import {Component, View} from 'angular2/metadata';
20-
import {CONST, NumberWrapper, isPresent} from 'angular2/src/facade/lang';
20+
import {CONST, NumberWrapper, isPresent, Json} from 'angular2/src/facade/lang';
2121
import {
2222
Promise,
2323
PromiseWrapper,
@@ -28,7 +28,7 @@ import {
2828

2929
import {RootRouter} from 'angular2/src/router/router';
3030
import {Pipeline} from 'angular2/src/router/pipeline';
31-
import {Router, RouterOutlet, RouterLink, RouteParams} from 'angular2/router';
31+
import {Router, RouterOutlet, RouterLink, RouteParams, ROUTE_DATA} from 'angular2/router';
3232
import {
3333
RouteConfig,
3434
Route,
@@ -253,6 +253,91 @@ export function main() {
253253
});
254254
}));
255255

256+
it('should inject RouteData into component', inject([AsyncTestCompleter], (async) => {
257+
compile()
258+
.then((_) => rtr.config([
259+
new Route({path: '/route-data', component: RouteDataCmp, data: {'isAdmin': true}})
260+
]))
261+
.then((_) => rtr.navigate('/route-data'))
262+
.then((_) => {
263+
rootTC.detectChanges();
264+
expect(rootTC.nativeElement).toHaveText(Json.stringify({'isAdmin': true}));
265+
async.done();
266+
});
267+
}));
268+
269+
it('should inject RouteData into component with AsyncRoute',
270+
inject([AsyncTestCompleter], (async) => {
271+
compile()
272+
.then((_) => rtr.config([
273+
new AsyncRoute(
274+
{path: '/route-data', loader: AsyncRouteDataCmp, data: {isAdmin: true}})
275+
]))
276+
.then((_) => rtr.navigate('/route-data'))
277+
.then((_) => {
278+
rootTC.detectChanges();
279+
expect(rootTC.nativeElement).toHaveText(Json.stringify({'isAdmin': true}));
280+
async.done();
281+
});
282+
}));
283+
284+
it('should inject nested RouteData into component', inject([AsyncTestCompleter], (async) => {
285+
compile()
286+
.then((_) => rtr.config([
287+
new Route({
288+
path: '/route-data-nested',
289+
component: RouteDataCmp,
290+
data: {'isAdmin': true, 'test': {'moreData': 'testing'}}
291+
})
292+
]))
293+
.then((_) => rtr.navigate('/route-data-nested'))
294+
.then((_) => {
295+
rootTC.detectChanges();
296+
expect(rootTC.nativeElement)
297+
.toHaveText(Json.stringify({'isAdmin': true, 'test': {'moreData': 'testing'}}));
298+
async.done();
299+
});
300+
}));
301+
302+
it('should inject null if the route has no data property',
303+
inject([AsyncTestCompleter], (async) => {
304+
compile()
305+
.then((_) => rtr.config(
306+
[new Route({path: '/route-data-default', component: RouteDataCmp})]))
307+
.then((_) => rtr.navigate('/route-data-default'))
308+
.then((_) => {
309+
rootTC.detectChanges();
310+
expect(rootTC.nativeElement).toHaveText('null');
311+
async.done();
312+
});
313+
}));
314+
315+
it('should allow an array as the route data', inject([AsyncTestCompleter], (async) => {
316+
compile()
317+
.then((_) => rtr.config([
318+
new Route({path: '/route-data-array', component: RouteDataCmp, data: [1, 2, 3]})
319+
]))
320+
.then((_) => rtr.navigate('/route-data-array'))
321+
.then((_) => {
322+
rootTC.detectChanges();
323+
expect(rootTC.nativeElement).toHaveText(Json.stringify([1, 2, 3]));
324+
async.done();
325+
});
326+
}));
327+
328+
it('should allow a string as the route data', inject([AsyncTestCompleter], (async) => {
329+
compile()
330+
.then((_) => rtr.config([
331+
new Route(
332+
{path: '/route-data-string', component: RouteDataCmp, data: 'hello world'})
333+
]))
334+
.then((_) => rtr.navigate('/route-data-string'))
335+
.then((_) => {
336+
rootTC.detectChanges();
337+
expect(rootTC.nativeElement).toHaveText(Json.stringify('hello world'));
338+
async.done();
339+
});
340+
}));
256341

257342
describe('lifecycle hooks', () => {
258343
it('should call the onActivate hook', inject([AsyncTestCompleter], (async) => {
@@ -633,6 +718,19 @@ class B {
633718
}
634719

635720

721+
function AsyncRouteDataCmp() {
722+
return PromiseWrapper.resolve(RouteDataCmp);
723+
}
724+
725+
@Component({selector: 'data-cmp'})
726+
@View({template: "{{myData}}"})
727+
class RouteDataCmp {
728+
myData: string;
729+
constructor(@Inject(ROUTE_DATA) data: any) {
730+
this.myData = isPresent(data) ? Json.stringify(data) : 'null';
731+
}
732+
}
733+
636734
@Component({selector: 'user-cmp'})
637735
@View({template: "hello {{user}}"})
638736
class UserCmp {

0 commit comments

Comments
 (0)
X Tutup