feat(router): add routing to async components
Note that this also removes the `components` option from `RouteConfig`. This functionality will be reintroduced with the more general `//` routing. See #2329 for more details.
This commit is contained in:
parent
548f3dd5cc
commit
cd95e078fe
|
@ -6,7 +6,6 @@ import {
|
||||||
List,
|
List,
|
||||||
ListWrapper
|
ListWrapper
|
||||||
} from 'angular2/src/facade/collection';
|
} from 'angular2/src/facade/collection';
|
||||||
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 {
|
||||||
|
@ -20,7 +19,7 @@ export class RouteParams {
|
||||||
*/
|
*/
|
||||||
export class Instruction {
|
export class Instruction {
|
||||||
component: any;
|
component: any;
|
||||||
private _children: StringMap<string, Instruction>;
|
child: Instruction;
|
||||||
|
|
||||||
// the part of the URL captured by this instruction
|
// the part of the URL captured by this instruction
|
||||||
capturedUrl: string;
|
capturedUrl: string;
|
||||||
|
@ -32,10 +31,10 @@ export class Instruction {
|
||||||
reuse: boolean;
|
reuse: boolean;
|
||||||
specificity: number;
|
specificity: number;
|
||||||
|
|
||||||
constructor({params, component, children, matchedUrl, parentSpecificity}: {
|
constructor({params, component, child, matchedUrl, parentSpecificity}: {
|
||||||
params?: StringMap<string, any>,
|
params?: StringMap<string, any>,
|
||||||
component?: any,
|
component?: any,
|
||||||
children?: StringMap<string, Instruction>,
|
child?: Instruction,
|
||||||
matchedUrl?: string,
|
matchedUrl?: string,
|
||||||
parentSpecificity?: number
|
parentSpecificity?: number
|
||||||
} = {}) {
|
} = {}) {
|
||||||
|
@ -43,61 +42,32 @@ export class Instruction {
|
||||||
this.capturedUrl = matchedUrl;
|
this.capturedUrl = matchedUrl;
|
||||||
this.accumulatedUrl = matchedUrl;
|
this.accumulatedUrl = matchedUrl;
|
||||||
this.specificity = parentSpecificity;
|
this.specificity = parentSpecificity;
|
||||||
if (isPresent(children)) {
|
if (isPresent(child)) {
|
||||||
this._children = children;
|
this.child = child;
|
||||||
var childUrl;
|
|
||||||
StringMapWrapper.forEach(this._children, (child, _) => {
|
|
||||||
childUrl = child.accumulatedUrl;
|
|
||||||
this.specificity += child.specificity;
|
this.specificity += child.specificity;
|
||||||
});
|
var childUrl = child.accumulatedUrl;
|
||||||
if (isPresent(childUrl)) {
|
if (isPresent(childUrl)) {
|
||||||
this.accumulatedUrl += childUrl;
|
this.accumulatedUrl += childUrl;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this._children = StringMapWrapper.create();
|
this.child = null;
|
||||||
}
|
}
|
||||||
this.component = component;
|
this.component = component;
|
||||||
this.params = params;
|
this.params = params;
|
||||||
}
|
}
|
||||||
|
|
||||||
hasChild(outletName: string): boolean {
|
hasChild(): boolean { return isPresent(this.child); }
|
||||||
return StringMapWrapper.contains(this._children, outletName);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the child instruction with the given outlet name
|
|
||||||
*/
|
|
||||||
getChild(outletName: string): Instruction {
|
|
||||||
return StringMapWrapper.get(this._children, outletName);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* (child:Instruction, outletName:string) => {}
|
|
||||||
*/
|
|
||||||
forEachChild(fn: Function): void { StringMapWrapper.forEach(this._children, fn); }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Does a synchronous, breadth-first traversal of the graph of instructions.
|
|
||||||
* Takes a function with signature:
|
|
||||||
* (child:Instruction, outletName:string) => {}
|
|
||||||
*/
|
|
||||||
traverseSync(fn: Function): void {
|
|
||||||
this.forEachChild(fn);
|
|
||||||
this.forEachChild((childInstruction, _) => childInstruction.traverseSync(fn));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Takes a currently active instruction and sets a reuse flag on each of this instruction's
|
* Takes a currently active instruction and sets a reuse flag on each of this instruction's
|
||||||
* children
|
* children
|
||||||
*/
|
*/
|
||||||
reuseComponentsFrom(oldInstruction: Instruction): void {
|
reuseComponentsFrom(oldInstruction: Instruction): void {
|
||||||
this.traverseSync((childInstruction, outletName) => {
|
var nextInstruction = this;
|
||||||
var oldInstructionChild = oldInstruction.getChild(outletName);
|
while (nextInstruction.reuse = shouldReuseComponent(nextInstruction, oldInstruction) &&
|
||||||
if (shouldReuseComponent(childInstruction, oldInstructionChild)) {
|
isPresent(oldInstruction = oldInstruction.child) &&
|
||||||
childInstruction.reuse = true;
|
isPresent(nextInstruction = nextInstruction.child))
|
||||||
}
|
;
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -105,13 +75,3 @@ 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<string, any>, fn): Promise<List<any>> {
|
|
||||||
return PromiseWrapper.all(mapObj(obj, fn));
|
|
||||||
}
|
|
||||||
|
|
||||||
function mapObj(obj: StringMap<any, any>, fn: Function): List<any> {
|
|
||||||
var result = ListWrapper.create();
|
|
||||||
StringMapWrapper.forEach(obj, (value, key) => ListWrapper.push(result, fn(value, key)));
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ import {List, Map} from 'angular2/src/facade/collection';
|
||||||
*
|
*
|
||||||
* Supported keys:
|
* Supported keys:
|
||||||
* - `path` (required)
|
* - `path` (required)
|
||||||
* - `component`, `components`, `redirectTo` (requires exactly one of these)
|
* - `component`, `redirectTo` (requires exactly one of these)
|
||||||
* - `as` (optional)
|
* - `as` (optional)
|
||||||
*/
|
*/
|
||||||
@CONST()
|
@CONST()
|
||||||
|
|
|
@ -8,7 +8,16 @@ import {
|
||||||
StringMap,
|
StringMap,
|
||||||
StringMapWrapper
|
StringMapWrapper
|
||||||
} from 'angular2/src/facade/collection';
|
} from 'angular2/src/facade/collection';
|
||||||
import {isPresent, isBlank, isType, StringWrapper, BaseException} from 'angular2/src/facade/lang';
|
import {Promise, PromiseWrapper} from 'angular2/src/facade/async';
|
||||||
|
import {
|
||||||
|
isPresent,
|
||||||
|
isBlank,
|
||||||
|
isType,
|
||||||
|
isMap,
|
||||||
|
isFunction,
|
||||||
|
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';
|
||||||
|
|
||||||
|
@ -26,16 +35,7 @@ export class RouteRegistry {
|
||||||
* 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')) {
|
assertValidConfig(config);
|
||||||
throw new BaseException('Route config does not contain "path"');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!StringMapWrapper.contains(config, 'component') &&
|
|
||||||
!StringMapWrapper.contains(config, 'components') &&
|
|
||||||
!StringMapWrapper.contains(config, '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);
|
||||||
|
|
||||||
|
@ -44,19 +44,21 @@ export class RouteRegistry {
|
||||||
MapWrapper.set(this._rules, parentComponent, recognizer);
|
MapWrapper.set(this._rules, parentComponent, recognizer);
|
||||||
}
|
}
|
||||||
|
|
||||||
config = normalizeConfig(config);
|
|
||||||
|
|
||||||
if (StringMapWrapper.contains(config, 'redirectTo')) {
|
if (StringMapWrapper.contains(config, 'redirectTo')) {
|
||||||
recognizer.addRedirect(config['path'], config['redirectTo']);
|
recognizer.addRedirect(config['path'], config['redirectTo']);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var components = config['components'];
|
config = StringMapWrapper.merge(
|
||||||
StringMapWrapper.forEach(components, (component, _) => this.configFromComponent(component));
|
config, {'component': normalizeComponentDeclaration(config['component'])});
|
||||||
|
|
||||||
|
var component = config['component'];
|
||||||
|
this.configFromComponent(component);
|
||||||
|
|
||||||
recognizer.addConfig(config['path'], config, config['as']);
|
recognizer.addConfig(config['path'], config, config['as']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reads the annotations of a component and configures the registry based on them
|
* Reads the annotations of a component and configures the registry based on them
|
||||||
*/
|
*/
|
||||||
|
@ -87,69 +89,58 @@ 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): Promise<Instruction> {
|
||||||
var componentRecognizer = MapWrapper.get(this._rules, parentComponent);
|
var componentRecognizer = MapWrapper.get(this._rules, parentComponent);
|
||||||
if (isBlank(componentRecognizer)) {
|
if (isBlank(componentRecognizer)) {
|
||||||
return null;
|
return PromiseWrapper.resolve(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Matches some beginning part of the given URL
|
// Matches some beginning part of the given URL
|
||||||
var possibleMatches = componentRecognizer.recognize(url);
|
var possibleMatches = componentRecognizer.recognize(url);
|
||||||
|
var matchPromises =
|
||||||
|
ListWrapper.map(possibleMatches, (candidate) => this._completeRouteMatch(candidate));
|
||||||
|
|
||||||
// A list of instructions that captures all of the given URL
|
return PromiseWrapper.all(matchPromises)
|
||||||
var fullSolutions = ListWrapper.create();
|
.then((solutions) => {
|
||||||
|
// remove nulls
|
||||||
for (var i = 0; i < possibleMatches.length; i++) {
|
var fullSolutions = ListWrapper.filter(solutions, (solution) => isPresent(solution));
|
||||||
var candidate: RouteMatch = possibleMatches[i];
|
|
||||||
|
|
||||||
// if the candidate captures all of the URL, add it to our list of solutions
|
|
||||||
if (candidate.unmatchedUrl.length == 0) {
|
|
||||||
ListWrapper.push(fullSolutions, routeMatchToInstruction(candidate, parentComponent));
|
|
||||||
} else {
|
|
||||||
// otherwise, recursively match the remaining part of the URL against the component's
|
|
||||||
// children
|
|
||||||
var children = StringMapWrapper.create(), allChildrenMatch = true,
|
|
||||||
components = StringMapWrapper.get(candidate.handler, 'components');
|
|
||||||
|
|
||||||
var componentNames = StringMapWrapper.keys(components);
|
|
||||||
for (var nameIndex = 0; nameIndex < componentNames.length; nameIndex++) {
|
|
||||||
var name = componentNames[nameIndex];
|
|
||||||
var component = StringMapWrapper.get(components, name);
|
|
||||||
|
|
||||||
var childInstruction = this.recognize(candidate.unmatchedUrl, component);
|
|
||||||
if (isPresent(childInstruction)) {
|
|
||||||
childInstruction.params = candidate.params;
|
|
||||||
children[name] = childInstruction;
|
|
||||||
} else {
|
|
||||||
allChildrenMatch = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (allChildrenMatch) {
|
|
||||||
ListWrapper.push(fullSolutions, new Instruction({
|
|
||||||
component: parentComponent,
|
|
||||||
children: children,
|
|
||||||
matchedUrl: candidate.matchedUrl,
|
|
||||||
parentSpecificity: candidate.specificity
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fullSolutions.length > 0) {
|
if (fullSolutions.length > 0) {
|
||||||
var mostSpecificSolution = fullSolutions[0];
|
return mostSpecific(fullSolutions);
|
||||||
for (var solutionIndex = 1; solutionIndex < fullSolutions.length; solutionIndex++) {
|
|
||||||
var solution = fullSolutions[solutionIndex];
|
|
||||||
if (solution.specificity > mostSpecificSolution.specificity) {
|
|
||||||
mostSpecificSolution = solution;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return mostSpecificSolution;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
_completeRouteMatch(candidate: RouteMatch): Promise<Instruction> {
|
||||||
|
return componentHandlerToComponentType(candidate.handler)
|
||||||
|
.then((componentType) => {
|
||||||
|
this.configFromComponent(componentType);
|
||||||
|
|
||||||
|
if (candidate.unmatchedUrl.length == 0) {
|
||||||
|
return new Instruction({
|
||||||
|
component: componentType,
|
||||||
|
params: candidate.params,
|
||||||
|
matchedUrl: candidate.matchedUrl,
|
||||||
|
parentSpecificity: candidate.specificity
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.recognize(candidate.unmatchedUrl, componentType)
|
||||||
|
.then(childInstruction => {
|
||||||
|
if (isBlank(childInstruction)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return new Instruction({
|
||||||
|
component: componentType,
|
||||||
|
child: childInstruction,
|
||||||
|
params: candidate.params,
|
||||||
|
matchedUrl: candidate.matchedUrl,
|
||||||
|
parentSpecificity: candidate.specificity
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
generate(name: string, params: StringMap<string, string>, hostComponent): string {
|
generate(name: string, params: StringMap<string, string>, hostComponent): string {
|
||||||
|
@ -159,42 +150,74 @@ export class RouteRegistry {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function routeMatchToInstruction(routeMatch: RouteMatch, parentComponent): Instruction {
|
|
||||||
var children = StringMapWrapper.create();
|
|
||||||
var components = StringMapWrapper.get(routeMatch.handler, 'components');
|
|
||||||
StringMapWrapper.forEach(components, (component, outletName) => {
|
|
||||||
children[outletName] =
|
|
||||||
new Instruction({component: component, params: routeMatch.params, parentSpecificity: 0});
|
|
||||||
});
|
|
||||||
return new Instruction({
|
|
||||||
component: parentComponent,
|
|
||||||
children: children,
|
|
||||||
matchedUrl: routeMatch.matchedUrl,
|
|
||||||
parentSpecificity: routeMatch.specificity
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Given a config object:
|
* A config should have a "path" property, and exactly one of:
|
||||||
* { 'component': Foo }
|
* - `component`
|
||||||
* Returns a new config object:
|
* - `redirectTo`
|
||||||
* { components: { default: Foo } }
|
|
||||||
*
|
|
||||||
* If the config object does not contain a `component` key, the original
|
|
||||||
* config object is returned.
|
|
||||||
*/
|
*/
|
||||||
function normalizeConfig(config: StringMap<string, any>): StringMap<string, any> {
|
var ALLOWED_TARGETS = ['component', 'redirectTo'];
|
||||||
if (!StringMapWrapper.contains(config, 'component')) {
|
function assertValidConfig(config: StringMap<string, any>): void {
|
||||||
return config;
|
if (!StringMapWrapper.contains(config, 'path')) {
|
||||||
|
throw new BaseException(`Route config should contain a "path" property`);
|
||||||
}
|
}
|
||||||
var newConfig = {'components': {'default': config['component']}};
|
var targets = 0;
|
||||||
|
ListWrapper.forEach(ALLOWED_TARGETS, (target) => {
|
||||||
StringMapWrapper.forEach(config, (value, key) => {
|
if (StringMapWrapper.contains(config, target)) {
|
||||||
if (key != 'component' && key != 'components') {
|
targets += 1;
|
||||||
newConfig[key] = value;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
if (targets != 1) {
|
||||||
return newConfig;
|
throw new BaseException(
|
||||||
|
`Route config should contain exactly one 'component', or 'redirectTo' property`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Returns a StringMap like: `{ 'constructor': SomeType, 'type': 'constructor' }`
|
||||||
|
*/
|
||||||
|
var VALID_COMPONENT_TYPES = ['constructor', 'loader'];
|
||||||
|
function normalizeComponentDeclaration(config: any): StringMap<string, any> {
|
||||||
|
if (isType(config)) {
|
||||||
|
return {'constructor': config, 'type': 'constructor'};
|
||||||
|
} else if (isMap(config)) {
|
||||||
|
if (isBlank(config['type'])) {
|
||||||
|
throw new BaseException(
|
||||||
|
`Component declaration when provided as a map should include a 'type' property`);
|
||||||
|
}
|
||||||
|
var componentType = config['type'];
|
||||||
|
if (!ListWrapper.contains(VALID_COMPONENT_TYPES, componentType)) {
|
||||||
|
throw new BaseException(`Invalid component type '${componentType}'`);
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
} else {
|
||||||
|
throw new BaseException(`Component declaration should be either a Map or a Type`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function componentHandlerToComponentType(handler): Promise<any> {
|
||||||
|
var componentDeclaration = handler['component'], type = componentDeclaration['type'];
|
||||||
|
|
||||||
|
if (type == 'constructor') {
|
||||||
|
return PromiseWrapper.resolve(componentDeclaration['constructor']);
|
||||||
|
} else if (type == 'loader') {
|
||||||
|
var resolverFunction = componentDeclaration['loader'];
|
||||||
|
return resolverFunction();
|
||||||
|
} else {
|
||||||
|
throw new BaseException(`Cannot extract the component type from a '${type}' component`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Given a list of instructions, returns the most specific instruction
|
||||||
|
*/
|
||||||
|
function mostSpecific(instructions: List<Instruction>): Instruction {
|
||||||
|
var mostSpecificSolution = instructions[0];
|
||||||
|
for (var solutionIndex = 1; solutionIndex < instructions.length; solutionIndex++) {
|
||||||
|
var solution = instructions[solutionIndex];
|
||||||
|
if (solution.specificity > mostSpecificSolution.specificity) {
|
||||||
|
mostSpecificSolution = solution;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return mostSpecificSolution;
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,7 +33,8 @@ export class Router {
|
||||||
previousUrl: string;
|
previousUrl: string;
|
||||||
|
|
||||||
private _currentInstruction: Instruction;
|
private _currentInstruction: Instruction;
|
||||||
private _outlets: Map<any, RouterOutlet>;
|
private _currentNavigation: Promise<any>;
|
||||||
|
private _outlet: RouterOutlet;
|
||||||
private _subject: EventEmitter;
|
private _subject: EventEmitter;
|
||||||
// todo(jeffbcross): rename _registry to registry since it is accessed from subclasses
|
// todo(jeffbcross): rename _registry to registry since it is accessed from subclasses
|
||||||
// todo(jeffbcross): rename _pipeline to pipeline since it is accessed from subclasses
|
// todo(jeffbcross): rename _pipeline to pipeline since it is accessed from subclasses
|
||||||
|
@ -41,9 +42,10 @@ export class Router {
|
||||||
public hostComponent: any) {
|
public hostComponent: any) {
|
||||||
this.navigating = false;
|
this.navigating = false;
|
||||||
this.previousUrl = null;
|
this.previousUrl = null;
|
||||||
this._outlets = MapWrapper.create();
|
this._outlet = null;
|
||||||
this._subject = new EventEmitter();
|
this._subject = new EventEmitter();
|
||||||
this._currentInstruction = null;
|
this._currentInstruction = null;
|
||||||
|
this._currentNavigation = PromiseWrapper.resolve(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -58,11 +60,11 @@ export class Router {
|
||||||
* Register an object to notify of route changes. You probably don't need to use this unless
|
* Register an object to notify of route changes. You probably don't need to use this unless
|
||||||
* you're writing a reusable component.
|
* you're writing a reusable component.
|
||||||
*/
|
*/
|
||||||
registerOutlet(outlet: RouterOutlet, name: string = 'default'): Promise<boolean> {
|
registerOutlet(outlet: RouterOutlet): Promise<boolean> {
|
||||||
MapWrapper.set(this._outlets, name, outlet);
|
// TODO: sibling routes
|
||||||
|
this._outlet = outlet;
|
||||||
if (isPresent(this._currentInstruction)) {
|
if (isPresent(this._currentInstruction)) {
|
||||||
var childInstruction = this._currentInstruction.getChild(name);
|
return outlet.activate(this._currentInstruction);
|
||||||
return outlet.activate(childInstruction);
|
|
||||||
}
|
}
|
||||||
return PromiseWrapper.resolve(true);
|
return PromiseWrapper.resolve(true);
|
||||||
}
|
}
|
||||||
|
@ -85,7 +87,6 @@ export class Router {
|
||||||
* { 'path': '/user/:id', 'component': UserComp },
|
* { 'path': '/user/:id', 'component': UserComp },
|
||||||
* ]);
|
* ]);
|
||||||
* ```
|
* ```
|
||||||
*
|
|
||||||
*/
|
*/
|
||||||
config(config: any): Promise<any> {
|
config(config: any): Promise<any> {
|
||||||
if (config instanceof List) {
|
if (config instanceof List) {
|
||||||
|
@ -99,20 +100,17 @@ export class Router {
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Navigate to a URL. Returns a promise that resolves to the canonical URL for the route.
|
* Navigate to a URL. Returns a promise that resolves when navigation is complete.
|
||||||
*
|
*
|
||||||
* 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<any> {
|
navigate(url: string): Promise<any> {
|
||||||
if (this.navigating) {
|
if (this.navigating) {
|
||||||
return PromiseWrapper.resolve(true);
|
return this._currentNavigation;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.lastNavigationAttempt = url;
|
this.lastNavigationAttempt = url;
|
||||||
|
return this._currentNavigation = this.recognize(url).then((matchedInstruction) => {
|
||||||
var matchedInstruction = this.recognize(url);
|
|
||||||
|
|
||||||
if (isBlank(matchedInstruction)) {
|
if (isBlank(matchedInstruction)) {
|
||||||
return PromiseWrapper.resolve(false);
|
return PromiseWrapper.resolve(false);
|
||||||
}
|
}
|
||||||
|
@ -123,15 +121,17 @@ 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);
|
|
||||||
this._finishNavigating();
|
this._finishNavigating();
|
||||||
|
ObservableWrapper.callNext(this._subject, matchedInstruction.accumulatedUrl);
|
||||||
});
|
});
|
||||||
|
|
||||||
PromiseWrapper.catchError(result, (_) => this._finishNavigating());
|
PromiseWrapper.catchError(result, (_) => this._finishNavigating());
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_startNavigating(): void { this.navigating = true; }
|
_startNavigating(): void { this.navigating = true; }
|
||||||
|
@ -146,49 +146,34 @@ export class Router {
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
* Updates this router and all descendant routers according to the given instruction
|
||||||
*/
|
*/
|
||||||
commit(instruction: Instruction): Promise<List<any>> {
|
commit(instruction: Instruction): Promise<any> {
|
||||||
this._currentInstruction = instruction;
|
this._currentInstruction = instruction;
|
||||||
|
if (isPresent(this._outlet)) {
|
||||||
// collect all outlets that do not have a corresponding child instruction
|
return this._outlet.activate(instruction);
|
||||||
// and remove them from the internal map of child outlets
|
|
||||||
var toDeactivate = ListWrapper.create();
|
|
||||||
MapWrapper.forEach(this._outlets, (outlet, outletName) => {
|
|
||||||
if (!instruction.hasChild(outletName)) {
|
|
||||||
MapWrapper.delete(this._outlets, outletName);
|
|
||||||
ListWrapper.push(toDeactivate, outlet);
|
|
||||||
}
|
}
|
||||||
});
|
return PromiseWrapper.resolve(true);
|
||||||
|
|
||||||
return PromiseWrapper.all(ListWrapper.map(toDeactivate, (outlet) => outlet.deactivate()))
|
|
||||||
.then((_) => this.activate(instruction));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Recursively remove all components contained by this router's outlets.
|
* Removes the contents of this router's outlet and all descendant outlets
|
||||||
* Calls deactivate hooks on all descendant components
|
|
||||||
*/
|
*/
|
||||||
deactivate(): Promise<any> { return this._eachOutletAsync((outlet) => outlet.deactivate); }
|
deactivate(): Promise<any> {
|
||||||
|
if (isPresent(this._outlet)) {
|
||||||
|
return this._outlet.deactivate();
|
||||||
/**
|
}
|
||||||
* Recursively activate.
|
return PromiseWrapper.resolve(true);
|
||||||
* Calls the "activate" hook on descendant components.
|
|
||||||
*/
|
|
||||||
activate(instruction: Instruction): Promise<any> {
|
|
||||||
return this._eachOutletAsync((outlet, name) => outlet.activate(instruction.getChild(name)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
_eachOutletAsync(fn): Promise<any> { 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 { return this._registry.recognize(url, this.hostComponent); }
|
recognize(url: string): Promise<Instruction> {
|
||||||
|
return this._registry.recognize(url, this.hostComponent);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -197,8 +182,8 @@ export class Router {
|
||||||
*/
|
*/
|
||||||
renavigate(): Promise<any> {
|
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 (isBlank(destination)) {
|
||||||
return PromiseWrapper.resolve(false);
|
return this._currentNavigation;
|
||||||
}
|
}
|
||||||
return this.navigate(destination);
|
return this.navigate(destination);
|
||||||
}
|
}
|
||||||
|
@ -237,13 +222,3 @@ class ChildRouter extends Router {
|
||||||
this.parent = parent;
|
this.parent = parent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapObjAsync(obj: Map<any, any>, fn: Function): Promise<any> {
|
|
||||||
return PromiseWrapper.all(mapObj(obj, fn));
|
|
||||||
}
|
|
||||||
|
|
||||||
function mapObj(obj: Map<any, any>, fn: Function): List<any> {
|
|
||||||
var result = ListWrapper.create();
|
|
||||||
MapWrapper.forEach(obj, (value, key) => ListWrapper.push(result, fn(value, key)));
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
|
@ -17,14 +17,6 @@ import {Instruction, RouteParams} from './instruction'
|
||||||
* ```
|
* ```
|
||||||
* <router-outlet></router-outlet>
|
* <router-outlet></router-outlet>
|
||||||
* ```
|
* ```
|
||||||
*
|
|
||||||
* Route outlets can also optionally have a name:
|
|
||||||
*
|
|
||||||
* ```
|
|
||||||
* <router-outlet name="side"></router-outlet>
|
|
||||||
* <router-outlet name="main"></router-outlet>
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
*/
|
*/
|
||||||
@Directive({
|
@Directive({
|
||||||
selector: 'router-outlet'
|
selector: 'router-outlet'
|
||||||
|
@ -38,28 +30,29 @@ export class RouterOutlet {
|
||||||
constructor(elementRef: ElementRef, private _loader: DynamicComponentLoader,
|
constructor(elementRef: ElementRef, private _loader: DynamicComponentLoader,
|
||||||
private _parentRouter: routerMod.Router, private _injector: Injector,
|
private _parentRouter: routerMod.Router, private _injector: Injector,
|
||||||
@Attribute('name') nameAttr: string) {
|
@Attribute('name') nameAttr: string) {
|
||||||
if (isBlank(nameAttr)) {
|
// TODO: reintroduce with new // sibling routes
|
||||||
nameAttr = 'default';
|
// if (isBlank(nameAttr)) {
|
||||||
}
|
// nameAttr = 'default';
|
||||||
|
//}
|
||||||
|
|
||||||
this._elementRef = elementRef;
|
this._elementRef = elementRef;
|
||||||
|
|
||||||
this._childRouter = null;
|
this._childRouter = null;
|
||||||
this._componentRef = null;
|
this._componentRef = null;
|
||||||
this._currentInstruction = null;
|
this._currentInstruction = null;
|
||||||
this._parentRouter.registerOutlet(this, nameAttr);
|
this._parentRouter.registerOutlet(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Given an instruction, update the contents of this viewport.
|
* Given an instruction, update the contents of this outlet.
|
||||||
*/
|
*/
|
||||||
activate(instruction: Instruction): Promise<any> {
|
activate(instruction: Instruction): Promise<any> {
|
||||||
// if we're able to reuse the component, we just have to pass along the instruction to the
|
// if we're able to reuse the component, we just have to pass along the instruction to the
|
||||||
// component's router
|
// component's router
|
||||||
// so it can propagate changes to its children
|
// so it can propagate changes to its children
|
||||||
if ((instruction == this._currentInstruction) ||
|
if ((instruction == this._currentInstruction || instruction.reuse) &&
|
||||||
instruction.reuse && isPresent(this._childRouter)) {
|
isPresent(this._childRouter)) {
|
||||||
return this._childRouter.commit(instruction);
|
return this._childRouter.commit(instruction.child);
|
||||||
}
|
}
|
||||||
|
|
||||||
this._currentInstruction = instruction;
|
this._currentInstruction = instruction;
|
||||||
|
@ -70,22 +63,25 @@ export class RouterOutlet {
|
||||||
bind(routerMod.Router).toValue(this._childRouter)
|
bind(routerMod.Router).toValue(this._childRouter)
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (isPresent(this._componentRef)) {
|
return this.deactivate()
|
||||||
this._componentRef.dispose();
|
.then((_) => this._loader.loadNextToExistingLocation(instruction.component,
|
||||||
}
|
this._elementRef, outletInjector))
|
||||||
|
|
||||||
return this._loader.loadNextToExistingLocation(instruction.component, this._elementRef,
|
|
||||||
outletInjector)
|
|
||||||
.then((componentRef) => {
|
.then((componentRef) => {
|
||||||
this._componentRef = componentRef;
|
this._componentRef = componentRef;
|
||||||
return this._childRouter.commit(instruction);
|
return this._childRouter.commit(instruction.child);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
deactivate(): Promise<any> {
|
deactivate(): Promise<any> {
|
||||||
return (isPresent(this._childRouter) ? this._childRouter.deactivate() :
|
return (isPresent(this._childRouter) ? this._childRouter.deactivate() :
|
||||||
PromiseWrapper.resolve(true))
|
PromiseWrapper.resolve(true))
|
||||||
.then((_) => this._componentRef.dispose());
|
.then((_) => {
|
||||||
|
if (isPresent(this._componentRef)) {
|
||||||
|
this._componentRef.dispose();
|
||||||
|
this._componentRef = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
canDeactivate(instruction: Instruction): Promise<boolean> {
|
canDeactivate(instruction: Instruction): Promise<boolean> {
|
||||||
|
|
|
@ -110,25 +110,6 @@ export function main() {
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
||||||
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) => {
|
it('should work with redirects', inject([AsyncTestCompleter, Location], (async, location) => {
|
||||||
compile()
|
compile()
|
||||||
.then((_) => rtr.config({'path': '/original', 'redirectTo': '/redirected'}))
|
.then((_) => rtr.config({'path': '/original', 'redirectTo': '/redirected'}))
|
||||||
|
|
|
@ -10,6 +10,8 @@ import {
|
||||||
SpyObject
|
SpyObject
|
||||||
} from 'angular2/test_lib';
|
} from 'angular2/test_lib';
|
||||||
|
|
||||||
|
import {Promise, PromiseWrapper} from 'angular2/src/facade/async';
|
||||||
|
|
||||||
import {RouteRegistry} from 'angular2/src/router/route_registry';
|
import {RouteRegistry} from 'angular2/src/router/route_registry';
|
||||||
import {RouteConfig} from 'angular2/src/router/route_config_decorator';
|
import {RouteConfig} from 'angular2/src/router/route_config_decorator';
|
||||||
|
|
||||||
|
@ -19,75 +21,121 @@ export function main() {
|
||||||
|
|
||||||
beforeEach(() => { registry = new RouteRegistry(); });
|
beforeEach(() => { registry = new RouteRegistry(); });
|
||||||
|
|
||||||
it('should match the full URL', () => {
|
it('should match the full URL', inject([AsyncTestCompleter], (async) => {
|
||||||
registry.config(rootHostComponent, {'path': '/', 'component': DummyCompA});
|
registry.config(rootHostComponent, {'path': '/', 'component': DummyCompA});
|
||||||
registry.config(rootHostComponent, {'path': '/test', 'component': DummyCompB});
|
registry.config(rootHostComponent, {'path': '/test', 'component': DummyCompB});
|
||||||
|
|
||||||
var instruction = registry.recognize('/test', rootHostComponent);
|
registry.recognize('/test', rootHostComponent).then((instruction) => {
|
||||||
|
expect(instruction.component).toBe(DummyCompB);
|
||||||
expect(instruction.getChild('default').component).toBe(DummyCompB);
|
async.done();
|
||||||
});
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
it('should prefer static segments to dynamic', () => {
|
it('should prefer static segments to dynamic', inject([AsyncTestCompleter], (async) => {
|
||||||
registry.config(rootHostComponent, {'path': '/:site', 'component': DummyCompB});
|
registry.config(rootHostComponent, {'path': '/:site', 'component': DummyCompB});
|
||||||
registry.config(rootHostComponent, {'path': '/home', 'component': DummyCompA});
|
registry.config(rootHostComponent, {'path': '/home', 'component': DummyCompA});
|
||||||
|
|
||||||
var instruction = registry.recognize('/home', rootHostComponent);
|
registry.recognize('/home', rootHostComponent).then((instruction) => {
|
||||||
|
expect(instruction.component).toBe(DummyCompA);
|
||||||
expect(instruction.getChild('default').component).toBe(DummyCompA);
|
async.done();
|
||||||
});
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
it('should prefer dynamic segments to star', () => {
|
it('should prefer dynamic segments to star', inject([AsyncTestCompleter], (async) => {
|
||||||
registry.config(rootHostComponent, {'path': '/:site', 'component': DummyCompA});
|
registry.config(rootHostComponent, {'path': '/:site', 'component': DummyCompA});
|
||||||
registry.config(rootHostComponent, {'path': '/*site', 'component': DummyCompB});
|
registry.config(rootHostComponent, {'path': '/*site', 'component': DummyCompB});
|
||||||
|
|
||||||
var instruction = registry.recognize('/home', rootHostComponent);
|
registry.recognize('/home', rootHostComponent).then((instruction) => {
|
||||||
|
expect(instruction.component).toBe(DummyCompA);
|
||||||
expect(instruction.getChild('default').component).toBe(DummyCompA);
|
async.done();
|
||||||
});
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
it('should prefer routes with more dynamic segments', () => {
|
it('should prefer routes with more dynamic segments', inject([AsyncTestCompleter], (async) => {
|
||||||
registry.config(rootHostComponent, {'path': '/:first/*rest', 'component': DummyCompA});
|
registry.config(rootHostComponent, {'path': '/:first/*rest', 'component': DummyCompA});
|
||||||
registry.config(rootHostComponent, {'path': '/*all', 'component': DummyCompB});
|
registry.config(rootHostComponent, {'path': '/*all', 'component': DummyCompB});
|
||||||
|
|
||||||
var instruction = registry.recognize('/some/path', rootHostComponent);
|
registry.recognize('/some/path', rootHostComponent).then((instruction) => {
|
||||||
|
expect(instruction.component).toBe(DummyCompA);
|
||||||
expect(instruction.getChild('default').component).toBe(DummyCompA);
|
async.done();
|
||||||
});
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
it('should prefer routes with more static segments', () => {
|
it('should prefer routes with more static segments', inject([AsyncTestCompleter], (async) => {
|
||||||
registry.config(rootHostComponent, {'path': '/first/:second', 'component': DummyCompA});
|
registry.config(rootHostComponent, {'path': '/first/:second', 'component': DummyCompA});
|
||||||
registry.config(rootHostComponent, {'path': '/:first/:second', 'component': DummyCompB});
|
registry.config(rootHostComponent, {'path': '/:first/:second', 'component': DummyCompB});
|
||||||
|
|
||||||
var instruction = registry.recognize('/first/second', rootHostComponent);
|
registry.recognize('/first/second', rootHostComponent).then((instruction) => {
|
||||||
|
expect(instruction.component).toBe(DummyCompA);
|
||||||
expect(instruction.getChild('default').component).toBe(DummyCompA);
|
async.done();
|
||||||
});
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
it('should prefer routes with static segments before dynamic segments', () => {
|
it('should prefer routes with static segments before dynamic segments', inject([AsyncTestCompleter], (async) => {
|
||||||
registry.config(rootHostComponent, {'path': '/first/second/:third', 'component': DummyCompB});
|
registry.config(rootHostComponent, {'path': '/first/second/:third', 'component': DummyCompB});
|
||||||
registry.config(rootHostComponent, {'path': '/first/:second/third', 'component': DummyCompA});
|
registry.config(rootHostComponent, {'path': '/first/:second/third', 'component': DummyCompA});
|
||||||
|
|
||||||
var instruction = registry.recognize('/first/second/third', rootHostComponent);
|
registry.recognize('/first/second/third', rootHostComponent).then((instruction) => {
|
||||||
|
expect(instruction.component).toBe(DummyCompB);
|
||||||
expect(instruction.getChild('default').component).toBe(DummyCompB);
|
async.done();
|
||||||
});
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
it('should match the full URL recursively', () => {
|
it('should match the full URL using child components', inject([AsyncTestCompleter], (async) => {
|
||||||
registry.config(rootHostComponent, {'path': '/first', 'component': DummyParentComp});
|
registry.config(rootHostComponent, {'path': '/first', 'component': DummyParentComp});
|
||||||
|
|
||||||
var instruction = registry.recognize('/first/second', rootHostComponent);
|
registry.recognize('/first/second', rootHostComponent).then((instruction) => {
|
||||||
|
expect(instruction.component).toBe(DummyParentComp);
|
||||||
|
expect(instruction.child.component).toBe(DummyCompB);
|
||||||
|
async.done();
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
var parentInstruction = instruction.getChild('default');
|
it('should match the URL using async child components', inject([AsyncTestCompleter], (async) => {
|
||||||
var childInstruction = parentInstruction.getChild('default');
|
registry.config(rootHostComponent, {'path': '/first', 'component': DummyAsyncComp});
|
||||||
|
|
||||||
expect(parentInstruction.component).toBe(DummyParentComp);
|
registry.recognize('/first/second', rootHostComponent).then((instruction) => {
|
||||||
expect(childInstruction.component).toBe(DummyCompB);
|
expect(instruction.component).toBe(DummyAsyncComp);
|
||||||
|
expect(instruction.child.component).toBe(DummyCompB);
|
||||||
|
async.done();
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should match the URL using an async parent component', inject([AsyncTestCompleter], (async) => {
|
||||||
|
registry.config(rootHostComponent, {'path': '/first', 'component': {'loader': AsyncParentLoader, 'type': 'loader'} });
|
||||||
|
|
||||||
|
registry.recognize('/first/second', rootHostComponent).then((instruction) => {
|
||||||
|
expect(instruction.component).toBe(DummyParentComp);
|
||||||
|
expect(instruction.child.component).toBe(DummyCompB);
|
||||||
|
async.done();
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should throw when a config does not have a component or redirectTo property', () => {
|
||||||
|
expect(() => registry.config(rootHostComponent, {'path': '/some/path' }))
|
||||||
|
.toThrowError('Route config should contain exactly one \'component\', or \'redirectTo\' property');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should throw when a config has an invalid component type', () => {
|
||||||
|
expect(() => registry.config(rootHostComponent, {'path': '/some/path', 'component': { 'type': 'intentionallyWrongComponentType' } }))
|
||||||
|
.toThrowError('Invalid component type \'intentionallyWrongComponentType\'');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function AsyncParentLoader() {
|
||||||
|
return PromiseWrapper.resolve(DummyParentComp);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AsyncChildLoader() {
|
||||||
|
return PromiseWrapper.resolve(DummyCompB);
|
||||||
|
}
|
||||||
|
|
||||||
|
@RouteConfig([
|
||||||
|
{ 'path': '/second', 'component': { 'loader': AsyncChildLoader, 'type': 'loader' } }
|
||||||
|
])
|
||||||
|
class DummyAsyncComp {}
|
||||||
|
|
||||||
class DummyCompA {}
|
class DummyCompA {}
|
||||||
class DummyCompB {}
|
class DummyCompB {}
|
||||||
|
|
||||||
|
|
|
@ -50,7 +50,7 @@ export function main() {
|
||||||
it('should navigate based on the initial URL state', inject([AsyncTestCompleter], (async) => {
|
it('should navigate based on the initial URL state', inject([AsyncTestCompleter], (async) => {
|
||||||
var outlet = makeDummyOutlet();
|
var outlet = makeDummyOutlet();
|
||||||
|
|
||||||
router.config({'path': '/', 'component': 'Index'})
|
router.config({'path': '/', 'component': DummyComponent})
|
||||||
.then((_) => router.registerOutlet(outlet))
|
.then((_) => router.registerOutlet(outlet))
|
||||||
.then((_) => {
|
.then((_) => {
|
||||||
expect(outlet.spy('activate')).toHaveBeenCalled();
|
expect(outlet.spy('activate')).toHaveBeenCalled();
|
||||||
|
@ -65,7 +65,7 @@ export function main() {
|
||||||
var outlet = makeDummyOutlet();
|
var outlet = makeDummyOutlet();
|
||||||
|
|
||||||
router.registerOutlet(outlet)
|
router.registerOutlet(outlet)
|
||||||
.then((_) => { return router.config({'path': '/a', 'component': 'A'}); })
|
.then((_) => router.config({'path': '/a', 'component': DummyComponent}))
|
||||||
.then((_) => router.navigate('/a'))
|
.then((_) => router.navigate('/a'))
|
||||||
.then((_) => {
|
.then((_) => {
|
||||||
expect(outlet.spy('activate')).toHaveBeenCalled();
|
expect(outlet.spy('activate')).toHaveBeenCalled();
|
||||||
|
@ -81,7 +81,7 @@ export function main() {
|
||||||
.then((_) => router.navigate('/a'))
|
.then((_) => router.navigate('/a'))
|
||||||
.then((_) => {
|
.then((_) => {
|
||||||
expect(outlet.spy('activate')).not.toHaveBeenCalled();
|
expect(outlet.spy('activate')).not.toHaveBeenCalled();
|
||||||
return router.config({'path': '/a', 'component': 'A'});
|
return router.config({'path': '/a', 'component': DummyComponent});
|
||||||
})
|
})
|
||||||
.then((_) => {
|
.then((_) => {
|
||||||
expect(outlet.spy('activate')).toHaveBeenCalled();
|
expect(outlet.spy('activate')).toHaveBeenCalled();
|
||||||
|
@ -97,6 +97,8 @@ class DummyOutlet extends SpyObject {
|
||||||
noSuchMethod(m) { return super.noSuchMethod(m) }
|
noSuchMethod(m) { return super.noSuchMethod(m) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class DummyComponent {}
|
||||||
|
|
||||||
function makeDummyOutlet() {
|
function makeDummyOutlet() {
|
||||||
var ref = new DummyOutlet();
|
var ref = new DummyOutlet();
|
||||||
ref.spy('activate').andCallFake((_) => PromiseWrapper.resolve(true));
|
ref.spy('activate').andCallFake((_) => PromiseWrapper.resolve(true));
|
||||||
|
|
Loading…
Reference in New Issue