feat(router): add initial implementation

This commit is contained in:
Brian Ford 2015-04-17 09:59:56 -07:00
parent e617ca6323
commit 1b2754dacd
16 changed files with 1095 additions and 0 deletions

View File

@ -50,6 +50,7 @@ module.exports = function(config) {
'/packages/core': 'http://localhost:9877/base/modules/core',
'/packages/change_detection': 'http://localhost:9877/base/modules/change_detection',
'/packages/reflection': 'http://localhost:9877/base/modules/reflection',
'/packages/router': 'http://localhost:9877/base/modules/router',
'/packages/di': 'http://localhost:9877/base/modules/di',
'/packages/directives': 'http://localhost:9877/base/modules/directives',
'/packages/facade': 'http://localhost:9877/base/modules/facade',

13
modules/angular2/router.js vendored Normal file
View File

@ -0,0 +1,13 @@
/**
* @module
* @public
* @description
* Maps application URLs into application states, to support deep-linking and navigation.
*/
export {Router} from './src/router/router';
export {RouterOutlet} from './src/router/router_outlet';
export {RouterLink} from './src/router/router_link';
export {RouteParams} from './src/router/instruction';
export {RouteConfig} from './src/router/route_config';

View File

@ -0,0 +1,73 @@
import {Map, MapWrapper, StringMap, StringMapWrapper, List, ListWrapper} from 'angular2/src/facade/collection';
import {Promise, PromiseWrapper} from 'angular2/src/facade/async';
import {isPresent} from 'angular2/src/facade/lang';
export class RouteParams {
params:Map<string, string>;
constructor(params:StringMap) {
this.params = params;
}
get(param:string) {
return StringMapWrapper.get(this.params, param);
}
}
export class Instruction {
component:any;
_children:Map<string, Instruction>;
router:any;
matchedUrl:string;
params:Map<string, string>;
constructor({params, component, children, matchedUrl}:{params:StringMap, component:any, children:Map, matchedUrl:string} = {}) {
this.matchedUrl = matchedUrl;
if (isPresent(children)) {
this._children = children;
var childUrl;
StringMapWrapper.forEach(this._children, (child, _) => {
childUrl = child.matchedUrl;
});
if (isPresent(childUrl)) {
this.matchedUrl += childUrl;
}
} else {
this._children = StringMapWrapper.create();
}
this.component = component;
this.params = params;
}
getChildInstruction(outletName:string) {
return StringMapWrapper.get(this._children, outletName);
}
forEachChild(fn:Function) {
StringMapWrapper.forEach(this._children, fn);
}
mapChildrenAsync(fn):Promise {
return mapObjAsync(this._children, fn);
}
/**
* Takes a function:
* (parent:Instruction, child:Instruction) => {}
*/
traverseSync(fn:Function) {
this.forEachChild((childInstruction, _) => fn(this, childInstruction));
this.forEachChild((childInstruction, _) => childInstruction.traverseSync(fn));
}
}
function mapObjAsync(obj:StringMap, fn) {
return PromiseWrapper.all(mapObj(obj, fn));
}
function mapObj(obj:StringMap, fn):List {
var result = ListWrapper.create();
StringMapWrapper.forEach(obj, (value, key) => ListWrapper.push(result, fn(value, key)));
return result;
}
export var noopInstruction = new Instruction();

View File

@ -0,0 +1,123 @@
import {RegExp, RegExpWrapper, RegExpMatcherWrapper, StringWrapper, isPresent} from 'angular2/src/facade/lang';
import {Map, MapWrapper, StringMap, StringMapWrapper, List, ListWrapper} from 'angular2/src/facade/collection';
import {escapeRegex} from './url';
class StaticSegment {
string:string;
regex:string;
name:string;
constructor(string:string) {
this.string = string;
this.name = '';
this.regex = escapeRegex(string);
}
generate(params) {
return this.string;
}
}
class DynamicSegment {
name:string;
regex:string;
constructor(name:string) {
this.name = name;
this.regex = "([^/]+)";
}
generate(params:StringMap) {
return StringMapWrapper.get(params, this.name);
}
}
class StarSegment {
name:string;
regex:string;
constructor(name:string) {
this.name = name;
this.regex = "(.+)";
}
generate(params:StringMap) {
return StringMapWrapper.get(params, this.name);
}
}
var paramMatcher = RegExpWrapper.create("^:([^\/]+)$");
var wildcardMatcher = RegExpWrapper.create("^\\*([^\/]+)$");
function parsePathString(route:string):List {
// normalize route as not starting with a "/". Recognition will
// also normalize.
if (route[0] === "/") {
route = StringWrapper.substring(route, 1);
}
var segments = splitBySlash(route);
var results = ListWrapper.create();
for (var i=0; i<segments.length; i++) {
var segment = segments[i],
match;
if (isPresent(match = RegExpWrapper.firstMatch(paramMatcher, segment))) {
ListWrapper.push(results, new DynamicSegment(match[1]));
} else if (isPresent(match = RegExpWrapper.firstMatch(wildcardMatcher, segment))) {
ListWrapper.push(results, new StarSegment(match[1]));
} else if (segment.length > 0) {
ListWrapper.push(results, new StaticSegment(segment));
}
}
return results;
}
var SLASH_RE = RegExpWrapper.create('/');
function splitBySlash (url:string):List<string> {
return StringWrapper.split(url, SLASH_RE);
}
// represents something like '/foo/:bar'
export class PathRecognizer {
segments:List;
regex:RegExp;
handler:any;
constructor(path:string, handler:any) {
this.handler = handler;
this.segments = ListWrapper.create();
var segments = parsePathString(path);
var regexString = '^';
ListWrapper.forEach(segments, (segment) => {
regexString += '/' + segment.regex;
});
this.regex = RegExpWrapper.create(regexString);
this.segments = segments;
}
parseParams(url:string):StringMap {
var params = StringMapWrapper.create();
var urlPart = url;
for(var i=0; i<this.segments.length; i++) {
var segment = this.segments[i];
var match = RegExpWrapper.firstMatch(RegExpWrapper.create('/' + segment.regex), urlPart);
urlPart = StringWrapper.substring(urlPart, match[0].length);
if (segment.name.length > 0) {
StringMapWrapper.set(params, segment.name, match[1]);
}
}
return params;
}
generate(params:StringMap):string {
return ListWrapper.join(ListWrapper.map(this.segments, (segment) => '/' + segment.generate(params)), '');
}
}

41
modules/angular2/src/router/pipeline.js vendored Normal file
View File

@ -0,0 +1,41 @@
import {Promise, PromiseWrapper} from 'angular2/src/facade/async';
import {List, ListWrapper} from 'angular2/src/facade/collection';
import {Instruction} from './instruction';
/**
* Responsible for performing each step of navigation.
* "Steps" are conceptually similar to "middleware"
*/
export class Pipeline {
steps:List;
constructor() {
this.steps = [
instruction => instruction.traverseSync((parentInstruction, childInstruction) => {
childInstruction.router = parentInstruction.router.childRouter(childInstruction.component);
}),
instruction => instruction.router.traverseOutlets((outlet, name) => {
return outlet.canDeactivate(instruction.getChildInstruction(name));
}),
instruction => instruction.router.traverseOutlets((outlet, name) => {
return outlet.canActivate(instruction.getChildInstruction(name));
}),
instruction => instruction.router.activateOutlets(instruction)
];
}
process(instruction:Instruction):Promise {
var steps = this.steps,
currentStep = 0;
function processOne(result:any = true):Promise {
if (currentStep >= steps.length) {
return PromiseWrapper.resolve(result);
}
var step = steps[currentStep];
currentStep += 1;
return PromiseWrapper.resolve(step(instruction)).then(processOne);
}
return processOne();
}
}

View File

@ -0,0 +1,18 @@
import {CONST} from 'angular2/src/facade/lang';
/**
* You use the RouteConfig annotation to ...
*/
export class RouteConfig {
path:string;
redirectTo:string;
component:any;
//TODO: "alias," or "as"
@CONST()
constructor({path, component, redirectTo}:{path:string, component:any, redirectTo:string} = {}) {
this.path = path;
this.component = component;
this.redirectTo = redirectTo;
}
}

View File

@ -0,0 +1,64 @@
import {RegExp, RegExpWrapper, StringWrapper, isPresent} from 'angular2/src/facade/lang';
import {Map, MapWrapper, List, ListWrapper, StringMap, StringMapWrapper} from 'angular2/src/facade/collection';
import {PathRecognizer} from './path_recognizer';
export class RouteRecognizer {
names:Map<string, PathRecognizer>;
redirects:Map<string, string>;
matchers:Map<RegExp, PathRecognizer>;
constructor() {
this.names = MapWrapper.create();
this.matchers = MapWrapper.create();
this.redirects = MapWrapper.create();
}
addRedirect(path:string, target:string) {
MapWrapper.set(this.redirects, path, target);
}
addConfig(path:string, handler:any, alias:string = null) {
var recognizer = new PathRecognizer(path, handler);
MapWrapper.set(this.matchers, recognizer.regex, recognizer);
if (isPresent(alias)) {
MapWrapper.set(this.names, alias, recognizer);
}
}
recognize(url:string):List<StringMap> {
var solutions = [];
MapWrapper.forEach(this.redirects, (target, path) => {
//TODO: "/" redirect case
if (StringWrapper.startsWith(url, path)) {
url = target + StringWrapper.substring(url, path.length);
}
});
MapWrapper.forEach(this.matchers, (pathRecognizer, regex) => {
var match;
if (isPresent(match = RegExpWrapper.firstMatch(regex, url))) {
var solution = StringMapWrapper.create();
StringMapWrapper.set(solution, 'handler', pathRecognizer.handler);
StringMapWrapper.set(solution, 'params', pathRecognizer.parseParams(url));
StringMapWrapper.set(solution, 'matchedUrl', match[0]);
var unmatchedUrl = StringWrapper.substring(url, match[0].length);
StringMapWrapper.set(solution, 'unmatchedUrl', unmatchedUrl);
ListWrapper.push(solutions, solution);
}
});
return solutions;
}
hasRoute(name:string) {
return MapWrapper.contains(this.names, name);
}
generate(name:string, params:any) {
var pathRecognizer = MapWrapper.get(this.names, name);
return pathRecognizer.generate(params);
}
}

View File

@ -0,0 +1,129 @@
import {RouteRecognizer} from './route_recognizer';
import {Instruction, noopInstruction} from './instruction';
import {List, ListWrapper, Map, MapWrapper, StringMap, StringMapWrapper} from 'angular2/src/facade/collection';
import {isPresent, isBlank, isType, StringWrapper} from 'angular2/src/facade/lang';
import {RouteConfig} from './route_config';
import {reflector} from 'angular2/src/reflection/reflection';
export class RouteRegistry {
_rules:Map<any, RouteRecognizer>;
constructor() {
this._rules = MapWrapper.create();
}
config(parentComponent, path:string, component:any, alias:string = null) {
if (parentComponent === 'app') {
parentComponent = '/';
}
var recognizer:RouteRecognizer;
if (MapWrapper.contains(this._rules, parentComponent)) {
recognizer = MapWrapper.get(this._rules, parentComponent);
} else {
recognizer = new RouteRecognizer();
MapWrapper.set(this._rules, parentComponent, recognizer);
}
this._configFromComponent(component);
//TODO: support sibling components
var components = StringMapWrapper.create();
StringMapWrapper.set(components, 'default', component);
var handler = StringMapWrapper.create();
StringMapWrapper.set(handler, 'components', components);
recognizer.addConfig(path, handler, alias);
}
_configFromComponent(component) {
if (!isType(component)) {
return;
}
// Don't read the annotations from a type more than once
// this prevents an infinite loop if a component routes recursively.
if (MapWrapper.contains(this._rules, component)) {
return;
}
var annotations = reflector.annotations(component);
if (isPresent(annotations)) {
for (var i=0; i<annotations.length; i++) {
var annotation = annotations[i];
if (annotation instanceof RouteConfig) {
this.config(component, annotation.path, annotation.component);
}
}
}
}
// TODO: make recognized context a class
// TODO: change parentComponent into parentContext
recognize(url:string, parentComponent = '/') {
var componentRecognizer = MapWrapper.get(this._rules, parentComponent);
if (isBlank(componentRecognizer)) {
return null;
}
var solutions = componentRecognizer.recognize(url);
for(var i = 0; i < solutions.length; i++) {
var candidate = solutions[i];
if (candidate['unmatchedUrl'].length == 0) {
return handlerToLeafInstructions(candidate, parentComponent);
}
var children = StringMapWrapper.create(),
allMapped = true;
StringMapWrapper.forEach(candidate['handler']['components'], (component, name) => {
if (!allMapped) {
return;
}
var childInstruction = this.recognize(candidate['unmatchedUrl'], component);
if (isPresent(childInstruction)) {
childInstruction.params = candidate['params'];
children[name] = childInstruction;
} else {
allMapped = false;
}
});
if (allMapped) {
return new Instruction({
component: parentComponent,
children: children,
matchedUrl: candidate['matchedUrl']
});
}
}
return null;
}
generate(name:string, params:any) {
//TODO: implement for hierarchical routes
var componentRecognizer = MapWrapper.get(this._rules, '/');
if (isPresent(componentRecognizer)) {
return componentRecognizer.generate(name, params);
}
}
}
function handlerToLeafInstructions(context, parentComponent) {
var children = StringMapWrapper.create();
StringMapWrapper.forEach(context['handler']['components'], (component, outletName) => {
children[outletName] = new Instruction({
component: component,
params: context['params']
});
});
return new Instruction({
component: parentComponent,
children: children,
matchedUrl: context['matchedUrl']
});
}

198
modules/angular2/src/router/router.js vendored Normal file
View File

@ -0,0 +1,198 @@
import {Promise, PromiseWrapper, EventEmitter, ObservableWrapper} from 'angular2/src/facade/async';
import {Map, MapWrapper, List, ListWrapper} from 'angular2/src/facade/collection';
import {isBlank} from 'angular2/src/facade/lang';
import {RouteRegistry} from './route_registry';
import {Pipeline} from './pipeline';
import {Instruction} from './instruction';
import {RouterOutlet} from './router_outlet';
/**
* # Router
* The router is responsible for mapping URLs to components.
*
* You can see the state of the router by inspecting the read-only field `router.navigating`.
* This may be useful for showing a spinner, for instance.
*
* @exportedAs angular2/router
*/
export class Router {
name;
parent:Router;
navigating:boolean;
lastNavigationAttempt: string;
previousUrl:string;
_pipeline:Pipeline;
_registry:RouteRegistry;
_outlets:Map<any, RouterOutlet>;
_children:Map<any, Router>;
_subject:EventEmitter;
constructor(registry:RouteRegistry, pipeline:Pipeline, parent:Router = null, name = '/') {
this.name = name;
this.navigating = false;
this.parent = parent;
this.previousUrl = null;
this._outlets = MapWrapper.create();
this._children = MapWrapper.create();
this._registry = registry;
this._pipeline = pipeline;
this._subject = new EventEmitter();
}
/**
* Constructs a child router. You probably don't need to use this unless you're writing a reusable component.
*/
childRouter(outletName = 'default') {
if (!MapWrapper.contains(this._children, outletName)) {
MapWrapper.set(this._children, outletName, new ChildRouter(this, outletName));
}
return MapWrapper.get(this._children, outletName);
}
/**
* 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 = 'default'):Promise {
MapWrapper.set(this._outlets, name, outlet);
return this.renavigate();
}
/**
* Update the routing configuration and trigger a navigation.
*
* # Usage
*
* ```
* router.config('/', SomeCmp);
* ```
*/
config(path:string, component, alias:string=null) {
this._registry.config(this.name, path, component, alias);
return this.renavigate();
}
/**
* Navigate to a URL. Returns a promise that resolves to the canonical URL for the route.
*/
navigate(url:string):Promise {
if (this.navigating) {
return PromiseWrapper.resolve(true);
}
this.lastNavigationAttempt = url;
var instruction = this.recognize(url);
if (isBlank(instruction)) {
return PromiseWrapper.resolve(false);
}
instruction.router = this;
this._startNavigating();
var result = this._pipeline.process(instruction)
.then((_) => {
ObservableWrapper.callNext(this._subject, instruction.matchedUrl);
})
.then((_) => this._finishNavigating());
PromiseWrapper.catchError(result, (_) => this._finishNavigating());
return result;
}
_startNavigating() {
this.navigating = true;
}
_finishNavigating() {
this.navigating = false;
}
/**
* Subscribe to URL updates from the router
*/
subscribe(onNext) {
ObservableWrapper.subscribe(this._subject, onNext);
}
activateOutlets(instruction:Instruction):Promise {
return this._queryOutlets((outlet, name) => {
return outlet.activate(instruction.getChildInstruction(name));
})
.then((_) => instruction.mapChildrenAsync((instruction, _) => {
return instruction.router.activateOutlets(instruction);
}));
}
traverseOutlets(fn):Promise {
return this._queryOutlets(fn)
.then((_) => mapObjAsync(this._children, (child, _) => child.traverseOutlets(fn)));
}
_queryOutlets(fn):Promise {
return mapObjAsync(this._outlets, fn);
}
/**
* Given a URL, returns an instruction representing the component graph
*/
recognize(url:string) {
return this._registry.recognize(url);
}
/**
* Navigates to either the last URL successfully navigated to, or the last URL requested if the router has yet to successfully navigate.
*/
renavigate():Promise {
var destination = isBlank(this.previousUrl) ? this.lastNavigationAttempt : this.previousUrl;
if (this.navigating || isBlank(destination)) {
return PromiseWrapper.resolve(false);
}
return this.navigate(destination);
}
/**
* 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:any) {
return this._registry.generate(name, params);
}
static getRoot():Router {
return new RootRouter(new Pipeline());
}
}
export class RootRouter extends Router {
constructor(pipeline:Pipeline) {
super(new RouteRegistry(), pipeline, null, '/');
}
}
class ChildRouter extends Router {
constructor(parent, name) {
super(parent._registry, parent._pipeline, parent, name);
this.parent = parent;
}
}
function mapObjAsync(obj:Map, fn) {
return PromiseWrapper.all(mapObj(obj, fn));
}
function mapObj(obj:Map, fn):List {
var result = ListWrapper.create();
MapWrapper.forEach(obj, (value, key) => ListWrapper.push(result, fn(value, key)));
return result;
}

View File

@ -0,0 +1,65 @@
import {Decorator} from 'angular2/annotations';
import {NgElement} from 'angular2/core';
import {isPresent} from 'angular2/src/facade/lang';
import {DOM} from 'angular2/src/dom/dom_adapter';
import {Router} from './router';
/**
* The RouterLink directive lets you link to specific parts of your app.
*
*
* Consider the following route configuration:
* ```
* @RouteConfig({
* path: '/user', component: UserCmp, alias: 'user'
* });
* class MyComp {}
* ```
*
* When linking to a route, you can write:
*
* ```
* <a router-link="user">link to user component</a>
* ```
*
* @exportedAs angular2/router
*/
@Decorator({
selector: '[router-link]',
properties: {
'route': 'routerLink',
'params': 'routerParams'
}
})
export class RouterLink {
_domEl;
_route:string;
_params:any;
_router:Router;
//TODO: handle click events
constructor(ngEl:NgElement, router:Router) {
this._domEl = ngEl.domElement;
this._router = router;
}
set route(changes) {
this._route = changes;
this.updateHref();
}
set params(changes) {
this._params = changes;
this.updateHref();
}
updateHref() {
if (isPresent(this._route) && isPresent(this._params)) {
var newHref = this._router.generate(this._route, this._params);
DOM.setAttribute(this._domEl, 'href', newHref);
}
}
}

View File

@ -0,0 +1,46 @@
import {Promise, PromiseWrapper} from 'angular2/src/facade/async';
import {Decorator} from 'angular2/annotations';
import {Compiler, ViewContainerRef} from 'angular2/core';
import {Injector, bind} from 'angular2/di';
import * as routerMod from './router';
import {Instruction, RouteParams} from './instruction'
@Decorator({
selector: 'router-outlet'
})
export class RouterOutlet {
_compiler:Compiler;
_injector:Injector;
_router:routerMod.Router;
_viewContainer:ViewContainerRef;
constructor(viewContainer:ViewContainerRef, compiler:Compiler, router:routerMod.Router, injector:Injector) {
this._router = router;
this._viewContainer = viewContainer;
this._compiler = compiler;
this._injector = injector;
this._router.registerOutlet(this);
}
activate(instruction:Instruction) {
return this._compiler.compileInHost(instruction.component).then((pv) => {
var outletInjector = this._injector.resolveAndCreateChild([
bind(RouteParams).toValue(new RouteParams(instruction.params)),
bind(routerMod.Router).toValue(instruction.router)
]);
this._viewContainer.clear();
this._viewContainer.create(0, pv, outletInjector);
});
}
canActivate(instruction:any) {
return PromiseWrapper.resolve(true);
}
canDeactivate(instruction:any) {
return PromiseWrapper.resolve(true);
}
}

13
modules/angular2/src/router/url.js vendored Normal file
View File

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

View File

@ -0,0 +1,156 @@
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, Viewport} from 'angular2/annotations';
import {View} from 'angular2/src/core/annotations/view';
import {RootRouter} from 'angular2/src/router/router';
import {Pipeline} from 'angular2/src/router/pipeline';
import {Router, RouterOutlet, RouterLink, RouteConfig, RouteParams} from 'angular2/router';
import {DOM} from 'angular2/src/dom/dom_adapter';
export function main() {
describe('Outlet Directive', () => {
var ctx, tb, view, router;
beforeEach(inject([TestBed], (testBed) => {
tb = testBed;
ctx = new MyComp();
}));
beforeEachBindings(() => {
router = new RootRouter(new Pipeline());
return [
bind(Router).toValue(router)
];
});
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((_) => router.config('/test', HelloCmp))
.then((_) => router.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((_) => router.config('/user/:name', UserCmp))
.then((_) => router.navigate('/user/brian'))
.then((_) => {
view.detectChanges();
expect(view.rootNodes).toHaveText('hello brian');
})
.then((_) => router.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((_) => router.config('/a', ParentCmp))
.then((_) => router.navigate('/a/b'))
.then((_) => {
view.detectChanges();
expect(view.rootNodes).toHaveText('outer { inner { hello } }');
async.done();
});
}));
it('should generate link hrefs', inject([AsyncTestCompleter], (async) => {
ctx.name = 'brian';
compile('<a href="hello" router-link="user" [router-params]="{name: name}">{{name}}</a>')
.then((_) => router.config('/user/:name', UserCmp, 'user'))
.then((_) => router.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();
});
}));
});
}
@Component({
selector: 'hello-cmp'
})
@View({
template: "{{greeting}}"
})
class HelloCmp {
greeting:string;
constructor() {
this.greeting = "hello";
}
}
@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()
class MyComp {
name;
}

View File

@ -0,0 +1,61 @@
import {
AsyncTestCompleter,
describe,
it, iit,
ddescribe, expect,
inject, beforeEach,
SpyObject} from 'angular2/test_lib';
import {RouteRecognizer} from 'angular2/src/router/route_recognizer';
export function main() {
describe('RouteRecognizer', () => {
var recognizer;
var handler = {
'components': { 'a': 'b' }
};
beforeEach(() => {
recognizer = new RouteRecognizer();
});
it('should work with a static segment', () => {
recognizer.addConfig('/test', handler);
expect(recognizer.recognize('/test')[0]).toEqual({
'handler': { 'components': { 'a': 'b' } },
'params': {},
'matchedUrl': '/test',
'unmatchedUrl': ''
});
});
it('should work with a dynamic segment', () => {
recognizer.addConfig('/user/:name', handler);
expect(recognizer.recognize('/user/brian')[0]).toEqual({
'handler': handler,
'params': { 'name': 'brian' },
'matchedUrl': '/user/brian',
'unmatchedUrl': ''
});
});
it('should allow redirects', () => {
recognizer.addRedirect('/a', '/b');
recognizer.addConfig('/b', handler);
var solutions = recognizer.recognize('/a');
expect(solutions.length).toBe(1);
expect(solutions[0]).toEqual({
'handler': handler,
'params': {},
'matchedUrl': '/b',
'unmatchedUrl': ''
});
});
it('should generate URLs', () => {
recognizer.addConfig('/app/user/:name', handler, 'user');
expect(recognizer.generate('user', {'name' : 'misko'})).toEqual('/app/user/misko');
});
});
}

View File

@ -0,0 +1,41 @@
import {
AsyncTestCompleter,
describe,
it, iit,
ddescribe, expect,
inject, beforeEach,
SpyObject} from 'angular2/test_lib';
import {RouteRegistry} from 'angular2/src/router/route_registry';
export function main() {
describe('RouteRegistry', () => {
var registry;
var handler = {};
var handler2 = {};
beforeEach(() => {
registry = new RouteRegistry();
});
it('should match the full URL', () => {
registry.config('/', '/', handler);
registry.config('/', '/test', handler2);
var instruction = registry.recognize('/test');
expect(instruction.getChildInstruction('default').component).toBe(handler2);
});
it('should match the full URL recursively', () => {
registry.config('/', '/first', handler);
registry.config(handler, '/second', handler2);
var instruction = registry.recognize('/first/second');
expect(instruction.getChildInstruction('default').component).toBe(handler);
expect(instruction.getChildInstruction('default').getChildInstruction('default').component).toBe(handler2);
});
});
}

View File

@ -0,0 +1,53 @@
import {
AsyncTestCompleter,
describe,
proxy,
it, iit,
ddescribe, expect,
inject, beforeEach,
SpyObject} from 'angular2/test_lib';
import {IMPLEMENTS} from 'angular2/src/facade/lang';
import {Promise, PromiseWrapper} from 'angular2/src/facade/async';
import {RootRouter, Viewport} from 'angular2/src/router/router';
import {Pipeline} from 'angular2/src/router/pipeline';
import {RouterOutlet} from 'angular2/src/router/router_outlet';
export function main() {
describe('Router', () => {
var router;
beforeEach(() => {
router = new RootRouter(new Pipeline());
});
it('should navigate after being configured', inject([AsyncTestCompleter], (async) => {
var outlet = makeDummyRef();
router.registerOutlet(outlet)
.then((_) => router.navigate('/a'))
.then((_) => {
expect(outlet.spy('activate')).not.toHaveBeenCalled();
return router.config('/a', {'component': 'A' });
})
.then((_) => {
expect(outlet.spy('activate')).toHaveBeenCalled();
async.done();
});
}));
});
}
@proxy
@IMPLEMENTS(RouterOutlet)
class DummyOutletRef extends SpyObject {noSuchMethod(m){return super.noSuchMethod(m)}}
function makeDummyRef() {
var ref = new DummyOutletRef();
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;
}