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
This commit is contained in:
parent
1f54e64fcf
commit
ed81cb94b0
|
@ -6,7 +6,7 @@ export class AsyncRouteHandler implements RouteHandler {
|
||||||
_resolvedComponent: Promise<any> = null;
|
_resolvedComponent: Promise<any> = null;
|
||||||
componentType: Type;
|
componentType: Type;
|
||||||
|
|
||||||
constructor(private _loader: Function) {}
|
constructor(private _loader: Function, public data?: Object) {}
|
||||||
|
|
||||||
resolveComponentType(): Promise<any> {
|
resolveComponentType(): Promise<any> {
|
||||||
if (isPresent(this._resolvedComponent)) {
|
if (isPresent(this._resolvedComponent)) {
|
||||||
|
|
|
@ -18,7 +18,6 @@ export class RouteParams {
|
||||||
get(param: string): string { return normalizeBlank(StringMapWrapper.get(this.params, param)); }
|
get(param: string): string { return normalizeBlank(StringMapWrapper.get(this.params, param)); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* `Instruction` is a tree of `ComponentInstructions`, with all the information needed
|
* `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.
|
* 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 specificity() { return this._recognizer.specificity; }
|
||||||
|
|
||||||
get terminal() { return this._recognizer.terminal; }
|
get terminal() { return this._recognizer.terminal; }
|
||||||
|
|
||||||
|
routeData(): Object { return this._recognizer.handler.data; }
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,13 @@ import {RouteConfig as RouteConfigAnnotation, RouteDefinition} from './route_con
|
||||||
import {makeDecorator} from 'angular2/src/util/decorators';
|
import {makeDecorator} from 'angular2/src/util/decorators';
|
||||||
import {List} from 'angular2/src/facade/collection';
|
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 =
|
export var RouteConfig: (configs: List<RouteDefinition>) => ClassDecorator =
|
||||||
makeDecorator(RouteConfigAnnotation);
|
makeDecorator(RouteConfigAnnotation);
|
||||||
|
|
|
@ -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 {List} from 'angular2/src/facade/collection';
|
||||||
import {RouteDefinition} from './route_definition';
|
import {RouteDefinition} from './route_definition';
|
||||||
export {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.
|
* You use the RouteConfig annotation to add routes to a component.
|
||||||
|
@ -10,6 +13,7 @@ export {RouteDefinition} from './route_definition';
|
||||||
* - `path` (required)
|
* - `path` (required)
|
||||||
* - `component`, `loader`, `redirectTo` (requires exactly one of these)
|
* - `component`, `loader`, `redirectTo` (requires exactly one of these)
|
||||||
* - `as` (optional)
|
* - `as` (optional)
|
||||||
|
* - `data` (optional)
|
||||||
*/
|
*/
|
||||||
@CONST()
|
@CONST()
|
||||||
export class RouteConfig {
|
export class RouteConfig {
|
||||||
|
@ -19,23 +23,29 @@ export class RouteConfig {
|
||||||
|
|
||||||
@CONST()
|
@CONST()
|
||||||
export class Route implements RouteDefinition {
|
export class Route implements RouteDefinition {
|
||||||
|
data: any;
|
||||||
path: string;
|
path: string;
|
||||||
component: Type;
|
component: Type;
|
||||||
as: string;
|
as: string;
|
||||||
// added next two properties to work around https://github.com/Microsoft/TypeScript/issues/4107
|
// added next two properties to work around https://github.com/Microsoft/TypeScript/issues/4107
|
||||||
loader: Function;
|
loader: Function;
|
||||||
redirectTo: string;
|
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.path = path;
|
||||||
this.component = component;
|
this.component = component;
|
||||||
this.as = as;
|
this.as = as;
|
||||||
this.loader = null;
|
this.loader = null;
|
||||||
this.redirectTo = null;
|
this.redirectTo = null;
|
||||||
|
this.data = data;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@CONST()
|
@CONST()
|
||||||
export class AuxRoute implements RouteDefinition {
|
export class AuxRoute implements RouteDefinition {
|
||||||
|
data: any = null;
|
||||||
path: string;
|
path: string;
|
||||||
component: Type;
|
component: Type;
|
||||||
as: string;
|
as: string;
|
||||||
|
@ -51,13 +61,15 @@ export class AuxRoute implements RouteDefinition {
|
||||||
|
|
||||||
@CONST()
|
@CONST()
|
||||||
export class AsyncRoute implements RouteDefinition {
|
export class AsyncRoute implements RouteDefinition {
|
||||||
|
data: any;
|
||||||
path: string;
|
path: string;
|
||||||
loader: Function;
|
loader: Function;
|
||||||
as: string;
|
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.path = path;
|
||||||
this.loader = loader;
|
this.loader = loader;
|
||||||
this.as = as;
|
this.as = as;
|
||||||
|
this.data = data;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -68,6 +80,7 @@ export class Redirect implements RouteDefinition {
|
||||||
as: string = null;
|
as: string = null;
|
||||||
// added next property to work around https://github.com/Microsoft/TypeScript/issues/4107
|
// added next property to work around https://github.com/Microsoft/TypeScript/issues/4107
|
||||||
loader: Function = null;
|
loader: Function = null;
|
||||||
|
data: any = null;
|
||||||
constructor({path, redirectTo}: {path: string, redirectTo: string}) {
|
constructor({path, redirectTo}: {path: string, redirectTo: string}) {
|
||||||
this.path = path;
|
this.path = path;
|
||||||
this.redirectTo = redirectTo;
|
this.redirectTo = redirectTo;
|
||||||
|
|
|
@ -6,6 +6,7 @@ export interface RouteDefinition {
|
||||||
loader?: Function;
|
loader?: Function;
|
||||||
redirectTo?: string;
|
redirectTo?: string;
|
||||||
as?: string;
|
as?: string;
|
||||||
|
data?: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ComponentDefinition {
|
export interface ComponentDefinition {
|
||||||
|
|
|
@ -4,4 +4,5 @@ import {Type} from 'angular2/src/facade/lang';
|
||||||
export interface RouteHandler {
|
export interface RouteHandler {
|
||||||
componentType: Type;
|
componentType: Type;
|
||||||
resolveComponentType(): Promise<any>;
|
resolveComponentType(): Promise<any>;
|
||||||
|
data?: Object;
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,7 +46,7 @@ export class RouteRecognizer {
|
||||||
var handler;
|
var handler;
|
||||||
|
|
||||||
if (config instanceof AuxRoute) {
|
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;
|
let path = config.path.startsWith('/') ? config.path.substring(1) : config.path;
|
||||||
var recognizer = new PathRecognizer(config.path, handler);
|
var recognizer = new PathRecognizer(config.path, handler);
|
||||||
this.auxRoutes.set(path, recognizer);
|
this.auxRoutes.set(path, recognizer);
|
||||||
|
@ -58,9 +58,9 @@ export class RouteRecognizer {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config instanceof Route) {
|
if (config instanceof Route) {
|
||||||
handler = new SyncRouteHandler(config.component);
|
handler = new SyncRouteHandler(config.component, config.data);
|
||||||
} else if (config instanceof AsyncRoute) {
|
} else if (config instanceof AsyncRoute) {
|
||||||
handler = new AsyncRouteHandler(config.loader);
|
handler = new AsyncRouteHandler(config.loader, config.data);
|
||||||
}
|
}
|
||||||
var recognizer = new PathRecognizer(config.path, handler);
|
var recognizer = new PathRecognizer(config.path, handler);
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@ import {Injector, bind, Dependency, UNDEFINED} from 'angular2/di';
|
||||||
|
|
||||||
import * as routerMod from './router';
|
import * as routerMod from './router';
|
||||||
import {Instruction, ComponentInstruction, RouteParams} from './instruction';
|
import {Instruction, ComponentInstruction, RouteParams} from './instruction';
|
||||||
|
import {ROUTE_DATA} from './route_config_impl';
|
||||||
import * as hookMod from './lifecycle_annotations';
|
import * as hookMod from './lifecycle_annotations';
|
||||||
import {hasLifecycleHook} from './route_lifecycle_reflector';
|
import {hasLifecycleHook} from './route_lifecycle_reflector';
|
||||||
|
|
||||||
|
@ -77,8 +78,9 @@ export class RouterOutlet {
|
||||||
this.childRouter = this._parentRouter.childRouter(componentType);
|
this.childRouter = this._parentRouter.childRouter(componentType);
|
||||||
|
|
||||||
var bindings = Injector.resolve([
|
var bindings = Injector.resolve([
|
||||||
bind(RouteParams)
|
bind(ROUTE_DATA)
|
||||||
.toValue(new RouteParams(instruction.params)),
|
.toValue(instruction.routeData()),
|
||||||
|
bind(RouteParams).toValue(new RouteParams(instruction.params)),
|
||||||
bind(routerMod.Router).toValue(this.childRouter)
|
bind(routerMod.Router).toValue(this.childRouter)
|
||||||
]);
|
]);
|
||||||
return this._loader.loadNextToLocation(componentType, this._elementRef, bindings)
|
return this._loader.loadNextToLocation(componentType, this._elementRef, bindings)
|
||||||
|
|
|
@ -5,7 +5,7 @@ import {Type} from 'angular2/src/facade/lang';
|
||||||
export class SyncRouteHandler implements RouteHandler {
|
export class SyncRouteHandler implements RouteHandler {
|
||||||
_resolvedComponent: Promise<any> = null;
|
_resolvedComponent: Promise<any> = null;
|
||||||
|
|
||||||
constructor(public componentType: Type) {
|
constructor(public componentType: Type, public data?: Object) {
|
||||||
this._resolvedComponent = PromiseWrapper.resolve(componentType);
|
this._resolvedComponent = PromiseWrapper.resolve(componentType);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,9 +15,9 @@ import {
|
||||||
xit
|
xit
|
||||||
} from 'angular2/test_lib';
|
} from 'angular2/test_lib';
|
||||||
|
|
||||||
import {Injector, bind} from 'angular2/di';
|
import {Injector, Inject, bind} from 'angular2/di';
|
||||||
import {Component, View} from 'angular2/metadata';
|
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 {
|
import {
|
||||||
Promise,
|
Promise,
|
||||||
PromiseWrapper,
|
PromiseWrapper,
|
||||||
|
@ -28,7 +28,7 @@ import {
|
||||||
|
|
||||||
import {RootRouter} from 'angular2/src/router/router';
|
import {RootRouter} from 'angular2/src/router/router';
|
||||||
import {Pipeline} from 'angular2/src/router/pipeline';
|
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 {
|
import {
|
||||||
RouteConfig,
|
RouteConfig,
|
||||||
Route,
|
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', () => {
|
describe('lifecycle hooks', () => {
|
||||||
it('should call the onActivate hook', inject([AsyncTestCompleter], (async) => {
|
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'})
|
@Component({selector: 'user-cmp'})
|
||||||
@View({template: "hello {{user}}"})
|
@View({template: "hello {{user}}"})
|
||||||
class UserCmp {
|
class UserCmp {
|
||||||
|
|
Loading…
Reference in New Issue