refactor(router): convert to typescript

Fixes #2001
This commit is contained in:
Jeff Cross 2015-05-29 14:58:41 -07:00
parent 4c8e11a577
commit ba07f39347
31 changed files with 900 additions and 977 deletions

View File

@ -14,7 +14,6 @@ export {RouteRegistry} from './src/router/route_registry';
export {BrowserLocation} from './src/router/browser_location'; export {BrowserLocation} from './src/router/browser_location';
export {Location} from './src/router/location'; export {Location} from './src/router/location';
export {Pipeline} from './src/router/pipeline'; export {Pipeline} from './src/router/pipeline';
export * from './src/router/route_config_annotation';
export * from './src/router/route_config_decorator'; export * from './src/router/route_config_decorator';
import {BrowserLocation} from './src/router/browser_location'; import {BrowserLocation} from './src/router/browser_location';
@ -27,18 +26,16 @@ import {Location} from './src/router/location';
import {appComponentTypeToken} from './src/core/application_tokens'; import {appComponentTypeToken} from './src/core/application_tokens';
import {bind} from './di'; import {bind} from './di';
import {CONST_EXPR} from './src/facade/lang'; import {CONST_EXPR} from './src/facade/lang';
import {List} from './src/facade/collection';
export const routerDirectives:List = CONST_EXPR([ export const routerDirectives: List<any> = CONST_EXPR([RouterOutlet, RouterLink]);
RouterOutlet,
RouterLink
]);
export var routerInjectables:List = [ export var routerInjectables: List<any> = [
RouteRegistry, RouteRegistry,
Pipeline, Pipeline,
BrowserLocation, BrowserLocation,
Location, Location,
bind(Router).toFactory((registry, pipeline, location, appRoot) => { bind(Router).toFactory((registry, pipeline, location, appRoot) =>
return new RootRouter(registry, pipeline, location, appRoot); { return new RootRouter(registry, pipeline, location, appRoot);},
}, [RouteRegistry, Pipeline, Location, appComponentTypeToken]) [RouteRegistry, Pipeline, Location, appComponentTypeToken])
]; ];

View File

@ -110,10 +110,10 @@ export class DomAdapter {
cssToRules(css: string): List<any> { throw _abstract(); } cssToRules(css: string): List<any> { throw _abstract(); }
supportsDOMEvents(): boolean { throw _abstract(); } supportsDOMEvents(): boolean { throw _abstract(); }
supportsNativeShadowDOM(): boolean { throw _abstract(); } supportsNativeShadowDOM(): boolean { throw _abstract(); }
getGlobalEventTarget(target: string) { throw _abstract(); } getGlobalEventTarget(target: string): any { throw _abstract(); }
getHistory() { throw _abstract(); } getHistory(): any { throw _abstract(); }
getLocation() { throw _abstract(); } getLocation(): any { throw _abstract(); }
getBaseHref() { throw _abstract(); } getBaseHref(): string { throw _abstract(); }
getUserAgent(): string { throw _abstract(); } getUserAgent(): string { throw _abstract(); }
setData(element, name: string, value: string) { throw _abstract(); } setData(element, name: string, value: string) { throw _abstract(); }
getData(element, name: string): string { throw _abstract(); } getData(element, name: string): string { throw _abstract(); }

View File

@ -15,7 +15,11 @@ export 'dart:html'
Node, Node,
MouseEvent, MouseEvent,
KeyboardEvent, KeyboardEvent,
Event; Event,
EventTarget,
History,
Location,
EventListener;
final _gc = context['gc']; final _gc = context['gc'];

View File

@ -10,3 +10,7 @@ export var gc = window['gc'] ? () => window['gc']() : () => null;
export const Event = Event; export const Event = Event;
export const MouseEvent = MouseEvent; export const MouseEvent = MouseEvent;
export const KeyboardEvent = KeyboardEvent; export const KeyboardEvent = KeyboardEvent;
export const EventTarget = EventTarget;
export const History = History;
export const Location = Location;
export const EventListener = EventListener;

View File

@ -9,10 +9,10 @@ import {Location} from 'angular2/src/router/location';
@proxy @proxy
@IMPLEMENTS(Location) @IMPLEMENTS(Location)
export class SpyLocation extends SpyObject { export class SpyLocation extends SpyObject {
urlChanges:List<string>; urlChanges: List<string>;
_path:string; _path: string;
_subject:EventEmitter; _subject: EventEmitter;
_baseHref:string; _baseHref: string;
constructor() { constructor() {
super(); super();
@ -22,29 +22,17 @@ export class SpyLocation extends SpyObject {
this._baseHref = ''; this._baseHref = '';
} }
setInitialPath(url:string) { setInitialPath(url: string) { this._path = url; }
this._path = url;
}
setBaseHref(url:string) { setBaseHref(url: string) { this._baseHref = url; }
this._baseHref = url;
}
path():string { path(): string { return this._path; }
return this._path;
}
simulateUrlPop(pathname:string) { simulateUrlPop(pathname: string) { ObservableWrapper.callNext(this._subject, {'url': pathname}); }
ObservableWrapper.callNext(this._subject, {
'url': pathname
});
}
normalizeAbsolutely(url) { normalizeAbsolutely(url) { return this._baseHref + url; }
return this._baseHref + url;
}
go(url:string) { go(url: string) {
url = this.normalizeAbsolutely(url); url = this.normalizeAbsolutely(url);
if (this._path == url) { if (this._path == url) {
return; return;
@ -65,5 +53,5 @@ export class SpyLocation extends SpyObject {
ObservableWrapper.subscribe(this._subject, onNext, onThrow, onReturn); ObservableWrapper.subscribe(this._subject, onNext, onThrow, onReturn);
} }
noSuchMethod(m){return super.noSuchMethod(m);} noSuchMethod(m) { return super.noSuchMethod(m); }
} }

View File

@ -1,37 +0,0 @@
import {DOM} from 'angular2/src/dom/dom_adapter';
export class BrowserLocation {
_location;
_history;
_baseHref:string;
constructor() {
this._location = DOM.getLocation();
this._history = DOM.getHistory();
this._baseHref = DOM.getBaseHref();
}
onPopState(fn: Function): void {
DOM.getGlobalEventTarget('window').addEventListener('popstate', fn, false);
}
getBaseHref(): string {
return this._baseHref;
}
path(): string {
return this._location.pathname;
}
pushState(state:any, title:string, url:string) {
this._history.pushState(state, title, url);
}
forward(): void {
this._history.forward();
}
back(): void {
this._history.back();
}
}

View File

@ -0,0 +1,30 @@
import {DOM} from 'angular2/src/dom/dom_adapter';
import {Injectable} from 'angular2/di';
import {EventListener, History, Location} from 'angular2/src/facade/browser';
@Injectable()
export class BrowserLocation {
private _location: Location;
private _history: History;
private _baseHref: string;
constructor() {
this._location = DOM.getLocation();
this._history = DOM.getHistory();
this._baseHref = DOM.getBaseHref();
}
onPopState(fn: EventListener): void {
DOM.getGlobalEventTarget('window').addEventListener('popstate', fn, false);
}
getBaseHref(): string { return this._baseHref; }
path(): string { return this._location.pathname; }
pushState(state: any, title: string, url: string) { this._history.pushState(state, title, url); }
forward(): void { this._history.forward(); }
back(): void { this._history.back(); }
}

View File

@ -1,37 +1,44 @@
import {Map, MapWrapper, StringMap, StringMapWrapper, List, ListWrapper} from 'angular2/src/facade/collection'; import {
Map,
MapWrapper,
StringMap,
StringMapWrapper,
List,
ListWrapper
} from 'angular2/src/facade/collection';
import {Promise, PromiseWrapper} from 'angular2/src/facade/async'; import {Promise, PromiseWrapper} from 'angular2/src/facade/async';
import {isPresent, normalizeBlank} from 'angular2/src/facade/lang'; import {isPresent, normalizeBlank} from 'angular2/src/facade/lang';
export class RouteParams { export class RouteParams {
params:StringMap<string, string>; constructor(public params: StringMap<string, string>) {}
constructor(params:StringMap) { get(param: string): string { return normalizeBlank(StringMapWrapper.get(this.params, param)); }
this.params = params;
}
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 * An `Instruction` represents the component hierarchy of the application based on a given route
*/ */
export class Instruction { export class Instruction {
component:any; component: any;
_children:Map<string, Instruction>; private _children: StringMap<string, Instruction>;
// the part of the URL captured by this instruction // the part of the URL captured by this instruction
capturedUrl:string; capturedUrl: string;
// the part of the URL captured by this instruction and all children // the part of the URL captured by this instruction and all children
accumulatedUrl:string; accumulatedUrl: string;
params:StringMap<string, string>; params: StringMap<string, string>;
reuse:boolean; reuse: boolean;
specificity:number; specificity: number;
constructor({params, component, children, matchedUrl, parentSpecificity}:{params:StringMap, component:any, children:Map, matchedUrl:string, parentSpecificity:number} = {}) { constructor({params, component, children, matchedUrl, parentSpecificity}: {
params?: StringMap<string, any>,
component?: any,
children?: StringMap<string, Instruction>,
matchedUrl?: string,
parentSpecificity?: number
} = {}) {
this.reuse = false; this.reuse = false;
this.capturedUrl = matchedUrl; this.capturedUrl = matchedUrl;
this.accumulatedUrl = matchedUrl; this.accumulatedUrl = matchedUrl;
@ -53,39 +60,38 @@ export class Instruction {
this.params = params; this.params = params;
} }
hasChild(outletName:string):boolean { hasChild(outletName: string): boolean {
return StringMapWrapper.contains(this._children, outletName); return StringMapWrapper.contains(this._children, outletName);
} }
/** /**
* Returns the child instruction with the given outlet name * Returns the child instruction with the given outlet name
*/ */
getChild(outletName:string):Instruction { getChild(outletName: string): Instruction {
return StringMapWrapper.get(this._children, outletName); return StringMapWrapper.get(this._children, outletName);
} }
/** /**
* (child:Instruction, outletName:string) => {} * (child:Instruction, outletName:string) => {}
*/ */
forEachChild(fn:Function): void { forEachChild(fn: Function): void { StringMapWrapper.forEach(this._children, fn); }
StringMapWrapper.forEach(this._children, fn);
}
/** /**
* Does a synchronous, breadth-first traversal of the graph of instructions. * Does a synchronous, breadth-first traversal of the graph of instructions.
* Takes a function with signature: * Takes a function with signature:
* (child:Instruction, outletName:string) => {} * (child:Instruction, outletName:string) => {}
*/ */
traverseSync(fn:Function): void { traverseSync(fn: Function): void {
this.forEachChild(fn); this.forEachChild(fn);
this.forEachChild((childInstruction, _) => childInstruction.traverseSync(fn)); this.forEachChild((childInstruction, _) => childInstruction.traverseSync(fn));
} }
/** /**
* Takes a currently active instruction and sets a reuse flag on each of this instruction's children * Takes a currently active instruction and sets a reuse flag on each of this instruction's
* children
*/ */
reuseComponentsFrom(oldInstruction:Instruction): void { reuseComponentsFrom(oldInstruction: Instruction): void {
this.traverseSync((childInstruction, outletName) => { this.traverseSync((childInstruction, outletName) => {
var oldInstructionChild = oldInstruction.getChild(outletName); var oldInstructionChild = oldInstruction.getChild(outletName);
if (shouldReuseComponent(childInstruction, oldInstructionChild)) { if (shouldReuseComponent(childInstruction, oldInstructionChild)) {
@ -95,16 +101,16 @@ export class Instruction {
} }
} }
function shouldReuseComponent(instr1:Instruction, instr2:Instruction): boolean { function shouldReuseComponent(instr1: Instruction, instr2: Instruction): boolean {
return instr1.component == instr2.component && return instr1.component == instr2.component &&
StringMapWrapper.equals(instr1.params, instr2.params); StringMapWrapper.equals(instr1.params, instr2.params);
} }
function mapObjAsync(obj:StringMap, fn): Promise { function mapObjAsync(obj: StringMap<string, any>, fn): Promise<List<any>> {
return PromiseWrapper.all(mapObj(obj, fn)); return PromiseWrapper.all(mapObj(obj, fn));
} }
function mapObj(obj:StringMap, fn: Function):List { function mapObj(obj: StringMap<any, any>, fn: Function): List<any> {
var result = ListWrapper.create(); var result = ListWrapper.create();
StringMapWrapper.forEach(obj, (value, key) => ListWrapper.push(result, fn(value, key))); StringMapWrapper.forEach(obj, (value, key) => ListWrapper.push(result, fn(value, key)));
return result; return result;

View File

@ -1,31 +1,24 @@
import {BrowserLocation} from './browser_location'; import {BrowserLocation} from './browser_location';
import {StringWrapper} from 'angular2/src/facade/lang'; import {StringWrapper} from 'angular2/src/facade/lang';
import {EventEmitter, ObservableWrapper} from 'angular2/src/facade/async'; import {EventEmitter, ObservableWrapper} from 'angular2/src/facade/async';
import {Injectable} from 'angular2/di';
@Injectable()
export class Location { export class Location {
_subject:EventEmitter; private _subject: EventEmitter;
_browserLocation:BrowserLocation; private _baseHref: string;
_baseHref:string;
constructor(browserLocation:BrowserLocation) { constructor(public _browserLocation: BrowserLocation) {
this._subject = new EventEmitter(); this._subject = new EventEmitter();
this._browserLocation = browserLocation;
this._baseHref = stripIndexHtml(this._browserLocation.getBaseHref()); this._baseHref = stripIndexHtml(this._browserLocation.getBaseHref());
this._browserLocation.onPopState((_) => this._onPopState(_)); this._browserLocation.onPopState((_) => this._onPopState(_));
} }
_onPopState(_): void { _onPopState(_): void { ObservableWrapper.callNext(this._subject, {'url': this.path()}); }
ObservableWrapper.callNext(this._subject, {
'url': this.path()
});
}
path(): string { path(): string { return this.normalize(this._browserLocation.path()); }
return this.normalize(this._browserLocation.path());
}
normalize(url: string): string { normalize(url: string): string { return this._stripBaseHref(stripIndexHtml(url)); }
return this._stripBaseHref(stripIndexHtml(url));
}
normalizeAbsolutely(url: string): string { normalizeAbsolutely(url: string): string {
if (url[0] != '/') { if (url[0] != '/') {
@ -48,18 +41,14 @@ export class Location {
return url; return url;
} }
go(url:string): void { go(url: string): void {
var finalUrl = this.normalizeAbsolutely(url); var finalUrl = this.normalizeAbsolutely(url);
this._browserLocation.pushState(null, '', finalUrl); this._browserLocation.pushState(null, '', finalUrl);
} }
forward(): void { forward(): void { this._browserLocation.forward(); }
this._browserLocation.forward();
}
back(): void { back(): void { this._browserLocation.back(); }
this._browserLocation.back();
}
subscribe(onNext, onThrow = null, onReturn = null): void { subscribe(onNext, onThrow = null, onReturn = null): void {
ObservableWrapper.subscribe(this._subject, onNext, onThrow, onReturn); ObservableWrapper.subscribe(this._subject, onNext, onThrow, onReturn);

View File

@ -1,35 +1,54 @@
import {RegExp, RegExpWrapper, RegExpMatcherWrapper, StringWrapper, isPresent, isBlank, BaseException, normalizeBlank} from 'angular2/src/facade/lang'; import {
import {Map, MapWrapper, StringMap, StringMapWrapper, List, ListWrapper} from 'angular2/src/facade/collection'; RegExp,
RegExpWrapper,
RegExpMatcherWrapper,
StringWrapper,
isPresent,
isBlank,
BaseException,
normalizeBlank
} from 'angular2/src/facade/lang';
import {
Map,
MapWrapper,
StringMap,
StringMapWrapper,
List,
ListWrapper
} from 'angular2/src/facade/collection';
import {IMPLEMENTS} from 'angular2/src/facade/lang';
import {escapeRegex} from './url'; import {escapeRegex} from './url';
class StaticSegment { // TODO(jeffbcross): implement as interface when ts2dart adds support:
string:string; // https://github.com/angular/ts2dart/issues/173
regex:string; export class Segment {
name:string; name: string;
regex: string;
}
constructor(string:string) { class StaticSegment extends Segment {
this.string = string; regex: string;
name: string;
constructor(public string: string) {
super();
this.name = ''; this.name = '';
this.regex = escapeRegex(string); this.regex = escapeRegex(string);
} }
generate(params): string { generate(params): string { return this.string; }
return this.string;
}
} }
@IMPLEMENTS(Segment)
class DynamicSegment { class DynamicSegment {
name:string; regex: string;
regex:string; constructor(public name: string) { this.regex = "([^/]+)"; }
constructor(name:string) {
this.name = name;
this.regex = "([^/]+)";
}
generate(params:StringMap<string, string>): string { generate(params: StringMap<string, string>): string {
if (!StringMapWrapper.contains(params, this.name)) { if (!StringMapWrapper.contains(params, this.name)) {
throw new BaseException(`Route generator for '${this.name}' was not included in parameters passed.`) throw new BaseException(
`Route generator for '${this.name}' was not included in parameters passed.`)
} }
return normalizeBlank(StringMapWrapper.get(params, this.name)); return normalizeBlank(StringMapWrapper.get(params, this.name));
} }
@ -37,14 +56,10 @@ class DynamicSegment {
class StarSegment { class StarSegment {
name:string; regex: string;
regex:string; constructor(public name: string) { this.regex = "(.+)"; }
constructor(name:string) {
this.name = name;
this.regex = "(.+)";
}
generate(params:StringMap<string, string>): string { generate(params: StringMap<string, string>): string {
return normalizeBlank(StringMapWrapper.get(params, this.name)); return normalizeBlank(StringMapWrapper.get(params, this.name));
} }
} }
@ -53,7 +68,7 @@ class StarSegment {
var paramMatcher = RegExpWrapper.create("^:([^\/]+)$"); var paramMatcher = RegExpWrapper.create("^:([^\/]+)$");
var wildcardMatcher = RegExpWrapper.create("^\\*([^\/]+)$"); var wildcardMatcher = RegExpWrapper.create("^\\*([^\/]+)$");
function parsePathString(route:string) { function parsePathString(route: string) {
// normalize route as not starting with a "/". Recognition will // normalize route as not starting with a "/". Recognition will
// also normalize. // also normalize.
if (route[0] === "/") { if (route[0] === "/") {
@ -64,19 +79,22 @@ function parsePathString(route:string) {
var results = ListWrapper.create(); var results = ListWrapper.create();
var specificity = 0; var specificity = 0;
// The "specificity" of a path is used to determine which route is used when multiple routes match a URL. // The "specificity" of a path is used to determine which route is used when multiple routes match
// Static segments (like "/foo") are the most specific, followed by dynamic segments (like "/:id"). Star segments // a URL.
// Static segments (like "/foo") are the most specific, followed by dynamic segments (like
// "/:id"). Star segments
// add no specificity. Segments at the start of the path are more specific than proceeding ones. // add no specificity. Segments at the start of the path are more specific than proceeding ones.
// The code below uses place values to combine the different types of segments into a single integer that we can // The code below uses place values to combine the different types of segments into a single
// sort later. Each static segment is worth hundreds of points of specificity (10000, 9900, ..., 200), and each // integer that we can
// sort later. Each static segment is worth hundreds of points of specificity (10000, 9900, ...,
// 200), and each
// dynamic segment is worth single points of specificity (100, 99, ... 2). // dynamic segment is worth single points of specificity (100, 99, ... 2).
if (segments.length > 98) { if (segments.length > 98) {
throw new BaseException(`'${route}' has more than the maximum supported number of segments.`); throw new BaseException(`'${route}' has more than the maximum supported number of segments.`);
} }
for (var i=0; i<segments.length; i++) { for (var i = 0; i < segments.length; i++) {
var segment = segments[i], var segment = segments[i], match;
match;
if (isPresent(match = RegExpWrapper.firstMatch(paramMatcher, segment))) { if (isPresent(match = RegExpWrapper.firstMatch(paramMatcher, segment))) {
ListWrapper.push(results, new DynamicSegment(match[1])); ListWrapper.push(results, new DynamicSegment(match[1]));
@ -92,22 +110,18 @@ function parsePathString(route:string) {
return {segments: results, specificity}; return {segments: results, specificity};
} }
function splitBySlash (url:string):List<string> { function splitBySlash(url: string): List<string> {
return url.split('/'); return url.split('/');
} }
// represents something like '/foo/:bar' // represents something like '/foo/:bar'
export class PathRecognizer { export class PathRecognizer {
segments:List; segments: List<Segment>;
regex:RegExp; regex: RegExp;
handler:any; specificity: number;
specificity:number;
path:string;
constructor(path:string, handler:any) { constructor(public path: string, public handler: any) {
this.path = path;
this.handler = handler;
this.segments = []; this.segments = [];
// TODO: use destructuring assignment // TODO: use destructuring assignment
@ -117,19 +131,17 @@ export class PathRecognizer {
var segments = parsed['segments']; var segments = parsed['segments'];
var regexString = '^'; var regexString = '^';
ListWrapper.forEach(segments, (segment) => { ListWrapper.forEach(segments, (segment) => { regexString += '/' + segment.regex; });
regexString += '/' + segment.regex;
});
this.regex = RegExpWrapper.create(regexString); this.regex = RegExpWrapper.create(regexString);
this.segments = segments; this.segments = segments;
this.specificity = specificity; this.specificity = specificity;
} }
parseParams(url:string):StringMap<string, string> { parseParams(url: string): StringMap<string, string> {
var params = StringMapWrapper.create(); var params = StringMapWrapper.create();
var urlPart = url; var urlPart = url;
for(var i=0; i<this.segments.length; i++) { for (var i = 0; i < this.segments.length; i++) {
var segment = this.segments[i]; var segment = this.segments[i];
var match = RegExpWrapper.firstMatch(RegExpWrapper.create('/' + segment.regex), urlPart); var match = RegExpWrapper.firstMatch(RegExpWrapper.create('/' + segment.regex), urlPart);
urlPart = StringWrapper.substring(urlPart, match[0].length); urlPart = StringWrapper.substring(urlPart, match[0].length);
@ -141,8 +153,8 @@ export class PathRecognizer {
return params; return params;
} }
generate(params:StringMap<string, string>):string { generate(params: StringMap<string, string>): string {
return ListWrapper.join(ListWrapper.map(this.segments, (segment) => return ListWrapper.join(
'/' + segment.generate(params)), ''); ListWrapper.map(this.segments, (segment) => '/' + segment.generate(params)), '');
} }
} }

View File

@ -7,19 +7,14 @@ import {Instruction} from './instruction';
* "Steps" are conceptually similar to "middleware" * "Steps" are conceptually similar to "middleware"
*/ */
export class Pipeline { export class Pipeline {
steps:List<Function>; steps: List<Function>;
constructor() { constructor() { this.steps = [instruction => instruction.router.activateOutlets(instruction)]; }
this.steps = [
instruction => instruction.router.activateOutlets(instruction)
];
}
process(instruction:Instruction):Promise { process(instruction: Instruction): Promise<any> {
var steps = this.steps, var steps = this.steps, currentStep = 0;
currentStep = 0;
function processOne(result:any = true):Promise { function processOne(result: any = true): Promise<any> {
if (currentStep >= steps.length) { if (currentStep >= steps.length) {
return PromiseWrapper.resolve(result); return PromiseWrapper.resolve(result);
} }

View File

@ -1,3 +0,0 @@
library angular2.router.route_config_annotations;
export './route_config_impl.dart';

View File

@ -1 +0,0 @@
export {RouteConfig as RouteConfigAnnotation} from './route_config_impl';

View File

@ -1,3 +1,3 @@
library angular2.router.route_config_decorator; library angular2.router.route_config_decorator;
/** This file is intentionally empty, as Dart does not have decorators */ export './route_config_impl.dart';

View File

@ -2,4 +2,3 @@ import {RouteConfig as RouteConfigAnnotation} from './route_config_impl';
import {makeDecorator} from 'angular2/src/util/decorators'; import {makeDecorator} from 'angular2/src/util/decorators';
export var RouteConfig = makeDecorator(RouteConfigAnnotation); export var RouteConfig = makeDecorator(RouteConfigAnnotation);

View File

@ -9,11 +9,7 @@ import {List, Map} from 'angular2/src/facade/collection';
* - `component`, `components`, `redirectTo` (requires exactly one of these) * - `component`, `components`, `redirectTo` (requires exactly one of these)
* - `as` (optional) * - `as` (optional)
*/ */
@CONST()
export class RouteConfig { export class RouteConfig {
configs:List<Map>; constructor(public configs: List<Map<any, any>>) {}
@CONST()
constructor(configs:List<Map>) {
this.configs = configs;
}
} }

View File

@ -1,16 +1,30 @@
import {RegExp, RegExpWrapper, StringWrapper, isPresent, BaseException} from 'angular2/src/facade/lang'; import {
import {Map, MapWrapper, List, ListWrapper, StringMap, StringMapWrapper} from 'angular2/src/facade/collection'; RegExp,
RegExpWrapper,
StringWrapper,
isPresent,
BaseException
} from 'angular2/src/facade/lang';
import {
Map,
MapWrapper,
List,
ListWrapper,
StringMap,
StringMapWrapper
} from 'angular2/src/facade/collection';
import {PathRecognizer} from './path_recognizer'; import {PathRecognizer} from './path_recognizer';
/** /**
* `RouteRecognizer` is responsible for recognizing routes for a single component. * `RouteRecognizer` is responsible for recognizing routes for a single component.
* It is consumed by `RouteRegistry`, which knows how to recognize an entire hierarchy of components. * It is consumed by `RouteRegistry`, which knows how to recognize an entire hierarchy of
* components.
*/ */
export class RouteRecognizer { export class RouteRecognizer {
names:Map<string, PathRecognizer>; names: Map<string, PathRecognizer>;
redirects:Map<string, string>; redirects: Map<string, string>;
matchers:Map<RegExp, PathRecognizer>; matchers: Map<RegExp, PathRecognizer>;
constructor() { constructor() {
this.names = MapWrapper.create(); this.names = MapWrapper.create();
@ -18,15 +32,14 @@ export class RouteRecognizer {
this.redirects = MapWrapper.create(); this.redirects = MapWrapper.create();
} }
addRedirect(path:string, target:string): void { addRedirect(path: string, target: string): void { MapWrapper.set(this.redirects, path, target); }
MapWrapper.set(this.redirects, path, target);
}
addConfig(path:string, handler:any, alias:string = null): void { addConfig(path: string, handler: any, alias: string = null): void {
var recognizer = new PathRecognizer(path, handler); var recognizer = new PathRecognizer(path, handler);
MapWrapper.forEach(this.matchers, (matcher, _) => { MapWrapper.forEach(this.matchers, (matcher, _) => {
if (recognizer.regex.toString() == matcher.regex.toString()) { if (recognizer.regex.toString() == matcher.regex.toString()) {
throw new BaseException(`Configuration '${path}' conflicts with existing route '${matcher.path}'`); throw new BaseException(
`Configuration '${path}' conflicts with existing route '${matcher.path}'`);
} }
}); });
MapWrapper.set(this.matchers, recognizer.regex, recognizer); MapWrapper.set(this.matchers, recognizer.regex, recognizer);
@ -40,11 +53,11 @@ export class RouteRecognizer {
* Given a URL, returns a list of `RouteMatch`es, which are partial recognitions for some route. * Given a URL, returns a list of `RouteMatch`es, which are partial recognitions for some route.
* *
*/ */
recognize(url:string):List<RouteMatch> { recognize(url: string): List<RouteMatch> {
var solutions = ListWrapper.create(); var solutions = ListWrapper.create();
MapWrapper.forEach(this.redirects, (target, path) => { MapWrapper.forEach(this.redirects, (target, path) => {
//TODO: "/" redirect case // TODO: "/" redirect case
if (StringWrapper.startsWith(url, path)) { if (StringWrapper.startsWith(url, path)) {
url = target + StringWrapper.substring(url, path.length); url = target + StringWrapper.substring(url, path.length);
} }
@ -53,7 +66,7 @@ export class RouteRecognizer {
MapWrapper.forEach(this.matchers, (pathRecognizer, regex) => { MapWrapper.forEach(this.matchers, (pathRecognizer, regex) => {
var match; var match;
if (isPresent(match = RegExpWrapper.firstMatch(regex, url))) { if (isPresent(match = RegExpWrapper.firstMatch(regex, url))) {
//TODO(btford): determine a good generic way to deal with terminal matches // TODO(btford): determine a good generic way to deal with terminal matches
var matchedUrl = '/'; var matchedUrl = '/';
var unmatchedUrl = ''; var unmatchedUrl = '';
if (url != '/') { if (url != '/') {
@ -61,37 +74,39 @@ export class RouteRecognizer {
unmatchedUrl = StringWrapper.substring(url, match[0].length); unmatchedUrl = StringWrapper.substring(url, match[0].length);
} }
ListWrapper.push(solutions, new RouteMatch({ ListWrapper.push(solutions, new RouteMatch({
specificity: pathRecognizer.specificity, specificity: pathRecognizer.specificity,
handler: pathRecognizer.handler, handler: pathRecognizer.handler,
params: pathRecognizer.parseParams(url), params: pathRecognizer.parseParams(url),
matchedUrl: matchedUrl, matchedUrl: matchedUrl,
unmatchedUrl: unmatchedUrl unmatchedUrl: unmatchedUrl
})); }));
} }
}); });
return solutions; return solutions;
} }
hasRoute(name:string): boolean { hasRoute(name: string): boolean { return MapWrapper.contains(this.names, name); }
return MapWrapper.contains(this.names, name);
}
generate(name:string, params:any): string { generate(name: string, params: any): string {
var pathRecognizer = MapWrapper.get(this.names, name); var pathRecognizer = MapWrapper.get(this.names, name);
return isPresent(pathRecognizer) ? pathRecognizer.generate(params) : null; return isPresent(pathRecognizer) ? pathRecognizer.generate(params) : null;
} }
} }
export class RouteMatch { export class RouteMatch {
specificity:number; specificity: number;
handler:StringMap<string, any>; handler: StringMap<string, any>;
params:StringMap<string, string>; params: StringMap<string, string>;
matchedUrl:string; matchedUrl: string;
unmatchedUrl:string; unmatchedUrl: string;
constructor({specificity, handler, params, matchedUrl, unmatchedUrl}: constructor({specificity, handler, params, matchedUrl, unmatchedUrl}: {
{specificity:number, handler:StringMap, params:StringMap, matchedUrl:string, unmatchedUrl:string} = {}) { specificity?: number,
handler?: StringMap<string, any>,
params?: StringMap<string, string>,
matchedUrl?: string,
unmatchedUrl?: string
} = {}) {
this.specificity = specificity; this.specificity = specificity;
this.handler = handler; this.handler = handler;
this.params = params; this.params = params;

View File

@ -1,25 +1,31 @@
import {RouteRecognizer, RouteMatch} from './route_recognizer'; import {RouteRecognizer, RouteMatch} from './route_recognizer';
import {Instruction, noopInstruction} from './instruction'; import {Instruction} from './instruction';
import {List, ListWrapper, Map, MapWrapper, StringMap, StringMapWrapper} from 'angular2/src/facade/collection'; import {
List,
ListWrapper,
Map,
MapWrapper,
StringMap,
StringMapWrapper
} from 'angular2/src/facade/collection';
import {isPresent, isBlank, isType, StringWrapper, BaseException} from 'angular2/src/facade/lang'; import {isPresent, isBlank, isType, StringWrapper, BaseException} from 'angular2/src/facade/lang';
import {RouteConfig} from './route_config_impl'; import {RouteConfig} from './route_config_impl';
import {reflector} from 'angular2/src/reflection/reflection'; import {reflector} from 'angular2/src/reflection/reflection';
/** /**
* The RouteRegistry holds route configurations for each component in an Angular app. * The RouteRegistry holds route configurations for each component in an Angular app.
* It is responsible for creating Instructions from URLs, and generating URLs based on route and parameters. * It is responsible for creating Instructions from URLs, and generating URLs based on route and
* parameters.
*/ */
export class RouteRegistry { export class RouteRegistry {
_rules:Map<any, RouteRecognizer>; _rules: Map<any, RouteRecognizer>;
constructor() { constructor() { this._rules = MapWrapper.create(); }
this._rules = MapWrapper.create();
}
/** /**
* Given a component and a configuration object, add the route to this registry * Given a component and a configuration object, add the route to this registry
*/ */
config(parentComponent, config:StringMap<string, any>): void { config(parentComponent, config: StringMap<string, any>): void {
if (!StringMapWrapper.contains(config, 'path')) { if (!StringMapWrapper.contains(config, 'path')) {
throw new BaseException('Route config does not contain "path"'); throw new BaseException('Route config does not contain "path"');
} }
@ -27,10 +33,11 @@ export class RouteRegistry {
if (!StringMapWrapper.contains(config, 'component') && if (!StringMapWrapper.contains(config, 'component') &&
!StringMapWrapper.contains(config, 'components') && !StringMapWrapper.contains(config, 'components') &&
!StringMapWrapper.contains(config, 'redirectTo')) { !StringMapWrapper.contains(config, 'redirectTo')) {
throw new BaseException('Route config does not contain "component," "components," or "redirectTo"'); throw new BaseException(
'Route config does not contain "component," "components," or "redirectTo"');
} }
var recognizer:RouteRecognizer = MapWrapper.get(this._rules, parentComponent); var recognizer: RouteRecognizer = MapWrapper.get(this._rules, parentComponent);
if (isBlank(recognizer)) { if (isBlank(recognizer)) {
recognizer = new RouteRecognizer(); recognizer = new RouteRecognizer();
@ -65,7 +72,7 @@ export class RouteRegistry {
} }
var annotations = reflector.annotations(component); var annotations = reflector.annotations(component);
if (isPresent(annotations)) { if (isPresent(annotations)) {
for (var i=0; i<annotations.length; i++) { for (var i = 0; i < annotations.length; i++) {
var annotation = annotations[i]; var annotation = annotations[i];
if (annotation instanceof RouteConfig) { if (annotation instanceof RouteConfig) {
@ -80,7 +87,7 @@ export class RouteRegistry {
* Given a URL and a parent component, return the most specific instruction for navigating * Given a URL and a parent component, return the most specific instruction for navigating
* the application into the state specified by the * the application into the state specified by the
*/ */
recognize(url:string, parentComponent): Instruction { recognize(url: string, parentComponent): Instruction {
var componentRecognizer = MapWrapper.get(this._rules, parentComponent); var componentRecognizer = MapWrapper.get(this._rules, parentComponent);
if (isBlank(componentRecognizer)) { if (isBlank(componentRecognizer)) {
return null; return null;
@ -93,16 +100,15 @@ export class RouteRegistry {
var fullSolutions = ListWrapper.create(); var fullSolutions = ListWrapper.create();
for (var i = 0; i < possibleMatches.length; i++) { for (var i = 0; i < possibleMatches.length; i++) {
var candidate : RouteMatch = possibleMatches[i]; var candidate: RouteMatch = possibleMatches[i];
// if the candidate captures all of the URL, add it to our list of solutions // if the candidate captures all of the URL, add it to our list of solutions
if (candidate.unmatchedUrl.length == 0) { if (candidate.unmatchedUrl.length == 0) {
ListWrapper.push(fullSolutions, routeMatchToInstruction(candidate, parentComponent)); ListWrapper.push(fullSolutions, routeMatchToInstruction(candidate, parentComponent));
} else { } else {
// otherwise, recursively match the remaining part of the URL against the component's
// otherwise, recursively match the remaining part of the URL against the component's children // children
var children = StringMapWrapper.create(), var children = StringMapWrapper.create(), allChildrenMatch = true,
allChildrenMatch = true,
components = StringMapWrapper.get(candidate.handler, 'components'); components = StringMapWrapper.get(candidate.handler, 'components');
var componentNames = StringMapWrapper.keys(components); var componentNames = StringMapWrapper.keys(components);
@ -122,11 +128,11 @@ export class RouteRegistry {
if (allChildrenMatch) { if (allChildrenMatch) {
ListWrapper.push(fullSolutions, new Instruction({ ListWrapper.push(fullSolutions, new Instruction({
component: parentComponent, component: parentComponent,
children: children, children: children,
matchedUrl: candidate.matchedUrl, matchedUrl: candidate.matchedUrl,
parentSpecificity: candidate.specificity parentSpecificity: candidate.specificity
})); }));
} }
} }
} }
@ -146,22 +152,19 @@ export class RouteRegistry {
return null; return null;
} }
generate(name:string, params:StringMap<string, string>, hostComponent): string { generate(name: string, params: StringMap<string, string>, hostComponent): string {
//TODO: implement for hierarchical routes // TODO: implement for hierarchical routes
var componentRecognizer = MapWrapper.get(this._rules, hostComponent); var componentRecognizer = MapWrapper.get(this._rules, hostComponent);
return isPresent(componentRecognizer) ? componentRecognizer.generate(name, params) : null; return isPresent(componentRecognizer) ? componentRecognizer.generate(name, params) : null;
} }
} }
function routeMatchToInstruction(routeMatch:RouteMatch, parentComponent): Instruction { function routeMatchToInstruction(routeMatch: RouteMatch, parentComponent): Instruction {
var children = StringMapWrapper.create(); var children = StringMapWrapper.create();
var components = StringMapWrapper.get(routeMatch.handler, 'components'); var components = StringMapWrapper.get(routeMatch.handler, 'components');
StringMapWrapper.forEach(components, (component, outletName) => { StringMapWrapper.forEach(components, (component, outletName) => {
children[outletName] = new Instruction({ children[outletName] =
component: component, new Instruction({component: component, params: routeMatch.params, parentSpecificity: 0});
params: routeMatch.params,
parentSpecificity: 0
});
}); });
return new Instruction({ return new Instruction({
component: parentComponent, component: parentComponent,
@ -181,15 +184,11 @@ function routeMatchToInstruction(routeMatch:RouteMatch, parentComponent): Instru
* If the config object does not contain a `component` key, the original * If the config object does not contain a `component` key, the original
* config object is returned. * config object is returned.
*/ */
function normalizeConfig(config:StringMap<string, any>): StringMap<string, any> { function normalizeConfig(config: StringMap<string, any>): StringMap<string, any> {
if (!StringMapWrapper.contains(config, 'component')) { if (!StringMapWrapper.contains(config, 'component')) {
return config; return config;
} }
var newConfig = { var newConfig = {'components': {'default': config['component']}};
'components': {
'default': config['component']
}
};
StringMapWrapper.forEach(config, (value, key) => { StringMapWrapper.forEach(config, (value, key) => {
if (key != 'component' && key != 'components') { if (key != 'component' && key != 'components') {

View File

@ -21,51 +21,44 @@ import {Location} from './location';
* The router holds reference to a number of "outlets." An outlet is a placeholder that the * The router holds reference to a number of "outlets." An outlet is a placeholder that the
* router dynamically fills in depending on the current URL. * router dynamically fills in depending on the current URL.
* *
* When the router navigates from a URL, it must first recognizes it and serialize it into an `Instruction`. * When the router navigates from a URL, it must first recognizes it and serialize it into an
* `Instruction`.
* The router uses the `RouteRegistry` to get an `Instruction`. * The router uses the `RouteRegistry` to get an `Instruction`.
* *
* @exportedAs angular2/router * @exportedAs angular2/router
*/ */
export class Router { export class Router {
hostComponent:any; navigating: boolean;
parent:Router;
navigating:boolean;
lastNavigationAttempt: string; lastNavigationAttempt: string;
previousUrl:string; previousUrl: string;
_currentInstruction:Instruction; private _currentInstruction: Instruction;
private _outlets: Map<any, RouterOutlet>;
_pipeline:Pipeline; private _subject: EventEmitter;
_registry:RouteRegistry; // todo(jeffbcross): rename _registry to registry since it is accessed from subclasses
_outlets:Map<any, RouterOutlet>; // todo(jeffbcross): rename _pipeline to pipeline since it is accessed from subclasses
_subject:EventEmitter; constructor(public _registry: RouteRegistry, public _pipeline: Pipeline, public parent: Router,
public hostComponent: any) {
constructor(registry:RouteRegistry, pipeline:Pipeline, parent:Router, hostComponent:any) {
this.hostComponent = hostComponent;
this.navigating = false; this.navigating = false;
this.parent = parent;
this.previousUrl = null; this.previousUrl = null;
this._outlets = MapWrapper.create(); this._outlets = MapWrapper.create();
this._registry = registry;
this._pipeline = pipeline;
this._subject = new EventEmitter(); this._subject = new EventEmitter();
this._currentInstruction = null; this._currentInstruction = null;
} }
/** /**
* Constructs a child router. You probably don't need to use this unless you're writing a reusable component. * Constructs a child router. You probably don't need to use this unless you're writing a reusable
* component.
*/ */
childRouter(hostComponent:any): Router { childRouter(hostComponent: any): Router { return new ChildRouter(this, hostComponent); }
return new ChildRouter(this, hostComponent);
}
/** /**
* Register an object to notify of route changes. You probably don't need to use this unless you're writing a reusable component. * Register an object to notify of route changes. You probably don't need to use this unless
* you're writing a reusable component.
*/ */
registerOutlet(outlet:RouterOutlet, name: string = 'default'): Promise { registerOutlet(outlet: RouterOutlet, name: string = 'default'): Promise<boolean> {
MapWrapper.set(this._outlets, name, outlet); MapWrapper.set(this._outlets, name, outlet);
if (isPresent(this._currentInstruction)) { if (isPresent(this._currentInstruction)) {
var childInstruction = this._currentInstruction.getChild(name); var childInstruction = this._currentInstruction.getChild(name);
@ -94,11 +87,10 @@ export class Router {
* ``` * ```
* *
*/ */
config(config:any): Promise { config(config: any): Promise<any> {
if (config instanceof List) { if (config instanceof List) {
config.forEach((configObject) => { config.forEach(
this._registry.config(this.hostComponent, configObject); (configObject) => { this._registry.config(this.hostComponent, configObject); });
});
} else { } else {
this._registry.config(this.hostComponent, config); this._registry.config(this.hostComponent, config);
} }
@ -112,7 +104,7 @@ export class Router {
* If the given URL begins with a `/`, router will navigate absolutely. * If the given URL begins with a `/`, router will navigate absolutely.
* If the given URL does not begin with `/`, the router will navigate relative to this component. * If the given URL does not begin with `/`, the router will navigate relative to this component.
*/ */
navigate(url:string):Promise { navigate(url: string): Promise<any> {
if (this.navigating) { if (this.navigating) {
return PromiseWrapper.resolve(true); return PromiseWrapper.resolve(true);
} }
@ -132,37 +124,31 @@ export class Router {
this._startNavigating(); this._startNavigating();
var result = this.commit(matchedInstruction) var result = this.commit(matchedInstruction)
.then((_) => { .then((_) => {
ObservableWrapper.callNext(this._subject, matchedInstruction.accumulatedUrl); ObservableWrapper.callNext(this._subject, matchedInstruction.accumulatedUrl);
this._finishNavigating(); this._finishNavigating();
}); });
PromiseWrapper.catchError(result, (_) => this._finishNavigating()); PromiseWrapper.catchError(result, (_) => this._finishNavigating());
return result; return result;
} }
_startNavigating(): void { _startNavigating(): void { this.navigating = true; }
this.navigating = true;
}
_finishNavigating(): void { _finishNavigating(): void { this.navigating = false; }
this.navigating = false;
}
/** /**
* Subscribe to URL updates from the router * Subscribe to URL updates from the router
*/ */
subscribe(onNext): void { subscribe(onNext): void { ObservableWrapper.subscribe(this._subject, onNext); }
ObservableWrapper.subscribe(this._subject, onNext);
}
/** /**
* *
*/ */
commit(instruction:Instruction):Promise { commit(instruction: Instruction): Promise<List<any>> {
this._currentInstruction = instruction; this._currentInstruction = instruction;
// collect all outlets that do not have a corresponding child instruction // collect all outlets that do not have a corresponding child instruction
@ -184,37 +170,32 @@ export class Router {
* Recursively remove all components contained by this router's outlets. * Recursively remove all components contained by this router's outlets.
* Calls deactivate hooks on all descendant components * Calls deactivate hooks on all descendant components
*/ */
deactivate():Promise { deactivate(): Promise<any> { return this._eachOutletAsync((outlet) => outlet.deactivate); }
return this._eachOutletAsync((outlet) => outlet.deactivate);
}
/** /**
* Recursively activate. * Recursively activate.
* Calls the "activate" hook on descendant components. * Calls the "activate" hook on descendant components.
*/ */
activate(instruction:Instruction):Promise { activate(instruction: Instruction): Promise<any> {
return this._eachOutletAsync((outlet, name) => outlet.activate(instruction.getChild(name))); return this._eachOutletAsync((outlet, name) => outlet.activate(instruction.getChild(name)));
} }
_eachOutletAsync(fn):Promise { _eachOutletAsync(fn): Promise<any> { return mapObjAsync(this._outlets, fn); }
return mapObjAsync(this._outlets, fn);
}
/** /**
* Given a URL, returns an instruction representing the component graph * Given a URL, returns an instruction representing the component graph
*/ */
recognize(url:string): Instruction { recognize(url: string): Instruction { return this._registry.recognize(url, this.hostComponent); }
return this._registry.recognize(url, this.hostComponent);
}
/** /**
* Navigates to either the last URL successfully navigated to, or the last URL requested if the router has yet to successfully navigate. * Navigates to either the last URL successfully navigated to, or the last URL requested if the
* router has yet to successfully navigate.
*/ */
renavigate():Promise { renavigate(): Promise<any> {
var destination = isBlank(this.previousUrl) ? this.lastNavigationAttempt : this.previousUrl; var destination = isBlank(this.previousUrl) ? this.lastNavigationAttempt : this.previousUrl;
if (this.navigating || isBlank(destination)) { if (this.navigating || isBlank(destination)) {
return PromiseWrapper.resolve(false); return PromiseWrapper.resolve(false);
@ -224,17 +205,19 @@ 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 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 { generate(name: string, params: StringMap<string, string>): string {
return this._registry.generate(name, params, this.hostComponent); return this._registry.generate(name, params, this.hostComponent);
} }
} }
export class RootRouter extends Router { export class RootRouter extends Router {
_location:Location; _location: Location;
constructor(registry:RouteRegistry, pipeline:Pipeline, location:Location, hostComponent:Type) { constructor(registry: RouteRegistry, pipeline: Pipeline, location: Location,
hostComponent: Type) {
super(registry, pipeline, null, hostComponent); super(registry, pipeline, null, hostComponent);
this._location = location; this._location = location;
this._location.subscribe((change) => this.navigate(change['url'])); this._location.subscribe((change) => this.navigate(change['url']));
@ -242,25 +225,24 @@ export class RootRouter extends Router {
this.navigate(location.path()); this.navigate(location.path());
} }
commit(instruction):Promise { commit(instruction): Promise<any> {
return super.commit(instruction).then((_) => { return super.commit(instruction)
this._location.go(instruction.accumulatedUrl); .then((_) => { this._location.go(instruction.accumulatedUrl); });
});
} }
} }
class ChildRouter extends Router { class ChildRouter extends Router {
constructor(parent:Router, hostComponent) { constructor(parent: Router, hostComponent) {
super(parent._registry, parent._pipeline, parent, hostComponent); super(parent._registry, parent._pipeline, parent, hostComponent);
this.parent = parent; this.parent = parent;
} }
} }
function mapObjAsync(obj:Map, fn): Promise { function mapObjAsync(obj: Map<any, any>, fn: Function): Promise<any> {
return PromiseWrapper.all(mapObj(obj, fn)); return PromiseWrapper.all(mapObj(obj, fn));
} }
function mapObj(obj:Map, fn):List { function mapObj(obj: Map<any, any>, fn: Function): List<any> {
var result = ListWrapper.create(); var result = ListWrapper.create();
MapWrapper.forEach(obj, (value, key) => ListWrapper.push(result, fn(value, key))); MapWrapper.forEach(obj, (value, key) => ListWrapper.push(result, fn(value, key)));
return result; return result;

View File

@ -1,4 +1,5 @@
import {Directive, onAllChangesDone} from 'angular2/src/core/annotations_impl/annotations'; import {onAllChangesDone} from 'angular2/src/core/annotations/annotations';
import {Directive} from 'angular2/src/core/annotations/decorators';
import {ElementRef} from 'angular2/core'; import {ElementRef} from 'angular2/core';
import {StringMap, StringMapWrapper} from 'angular2/src/facade/collection'; import {StringMap, StringMapWrapper} from 'angular2/src/facade/collection';
@ -31,27 +32,21 @@ import {Location} from './location';
*/ */
@Directive({ @Directive({
selector: '[router-link]', selector: '[router-link]',
properties: [ properties: ['route: routerLink', 'params: routerParams'],
'route: routerLink',
'params: routerParams'
],
lifecycle: [onAllChangesDone] lifecycle: [onAllChangesDone]
}) })
export class RouterLink { export class RouterLink {
_domEl; private _domEl;
_route:string; private _route: string;
_params:StringMap<string, string>; private _params: StringMap<string, string>;
_router:Router;
_location:Location;
// the url displayed on the anchor element. // the url displayed on the anchor element.
_visibleHref: string; _visibleHref: string;
// the url passed to the router navigation. // the url passed to the router navigation.
_navigationHref: string; _navigationHref: string;
constructor(elementRef:ElementRef, router:Router, location:Location) { constructor(elementRef: ElementRef, private _router: Router, private _location: Location) {
this._domEl = elementRef.domElement; this._domEl = elementRef.domElement;
this._router = router;
this._location = location;
this._params = StringMapWrapper.create(); this._params = StringMapWrapper.create();
DOM.on(this._domEl, 'click', (evt) => { DOM.on(this._domEl, 'click', (evt) => {
DOM.preventDefault(evt); DOM.preventDefault(evt);
@ -59,13 +54,9 @@ export class RouterLink {
}); });
} }
set route(changes: string) { set route(changes: string) { this._route = changes; }
this._route = changes;
}
set params(changes: StringMap) { set params(changes: StringMap<string, string>) { this._params = changes; }
this._params = changes;
}
onAllChangesDone(): void { onAllChangesDone(): void {
if (isPresent(this._route) && isPresent(this._params)) { if (isPresent(this._route) && isPresent(this._params)) {

View File

@ -1,8 +1,7 @@
import {Promise, PromiseWrapper} from 'angular2/src/facade/async'; import {Promise, PromiseWrapper} from 'angular2/src/facade/async';
import {isBlank, isPresent} from 'angular2/src/facade/lang'; import {isBlank, isPresent} from 'angular2/src/facade/lang';
import {Directive} from 'angular2/src/core/annotations_impl/annotations'; import {Directive, Attribute} from 'angular2/src/core/annotations/decorators';
import {Attribute} from 'angular2/src/core/annotations_impl/di';
import {DynamicComponentLoader, ComponentRef, ElementRef} from 'angular2/core'; import {DynamicComponentLoader, ComponentRef, ElementRef} from 'angular2/core';
import {Injector, bind} from 'angular2/di'; import {Injector, bind} from 'angular2/di';
@ -31,22 +30,19 @@ import {Instruction, RouteParams} from './instruction'
selector: 'router-outlet' selector: 'router-outlet'
}) })
export class RouterOutlet { export class RouterOutlet {
_injector:Injector; private _childRouter: routerMod.Router;
_parentRouter:routerMod.Router; private _componentRef: ComponentRef;
_childRouter:routerMod.Router; private _elementRef: ElementRef;
_loader:DynamicComponentLoader; private _currentInstruction: Instruction;
_componentRef:ComponentRef;
_elementRef:ElementRef;
_currentInstruction:Instruction;
constructor(elementRef:ElementRef, loader:DynamicComponentLoader, router:routerMod.Router, injector:Injector, @Attribute('name') nameAttr:String) { constructor(elementRef: ElementRef, private _loader: DynamicComponentLoader,
private _parentRouter: routerMod.Router, private _injector: Injector,
@Attribute('name') nameAttr: string) {
if (isBlank(nameAttr)) { if (isBlank(nameAttr)) {
nameAttr = 'default'; nameAttr = 'default';
} }
this._loader = loader;
this._parentRouter = router;
this._elementRef = elementRef; this._elementRef = elementRef;
this._injector = injector;
this._childRouter = null; this._childRouter = null;
this._componentRef = null; this._componentRef = null;
@ -57,17 +53,20 @@ export class RouterOutlet {
/** /**
* Given an instruction, update the contents of this viewport. * Given an instruction, update the contents of this viewport.
*/ */
activate(instruction:Instruction): Promise { activate(instruction: Instruction): Promise<any> {
// if we're able to reuse the component, we just have to pass along the instruction to the component's router // if we're able to reuse the component, we just have to pass along the instruction to the
// component's router
// so it can propagate changes to its children // so it can propagate changes to its children
if ((instruction == this._currentInstruction) || instruction.reuse && isPresent(this._childRouter)) { if ((instruction == this._currentInstruction) ||
instruction.reuse && isPresent(this._childRouter)) {
return this._childRouter.commit(instruction); return this._childRouter.commit(instruction);
} }
this._currentInstruction = instruction; this._currentInstruction = instruction;
this._childRouter = this._parentRouter.childRouter(instruction.component); this._childRouter = this._parentRouter.childRouter(instruction.component);
var outletInjector = this._injector.resolveAndCreateChild([ var outletInjector = this._injector.resolveAndCreateChild([
bind(RouteParams).toValue(new RouteParams(instruction.params)), bind(RouteParams)
.toValue(new RouteParams(instruction.params)),
bind(routerMod.Router).toValue(this._childRouter) bind(routerMod.Router).toValue(this._childRouter)
]); ]);
@ -75,18 +74,21 @@ export class RouterOutlet {
this._componentRef.dispose(); this._componentRef.dispose();
} }
return this._loader.loadNextToExistingLocation(instruction.component, this._elementRef, outletInjector).then((componentRef) => { return this._loader.loadNextToExistingLocation(instruction.component, this._elementRef,
this._componentRef = componentRef; outletInjector)
return this._childRouter.commit(instruction); .then((componentRef) => {
}); this._componentRef = componentRef;
return this._childRouter.commit(instruction);
});
} }
deactivate():Promise { deactivate(): Promise<any> {
return (isPresent(this._childRouter) ? this._childRouter.deactivate() : PromiseWrapper.resolve(true)) return (isPresent(this._childRouter) ? this._childRouter.deactivate() :
.then((_) =>this._componentRef.dispose()); PromiseWrapper.resolve(true))
.then((_) => this._componentRef.dispose());
} }
canDeactivate(instruction:Instruction): Promise<boolean> { canDeactivate(instruction: Instruction): Promise<boolean> {
// TODO: how to get ahold of the component instance here? // TODO: how to get ahold of the component instance here?
return PromiseWrapper.resolve(true); return PromiseWrapper.resolve(true);
} }

View File

@ -1,13 +1,9 @@
import {RegExpWrapper, StringWrapper} from 'angular2/src/facade/lang'; import {RegExpWrapper, StringWrapper} from 'angular2/src/facade/lang';
var specialCharacters = [ var specialCharacters = ['/', '.', '*', '+', '?', '|', '(', ')', '[', ']', '{', '}', '\\'];
'/', '.', '*', '+', '?', '|', '(', ')', '[', ']', '{', '}', '\\'
];
var escapeRe = RegExpWrapper.create('(\\' + specialCharacters.join('|\\') + ')', 'g'); var escapeRe = RegExpWrapper.create('(\\' + specialCharacters.join('|\\') + ')', 'g');
export function escapeRegex(string:string): string { export function escapeRegex(string: string): string {
return StringWrapper.replaceAllMapped(string, escapeRe, (match) => { return StringWrapper.replaceAllMapped(string, escapeRe, (match) => { return "\\" + match; });
return "\\" + match;
});
} }

View File

@ -2,10 +2,15 @@ import {
AsyncTestCompleter, AsyncTestCompleter,
describe, describe,
proxy, proxy,
it, iit, it,
ddescribe, expect, iit,
inject, beforeEach, beforeEachBindings, ddescribe,
SpyObject} from 'angular2/test_lib'; expect,
inject,
beforeEach,
beforeEachBindings,
SpyObject
} from 'angular2/test_lib';
import {IMPLEMENTS} from 'angular2/src/facade/lang'; import {IMPLEMENTS} from 'angular2/src/facade/lang';
import {EventEmitter, ObservableWrapper} from 'angular2/src/facade/async'; import {EventEmitter, ObservableWrapper} from 'angular2/src/facade/async';
@ -13,7 +18,6 @@ import {BrowserLocation} from 'angular2/src/router/browser_location';
import {Location} from 'angular2/src/router/location'; import {Location} from 'angular2/src/router/location';
export function main() { export function main() {
describe('Location', () => { describe('Location', () => {
var browserLocation, location; var browserLocation, location;
@ -27,28 +31,31 @@ export function main() {
it('should normalize relative urls on navigate', () => { it('should normalize relative urls on navigate', () => {
location.go('user/btford'); location.go('user/btford');
expect(browserLocation.spy('pushState')).toHaveBeenCalledWith(null, '', '/my/app/user/btford'); expect(browserLocation.spy('pushState'))
.toHaveBeenCalledWith(null, '', '/my/app/user/btford');
}); });
it('should not append urls with leading slash on navigate', () => { it('should not append urls with leading slash on navigate', () => {
location.go('/my/app/user/btford'); location.go('/my/app/user/btford');
expect(browserLocation.spy('pushState')).toHaveBeenCalledWith(null, '', '/my/app/user/btford'); expect(browserLocation.spy('pushState'))
.toHaveBeenCalledWith(null, '', '/my/app/user/btford');
}); });
it('should remove index.html from base href', () => { it('should remove index.html from base href', () => {
browserLocation.baseHref = '/my/app/index.html'; browserLocation.baseHref = '/my/app/index.html';
location = new Location(browserLocation); location = new Location(browserLocation);
location.go('user/btford'); location.go('user/btford');
expect(browserLocation.spy('pushState')).toHaveBeenCalledWith(null, '', '/my/app/user/btford'); expect(browserLocation.spy('pushState'))
.toHaveBeenCalledWith(null, '', '/my/app/user/btford');
}); });
it('should normalize urls on popstate', inject([AsyncTestCompleter], (async) => { it('should normalize urls on popstate', inject([AsyncTestCompleter], (async) => {
browserLocation.simulatePopState('/my/app/user/btford'); browserLocation.simulatePopState('/my/app/user/btford');
location.subscribe((ev) => { location.subscribe((ev) => {
expect(ev['url']).toEqual('/user/btford'); expect(ev['url']).toEqual('/user/btford');
async.done(); async.done();
}) })
})); }));
it('should normalize location path', () => { it('should normalize location path', () => {
browserLocation.internalPath = '/my/app/user/btford'; browserLocation.internalPath = '/my/app/user/btford';
@ -62,7 +69,7 @@ export function main() {
class DummyBrowserLocation extends SpyObject { class DummyBrowserLocation extends SpyObject {
baseHref; baseHref;
internalPath; internalPath;
_subject:EventEmitter; _subject: EventEmitter;
constructor() { constructor() {
super(); super();
this.internalPath = '/'; this.internalPath = '/';
@ -74,17 +81,11 @@ class DummyBrowserLocation extends SpyObject {
ObservableWrapper.callNext(this._subject, null); ObservableWrapper.callNext(this._subject, null);
} }
path() { path() { return this.internalPath; }
return this.internalPath;
}
onPopState(fn) { onPopState(fn) { ObservableWrapper.subscribe(this._subject, fn); }
ObservableWrapper.subscribe(this._subject, fn);
}
getBaseHref() { getBaseHref() { return this.baseHref; }
return this.baseHref;
}
noSuchMethod(m){return super.noSuchMethod(m);} noSuchMethod(m) { return super.noSuchMethod(m); }
} }

View File

@ -1,348 +0,0 @@
import {
AsyncTestCompleter,
beforeEach,
ddescribe,
xdescribe,
describe,
el,
expect,
iit,
inject,
beforeEachBindings,
it,
xit
} from 'angular2/test_lib';
import {TestBed} from 'angular2/test';
import {Injector, bind} from 'angular2/di';
import {Component} from 'angular2/src/core/annotations_impl/annotations';
import {View} from 'angular2/src/core/annotations_impl/view';
import {RootRouter} from 'angular2/src/router/router';
import {Pipeline} from 'angular2/src/router/pipeline';
import {Router, RouterOutlet, RouterLink, RouteParams} from 'angular2/router';
import {RouteConfig} from 'angular2/src/router/route_config_impl';
import {DOM} from 'angular2/src/dom/dom_adapter';
import {SpyLocation} from 'angular2/src/mock/location_mock';
import {Location} from 'angular2/src/router/location';
import {RouteRegistry} from 'angular2/src/router/route_registry';
import {DirectiveResolver} from 'angular2/src/core/compiler/directive_resolver';
var teamCmpCount;
export function main() {
describe('Outlet Directive', () => {
var ctx, tb, view, rtr, location;
beforeEachBindings(() => [
Pipeline,
RouteRegistry,
DirectiveResolver,
bind(Location).toClass(SpyLocation),
bind(Router).toFactory((registry, pipeline, location) => {
return new RootRouter(registry, pipeline, location, MyComp);
}, [RouteRegistry, Pipeline, Location])
]);
beforeEach(inject([TestBed, Router, Location], (testBed, router, loc) => {
tb = testBed;
ctx = new MyComp();
rtr = router;
location = loc;
teamCmpCount = 0;
}));
function compile(template:string = "<router-outlet></router-outlet>") {
tb.overrideView(MyComp, new View({template: ('<div>' + template + '</div>'), directives: [RouterOutlet, RouterLink]}));
return tb.createView(MyComp, {context: ctx}).then((v) => {
view = v;
});
}
it('should work in a simple case', inject([AsyncTestCompleter], (async) => {
compile()
.then((_) => rtr.config({'path': '/test', 'component': HelloCmp}))
.then((_) => rtr.navigate('/test'))
.then((_) => {
view.detectChanges();
expect(view.rootNodes).toHaveText('hello');
async.done();
});
}));
it('should navigate between components with different parameters', inject([AsyncTestCompleter], (async) => {
compile()
.then((_) => rtr.config({'path': '/user/:name', 'component': UserCmp}))
.then((_) => rtr.navigate('/user/brian'))
.then((_) => {
view.detectChanges();
expect(view.rootNodes).toHaveText('hello brian');
})
.then((_) => rtr.navigate('/user/igor'))
.then((_) => {
view.detectChanges();
expect(view.rootNodes).toHaveText('hello igor');
async.done();
});
}));
it('should work with child routers', inject([AsyncTestCompleter], (async) => {
compile('outer { <router-outlet></router-outlet> }')
.then((_) => rtr.config({'path': '/a', 'component': ParentCmp}))
.then((_) => rtr.navigate('/a/b'))
.then((_) => {
view.detectChanges();
expect(view.rootNodes).toHaveText('outer { inner { hello } }');
async.done();
});
}));
it('should work with sibling routers', inject([AsyncTestCompleter], (async) => {
compile('left { <router-outlet name="left"></router-outlet> } | right { <router-outlet name="right"></router-outlet> }')
.then((_) => rtr.config({'path': '/ab', 'components': {'left': A, 'right': B} }))
.then((_) => rtr.config({'path': '/ba', 'components': {'left': B, 'right': A} }))
.then((_) => rtr.navigate('/ab'))
.then((_) => {
view.detectChanges();
expect(view.rootNodes).toHaveText('left { A } | right { B }');
})
.then((_) => rtr.navigate('/ba'))
.then((_) => {
view.detectChanges();
expect(view.rootNodes).toHaveText('left { B } | right { A }');
async.done();
});
}));
it('should work with redirects', inject([AsyncTestCompleter, Location], (async, location) => {
compile()
.then((_) => rtr.config({'path': '/original', 'redirectTo': '/redirected' }))
.then((_) => rtr.config({'path': '/redirected', 'component': A }))
.then((_) => rtr.navigate('/original'))
.then((_) => {
view.detectChanges();
expect(view.rootNodes).toHaveText('A');
expect(location.urlChanges).toEqual(['/redirected']);
async.done();
});
}));
function getHref(view) {
return DOM.getAttribute(view.rootNodes[0].childNodes[0], 'href');
}
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>')
.then((_) => rtr.config({'path': '/user', 'component': UserCmp, 'as': 'user'}))
.then((_) => rtr.navigate('/a/b'))
.then((_) => {
view.detectChanges();
expect(getHref(view)).toEqual('/my/base/user');
async.done();
});
}));
it('should generate link hrefs without params', inject([AsyncTestCompleter], (async) => {
compile('<a href="hello" router-link="user"></a>')
.then((_) => rtr.config({'path': '/user', 'component': UserCmp, 'as': 'user'}))
.then((_) => rtr.navigate('/a/b'))
.then((_) => {
view.detectChanges();
expect(getHref(view)).toEqual('/user');
async.done();
});
}));
it('should reuse common parent components', inject([AsyncTestCompleter], (async) => {
compile()
.then((_) => rtr.config({'path': '/team/:id', 'component': TeamCmp }))
.then((_) => rtr.navigate('/team/angular/user/rado'))
.then((_) => {
view.detectChanges();
expect(teamCmpCount).toBe(1);
expect(view.rootNodes).toHaveText('team angular { hello rado }');
})
.then((_) => rtr.navigate('/team/angular/user/victor'))
.then((_) => {
view.detectChanges();
expect(teamCmpCount).toBe(1);
expect(view.rootNodes).toHaveText('team angular { hello victor }');
async.done();
});
}));
it('should generate link hrefs with params', inject([AsyncTestCompleter], (async) => {
ctx.name = 'brian';
compile('<a href="hello" router-link="user" [router-params]="{name: name}">{{name}}</a>')
.then((_) => rtr.config({'path': '/user/:name', 'component': UserCmp, 'as': 'user'}))
.then((_) => rtr.navigate('/a/b'))
.then((_) => {
view.detectChanges();
expect(view.rootNodes).toHaveText('brian');
expect(DOM.getAttribute(view.rootNodes[0].childNodes[0], 'href')).toEqual('/user/brian');
async.done();
});
}));
describe('when clicked', () => {
var clickOnElement = function(view) {
var anchorEl = view.rootNodes[0].childNodes[0];
var dispatchedEvent = DOM.createMouseEvent('click');
DOM.dispatchEvent(anchorEl, dispatchedEvent);
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>')
.then((_) => rtr.config({
'path': '/user',
'component': UserCmp,
'as': 'user'
}))
.then((_) => rtr.navigate('/a/b'))
.then((_) => {
view.detectChanges();
var dispatchedEvent = clickOnElement(view);
expect(dispatchedEvent.defaultPrevented || !dispatchedEvent.returnValue).toBe(true);
// router navigation is async.
rtr.subscribe((_) => {
expect(location.urlChanges).toEqual(['/user']);
async.done();
});
});
}));
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>')
.then((_) => rtr.config({
'path': '/user',
'component': UserCmp,
'as': 'user'
}))
.then((_) => rtr.navigate('/a/b'))
.then((_) => {
view.detectChanges();
var dispatchedEvent = clickOnElement(view);
expect(dispatchedEvent.defaultPrevented || !dispatchedEvent.returnValue).toBe(true);
// router navigation is async.
rtr.subscribe((_) => {
expect(location.urlChanges).toEqual(['/base/user']);
async.done();
});
});
}));
});
});
}
@Component({
selector: 'hello-cmp'
})
@View({
template: "{{greeting}}"
})
class HelloCmp {
greeting:string;
constructor() {
this.greeting = "hello";
}
}
@Component({
selector: 'a-cmp'
})
@View({
template: "A"
})
class A {}
@Component({
selector: 'b-cmp'
})
@View({
template: "B"
})
class B {}
@Component({
selector: 'user-cmp'
})
@View({
template: "hello {{user}}"
})
class UserCmp {
user:string;
constructor(params:RouteParams) {
this.user = params.get('name');
}
}
@Component({
selector: 'parent-cmp'
})
@View({
template: "inner { <router-outlet></router-outlet> }",
directives: [RouterOutlet]
})
@RouteConfig([{
path: '/b',
component: HelloCmp
}])
class ParentCmp {
constructor() {}
}
@Component({
selector: 'team-cmp'
})
@View({
template: "team {{id}} { <router-outlet></router-outlet> }",
directives: [RouterOutlet]
})
@RouteConfig([{
path: '/user/:name',
component: UserCmp
}])
class TeamCmp {
id:string;
constructor(params:RouteParams) {
this.id = params.get('id');
teamCmpCount += 1;
}
}
@Component({
selector: 'my-comp'
})
class MyComp {
name;
}

View File

@ -0,0 +1,314 @@
import {
AsyncTestCompleter,
beforeEach,
ddescribe,
xdescribe,
describe,
el,
expect,
iit,
inject,
beforeEachBindings,
it,
xit
} from 'angular2/test_lib';
import {TestBed} from 'angular2/test';
import {Injector, bind} from 'angular2/di';
import {Component, View} from 'angular2/src/core/annotations/decorators';
import * as annotations from 'angular2/src/core/annotations_impl/view';
import {CONST} from 'angular2/src/facade/lang';
import {RootRouter} from 'angular2/src/router/router';
import {Pipeline} from 'angular2/src/router/pipeline';
import {Router, RouterOutlet, RouterLink, RouteParams} from 'angular2/router';
import {RouteConfig} from 'angular2/src/router/route_config_decorator';
import {DOM} from 'angular2/src/dom/dom_adapter';
import {SpyLocation} from 'angular2/src/mock/location_mock';
import {Location} from 'angular2/src/router/location';
import {RouteRegistry} from 'angular2/src/router/route_registry';
import {DirectiveResolver} from 'angular2/src/core/compiler/directive_resolver';
var teamCmpCount;
export function main() {
describe('Outlet Directive', () => {
var ctx: MyComp;
var tb: TestBed;
var view, rtr, location;
beforeEachBindings(() => [
Pipeline,
RouteRegistry,
DirectiveResolver,
bind(Location).toClass(SpyLocation),
bind(Router).toFactory((registry, pipeline, location) =>
{ return new RootRouter(registry, pipeline, location, MyComp); },
[RouteRegistry, Pipeline, Location])
]);
beforeEach(inject([TestBed, Router, Location], (testBed, router, loc) => {
tb = testBed;
ctx = new MyComp();
rtr = router;
location = loc;
teamCmpCount = 0;
}));
function compile(template: string = "<router-outlet></router-outlet>") {
tb.overrideView(MyComp, new annotations.View({
template: ('<div>' + template + '</div>'),
directives: [RouterOutlet, RouterLink]
}));
return tb.createView(MyComp, {context: ctx}).then((v) => { view = v; });
}
it('should work in a simple case', inject([AsyncTestCompleter], (async) => {
compile()
.then((_) => rtr.config({'path': '/test', 'component': HelloCmp}))
.then((_) => rtr.navigate('/test'))
.then((_) => {
view.detectChanges();
expect(view.rootNodes).toHaveText('hello');
async.done();
});
}));
it('should navigate between components with different parameters',
inject([AsyncTestCompleter], (async) => {
compile()
.then((_) => rtr.config({'path': '/user/:name', 'component': UserCmp}))
.then((_) => rtr.navigate('/user/brian'))
.then((_) =>
{
view.detectChanges();
expect(view.rootNodes).toHaveText('hello brian');
})
.then((_) => rtr.navigate('/user/igor'))
.then((_) => {
view.detectChanges();
expect(view.rootNodes).toHaveText('hello igor');
async.done();
});
}));
it('should work with child routers', inject([AsyncTestCompleter], (async) => {
compile('outer { <router-outlet></router-outlet> }')
.then((_) => rtr.config({'path': '/a', 'component': ParentCmp}))
.then((_) => rtr.navigate('/a/b'))
.then((_) => {
view.detectChanges();
expect(view.rootNodes).toHaveText('outer { inner { hello } }');
async.done();
});
}));
it('should work with sibling routers', inject([AsyncTestCompleter], (async) => {
compile(
'left { <router-outlet name="left"></router-outlet> } | right { <router-outlet name="right"></router-outlet> }')
.then((_) => rtr.config({'path': '/ab', 'components': {'left': A, 'right': B}}))
.then((_) => rtr.config({'path': '/ba', 'components': {'left': B, 'right': A}}))
.then((_) => rtr.navigate('/ab'))
.then((_) =>
{
view.detectChanges();
expect(view.rootNodes).toHaveText('left { A } | right { B }');
})
.then((_) => rtr.navigate('/ba'))
.then((_) => {
view.detectChanges();
expect(view.rootNodes).toHaveText('left { B } | right { A }');
async.done();
});
}));
it('should work with redirects', inject([AsyncTestCompleter, Location], (async, location) => {
compile()
.then((_) => rtr.config({'path': '/original', 'redirectTo': '/redirected'}))
.then((_) => rtr.config({'path': '/redirected', 'component': A}))
.then((_) => rtr.navigate('/original'))
.then((_) => {
view.detectChanges();
expect(view.rootNodes).toHaveText('A');
expect(location.urlChanges).toEqual(['/redirected']);
async.done();
});
}));
function getHref(view) { return DOM.getAttribute(view.rootNodes[0].childNodes[0], 'href'); }
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>')
.then((_) => rtr.config({'path': '/user', 'component': UserCmp, 'as': 'user'}))
.then((_) => rtr.navigate('/a/b'))
.then((_) => {
view.detectChanges();
expect(getHref(view)).toEqual('/my/base/user');
async.done();
});
}));
it('should generate link hrefs without params', inject([AsyncTestCompleter], (async) => {
compile('<a href="hello" router-link="user"></a>')
.then((_) => rtr.config({'path': '/user', 'component': UserCmp, 'as': 'user'}))
.then((_) => rtr.navigate('/a/b'))
.then((_) => {
view.detectChanges();
expect(getHref(view)).toEqual('/user');
async.done();
});
}));
it('should reuse common parent components', inject([AsyncTestCompleter], (async) => {
compile()
.then((_) => rtr.config({'path': '/team/:id', 'component': TeamCmp}))
.then((_) => rtr.navigate('/team/angular/user/rado'))
.then((_) =>
{
view.detectChanges();
expect(teamCmpCount).toBe(1);
expect(view.rootNodes).toHaveText('team angular { hello rado }');
})
.then((_) => rtr.navigate('/team/angular/user/victor'))
.then((_) => {
view.detectChanges();
expect(teamCmpCount).toBe(1);
expect(view.rootNodes).toHaveText('team angular { hello victor }');
async.done();
});
}));
it('should generate link hrefs with params', inject([AsyncTestCompleter], (async) => {
ctx.name = 'brian';
compile('<a href="hello" router-link="user" [router-params]="{name: name}">{{name}}</a>')
.then((_) => rtr.config({'path': '/user/:name', 'component': UserCmp, 'as': 'user'}))
.then((_) => rtr.navigate('/a/b'))
.then((_) => {
view.detectChanges();
expect(view.rootNodes).toHaveText('brian');
expect(DOM.getAttribute(view.rootNodes[0].childNodes[0], 'href'))
.toEqual('/user/brian');
async.done();
});
}));
describe('when clicked', () => {
var clickOnElement = function(view) {
var anchorEl = view.rootNodes[0].childNodes[0];
var dispatchedEvent = DOM.createMouseEvent('click');
DOM.dispatchEvent(anchorEl, dispatchedEvent);
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>')
.then((_) => rtr.config({'path': '/user', 'component': UserCmp, 'as': 'user'}))
.then((_) => rtr.navigate('/a/b'))
.then((_) => {
view.detectChanges();
var dispatchedEvent = clickOnElement(view);
expect(dispatchedEvent.defaultPrevented || !dispatchedEvent.returnValue)
.toBe(true);
// router navigation is async.
rtr.subscribe((_) => {
expect(location.urlChanges).toEqual(['/user']);
async.done();
});
});
}));
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>')
.then((_) => rtr.config({'path': '/user', 'component': UserCmp, 'as': 'user'}))
.then((_) => rtr.navigate('/a/b'))
.then((_) => {
view.detectChanges();
var dispatchedEvent = clickOnElement(view);
expect(dispatchedEvent.defaultPrevented || !dispatchedEvent.returnValue)
.toBe(true);
// router navigation is async.
rtr.subscribe((_) => {
expect(location.urlChanges).toEqual(['/base/user']);
async.done();
});
});
}));
});
});
}
@Component({selector: 'hello-cmp'})
@View({template: "{{greeting}}"})
class HelloCmp {
greeting: string;
constructor() { this.greeting = "hello"; }
}
@Component({selector: 'a-cmp'})
@View({template: "A"})
class A {
}
@Component({selector: 'b-cmp'})
@View({template: "B"})
class B {
}
@Component({selector: 'user-cmp'})
@View({template: "hello {{user}}"})
class UserCmp {
user: string;
constructor(params: RouteParams) { this.user = params.get('name'); }
}
@Component({selector: 'parent-cmp'})
@View({template: "inner { <router-outlet></router-outlet> }", directives: [RouterOutlet]})
@RouteConfig([{path: '/b', component: HelloCmp}])
class ParentCmp {
constructor() {}
}
@Component({selector: 'team-cmp'})
@View({template: "team {{id}} { <router-outlet></router-outlet> }", directives: [RouterOutlet]})
@RouteConfig([{path: '/user/:name', component: UserCmp}])
class TeamCmp {
id: string;
constructor(params: RouteParams) {
this.id = params.get('id');
teamCmpCount += 1;
}
}
@Component({selector: 'my-comp'})
class MyComp {
name;
}

View File

@ -1,26 +1,24 @@
import { import {
AsyncTestCompleter, AsyncTestCompleter,
describe, describe,
it, iit, it,
ddescribe, expect, iit,
inject, beforeEach, ddescribe,
SpyObject} from 'angular2/test_lib'; expect,
inject,
beforeEach,
SpyObject
} from 'angular2/test_lib';
import {RouteRecognizer} from 'angular2/src/router/route_recognizer'; import {RouteRecognizer} from 'angular2/src/router/route_recognizer';
export function main() { export function main() {
describe('RouteRecognizer', () => { describe('RouteRecognizer', () => {
var recognizer; var recognizer;
var handler = { var handler = {'components': {'a': 'b'}};
'components': { 'a': 'b' } var handler2 = {'components': {'b': 'c'}};
};
var handler2 = {
'components': { 'b': 'c' }
};
beforeEach(() => { beforeEach(() => { recognizer = new RouteRecognizer(); });
recognizer = new RouteRecognizer();
});
it('should recognize a static segment', () => { it('should recognize a static segment', () => {
@ -40,7 +38,7 @@ export function main() {
recognizer.addConfig('/user/:name', handler); recognizer.addConfig('/user/:name', handler);
var solution = recognizer.recognize('/user/brian')[0]; var solution = recognizer.recognize('/user/brian')[0];
expect(solution.handler).toEqual(handler); expect(solution.handler).toEqual(handler);
expect(solution.params).toEqual({ 'name': 'brian' }); expect(solution.params).toEqual({'name': 'brian'});
}); });
@ -48,23 +46,22 @@ export function main() {
recognizer.addConfig('/first/*rest', handler); recognizer.addConfig('/first/*rest', handler);
var solution = recognizer.recognize('/first/second/third')[0]; var solution = recognizer.recognize('/first/second/third')[0];
expect(solution.handler).toEqual(handler); expect(solution.handler).toEqual(handler);
expect(solution.params).toEqual({ 'rest': 'second/third' }); expect(solution.params).toEqual({'rest': 'second/third'});
}); });
it('should throw when given two routes that start with the same static segment', () => { it('should throw when given two routes that start with the same static segment', () => {
recognizer.addConfig('/hello', handler); recognizer.addConfig('/hello', handler);
expect(() => recognizer.addConfig('/hello', handler2)).toThrowError( expect(() => recognizer.addConfig('/hello', handler2))
'Configuration \'/hello\' conflicts with existing route \'/hello\'' .toThrowError('Configuration \'/hello\' conflicts with existing route \'/hello\'');
);
}); });
it('should throw when given two routes that have dynamic segments in the same order', () => { it('should throw when given two routes that have dynamic segments in the same order', () => {
recognizer.addConfig('/hello/:person/how/:doyoudou', handler); recognizer.addConfig('/hello/:person/how/:doyoudou', handler);
expect(() => recognizer.addConfig('/hello/:friend/how/:areyou', handler2)).toThrowError( expect(() => recognizer.addConfig('/hello/:friend/how/:areyou', handler2))
'Configuration \'/hello/:friend/how/:areyou\' conflicts with existing route \'/hello/:person/how/:doyoudou\'' .toThrowError(
); 'Configuration \'/hello/:friend/how/:areyou\' conflicts with existing route \'/hello/:person/how/:doyoudou\'');
}); });
@ -82,14 +79,14 @@ export function main() {
it('should generate URLs', () => { it('should generate URLs', () => {
recognizer.addConfig('/app/user/:name', handler, 'user'); recognizer.addConfig('/app/user/:name', handler, 'user');
expect(recognizer.generate('user', {'name' : 'misko'})).toEqual('/app/user/misko'); expect(recognizer.generate('user', {'name': 'misko'})).toEqual('/app/user/misko');
}); });
it('should throw in the absence of required params URLs', () => { it('should throw in the absence of required params URLs', () => {
recognizer.addConfig('/app/user/:name', handler, 'user'); recognizer.addConfig('/app/user/:name', handler, 'user');
expect(() => recognizer.generate('user', {})).toThrowError( expect(() => recognizer.generate('user', {}))
'Route generator for \'name\' was not included in parameters passed.'); .toThrowError('Route generator for \'name\' was not included in parameters passed.');
}); });
}); });
} }

View File

@ -1,22 +1,23 @@
import { import {
AsyncTestCompleter, AsyncTestCompleter,
describe, describe,
it, iit, it,
ddescribe, expect, iit,
inject, beforeEach, ddescribe,
SpyObject} from 'angular2/test_lib'; expect,
inject,
beforeEach,
SpyObject
} from 'angular2/test_lib';
import {RouteRegistry} from 'angular2/src/router/route_registry'; import {RouteRegistry} from 'angular2/src/router/route_registry';
import {RouteConfig} from 'angular2/src/router/route_config_impl'; import {RouteConfig} from 'angular2/src/router/route_config_decorator';
export function main() { export function main() {
describe('RouteRegistry', () => { describe('RouteRegistry', () => {
var registry, var registry, rootHostComponent = new Object();
rootHostComponent = new Object();
beforeEach(() => { beforeEach(() => { registry = new RouteRegistry(); });
registry = new RouteRegistry();
});
it('should match the full URL', () => { it('should match the full URL', () => {
registry.config(rootHostComponent, {'path': '/', 'component': DummyCompA}); registry.config(rootHostComponent, {'path': '/', 'component': DummyCompA});
@ -87,10 +88,9 @@ export function main() {
}); });
} }
@RouteConfig([
{'path': '/second', 'component': DummyCompB }
])
class DummyParentComp {}
class DummyCompA {} class DummyCompA {}
class DummyCompB {} class DummyCompB {}
@RouteConfig([{'path': '/second', 'component': DummyCompB}])
class DummyParentComp {
}

View File

@ -1,78 +0,0 @@
import {
AsyncTestCompleter,
beforeEach,
ddescribe,
describe,
expect,
iit,
inject,
it,
xdescribe,
xit,
} from 'angular2/test_lib';
import {bootstrap} from 'angular2/src/core/application';
import {Component, Directive} from 'angular2/src/core/annotations_impl/annotations';
import {DOM} from 'angular2/src/dom/dom_adapter';
import {bind} from 'angular2/di';
import {View} from 'angular2/src/core/annotations_impl/view';
import {DOCUMENT_TOKEN} from 'angular2/src/render/dom/dom_renderer';
import {RouteConfig} from 'angular2/src/router/route_config_impl';
import {routerInjectables, Router, RouteParams, RouterOutlet} from 'angular2/router';
import {SpyLocation} from 'angular2/src/mock/location_mock';
import {Location} from 'angular2/src/router/location';
export function main() {
describe('router injectables', () => {
var fakeDoc, el, testBindings;
beforeEach(() => {
fakeDoc = DOM.createHtmlDocument();
el = DOM.createElement('app-cmp', fakeDoc);
DOM.appendChild(fakeDoc.body, el);
testBindings = [
routerInjectables,
bind(Location).toClass(SpyLocation),
bind(DOCUMENT_TOKEN).toValue(fakeDoc)
];
});
it('should support bootstrap a simple app', inject([AsyncTestCompleter], (async) => {
bootstrap(AppCmp, testBindings).then((applicationRef) => {
var router = applicationRef.hostComponent.router;
router.subscribe((_) => {
expect(el).toHaveText('outer { hello }');
async.done();
});
});
}));
//TODO: add a test in which the child component has bindings
});
}
@Component({
selector: 'hello-cmp'
})
@View({
template: "hello"
})
class HelloCmp {}
@Component({
selector: 'app-cmp'
})
@View({
template: "outer { <router-outlet></router-outlet> }",
directives: [RouterOutlet]
})
@RouteConfig([{
path: '/', component: HelloCmp
}])
class AppCmp {
router:Router;
constructor(router:Router) {
this.router = router;
}
}

View File

@ -0,0 +1,67 @@
import {
AsyncTestCompleter,
beforeEach,
ddescribe,
describe,
expect,
iit,
inject,
it,
xdescribe,
xit,
} from 'angular2/test_lib';
import {bootstrap} from 'angular2/src/core/application';
import {Component, Directive, View} from 'angular2/src/core/annotations/decorators';
import {DOM} from 'angular2/src/dom/dom_adapter';
import {bind} from 'angular2/di';
import {DOCUMENT_TOKEN} from 'angular2/src/render/dom/dom_renderer';
import {RouteConfig} from 'angular2/src/router/route_config_decorator';
import {routerInjectables, Router} from 'angular2/router';
import {RouterOutlet} from 'angular2/src/router/router_outlet';
import {SpyLocation} from 'angular2/src/mock/location_mock';
import {Location} from 'angular2/src/router/location';
export function main() {
describe('router injectables', () => {
var fakeDoc, el, testBindings;
beforeEach(() => {
fakeDoc = DOM.createHtmlDocument();
el = DOM.createElement('app-cmp', fakeDoc);
DOM.appendChild(fakeDoc.body, el);
testBindings = [
routerInjectables,
bind(Location).toClass(SpyLocation),
bind(DOCUMENT_TOKEN).toValue(fakeDoc)
];
});
it('should support bootstrap a simple app', inject([AsyncTestCompleter], (async) => {
bootstrap(AppCmp, testBindings)
.then((applicationRef) => {
var router = applicationRef.hostComponent.router;
router.subscribe((_) => {
expect(el).toHaveText('outer { hello }');
async.done();
});
});
}));
// TODO: add a test in which the child component has bindings
});
}
@Component({selector: 'hello-cmp'})
@View({template: "hello"})
class HelloCmp {
}
@Component({selector: 'app-cmp'})
@View({template: "outer { <router-outlet></router-outlet> }", directives: [RouterOutlet]})
@RouteConfig([{path: '/', component: HelloCmp}])
class AppCmp {
router: Router;
constructor(router: Router) { this.router = router; }
}

View File

@ -1,103 +0,0 @@
import {
AsyncTestCompleter,
describe,
proxy,
it, iit,
ddescribe, expect,
inject, beforeEach, beforeEachBindings,
SpyObject} from 'angular2/test_lib';
import {IMPLEMENTS} from 'angular2/src/facade/lang';
import {Promise, PromiseWrapper} from 'angular2/src/facade/async';
import {Router, RootRouter} from 'angular2/src/router/router';
import {Pipeline} from 'angular2/src/router/pipeline';
import {RouterOutlet} from 'angular2/src/router/router_outlet';
import {SpyLocation} from 'angular2/src/mock/location_mock'
import {Location} from 'angular2/src/router/location';
import {RouteRegistry} from 'angular2/src/router/route_registry';
import {DirectiveResolver} from 'angular2/src/core/compiler/directive_resolver';
import {bind} from 'angular2/di';
export function main() {
describe('Router', () => {
var router,
location;
beforeEachBindings(() => [
Pipeline,
RouteRegistry,
DirectiveResolver,
bind(Location).toClass(SpyLocation),
bind(Router).toFactory((registry, pipeline, location) => {
return new RootRouter(registry, pipeline, location, AppCmp);
}, [RouteRegistry, Pipeline, Location])
]);
beforeEach(inject([Router, Location], (rtr, loc) => {
router = rtr;
location = loc;
}));
it('should navigate based on the initial URL state', inject([AsyncTestCompleter], (async) => {
var outlet = makeDummyOutlet();
router.config({'path': '/', 'component': 'Index' })
.then((_) => router.registerOutlet(outlet))
.then((_) => {
expect(outlet.spy('activate')).toHaveBeenCalled();
expect(location.urlChanges).toEqual([]);
async.done();
});
}));
it('should activate viewports and update URL on navigate', inject([AsyncTestCompleter], (async) => {
var outlet = makeDummyOutlet();
router.registerOutlet(outlet)
.then((_) => {
return router.config({'path': '/a', 'component': 'A' });
})
.then((_) => router.navigate('/a'))
.then((_) => {
expect(outlet.spy('activate')).toHaveBeenCalled();
expect(location.urlChanges).toEqual(['/a']);
async.done();
});
}));
it('should navigate after being configured', inject([AsyncTestCompleter], (async) => {
var outlet = makeDummyOutlet();
router.registerOutlet(outlet)
.then((_) => router.navigate('/a'))
.then((_) => {
expect(outlet.spy('activate')).not.toHaveBeenCalled();
return router.config({'path': '/a', 'component': 'A' });
})
.then((_) => {
expect(outlet.spy('activate')).toHaveBeenCalled();
async.done();
});
}));
});
}
@proxy
@IMPLEMENTS(RouterOutlet)
class DummyOutlet extends SpyObject {noSuchMethod(m){return super.noSuchMethod(m)}}
function makeDummyOutlet() {
var ref = new DummyOutlet();
ref.spy('activate').andCallFake((_) => PromiseWrapper.resolve(true));
ref.spy('canActivate').andCallFake((_) => PromiseWrapper.resolve(true));
ref.spy('canDeactivate').andCallFake((_) => PromiseWrapper.resolve(true));
ref.spy('deactivate').andCallFake((_) => PromiseWrapper.resolve(true));
return ref;
}
class AppCmp {}

View File

@ -0,0 +1,109 @@
import {
AsyncTestCompleter,
describe,
proxy,
it,
iit,
ddescribe,
expect,
inject,
beforeEach,
beforeEachBindings,
SpyObject
} from 'angular2/test_lib';
import {IMPLEMENTS} from 'angular2/src/facade/lang';
import {Promise, PromiseWrapper} from 'angular2/src/facade/async';
import {Router, RootRouter} from 'angular2/src/router/router';
import {Pipeline} from 'angular2/src/router/pipeline';
import {RouterOutlet} from 'angular2/src/router/router_outlet';
import {SpyLocation} from 'angular2/src/mock/location_mock';
import {Location} from 'angular2/src/router/location';
import {RouteRegistry} from 'angular2/src/router/route_registry';
import {DirectiveResolver} from 'angular2/src/core/compiler/directive_resolver';
import {bind} from 'angular2/di';
export function main() {
describe('Router', () => {
var router, location;
beforeEachBindings(() => [
Pipeline,
RouteRegistry,
DirectiveResolver,
bind(Location).toClass(SpyLocation),
bind(Router).toFactory((registry, pipeline, location) =>
{ return new RootRouter(registry, pipeline, location, AppCmp); },
[RouteRegistry, Pipeline, Location])
]);
beforeEach(inject([Router, Location], (rtr, loc) => {
router = rtr;
location = loc;
}));
it('should navigate based on the initial URL state', inject([AsyncTestCompleter], (async) => {
var outlet = makeDummyOutlet();
router.config({'path': '/', 'component': 'Index'})
.then((_) => router.registerOutlet(outlet))
.then((_) => {
expect(outlet.spy('activate')).toHaveBeenCalled();
expect(location.urlChanges).toEqual([]);
async.done();
});
}));
it('should activate viewports and update URL on navigate',
inject([AsyncTestCompleter], (async) => {
var outlet = makeDummyOutlet();
router.registerOutlet(outlet)
.then((_) => { return router.config({'path': '/a', 'component': 'A'}); })
.then((_) => router.navigate('/a'))
.then((_) => {
expect(outlet.spy('activate')).toHaveBeenCalled();
expect(location.urlChanges).toEqual(['/a']);
async.done();
});
}));
it('should navigate after being configured', inject([AsyncTestCompleter], (async) => {
var outlet = makeDummyOutlet();
router.registerOutlet(outlet)
.then((_) => router.navigate('/a'))
.then((_) =>
{
expect(outlet.spy('activate')).not.toHaveBeenCalled();
return router.config({'path': '/a', 'component': 'A'});
})
.then((_) => {
expect(outlet.spy('activate')).toHaveBeenCalled();
async.done();
});
}));
});
}
@proxy
@IMPLEMENTS(RouterOutlet)
class DummyOutlet extends SpyObject {
noSuchMethod(m) { return super.noSuchMethod(m) }
}
function makeDummyOutlet() {
var ref = new DummyOutlet();
ref.spy('activate').andCallFake((_) => PromiseWrapper.resolve(true));
ref.spy('canActivate').andCallFake((_) => PromiseWrapper.resolve(true));
ref.spy('canDeactivate').andCallFake((_) => PromiseWrapper.resolve(true));
ref.spy('deactivate').andCallFake((_) => PromiseWrapper.resolve(true));
return ref;
}
class AppCmp {}