parent
2335075506
commit
f66ce096d8
|
@ -34,7 +34,8 @@ import {List} from './src/facade/collection';
|
|||
export const routerDirectives: List<any> = CONST_EXPR([RouterOutlet, RouterLink]);
|
||||
|
||||
export var routerInjectables: List<any> = [
|
||||
RouteRegistry,
|
||||
bind(RouteRegistry)
|
||||
.toFactory((appRoot) => new RouteRegistry(appRoot), [appComponentTypeToken]),
|
||||
Pipeline,
|
||||
bind(LocationStrategy).toClass(HTML5LocationStrategy),
|
||||
Location,
|
||||
|
|
|
@ -239,7 +239,6 @@ export class ListWrapper {
|
|||
}
|
||||
static toString<T>(l: List<T>): string { return l.toString(); }
|
||||
static toJSON<T>(l: List<T>): string { return JSON.stringify(l); }
|
||||
|
||||
}
|
||||
|
||||
export function isListLikeIterable(obj): boolean {
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
import {RouteHandler} from './route_handler';
|
||||
import {Promise, PromiseWrapper} from 'angular2/src/facade/async';
|
||||
import {isPresent, Type} from 'angular2/src/facade/lang';
|
||||
|
||||
export class AsyncRouteHandler implements RouteHandler {
|
||||
_resolvedComponent: Promise<any> = null;
|
||||
componentType: Type;
|
||||
|
||||
constructor(private _loader: Function) {}
|
||||
|
||||
resolveComponentType(): Promise<any> {
|
||||
if (isPresent(this._resolvedComponent)) {
|
||||
return this._resolvedComponent;
|
||||
}
|
||||
|
||||
return this._resolvedComponent = this._loader().then((componentType) => {
|
||||
this.componentType = componentType;
|
||||
return componentType;
|
||||
});
|
||||
}
|
||||
}
|
|
@ -6,7 +6,9 @@ import {
|
|||
List,
|
||||
ListWrapper
|
||||
} from 'angular2/src/facade/collection';
|
||||
import {isPresent, normalizeBlank} from 'angular2/src/facade/lang';
|
||||
import {isPresent, isBlank, normalizeBlank} from 'angular2/src/facade/lang';
|
||||
|
||||
import {PathRecognizer} from './path_recognizer';
|
||||
|
||||
export class RouteParams {
|
||||
constructor(public params: StringMap<string, string>) {}
|
||||
|
@ -14,34 +16,24 @@ export class RouteParams {
|
|||
get(param: string): string { return normalizeBlank(StringMapWrapper.get(this.params, param)); }
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* An `Instruction` represents the component hierarchy of the application based on a given route
|
||||
*/
|
||||
export class Instruction {
|
||||
component: any;
|
||||
child: Instruction;
|
||||
|
||||
// the part of the URL captured by this instruction
|
||||
capturedUrl: string;
|
||||
|
||||
// the part of the URL captured by this instruction and all children
|
||||
// "capturedUrl" is the part of the URL captured by this instruction
|
||||
// "accumulatedUrl" is the part of the URL captured by this instruction and all children
|
||||
accumulatedUrl: string;
|
||||
|
||||
params: StringMap<string, string>;
|
||||
reuse: boolean;
|
||||
reuse: boolean = false;
|
||||
specificity: number;
|
||||
|
||||
constructor({params, component, child, matchedUrl, parentSpecificity}: {
|
||||
params?: StringMap<string, any>,
|
||||
component?: any,
|
||||
child?: Instruction,
|
||||
matchedUrl?: string,
|
||||
parentSpecificity?: number
|
||||
} = {}) {
|
||||
this.reuse = false;
|
||||
this.capturedUrl = matchedUrl;
|
||||
this.accumulatedUrl = matchedUrl;
|
||||
this.specificity = parentSpecificity;
|
||||
private _params: StringMap<string, string>;
|
||||
|
||||
constructor(public component: any, public capturedUrl: string,
|
||||
private _recognizer: PathRecognizer, public child: Instruction = null) {
|
||||
this.accumulatedUrl = capturedUrl;
|
||||
this.specificity = _recognizer.specificity;
|
||||
if (isPresent(child)) {
|
||||
this.child = child;
|
||||
this.specificity += child.specificity;
|
||||
|
@ -49,11 +41,14 @@ export class Instruction {
|
|||
if (isPresent(childUrl)) {
|
||||
this.accumulatedUrl += childUrl;
|
||||
}
|
||||
} else {
|
||||
this.child = null;
|
||||
}
|
||||
this.component = component;
|
||||
this.params = params;
|
||||
}
|
||||
|
||||
params(): StringMap<string, string> {
|
||||
if (isBlank(this._params)) {
|
||||
this._params = this._recognizer.parseParams(this.capturedUrl);
|
||||
}
|
||||
return this._params;
|
||||
}
|
||||
|
||||
hasChild(): boolean { return isPresent(this.child); }
|
||||
|
@ -73,5 +68,5 @@ export class Instruction {
|
|||
|
||||
function shouldReuseComponent(instr1: Instruction, instr2: Instruction): boolean {
|
||||
return instr1.component == instr2.component &&
|
||||
StringMapWrapper.equals(instr1.params, instr2.params);
|
||||
StringMapWrapper.equals(instr1.params(), instr2.params());
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import {
|
|||
BaseException,
|
||||
normalizeBlank
|
||||
} from 'angular2/src/facade/lang';
|
||||
import {Promise, PromiseWrapper} from 'angular2/src/facade/async';
|
||||
import {
|
||||
Map,
|
||||
MapWrapper,
|
||||
|
@ -19,6 +20,7 @@ import {
|
|||
import {IMPLEMENTS} from 'angular2/src/facade/lang';
|
||||
|
||||
import {escapeRegex} from './url';
|
||||
import {RouteHandler} from './route_handler';
|
||||
|
||||
// TODO(jeffbcross): implement as interface when ts2dart adds support:
|
||||
// https://github.com/angular/ts2dart/issues/173
|
||||
|
@ -27,7 +29,7 @@ export class Segment {
|
|||
regex: string;
|
||||
}
|
||||
|
||||
export class ContinuationSegment extends Segment {
|
||||
class ContinuationSegment extends Segment {
|
||||
generate(params): string { return ''; }
|
||||
}
|
||||
|
||||
|
@ -52,7 +54,7 @@ class DynamicSegment {
|
|||
generate(params: StringMap<string, string>): string {
|
||||
if (!StringMapWrapper.contains(params, this.name)) {
|
||||
throw new BaseException(
|
||||
`Route generator for '${this.name}' was not included in parameters passed.`)
|
||||
`Route generator for '${this.name}' was not included in parameters passed.`);
|
||||
}
|
||||
return normalizeBlank(StringMapWrapper.get(params, this.name));
|
||||
}
|
||||
|
@ -135,7 +137,7 @@ export class PathRecognizer {
|
|||
specificity: number;
|
||||
terminal: boolean = true;
|
||||
|
||||
constructor(public path: string, public handler: any) {
|
||||
constructor(public path: string, public handler: RouteHandler) {
|
||||
var parsed = parsePathString(path);
|
||||
var specificity = parsed['specificity'];
|
||||
var segments = parsed['segments'];
|
||||
|
@ -178,7 +180,9 @@ export class PathRecognizer {
|
|||
}
|
||||
|
||||
generate(params: StringMap<string, string>): string {
|
||||
return ListWrapper.join(
|
||||
ListWrapper.map(this.segments, (segment) => '/' + segment.generate(params)), '');
|
||||
return ListWrapper.join(ListWrapper.map(this.segments, (segment) => segment.generate(params)),
|
||||
'/');
|
||||
}
|
||||
|
||||
resolveComponentType(): Promise<any> { return this.handler.resolveComponentType(); }
|
||||
}
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
import {Promise, PromiseWrapper} from 'angular2/src/facade/async';
|
||||
import {Type} from 'angular2/src/facade/lang';
|
||||
|
||||
export interface RouteHandler {
|
||||
componentType: Type;
|
||||
resolveComponentType(): Promise<any>;
|
||||
}
|
|
@ -2,7 +2,10 @@ import {
|
|||
RegExp,
|
||||
RegExpWrapper,
|
||||
StringWrapper,
|
||||
isBlank,
|
||||
isPresent,
|
||||
isType,
|
||||
isStringMap,
|
||||
BaseException
|
||||
} from 'angular2/src/facade/lang';
|
||||
import {
|
||||
|
@ -14,7 +17,10 @@ import {
|
|||
StringMapWrapper
|
||||
} from 'angular2/src/facade/collection';
|
||||
|
||||
import {PathRecognizer, ContinuationSegment} from './path_recognizer';
|
||||
import {PathRecognizer} from './path_recognizer';
|
||||
import {RouteHandler} from './route_handler';
|
||||
import {AsyncRouteHandler} from './async_route_handler';
|
||||
import {SyncRouteHandler} from './sync_route_handler';
|
||||
|
||||
/**
|
||||
* `RouteRecognizer` is responsible for recognizing routes for a single component.
|
||||
|
@ -33,7 +39,8 @@ export class RouteRecognizer {
|
|||
this.redirects.set(path, target);
|
||||
}
|
||||
|
||||
addConfig(path: string, handler: any, alias: string = null): boolean {
|
||||
addConfig(path: string, handlerObj: any, alias: string = null): boolean {
|
||||
var handler = configObjToHandler(handlerObj['component']);
|
||||
var recognizer = new PathRecognizer(path, handler);
|
||||
MapWrapper.forEach(this.matchers, (matcher, _) => {
|
||||
if (recognizer.regex.toString() == matcher.regex.toString()) {
|
||||
|
@ -65,28 +72,21 @@ export class RouteRecognizer {
|
|||
if (path == url) {
|
||||
url = target;
|
||||
}
|
||||
} else if (StringWrapper.startsWith(url, path)) {
|
||||
url = target + StringWrapper.substring(url, path.length);
|
||||
} else if (url.startsWith(path)) {
|
||||
url = target + url.substring(path.length);
|
||||
}
|
||||
});
|
||||
|
||||
MapWrapper.forEach(this.matchers, (pathRecognizer, regex) => {
|
||||
var match;
|
||||
if (isPresent(match = RegExpWrapper.firstMatch(regex, url))) {
|
||||
// TODO(btford): determine a good generic way to deal with terminal matches
|
||||
var matchedUrl = '/';
|
||||
var unmatchedUrl = '';
|
||||
if (url != '/') {
|
||||
matchedUrl = match[0];
|
||||
unmatchedUrl = StringWrapper.substring(url, match[0].length);
|
||||
unmatchedUrl = url.substring(match[0].length);
|
||||
}
|
||||
solutions.push(new RouteMatch({
|
||||
specificity: pathRecognizer.specificity,
|
||||
handler: pathRecognizer.handler,
|
||||
params: pathRecognizer.parseParams(url),
|
||||
matchedUrl: matchedUrl,
|
||||
unmatchedUrl: unmatchedUrl
|
||||
}));
|
||||
solutions.push(new RouteMatch(pathRecognizer, matchedUrl, unmatchedUrl));
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -95,30 +95,39 @@ export class RouteRecognizer {
|
|||
|
||||
hasRoute(name: string): boolean { return this.names.has(name); }
|
||||
|
||||
generate(name: string, params: any): string {
|
||||
var pathRecognizer = this.names.get(name);
|
||||
return isPresent(pathRecognizer) ? pathRecognizer.generate(params) : null;
|
||||
generate(name: string, params: any): StringMap<string, any> {
|
||||
var pathRecognizer: PathRecognizer = this.names.get(name);
|
||||
if (isBlank(pathRecognizer)) {
|
||||
return null;
|
||||
}
|
||||
var url = pathRecognizer.generate(params);
|
||||
return {url, 'nextComponent': pathRecognizer.handler.componentType};
|
||||
}
|
||||
}
|
||||
|
||||
export class RouteMatch {
|
||||
specificity: number;
|
||||
handler: StringMap<string, any>;
|
||||
params: StringMap<string, string>;
|
||||
matchedUrl: string;
|
||||
unmatchedUrl: string;
|
||||
constructor(public recognizer: PathRecognizer, public matchedUrl: string,
|
||||
public unmatchedUrl: string) {}
|
||||
|
||||
constructor({specificity, handler, params, matchedUrl, unmatchedUrl}: {
|
||||
specificity?: number,
|
||||
handler?: StringMap<string, any>,
|
||||
params?: StringMap<string, string>,
|
||||
matchedUrl?: string,
|
||||
unmatchedUrl?: string
|
||||
} = {}) {
|
||||
this.specificity = specificity;
|
||||
this.handler = handler;
|
||||
this.params = params;
|
||||
this.matchedUrl = matchedUrl;
|
||||
this.unmatchedUrl = unmatchedUrl;
|
||||
}
|
||||
params(): StringMap<string, string> { return this.recognizer.parseParams(this.matchedUrl); }
|
||||
}
|
||||
|
||||
function configObjToHandler(config: any): RouteHandler {
|
||||
if (isType(config)) {
|
||||
return new SyncRouteHandler(config);
|
||||
} else if (isStringMap(config)) {
|
||||
if (isBlank(config['type'])) {
|
||||
throw new BaseException(
|
||||
`Component declaration when provided as a map should include a 'type' property`);
|
||||
}
|
||||
var componentType = config['type'];
|
||||
if (componentType == 'constructor') {
|
||||
return new SyncRouteHandler(config['constructor']);
|
||||
} else if (componentType == 'loader') {
|
||||
return new AsyncRouteHandler(config['loader']);
|
||||
} else {
|
||||
throw new BaseException(`oops`);
|
||||
}
|
||||
}
|
||||
throw new BaseException(`Unexpected component "${config}".`);
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ import {
|
|||
isPresent,
|
||||
isBlank,
|
||||
isType,
|
||||
isString,
|
||||
isStringMap,
|
||||
isFunction,
|
||||
StringWrapper,
|
||||
|
@ -29,7 +30,9 @@ import {Injectable} from 'angular2/di';
|
|||
*/
|
||||
@Injectable()
|
||||
export class RouteRegistry {
|
||||
_rules: Map<any, RouteRecognizer> = new Map();
|
||||
private _rules: Map<any, RouteRecognizer> = new Map();
|
||||
|
||||
constructor(private _rootHostComponent: any) {}
|
||||
|
||||
/**
|
||||
* Given a component and a configuration object, add the route to this registry
|
||||
|
@ -118,40 +121,80 @@ export class RouteRegistry {
|
|||
}
|
||||
|
||||
|
||||
_completeRouteMatch(candidate: RouteMatch): Promise<Instruction> {
|
||||
return componentHandlerToComponentType(candidate.handler)
|
||||
.then((componentType) => {
|
||||
this.configFromComponent(componentType);
|
||||
_completeRouteMatch(partialMatch: RouteMatch): Promise<Instruction> {
|
||||
var recognizer = partialMatch.recognizer;
|
||||
var handler = recognizer.handler;
|
||||
return handler.resolveComponentType().then((componentType) => {
|
||||
this.configFromComponent(componentType);
|
||||
|
||||
if (candidate.unmatchedUrl.length == 0) {
|
||||
return new Instruction({
|
||||
component: componentType,
|
||||
params: candidate.params,
|
||||
matchedUrl: candidate.matchedUrl,
|
||||
parentSpecificity: candidate.specificity
|
||||
});
|
||||
}
|
||||
if (partialMatch.unmatchedUrl.length == 0) {
|
||||
return new Instruction(componentType, partialMatch.matchedUrl, recognizer);
|
||||
}
|
||||
|
||||
return this.recognize(candidate.unmatchedUrl, componentType)
|
||||
.then(childInstruction => {
|
||||
if (isBlank(childInstruction)) {
|
||||
return null;
|
||||
}
|
||||
return new Instruction({
|
||||
component: componentType,
|
||||
child: childInstruction,
|
||||
params: candidate.params,
|
||||
matchedUrl: candidate.matchedUrl,
|
||||
parentSpecificity: candidate.specificity
|
||||
});
|
||||
});
|
||||
});
|
||||
return this.recognize(partialMatch.unmatchedUrl, componentType)
|
||||
.then(childInstruction => {
|
||||
if (isBlank(childInstruction)) {
|
||||
return null;
|
||||
} else {
|
||||
return new Instruction(componentType, partialMatch.matchedUrl, recognizer,
|
||||
childInstruction);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
generate(name: string, params: StringMap<string, string>, hostComponent): string {
|
||||
// TODO: implement for hierarchical routes
|
||||
var componentRecognizer = this._rules.get(hostComponent);
|
||||
return isPresent(componentRecognizer) ? componentRecognizer.generate(name, params) : null;
|
||||
/**
|
||||
* Given a list with component names and params like: `['./user', {id: 3 }]`
|
||||
* generates a url with a leading slash relative to the provided `parentComponent`.
|
||||
*/
|
||||
generate(linkParams: List<any>, parentComponent): string {
|
||||
let normalizedLinkParams = splitAndFlattenLinkParams(linkParams);
|
||||
let url = '/';
|
||||
|
||||
let componentCursor = parentComponent;
|
||||
|
||||
// The first segment should be either '.' (generate from parent) or '' (generate from root).
|
||||
// When we normalize above, we strip all the slashes, './' becomes '.' and '/' becomes ''.
|
||||
if (normalizedLinkParams[0] == '') {
|
||||
componentCursor = this._rootHostComponent;
|
||||
} else if (normalizedLinkParams[0] != '.') {
|
||||
throw new BaseException(
|
||||
`Link "${ListWrapper.toJSON(linkParams)}" must start with "/" or "./"`);
|
||||
}
|
||||
|
||||
if (normalizedLinkParams[normalizedLinkParams.length - 1] == '') {
|
||||
ListWrapper.removeLast(normalizedLinkParams);
|
||||
}
|
||||
|
||||
if (normalizedLinkParams.length < 2) {
|
||||
throw new BaseException(
|
||||
`Link "${ListWrapper.toJSON(linkParams)}" must include a route name.`);
|
||||
}
|
||||
|
||||
for (let i = 1; i < normalizedLinkParams.length; i += 1) {
|
||||
let segment = normalizedLinkParams[i];
|
||||
if (!isString(segment)) {
|
||||
throw new BaseException(`Unexpected segment "${segment}" in link DSL. Expected a string.`);
|
||||
}
|
||||
let params = null;
|
||||
if (i + 1 < normalizedLinkParams.length) {
|
||||
let nextSegment = normalizedLinkParams[i + 1];
|
||||
if (isStringMap(nextSegment)) {
|
||||
params = nextSegment;
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
|
||||
var componentRecognizer = this._rules.get(componentCursor);
|
||||
if (isBlank(componentRecognizer)) {
|
||||
throw new BaseException(`Could not find route config for "${segment}".`);
|
||||
}
|
||||
var response = componentRecognizer.generate(segment, params);
|
||||
url += response['url'];
|
||||
componentCursor = response['nextComponent'];
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -200,19 +243,6 @@ function normalizeComponentDeclaration(config: any): StringMap<string, any> {
|
|||
}
|
||||
}
|
||||
|
||||
function componentHandlerToComponentType(handler): Promise<any> {
|
||||
var componentDeclaration = handler['component'], type = componentDeclaration['type'];
|
||||
|
||||
if (type == 'constructor') {
|
||||
return PromiseWrapper.resolve(componentDeclaration['constructor']);
|
||||
} else if (type == 'loader') {
|
||||
var resolverFunction = componentDeclaration['loader'];
|
||||
return resolverFunction();
|
||||
} else {
|
||||
throw new BaseException(`Cannot extract the component type from a '${type}' component`);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Given a list of instructions, returns the most specific instruction
|
||||
*/
|
||||
|
@ -244,3 +274,18 @@ function assertTerminalComponent(component, path) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Given: ['/a/b', {c: 2}]
|
||||
* Returns: ['', 'a', 'b', {c: 2}]
|
||||
*/
|
||||
var SLASH = new RegExp('/');
|
||||
function splitAndFlattenLinkParams(linkParams: List<any>): List<any> {
|
||||
return ListWrapper.reduce(linkParams, (accumulation, item) => {
|
||||
if (isString(item)) {
|
||||
return ListWrapper.concat(accumulation, StringWrapper.split(item, SLASH));
|
||||
}
|
||||
accumulation.push(item);
|
||||
return accumulation;
|
||||
}, []);
|
||||
}
|
||||
|
|
|
@ -191,8 +191,8 @@ export class Router {
|
|||
* Generate a URL from a component name and optional map of parameters. The URL is relative to the
|
||||
* app's base href.
|
||||
*/
|
||||
generate(name: string, params: StringMap<string, string>): string {
|
||||
return this._registry.generate(name, params, this.hostComponent);
|
||||
generate(linkParams: List<any>): string {
|
||||
return this._registry.generate(linkParams, this.hostComponent);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,18 +1,12 @@
|
|||
import {onAllChangesDone} from 'angular2/src/core/annotations/annotations';
|
||||
import {Directive} from 'angular2/src/core/annotations/decorators';
|
||||
import {ElementRef} from 'angular2/core';
|
||||
import {StringMap, StringMapWrapper} from 'angular2/src/facade/collection';
|
||||
|
||||
import {isPresent} from 'angular2/src/facade/lang';
|
||||
import {List, StringMap, StringMapWrapper} from 'angular2/src/facade/collection';
|
||||
|
||||
import {Router} from './router';
|
||||
import {Location} from './location';
|
||||
import {Renderer} from 'angular2/src/render/api';
|
||||
|
||||
/**
|
||||
* The RouterLink directive lets you link to specific parts of your app.
|
||||
*
|
||||
*
|
||||
* Consider the following route configuration:
|
||||
|
||||
* ```
|
||||
|
@ -22,48 +16,47 @@ import {Renderer} from 'angular2/src/render/api';
|
|||
* class MyComp {}
|
||||
* ```
|
||||
*
|
||||
* When linking to a route, you can write:
|
||||
* When linking to this `user` route, you can write:
|
||||
*
|
||||
* ```
|
||||
* <a router-link="user">link to user component</a>
|
||||
* <a [router-link]="['./user']">link to user component</a>
|
||||
* ```
|
||||
*
|
||||
* RouterLink expects the value to be an array of route names, followed by the params
|
||||
* for that level of routing. For instance `['/team', {teamId: 1}, 'user', {userId: 2}]`
|
||||
* means that we want to generate a link for the `team` route with params `{teamId: 1}`,
|
||||
* and with a child route `user` with params `{userId: 2}`.
|
||||
*
|
||||
* The first route name should be prepended with either `./` or `/`.
|
||||
* If the route begins with `/`, the router will look up the route from the root of the app.
|
||||
* If the route begins with `./`, the router will instead look in the current component's
|
||||
* children for the route.
|
||||
*
|
||||
* @exportedAs angular2/router
|
||||
*/
|
||||
@Directive({
|
||||
selector: '[router-link]',
|
||||
properties: ['route: routerLink', 'params: routerParams'],
|
||||
lifecycle: [onAllChangesDone],
|
||||
host: {'(^click)': 'onClick()'}
|
||||
properties: ['routeParams: routerLink'],
|
||||
host: {'(^click)': 'onClick()', '[attr.href]': 'visibleHref'}
|
||||
})
|
||||
export class RouterLink {
|
||||
private _route: string;
|
||||
private _params: StringMap<string, string> = StringMapWrapper.create();
|
||||
private _routeParams: List<any>;
|
||||
|
||||
// the url displayed on the anchor element.
|
||||
_visibleHref: string;
|
||||
visibleHref: string;
|
||||
// the url passed to the router navigation.
|
||||
_navigationHref: string;
|
||||
|
||||
constructor(private _elementRef: ElementRef, private _router: Router, private _location: Location,
|
||||
private _renderer: Renderer) {}
|
||||
constructor(private _router: Router, private _location: Location) {}
|
||||
|
||||
set route(changes: string) { this._route = changes; }
|
||||
|
||||
set params(changes: StringMap<string, string>) { this._params = changes; }
|
||||
set routeParams(changes: List<any>) {
|
||||
this._routeParams = changes;
|
||||
this._navigationHref = this._router.generate(this._routeParams);
|
||||
this.visibleHref = this._location.normalizeAbsolutely(this._navigationHref);
|
||||
}
|
||||
|
||||
onClick(): boolean {
|
||||
this._router.navigate(this._navigationHref);
|
||||
return false;
|
||||
}
|
||||
|
||||
onAllChangesDone(): void {
|
||||
if (isPresent(this._route) && isPresent(this._params)) {
|
||||
this._navigationHref = this._router.generate(this._route, this._params);
|
||||
this._visibleHref = this._location.normalizeAbsolutely(this._navigationHref);
|
||||
// Keeping the link on the element to support contextual menu `copy link`
|
||||
// and other in-browser affordances.
|
||||
this._renderer.setElementAttribute(this._elementRef, 'href', this._visibleHref);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -57,7 +57,7 @@ export class RouterOutlet {
|
|||
this._childRouter = this._parentRouter.childRouter(instruction.component);
|
||||
var outletInjector = this._injector.resolveAndCreateChild([
|
||||
bind(RouteParams)
|
||||
.toValue(new RouteParams(instruction.params)),
|
||||
.toValue(new RouteParams(instruction.params())),
|
||||
bind(routerMod.Router).toValue(this._childRouter)
|
||||
]);
|
||||
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
import {RouteHandler} from './route_handler';
|
||||
import {Promise, PromiseWrapper} from 'angular2/src/facade/async';
|
||||
import {Type} from 'angular2/src/facade/lang';
|
||||
|
||||
export class SyncRouteHandler implements RouteHandler {
|
||||
_resolvedComponent: Promise<any> = null;
|
||||
|
||||
constructor(public componentType: Type) {
|
||||
this._resolvedComponent = PromiseWrapper.resolve(componentType);
|
||||
}
|
||||
|
||||
resolveComponentType(): Promise<any> { return this._resolvedComponent; }
|
||||
}
|
|
@ -42,7 +42,7 @@ export function main() {
|
|||
|
||||
beforeEachBindings(() => [
|
||||
Pipeline,
|
||||
RouteRegistry,
|
||||
bind(RouteRegistry).toFactory(() => new RouteRegistry(MyComp)),
|
||||
DirectiveResolver,
|
||||
bind(Location).toClass(SpyLocation),
|
||||
bind(Router)
|
||||
|
@ -129,7 +129,7 @@ export function main() {
|
|||
it('should generate absolute hrefs that include the base href',
|
||||
inject([AsyncTestCompleter], (async) => {
|
||||
location.setBaseHref('/my/base');
|
||||
compile('<a href="hello" router-link="user"></a>')
|
||||
compile('<a href="hello" [router-link]="[\'./user\']"></a>')
|
||||
.then((_) => rtr.config({'path': '/user', 'component': UserCmp, 'as': 'user'}))
|
||||
.then((_) => rtr.navigate('/a/b'))
|
||||
.then((_) => {
|
||||
|
@ -141,7 +141,7 @@ export function main() {
|
|||
|
||||
|
||||
it('should generate link hrefs without params', inject([AsyncTestCompleter], (async) => {
|
||||
compile('<a href="hello" router-link="user"></a>')
|
||||
compile('<a href="hello" [router-link]="[\'./user\']"></a>')
|
||||
.then((_) => rtr.config({'path': '/user', 'component': UserCmp, 'as': 'user'}))
|
||||
.then((_) => rtr.navigate('/a/b'))
|
||||
.then((_) => {
|
||||
|
@ -172,7 +172,7 @@ export function main() {
|
|||
|
||||
|
||||
it('should generate link hrefs with params', inject([AsyncTestCompleter], (async) => {
|
||||
compile('<a href="hello" router-link="user" [router-params]="{name: name}">{{name}}</a>')
|
||||
compile('<a href="hello" [router-link]="[\'./user\', {name: name}]">{{name}}</a>')
|
||||
.then((_) => rtr.config({'path': '/user/:name', 'component': UserCmp, 'as': 'user'}))
|
||||
.then((_) => rtr.navigate('/a/b'))
|
||||
.then((_) => {
|
||||
|
@ -194,10 +194,8 @@ export function main() {
|
|||
return dispatchedEvent;
|
||||
};
|
||||
|
||||
it('test', inject([AsyncTestCompleter], (async) => { async.done(); }));
|
||||
|
||||
it('should navigate to link hrefs without params', inject([AsyncTestCompleter], (async) => {
|
||||
compile('<a href="hello" router-link="user"></a>')
|
||||
compile('<a href="hello" [router-link]="[\'./user\']"></a>')
|
||||
.then((_) => rtr.config({'path': '/user', 'component': UserCmp, 'as': 'user'}))
|
||||
.then((_) => rtr.navigate('/a/b'))
|
||||
.then((_) => {
|
||||
|
@ -218,7 +216,7 @@ export function main() {
|
|||
it('should navigate to link hrefs in presence of base href',
|
||||
inject([AsyncTestCompleter], (async) => {
|
||||
location.setBaseHref('/base');
|
||||
compile('<a href="hello" router-link="user"></a>')
|
||||
compile('<a href="hello" [router-link]="[\'./user\']"></a>')
|
||||
.then((_) => rtr.config({'path': '/user', 'component': UserCmp, 'as': 'user'}))
|
||||
.then((_) => rtr.navigate('/a/b'))
|
||||
.then((_) => {
|
||||
|
|
|
@ -10,43 +10,44 @@ import {
|
|||
SpyObject
|
||||
} from 'angular2/test_lib';
|
||||
|
||||
import {RouteRecognizer} from 'angular2/src/router/route_recognizer';
|
||||
import {RouteRecognizer, RouteMatch} from 'angular2/src/router/route_recognizer';
|
||||
|
||||
export function main() {
|
||||
describe('RouteRecognizer', () => {
|
||||
var recognizer;
|
||||
var handler = {'components': {'a': 'b'}};
|
||||
var handler2 = {'components': {'b': 'c'}};
|
||||
var handler = {'component': DummyCmpA};
|
||||
var handler2 = {'component': DummyCmpB};
|
||||
|
||||
beforeEach(() => { recognizer = new RouteRecognizer(); });
|
||||
|
||||
|
||||
it('should recognize a static segment', () => {
|
||||
recognizer.addConfig('/test', handler);
|
||||
expect(recognizer.recognize('/test')[0].handler).toEqual(handler);
|
||||
var solution = recognizer.recognize('/test')[0];
|
||||
expect(getComponentType(solution)).toEqual(handler['component']);
|
||||
});
|
||||
|
||||
|
||||
it('should recognize a single slash', () => {
|
||||
recognizer.addConfig('/', handler);
|
||||
var solution = recognizer.recognize('/')[0];
|
||||
expect(solution.handler).toEqual(handler);
|
||||
expect(getComponentType(solution)).toEqual(handler['component']);
|
||||
});
|
||||
|
||||
|
||||
it('should recognize a dynamic segment', () => {
|
||||
recognizer.addConfig('/user/:name', handler);
|
||||
var solution = recognizer.recognize('/user/brian')[0];
|
||||
expect(solution.handler).toEqual(handler);
|
||||
expect(solution.params).toEqual({'name': 'brian'});
|
||||
expect(getComponentType(solution)).toEqual(handler['component']);
|
||||
expect(solution.params()).toEqual({'name': 'brian'});
|
||||
});
|
||||
|
||||
|
||||
it('should recognize a star segment', () => {
|
||||
recognizer.addConfig('/first/*rest', handler);
|
||||
var solution = recognizer.recognize('/first/second/third')[0];
|
||||
expect(solution.handler).toEqual(handler);
|
||||
expect(solution.params).toEqual({'rest': 'second/third'});
|
||||
expect(getComponentType(solution)).toEqual(handler['component']);
|
||||
expect(solution.params()).toEqual({'rest': 'second/third'});
|
||||
});
|
||||
|
||||
|
||||
|
@ -72,7 +73,7 @@ export function main() {
|
|||
expect(solutions.length).toBe(1);
|
||||
|
||||
var solution = solutions[0];
|
||||
expect(solution.handler).toEqual(handler);
|
||||
expect(getComponentType(solution)).toEqual(handler['component']);
|
||||
expect(solution.matchedUrl).toEqual('/b');
|
||||
});
|
||||
|
||||
|
@ -83,7 +84,7 @@ export function main() {
|
|||
expect(solutions.length).toBe(1);
|
||||
|
||||
var solution = solutions[0];
|
||||
expect(solution.handler).toEqual(handler);
|
||||
expect(getComponentType(solution)).toEqual(handler['component']);
|
||||
expect(solution.matchedUrl).toEqual('/bar');
|
||||
});
|
||||
|
||||
|
@ -106,16 +107,23 @@ export function main() {
|
|||
expect(solutions[0].matchedUrl).toBe('/matias');
|
||||
});
|
||||
|
||||
it('should generate URLs', () => {
|
||||
it('should generate URLs with params', () => {
|
||||
recognizer.addConfig('/app/user/:name', handler, 'user');
|
||||
expect(recognizer.generate('user', {'name': 'misko'})).toEqual('/app/user/misko');
|
||||
expect(recognizer.generate('user', {'name': 'misko'})['url']).toEqual('app/user/misko');
|
||||
});
|
||||
|
||||
|
||||
it('should throw in the absence of required params URLs', () => {
|
||||
recognizer.addConfig('/app/user/:name', handler, 'user');
|
||||
expect(() => recognizer.generate('user', {}))
|
||||
recognizer.addConfig('app/user/:name', handler, 'user');
|
||||
expect(() => recognizer.generate('user', {})['url'])
|
||||
.toThrowError('Route generator for \'name\' was not included in parameters passed.');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getComponentType(routeMatch: RouteMatch): any {
|
||||
return routeMatch.recognizer.handler.componentType;
|
||||
}
|
||||
|
||||
class DummyCmpA {}
|
||||
class DummyCmpB {}
|
||||
|
|
|
@ -11,6 +11,7 @@ import {
|
|||
} from 'angular2/test_lib';
|
||||
|
||||
import {Promise, PromiseWrapper} from 'angular2/src/facade/async';
|
||||
import {ListWrapper} from 'angular2/src/facade/collection';
|
||||
|
||||
import {RouteRegistry} from 'angular2/src/router/route_registry';
|
||||
import {RouteConfig} from 'angular2/src/router/route_config_decorator';
|
||||
|
@ -19,7 +20,7 @@ export function main() {
|
|||
describe('RouteRegistry', () => {
|
||||
var registry, rootHostComponent = new Object();
|
||||
|
||||
beforeEach(() => { registry = new RouteRegistry(); });
|
||||
beforeEach(() => { registry = new RouteRegistry(rootHostComponent); });
|
||||
|
||||
it('should match the full URL', inject([AsyncTestCompleter], (async) => {
|
||||
registry.config(rootHostComponent, {'path': '/', 'component': DummyCompA});
|
||||
|
@ -32,6 +33,68 @@ export function main() {
|
|||
});
|
||||
}));
|
||||
|
||||
it('should generate URLs starting at the given component', () => {
|
||||
registry.config(rootHostComponent,
|
||||
{'path': '/first/...', 'component': DummyParentComp, 'as': 'firstCmp'});
|
||||
|
||||
expect(registry.generate(['./firstCmp/secondCmp'], rootHostComponent))
|
||||
.toEqual('/first/second');
|
||||
expect(registry.generate(['./secondCmp'], DummyParentComp)).toEqual('/second');
|
||||
});
|
||||
|
||||
it('should generate URLs with params', () => {
|
||||
registry.config(
|
||||
rootHostComponent,
|
||||
{'path': '/first/:param/...', 'component': DummyParentParamComp, 'as': 'firstCmp'});
|
||||
|
||||
var url = registry.generate(['./firstCmp', {param: 'one'}, 'secondCmp', {param: 'two'}],
|
||||
rootHostComponent);
|
||||
expect(url).toEqual('/first/one/second/two');
|
||||
});
|
||||
|
||||
it('should generate URLs from the root component when the path starts with /', () => {
|
||||
registry.config(rootHostComponent,
|
||||
{'path': '/first/...', 'component': DummyParentComp, 'as': 'firstCmp'});
|
||||
|
||||
expect(registry.generate(['/firstCmp', 'secondCmp'], rootHostComponent))
|
||||
.toEqual('/first/second');
|
||||
expect(registry.generate(['/firstCmp', 'secondCmp'], DummyParentComp))
|
||||
.toEqual('/first/second');
|
||||
expect(registry.generate(['/firstCmp/secondCmp'], DummyParentComp)).toEqual('/first/second');
|
||||
});
|
||||
|
||||
it('should generate URLs of loaded components after they are loaded',
|
||||
inject([AsyncTestCompleter], (async) => {
|
||||
registry.config(rootHostComponent, {
|
||||
'path': '/first/...',
|
||||
'component': {'type': 'loader', 'loader': AsyncParentLoader},
|
||||
'as': 'firstCmp'
|
||||
});
|
||||
|
||||
expect(() => registry.generate(['/firstCmp/secondCmp'], rootHostComponent))
|
||||
.toThrowError('Could not find route config for "secondCmp".');
|
||||
|
||||
registry.recognize('/first/second', rootHostComponent)
|
||||
.then((_) => {
|
||||
expect(registry.generate(['/firstCmp/secondCmp'], rootHostComponent))
|
||||
.toEqual('/first/second');
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should throw when linkParams does not start with a "/" or "./"', () => {
|
||||
expect(() => registry.generate(['firstCmp', 'secondCmp'], rootHostComponent))
|
||||
.toThrowError(
|
||||
`Link "${ListWrapper.toJSON(['firstCmp', 'secondCmp'])}" must start with "/" or "./"`);
|
||||
});
|
||||
|
||||
it('should throw when linkParams does not include a route name', () => {
|
||||
expect(() => registry.generate(['./'], rootHostComponent))
|
||||
.toThrowError(`Link "${ListWrapper.toJSON(['./'])}" must include a route name.`);
|
||||
expect(() => registry.generate(['/'], rootHostComponent))
|
||||
.toThrowError(`Link "${ListWrapper.toJSON(['/'])}" must include a route name.`);
|
||||
});
|
||||
|
||||
it('should prefer static segments to dynamic', inject([AsyncTestCompleter], (async) => {
|
||||
registry.config(rootHostComponent, {'path': '/:site', 'component': DummyCompB});
|
||||
registry.config(rootHostComponent, {'path': '/home', 'component': DummyCompA});
|
||||
|
@ -172,6 +235,11 @@ class DummyAsyncComp {
|
|||
class DummyCompA {}
|
||||
class DummyCompB {}
|
||||
|
||||
@RouteConfig([{'path': '/second', 'component': DummyCompB}])
|
||||
@RouteConfig([{'path': '/second', 'component': DummyCompB, 'as': 'secondCmp'}])
|
||||
class DummyParentComp {
|
||||
}
|
||||
|
||||
|
||||
@RouteConfig([{'path': '/second/:param', 'component': DummyCompB, 'as': 'secondCmp'}])
|
||||
class DummyParentParamComp {
|
||||
}
|
||||
|
|
|
@ -31,7 +31,7 @@ export function main() {
|
|||
|
||||
beforeEachBindings(() => [
|
||||
Pipeline,
|
||||
RouteRegistry,
|
||||
bind(RouteRegistry).toFactory(() => new RouteRegistry(AppCmp)),
|
||||
DirectiveResolver,
|
||||
bind(Location).toClass(SpyLocation),
|
||||
bind(Router)
|
||||
|
|
Loading…
Reference in New Issue