feat(router): add initial implementation
This commit is contained in:
parent
e617ca6323
commit
1b2754dacd
|
@ -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',
|
||||
|
|
|
@ -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';
|
|
@ -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();
|
|
@ -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)), '');
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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']
|
||||
});
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
});
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
|
||||
});
|
||||
}
|
|
@ -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;
|
||||
}
|
Loading…
Reference in New Issue