refactor(router): RouteData as a type
BREAKING CHANGE The ROUTE_DATA token has been removed and replaced with a type RouteData, allowing a type injection like we do with RouteParams. Before: constructor(routeParams: RouteParams, @Inject(ROUTE_DATA) routeData) { let id = routeParams.get('id'); let name = ROUTE_DATA.name; } After: constructor(routeParams: RouteParams, routeData: RouteData) { let id = routeParams.get('id'); let name = routeData.get('name'); } Fixes #4392 Closes #4428
This commit is contained in:
parent
fbe748f273
commit
b87da8f47c
|
@ -7,7 +7,7 @@
|
||||||
export {Router} from './src/router/router';
|
export {Router} from './src/router/router';
|
||||||
export {RouterOutlet} from './src/router/router_outlet';
|
export {RouterOutlet} from './src/router/router_outlet';
|
||||||
export {RouterLink} from './src/router/router_link';
|
export {RouterLink} from './src/router/router_link';
|
||||||
export {RouteParams} from './src/router/instruction';
|
export {RouteParams, RouteData} from './src/router/instruction';
|
||||||
export {RouteRegistry} from './src/router/route_registry';
|
export {RouteRegistry} from './src/router/route_registry';
|
||||||
export {LocationStrategy} from './src/router/location_strategy';
|
export {LocationStrategy} from './src/router/location_strategy';
|
||||||
export {HashLocationStrategy} from './src/router/hash_location_strategy';
|
export {HashLocationStrategy} from './src/router/hash_location_strategy';
|
||||||
|
@ -19,7 +19,6 @@ export {OnActivate, OnDeactivate, OnReuse, CanDeactivate, CanReuse} from './src/
|
||||||
export {CanActivate} from './src/router/lifecycle_annotations';
|
export {CanActivate} from './src/router/lifecycle_annotations';
|
||||||
export {Instruction, ComponentInstruction} from './src/router/instruction';
|
export {Instruction, ComponentInstruction} from './src/router/instruction';
|
||||||
export {OpaqueToken} from 'angular2/angular2';
|
export {OpaqueToken} from 'angular2/angular2';
|
||||||
export {ROUTE_DATA} from './src/router/route_data';
|
|
||||||
|
|
||||||
import {LocationStrategy} from './src/router/location_strategy';
|
import {LocationStrategy} from './src/router/location_strategy';
|
||||||
import {PathLocationStrategy} from './src/router/path_location_strategy';
|
import {PathLocationStrategy} from './src/router/path_location_strategy';
|
||||||
|
|
|
@ -7,7 +7,7 @@ export class AsyncRouteHandler implements RouteHandler {
|
||||||
_resolvedComponent: Promise<any> = null;
|
_resolvedComponent: Promise<any> = null;
|
||||||
componentType: Type;
|
componentType: Type;
|
||||||
|
|
||||||
constructor(private _loader: Function, public data?: Object) {}
|
constructor(private _loader: Function, public data?: {[key: string]: any}) {}
|
||||||
|
|
||||||
resolveComponentType(): Promise<any> {
|
resolveComponentType(): Promise<any> {
|
||||||
if (isPresent(this._resolvedComponent)) {
|
if (isPresent(this._resolvedComponent)) {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import {Map, MapWrapper, StringMapWrapper, ListWrapper} from 'angular2/src/core/facade/collection';
|
import {Map, MapWrapper, StringMapWrapper, ListWrapper} from 'angular2/src/core/facade/collection';
|
||||||
import {unimplemented} from 'angular2/src/core/facade/exceptions';
|
import {unimplemented} from 'angular2/src/core/facade/exceptions';
|
||||||
import {isPresent, isBlank, normalizeBlank, Type} from 'angular2/src/core/facade/lang';
|
import {isPresent, isBlank, normalizeBlank, Type, CONST_EXPR} from 'angular2/src/core/facade/lang';
|
||||||
import {Promise} from 'angular2/src/core/facade/async';
|
import {Promise} from 'angular2/src/core/facade/async';
|
||||||
|
|
||||||
import {PathRecognizer} from './path_recognizer';
|
import {PathRecognizer} from './path_recognizer';
|
||||||
|
@ -41,6 +41,44 @@ 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)); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `RouteData` is an immutable map of additional data you can configure in your {@link Route}.
|
||||||
|
*
|
||||||
|
* You can inject `RouteData` into the constructor of a component to use it.
|
||||||
|
*
|
||||||
|
* ## Example
|
||||||
|
*
|
||||||
|
* ```
|
||||||
|
* import {bootstrap, Component, View} from 'angular2/angular2';
|
||||||
|
* import {Router, ROUTER_DIRECTIVES, routerBindings, RouteConfig} from 'angular2/router';
|
||||||
|
*
|
||||||
|
* @Component({...})
|
||||||
|
* @View({directives: [ROUTER_DIRECTIVES]})
|
||||||
|
* @RouteConfig([
|
||||||
|
* {path: '/user/:id', component: UserCmp, as: 'UserCmp', data: {isAdmin: true}},
|
||||||
|
* ])
|
||||||
|
* class AppCmp {}
|
||||||
|
*
|
||||||
|
* @Component({...})
|
||||||
|
* @View({ template: 'user: {{isAdmin}}' })
|
||||||
|
* class UserCmp {
|
||||||
|
* string: isAdmin;
|
||||||
|
* constructor(data: RouteData) {
|
||||||
|
* this.isAdmin = data.get('isAdmin');
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* bootstrap(AppCmp, routerBindings(AppCmp));
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export class RouteData {
|
||||||
|
constructor(public data: {[key: string]: any} = CONST_EXPR({})) {}
|
||||||
|
|
||||||
|
get(key: string): any { return normalizeBlank(StringMapWrapper.get(this.data, key)); }
|
||||||
|
}
|
||||||
|
|
||||||
|
var BLANK_ROUTE_DATA = new RouteData();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* `Instruction` is a tree of {@link ComponentInstruction}s with all the information needed
|
* `Instruction` is a tree of {@link ComponentInstruction}s 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.
|
||||||
|
@ -176,23 +214,30 @@ export abstract class ComponentInstruction {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the route data of the given route that was specified in the {@link RouteDefinition},
|
* Returns the route data of the given route that was specified in the {@link RouteDefinition},
|
||||||
* or `null` if no route data was specified.
|
* or an empty object if no route data was specified.
|
||||||
*/
|
*/
|
||||||
abstract routeData(): Object;
|
get routeData(): RouteData { return unimplemented(); };
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ComponentInstruction_ extends ComponentInstruction {
|
export class ComponentInstruction_ extends ComponentInstruction {
|
||||||
|
private _routeData: RouteData;
|
||||||
|
|
||||||
constructor(urlPath: string, urlParams: string[], private _recognizer: PathRecognizer,
|
constructor(urlPath: string, urlParams: string[], private _recognizer: PathRecognizer,
|
||||||
params: {[key: string]: any} = null) {
|
params: {[key: string]: any} = null) {
|
||||||
super();
|
super();
|
||||||
this.urlPath = urlPath;
|
this.urlPath = urlPath;
|
||||||
this.urlParams = urlParams;
|
this.urlParams = urlParams;
|
||||||
this.params = params;
|
this.params = params;
|
||||||
|
if (isPresent(this._recognizer.handler.data)) {
|
||||||
|
this._routeData = new RouteData(this._recognizer.handler.data);
|
||||||
|
} else {
|
||||||
|
this._routeData = BLANK_ROUTE_DATA;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get componentType() { return this._recognizer.handler.componentType; }
|
get componentType() { return this._recognizer.handler.componentType; }
|
||||||
resolveComponentType(): Promise<Type> { return this._recognizer.handler.resolveComponentType(); }
|
resolveComponentType(): Promise<Type> { return this._recognizer.handler.resolveComponentType(); }
|
||||||
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; }
|
get routeData(): RouteData { return this._routeData; }
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,7 +20,7 @@ export class RouteConfig {
|
||||||
* - `component` a component type.
|
* - `component` a component type.
|
||||||
* - `as` is an optional `CamelCase` string representing the name of the route.
|
* - `as` is an optional `CamelCase` string representing the name of the route.
|
||||||
* - `data` is an optional property of any type representing arbitrary route metadata for the given
|
* - `data` is an optional property of any type representing arbitrary route metadata for the given
|
||||||
* route. It is injectable via the {@link ROUTE_DATA} token.
|
* route. It is injectable via {@link RouteData}.
|
||||||
*
|
*
|
||||||
* ### Example
|
* ### Example
|
||||||
* ```
|
* ```
|
||||||
|
@ -34,7 +34,7 @@ export class RouteConfig {
|
||||||
*/
|
*/
|
||||||
@CONST()
|
@CONST()
|
||||||
export class Route implements RouteDefinition {
|
export class Route implements RouteDefinition {
|
||||||
data: any;
|
data: {[key: string]: any};
|
||||||
path: string;
|
path: string;
|
||||||
component: Type;
|
component: Type;
|
||||||
as: string;
|
as: string;
|
||||||
|
@ -42,7 +42,7 @@ export class Route implements RouteDefinition {
|
||||||
loader: Function;
|
loader: Function;
|
||||||
redirectTo: string;
|
redirectTo: string;
|
||||||
constructor({path, component, as, data}:
|
constructor({path, component, as, data}:
|
||||||
{path: string, component: Type, as?: string, data?: any}) {
|
{path: string, component: Type, as?: string, data?: {[key: string]: any}}) {
|
||||||
this.path = path;
|
this.path = path;
|
||||||
this.component = component;
|
this.component = component;
|
||||||
this.as = as;
|
this.as = as;
|
||||||
|
@ -60,7 +60,7 @@ export class Route implements RouteDefinition {
|
||||||
* - `component` a component type.
|
* - `component` a component type.
|
||||||
* - `as` is an optional `CamelCase` string representing the name of the route.
|
* - `as` is an optional `CamelCase` string representing the name of the route.
|
||||||
* - `data` is an optional property of any type representing arbitrary route metadata for the given
|
* - `data` is an optional property of any type representing arbitrary route metadata for the given
|
||||||
* route. It is injectable via the {@link ROUTE_DATA} token.
|
* route. It is injectable via {@link RouteData}.
|
||||||
*
|
*
|
||||||
* ### Example
|
* ### Example
|
||||||
* ```
|
* ```
|
||||||
|
@ -74,7 +74,7 @@ export class Route implements RouteDefinition {
|
||||||
*/
|
*/
|
||||||
@CONST()
|
@CONST()
|
||||||
export class AuxRoute implements RouteDefinition {
|
export class AuxRoute implements RouteDefinition {
|
||||||
data: any = null;
|
data: {[key: string]: any} = null;
|
||||||
path: string;
|
path: string;
|
||||||
component: Type;
|
component: Type;
|
||||||
as: string;
|
as: string;
|
||||||
|
@ -97,7 +97,7 @@ export class AuxRoute implements RouteDefinition {
|
||||||
* - `loader` is a function that returns a promise that resolves to a component.
|
* - `loader` is a function that returns a promise that resolves to a component.
|
||||||
* - `as` is an optional `CamelCase` string representing the name of the route.
|
* - `as` is an optional `CamelCase` string representing the name of the route.
|
||||||
* - `data` is an optional property of any type representing arbitrary route metadata for the given
|
* - `data` is an optional property of any type representing arbitrary route metadata for the given
|
||||||
* route. It is injectable via the {@link ROUTE_DATA} token.
|
* route. It is injectable via {@link RouteData}.
|
||||||
*
|
*
|
||||||
* ### Example
|
* ### Example
|
||||||
* ```
|
* ```
|
||||||
|
@ -111,11 +111,12 @@ export class AuxRoute implements RouteDefinition {
|
||||||
*/
|
*/
|
||||||
@CONST()
|
@CONST()
|
||||||
export class AsyncRoute implements RouteDefinition {
|
export class AsyncRoute implements RouteDefinition {
|
||||||
data: any;
|
data: {[key: string]: any};
|
||||||
path: string;
|
path: string;
|
||||||
loader: Function;
|
loader: Function;
|
||||||
as: string;
|
as: string;
|
||||||
constructor({path, loader, as, data}: {path: string, loader: Function, as?: string, data?: any}) {
|
constructor({path, loader, as, data}:
|
||||||
|
{path: string, loader: Function, as?: string, data?: {[key: string]: any}}) {
|
||||||
this.path = path;
|
this.path = path;
|
||||||
this.loader = loader;
|
this.loader = loader;
|
||||||
this.as = as;
|
this.as = as;
|
||||||
|
|
|
@ -1,4 +0,0 @@
|
||||||
import {OpaqueToken} from 'angular2/angular2';
|
|
||||||
import {CONST_EXPR} from 'angular2/src/core/facade/lang';
|
|
||||||
|
|
||||||
export const ROUTE_DATA: OpaqueToken = CONST_EXPR(new OpaqueToken('routeData'));
|
|
|
@ -4,5 +4,5 @@ import {Type} from 'angular2/src/core/facade/lang';
|
||||||
export interface RouteHandler {
|
export interface RouteHandler {
|
||||||
componentType: Type;
|
componentType: Type;
|
||||||
resolveComponentType(): Promise<any>;
|
resolveComponentType(): Promise<any>;
|
||||||
data?: Object;
|
data?: {[key: string]: any};
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,8 +15,7 @@ import {
|
||||||
} from 'angular2/angular2';
|
} from 'angular2/angular2';
|
||||||
|
|
||||||
import * as routerMod from './router';
|
import * as routerMod from './router';
|
||||||
import {ComponentInstruction, RouteParams} from './instruction';
|
import {ComponentInstruction, RouteParams, RouteData} from './instruction';
|
||||||
import {ROUTE_DATA} from './route_data';
|
|
||||||
import * as hookMod from './lifecycle_annotations';
|
import * as hookMod from './lifecycle_annotations';
|
||||||
import {hasLifecycleHook} from './route_lifecycle_reflector';
|
import {hasLifecycleHook} from './route_lifecycle_reflector';
|
||||||
|
|
||||||
|
@ -58,7 +57,7 @@ export class RouterOutlet {
|
||||||
var childRouter = this._parentRouter.childRouter(componentType);
|
var childRouter = this._parentRouter.childRouter(componentType);
|
||||||
|
|
||||||
var providers = Injector.resolve([
|
var providers = Injector.resolve([
|
||||||
provide(ROUTE_DATA, {useValue: nextInstruction.routeData()}),
|
provide(RouteData, {useValue: nextInstruction.routeData}),
|
||||||
provide(RouteParams, {useValue: new RouteParams(nextInstruction.params)}),
|
provide(RouteParams, {useValue: new RouteParams(nextInstruction.params)}),
|
||||||
provide(routerMod.Router, {useValue: childRouter})
|
provide(routerMod.Router, {useValue: childRouter})
|
||||||
]);
|
]);
|
||||||
|
|
|
@ -6,7 +6,7 @@ export class SyncRouteHandler implements RouteHandler {
|
||||||
/** @internal */
|
/** @internal */
|
||||||
_resolvedComponent: Promise<any> = null;
|
_resolvedComponent: Promise<any> = null;
|
||||||
|
|
||||||
constructor(public componentType: Type, public data?: Object) {
|
constructor(public componentType: Type, public data?: {[key: string]: any}) {
|
||||||
this._resolvedComponent = PromiseWrapper.resolve(componentType);
|
this._resolvedComponent = PromiseWrapper.resolve(componentType);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -26,7 +26,7 @@ import {
|
||||||
} from 'angular2/src/core/facade/async';
|
} from 'angular2/src/core/facade/async';
|
||||||
|
|
||||||
import {RootRouter} from 'angular2/src/router/router';
|
import {RootRouter} from 'angular2/src/router/router';
|
||||||
import {Router, RouterOutlet, RouterLink, RouteParams, ROUTE_DATA} from 'angular2/router';
|
import {Router, RouterOutlet, RouterLink, RouteParams} from 'angular2/router';
|
||||||
import {
|
import {
|
||||||
RouteConfig,
|
RouteConfig,
|
||||||
Route,
|
Route,
|
||||||
|
|
|
@ -16,11 +16,10 @@ import {
|
||||||
} from 'angular2/testing_internal';
|
} from 'angular2/testing_internal';
|
||||||
|
|
||||||
import {provide, Component, View, Injector, Inject} from 'angular2/core';
|
import {provide, Component, View, Injector, Inject} from 'angular2/core';
|
||||||
import {CONST, NumberWrapper, isPresent, Json} from 'angular2/src/core/facade/lang';
|
|
||||||
import {Promise, PromiseWrapper} from 'angular2/src/core/facade/async';
|
import {Promise, PromiseWrapper} from 'angular2/src/core/facade/async';
|
||||||
|
|
||||||
import {RootRouter} from 'angular2/src/router/router';
|
import {RootRouter} from 'angular2/src/router/router';
|
||||||
import {Router, RouterOutlet, RouterLink, RouteParams, ROUTE_DATA} from 'angular2/router';
|
import {Router, RouterOutlet, RouterLink, RouteParams, RouteData} from 'angular2/router';
|
||||||
import {
|
import {
|
||||||
RouteConfig,
|
RouteConfig,
|
||||||
Route,
|
Route,
|
||||||
|
@ -200,13 +199,12 @@ export function main() {
|
||||||
it('should inject route data into component', inject([AsyncTestCompleter], (async) => {
|
it('should inject route data into component', inject([AsyncTestCompleter], (async) => {
|
||||||
compile()
|
compile()
|
||||||
.then((_) => rtr.config([
|
.then((_) => rtr.config([
|
||||||
new Route({path: '/route-data', component: RouteDataCmp, data: {'isAdmin': true}})
|
new Route({path: '/route-data', component: RouteDataCmp, data: {isAdmin: true}})
|
||||||
]))
|
]))
|
||||||
.then((_) => rtr.navigateByUrl('/route-data'))
|
.then((_) => rtr.navigateByUrl('/route-data'))
|
||||||
.then((_) => {
|
.then((_) => {
|
||||||
rootTC.detectChanges();
|
rootTC.detectChanges();
|
||||||
expect(rootTC.debugElement.nativeElement)
|
expect(rootTC.debugElement.nativeElement).toHaveText('true');
|
||||||
.toHaveText(Json.stringify({'isAdmin': true}));
|
|
||||||
async.done();
|
async.done();
|
||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
|
@ -221,13 +219,12 @@ export function main() {
|
||||||
.then((_) => rtr.navigateByUrl('/route-data'))
|
.then((_) => rtr.navigateByUrl('/route-data'))
|
||||||
.then((_) => {
|
.then((_) => {
|
||||||
rootTC.detectChanges();
|
rootTC.detectChanges();
|
||||||
expect(rootTC.debugElement.nativeElement)
|
expect(rootTC.debugElement.nativeElement).toHaveText('true');
|
||||||
.toHaveText(Json.stringify({'isAdmin': true}));
|
|
||||||
async.done();
|
async.done();
|
||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
|
|
||||||
it('should inject null if the route has no data property',
|
it('should inject empty object if the route has no data property',
|
||||||
inject([AsyncTestCompleter], (async) => {
|
inject([AsyncTestCompleter], (async) => {
|
||||||
compile()
|
compile()
|
||||||
.then((_) => rtr.config(
|
.then((_) => rtr.config(
|
||||||
|
@ -235,34 +232,7 @@ export function main() {
|
||||||
.then((_) => rtr.navigateByUrl('/route-data-default'))
|
.then((_) => rtr.navigateByUrl('/route-data-default'))
|
||||||
.then((_) => {
|
.then((_) => {
|
||||||
rootTC.detectChanges();
|
rootTC.detectChanges();
|
||||||
expect(rootTC.debugElement.nativeElement).toHaveText('null');
|
expect(rootTC.debugElement.nativeElement).toHaveText('');
|
||||||
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.navigateByUrl('/route-data-array'))
|
|
||||||
.then((_) => {
|
|
||||||
rootTC.detectChanges();
|
|
||||||
expect(rootTC.debugElement.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.navigateByUrl('/route-data-string'))
|
|
||||||
.then((_) => {
|
|
||||||
rootTC.detectChanges();
|
|
||||||
expect(rootTC.debugElement.nativeElement).toHaveText(Json.stringify('hello world'));
|
|
||||||
async.done();
|
async.done();
|
||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
|
@ -298,10 +268,8 @@ function AsyncRouteDataCmp() {
|
||||||
@Component({selector: 'data-cmp'})
|
@Component({selector: 'data-cmp'})
|
||||||
@View({template: "{{myData}}"})
|
@View({template: "{{myData}}"})
|
||||||
class RouteDataCmp {
|
class RouteDataCmp {
|
||||||
myData: string;
|
myData: boolean;
|
||||||
constructor(@Inject(ROUTE_DATA) data: any) {
|
constructor(data: RouteData) { this.myData = data.get('isAdmin'); }
|
||||||
this.myData = isPresent(data) ? Json.stringify(data) : 'null';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Component({selector: 'user-cmp'})
|
@Component({selector: 'user-cmp'})
|
||||||
|
|
|
@ -32,9 +32,13 @@ import {
|
||||||
} from 'angular2/router';
|
} from 'angular2/router';
|
||||||
|
|
||||||
import {DOM} from 'angular2/src/core/dom/dom_adapter';
|
import {DOM} from 'angular2/src/core/dom/dom_adapter';
|
||||||
import {ComponentInstruction_} from "angular2/src/router/instruction";
|
import {ComponentInstruction_} from 'angular2/src/router/instruction';
|
||||||
|
import {PathRecognizer} from 'angular2/src/router/path_recognizer';
|
||||||
|
import {SyncRouteHandler} from 'angular2/src/router/sync_route_handler';
|
||||||
|
|
||||||
var dummyInstruction = new Instruction(new ComponentInstruction_('detail', [], null), null, {});
|
let dummyPathRecognizer = new PathRecognizer('', new SyncRouteHandler(null));
|
||||||
|
let dummyInstruction =
|
||||||
|
new Instruction(new ComponentInstruction_('detail', [], dummyPathRecognizer), null, {});
|
||||||
|
|
||||||
export function main() {
|
export function main() {
|
||||||
describe('router-link directive', function() {
|
describe('router-link directive', function() {
|
||||||
|
|
Loading…
Reference in New Issue