parent
909e70bd61
commit
2a2f9a9a19
|
@ -195,6 +195,10 @@ var ListWrapper = {
|
|||
return array[0];
|
||||
},
|
||||
|
||||
last: function(array) {
|
||||
return (array && array.length) > 0 ? array[array.length - 1] : null;
|
||||
},
|
||||
|
||||
map: function (l, fn) {
|
||||
return l.map(fn);
|
||||
},
|
||||
|
|
|
@ -7,7 +7,8 @@ import {
|
|||
AbstractRecognizer,
|
||||
RouteRecognizer,
|
||||
RedirectRecognizer,
|
||||
RouteMatch
|
||||
RouteMatch,
|
||||
PathMatch
|
||||
} from './route_recognizer';
|
||||
import {Route, AsyncRoute, AuxRoute, Redirect, RouteDefinition} from './route_config_impl';
|
||||
import {AsyncRouteHandler} from './async_route_handler';
|
||||
|
@ -117,6 +118,11 @@ export class ComponentRecognizer {
|
|||
}
|
||||
});
|
||||
|
||||
// handle cases where we are routing just to an aux route
|
||||
if (solutions.length == 0 && isPresent(urlParse) && urlParse.auxiliary.length > 0) {
|
||||
return [PromiseWrapper.resolve(new PathMatch(null, null, urlParse.auxiliary))];
|
||||
}
|
||||
|
||||
return solutions;
|
||||
}
|
||||
|
||||
|
|
|
@ -111,9 +111,9 @@ export abstract class Instruction {
|
|||
public child: Instruction;
|
||||
public auxInstruction: {[key: string]: Instruction} = {};
|
||||
|
||||
get urlPath(): string { return this.component.urlPath; }
|
||||
get urlPath(): string { return isPresent(this.component) ? this.component.urlPath : ''; }
|
||||
|
||||
get urlParams(): string[] { return this.component.urlParams; }
|
||||
get urlParams(): string[] { return isPresent(this.component) ? this.component.urlParams : []; }
|
||||
|
||||
get specificity(): number {
|
||||
var total = 0;
|
||||
|
@ -181,7 +181,7 @@ export abstract class Instruction {
|
|||
|
||||
/** @internal */
|
||||
_stringifyMatrixParams(): string {
|
||||
return this.urlParams.length > 0 ? (';' + this.component.urlParams.join(';')) : '';
|
||||
return this.urlParams.length > 0 ? (';' + this.urlParams.join(';')) : '';
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
|
|
|
@ -144,20 +144,18 @@ export class RouteRegistry {
|
|||
*/
|
||||
recognize(url: string, ancestorInstructions: Instruction[]): Promise<Instruction> {
|
||||
var parsedUrl = parser.parse(url);
|
||||
return this._recognize(parsedUrl, ancestorInstructions);
|
||||
return this._recognize(parsedUrl, []);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Recognizes all parent-child routes, but creates unresolved auxiliary routes
|
||||
*/
|
||||
|
||||
private _recognize(parsedUrl: Url, ancestorInstructions: Instruction[],
|
||||
_aux = false): Promise<Instruction> {
|
||||
var parentComponent =
|
||||
ancestorInstructions.length > 0 ?
|
||||
ancestorInstructions[ancestorInstructions.length - 1].component.componentType :
|
||||
this._rootComponent;
|
||||
var parentInstruction = ListWrapper.last(ancestorInstructions);
|
||||
var parentComponent = isPresent(parentInstruction) ? parentInstruction.component.componentType :
|
||||
this._rootComponent;
|
||||
|
||||
var componentRecognizer = this._rules.get(parentComponent);
|
||||
if (isBlank(componentRecognizer)) {
|
||||
|
@ -174,14 +172,13 @@ export class RouteRegistry {
|
|||
|
||||
if (candidate instanceof PathMatch) {
|
||||
var auxParentInstructions =
|
||||
ancestorInstructions.length > 0 ?
|
||||
[ancestorInstructions[ancestorInstructions.length - 1]] :
|
||||
[];
|
||||
ancestorInstructions.length > 0 ? [ListWrapper.last(ancestorInstructions)] : [];
|
||||
var auxInstructions =
|
||||
this._auxRoutesToUnresolved(candidate.remainingAux, auxParentInstructions);
|
||||
|
||||
var instruction = new ResolvedInstruction(candidate.instruction, null, auxInstructions);
|
||||
|
||||
if (candidate.instruction.terminal) {
|
||||
if (isBlank(candidate.instruction) || candidate.instruction.terminal) {
|
||||
return instruction;
|
||||
}
|
||||
|
||||
|
@ -203,7 +200,8 @@ export class RouteRegistry {
|
|||
}
|
||||
|
||||
if (candidate instanceof RedirectMatch) {
|
||||
var instruction = this.generate(candidate.redirectTo, ancestorInstructions);
|
||||
var instruction =
|
||||
this.generate(candidate.redirectTo, ancestorInstructions.concat([null]));
|
||||
return new RedirectInstruction(instruction.component, instruction.child,
|
||||
instruction.auxInstruction);
|
||||
}
|
||||
|
@ -237,69 +235,88 @@ export class RouteRegistry {
|
|||
* route boundary.
|
||||
*/
|
||||
generate(linkParams: any[], ancestorInstructions: Instruction[], _aux = false): Instruction {
|
||||
let normalizedLinkParams = splitAndFlattenLinkParams(linkParams);
|
||||
|
||||
var first = ListWrapper.first(normalizedLinkParams);
|
||||
var rest = ListWrapper.slice(normalizedLinkParams, 1);
|
||||
var params = splitAndFlattenLinkParams(linkParams);
|
||||
var prevInstruction;
|
||||
|
||||
// The first segment should be either '.' (generate from parent) or '' (generate from root).
|
||||
// When we normalize above, we strip all the slashes, './' becomes '.' and '/' becomes ''.
|
||||
if (first == '') {
|
||||
if (ListWrapper.first(params) == '') {
|
||||
params.shift();
|
||||
prevInstruction = ListWrapper.first(ancestorInstructions);
|
||||
ancestorInstructions = [];
|
||||
} else if (first == '..') {
|
||||
// we already captured the first instance of "..", so we need to pop off an ancestor
|
||||
ancestorInstructions.pop();
|
||||
while (ListWrapper.first(rest) == '..') {
|
||||
rest = ListWrapper.slice(rest, 1);
|
||||
ancestorInstructions.pop();
|
||||
if (ancestorInstructions.length <= 0) {
|
||||
throw new BaseException(
|
||||
`Link "${ListWrapper.toJSON(linkParams)}" has too many "../" segments.`);
|
||||
} else {
|
||||
prevInstruction = ancestorInstructions.length > 0 ? ancestorInstructions.pop() : null;
|
||||
|
||||
if (ListWrapper.first(params) == '.') {
|
||||
params.shift();
|
||||
} else if (ListWrapper.first(params) == '..') {
|
||||
while (ListWrapper.first(params) == '..') {
|
||||
if (ancestorInstructions.length <= 0) {
|
||||
throw new BaseException(
|
||||
`Link "${ListWrapper.toJSON(linkParams)}" has too many "../" segments.`);
|
||||
}
|
||||
prevInstruction = ancestorInstructions.pop();
|
||||
params = ListWrapper.slice(params, 1);
|
||||
}
|
||||
|
||||
// we're on to implicit child/sibling route
|
||||
} else {
|
||||
// we must only peak at the link param, and not consume it
|
||||
let routeName = ListWrapper.first(params);
|
||||
let parentComponentType = this._rootComponent;
|
||||
let grandparentComponentType = null;
|
||||
|
||||
if (ancestorInstructions.length > 1) {
|
||||
let parentComponentInstruction = ancestorInstructions[ancestorInstructions.length - 1];
|
||||
let grandComponentInstruction = ancestorInstructions[ancestorInstructions.length - 2];
|
||||
|
||||
parentComponentType = parentComponentInstruction.component.componentType;
|
||||
grandparentComponentType = grandComponentInstruction.component.componentType;
|
||||
} else if (ancestorInstructions.length == 1) {
|
||||
parentComponentType = ancestorInstructions[0].component.componentType;
|
||||
grandparentComponentType = this._rootComponent;
|
||||
}
|
||||
|
||||
// For a link with no leading `./`, `/`, or `../`, we look for a sibling and child.
|
||||
// If both exist, we throw. Otherwise, we prefer whichever exists.
|
||||
var childRouteExists = this.hasRoute(routeName, parentComponentType);
|
||||
var parentRouteExists = isPresent(grandparentComponentType) &&
|
||||
this.hasRoute(routeName, grandparentComponentType);
|
||||
|
||||
if (parentRouteExists && childRouteExists) {
|
||||
let msg =
|
||||
`Link "${ListWrapper.toJSON(linkParams)}" is ambiguous, use "./" or "../" to disambiguate.`;
|
||||
throw new BaseException(msg);
|
||||
}
|
||||
|
||||
if (parentRouteExists) {
|
||||
prevInstruction = ancestorInstructions.pop();
|
||||
}
|
||||
}
|
||||
} else if (first != '.') {
|
||||
let parentComponent = this._rootComponent;
|
||||
let grandparentComponent = null;
|
||||
if (ancestorInstructions.length > 1) {
|
||||
parentComponent =
|
||||
ancestorInstructions[ancestorInstructions.length - 1].component.componentType;
|
||||
grandparentComponent =
|
||||
ancestorInstructions[ancestorInstructions.length - 2].component.componentType;
|
||||
} else if (ancestorInstructions.length == 1) {
|
||||
parentComponent = ancestorInstructions[0].component.componentType;
|
||||
grandparentComponent = this._rootComponent;
|
||||
}
|
||||
|
||||
// For a link with no leading `./`, `/`, or `../`, we look for a sibling and child.
|
||||
// If both exist, we throw. Otherwise, we prefer whichever exists.
|
||||
var childRouteExists = this.hasRoute(first, parentComponent);
|
||||
var parentRouteExists =
|
||||
isPresent(grandparentComponent) && this.hasRoute(first, grandparentComponent);
|
||||
|
||||
if (parentRouteExists && childRouteExists) {
|
||||
let msg =
|
||||
`Link "${ListWrapper.toJSON(linkParams)}" is ambiguous, use "./" or "../" to disambiguate.`;
|
||||
throw new BaseException(msg);
|
||||
}
|
||||
if (parentRouteExists) {
|
||||
ancestorInstructions.pop();
|
||||
}
|
||||
rest = linkParams;
|
||||
}
|
||||
|
||||
if (rest[rest.length - 1] == '') {
|
||||
rest.pop();
|
||||
if (params[params.length - 1] == '') {
|
||||
params.pop();
|
||||
}
|
||||
|
||||
if (rest.length < 1) {
|
||||
if (params.length > 0 && params[0] == '') {
|
||||
params.shift();
|
||||
}
|
||||
|
||||
if (params.length < 1) {
|
||||
let msg = `Link "${ListWrapper.toJSON(linkParams)}" must include a route name.`;
|
||||
throw new BaseException(msg);
|
||||
}
|
||||
|
||||
var generatedInstruction = this._generate(rest, ancestorInstructions, _aux);
|
||||
var generatedInstruction =
|
||||
this._generate(params, ancestorInstructions, prevInstruction, _aux, linkParams);
|
||||
|
||||
// we don't clone the first (root) element
|
||||
for (var i = ancestorInstructions.length - 1; i >= 0; i--) {
|
||||
let ancestorInstruction = ancestorInstructions[i];
|
||||
if (isBlank(ancestorInstruction)) {
|
||||
break;
|
||||
}
|
||||
generatedInstruction = ancestorInstruction.replaceChild(generatedInstruction);
|
||||
}
|
||||
|
||||
|
@ -308,95 +325,113 @@ export class RouteRegistry {
|
|||
|
||||
|
||||
/*
|
||||
* Internal helper that does not make any assertions about the beginning of the link DSL
|
||||
* Internal helper that does not make any assertions about the beginning of the link DSL.
|
||||
* `ancestorInstructions` are parents that will be cloned.
|
||||
* `prevInstruction` is the existing instruction that would be replaced, but which might have
|
||||
* aux routes that need to be cloned.
|
||||
*/
|
||||
private _generate(linkParams: any[], ancestorInstructions: Instruction[],
|
||||
_aux = false): Instruction {
|
||||
let parentComponent =
|
||||
ancestorInstructions.length > 0 ?
|
||||
ancestorInstructions[ancestorInstructions.length - 1].component.componentType :
|
||||
this._rootComponent;
|
||||
prevInstruction: Instruction, _aux = false, _originalLink: any[]): Instruction {
|
||||
let parentComponentType = this._rootComponent;
|
||||
let componentInstruction = null;
|
||||
let auxInstructions: {[key: string]: Instruction} = {};
|
||||
|
||||
let parentInstruction: Instruction = ListWrapper.last(ancestorInstructions);
|
||||
if (isPresent(parentInstruction) && isPresent(parentInstruction.component)) {
|
||||
parentComponentType = parentInstruction.component.componentType;
|
||||
}
|
||||
|
||||
if (linkParams.length == 0) {
|
||||
return this.generateDefault(parentComponent);
|
||||
}
|
||||
let linkIndex = 0;
|
||||
let routeName = linkParams[linkIndex];
|
||||
|
||||
if (!isString(routeName)) {
|
||||
throw new BaseException(`Unexpected segment "${routeName}" in link DSL. Expected a string.`);
|
||||
} else if (routeName == '' || routeName == '.' || routeName == '..') {
|
||||
throw new BaseException(`"${routeName}/" is only allowed at the beginning of a link DSL.`);
|
||||
}
|
||||
|
||||
let params = {};
|
||||
if (linkIndex + 1 < linkParams.length) {
|
||||
let nextSegment = linkParams[linkIndex + 1];
|
||||
if (isStringMap(nextSegment) && !isArray(nextSegment)) {
|
||||
params = nextSegment;
|
||||
linkIndex += 1;
|
||||
let defaultInstruction = this.generateDefault(parentComponentType);
|
||||
if (isBlank(defaultInstruction)) {
|
||||
throw new BaseException(
|
||||
`Link "${ListWrapper.toJSON(_originalLink)}" does not resolve to a terminal instruction.`);
|
||||
}
|
||||
return defaultInstruction;
|
||||
}
|
||||
|
||||
let auxInstructions: {[key: string]: Instruction} = {};
|
||||
var nextSegment;
|
||||
while (linkIndex + 1 < linkParams.length && isArray(nextSegment = linkParams[linkIndex + 1])) {
|
||||
let auxParentInstruction = ancestorInstructions.length > 0 ?
|
||||
[ancestorInstructions[ancestorInstructions.length - 1]] :
|
||||
[];
|
||||
let auxInstruction = this._generate(nextSegment, auxParentInstruction, true);
|
||||
// for non-aux routes, we want to reuse the predecessor's existing primary and aux routes
|
||||
// and only override routes for which the given link DSL provides
|
||||
if (isPresent(prevInstruction) && !_aux) {
|
||||
auxInstructions = StringMapWrapper.merge(prevInstruction.auxInstruction, auxInstructions);
|
||||
componentInstruction = prevInstruction.component;
|
||||
}
|
||||
|
||||
var componentRecognizer = this._rules.get(parentComponentType);
|
||||
if (isBlank(componentRecognizer)) {
|
||||
throw new BaseException(
|
||||
`Component "${getTypeNameForDebugging(parentComponentType)}" has no route config.`);
|
||||
}
|
||||
|
||||
let linkParamIndex = 0;
|
||||
let routeParams = {};
|
||||
|
||||
// first, recognize the primary route if one is provided
|
||||
if (linkParamIndex < linkParams.length && isString(linkParams[linkParamIndex])) {
|
||||
let routeName = linkParams[linkParamIndex];
|
||||
if (routeName == '' || routeName == '.' || routeName == '..') {
|
||||
throw new BaseException(`"${routeName}/" is only allowed at the beginning of a link DSL.`);
|
||||
}
|
||||
linkParamIndex += 1;
|
||||
if (linkParamIndex < linkParams.length) {
|
||||
let linkParam = linkParams[linkParamIndex];
|
||||
if (isStringMap(linkParam) && !isArray(linkParam)) {
|
||||
routeParams = linkParam;
|
||||
linkParamIndex += 1;
|
||||
}
|
||||
}
|
||||
var routeRecognizer =
|
||||
(_aux ? componentRecognizer.auxNames : componentRecognizer.names).get(routeName);
|
||||
|
||||
if (isBlank(routeRecognizer)) {
|
||||
throw new BaseException(
|
||||
`Component "${getTypeNameForDebugging(parentComponentType)}" has no route named "${routeName}".`);
|
||||
}
|
||||
|
||||
// Create an "unresolved instruction" for async routes
|
||||
// we'll figure out the rest of the route when we resolve the instruction and
|
||||
// perform a navigation
|
||||
if (isBlank(routeRecognizer.handler.componentType)) {
|
||||
var compInstruction = routeRecognizer.generateComponentPathValues(routeParams);
|
||||
return new UnresolvedInstruction(() => {
|
||||
return routeRecognizer.handler.resolveComponentType().then((_) => {
|
||||
return this._generate(linkParams, ancestorInstructions, prevInstruction, _aux,
|
||||
_originalLink);
|
||||
});
|
||||
}, compInstruction['urlPath'], compInstruction['urlParams']);
|
||||
}
|
||||
|
||||
componentInstruction = _aux ? componentRecognizer.generateAuxiliary(routeName, routeParams) :
|
||||
componentRecognizer.generate(routeName, routeParams);
|
||||
}
|
||||
|
||||
// Next, recognize auxiliary instructions.
|
||||
// If we have an ancestor instruction, we preserve whatever aux routes are active from it.
|
||||
while (linkParamIndex < linkParams.length && isArray(linkParams[linkParamIndex])) {
|
||||
let auxParentInstruction = [parentInstruction];
|
||||
let auxInstruction = this._generate(linkParams[linkParamIndex], auxParentInstruction, null,
|
||||
true, _originalLink);
|
||||
|
||||
// TODO: this will not work for aux routes with parameters or multiple segments
|
||||
auxInstructions[auxInstruction.component.urlPath] = auxInstruction;
|
||||
linkIndex += 1;
|
||||
linkParamIndex += 1;
|
||||
}
|
||||
|
||||
var componentRecognizer = this._rules.get(parentComponent);
|
||||
if (isBlank(componentRecognizer)) {
|
||||
throw new BaseException(
|
||||
`Component "${getTypeNameForDebugging(parentComponent)}" has no route config.`);
|
||||
}
|
||||
|
||||
var routeRecognizer =
|
||||
(_aux ? componentRecognizer.auxNames : componentRecognizer.names).get(routeName);
|
||||
|
||||
if (!isPresent(routeRecognizer)) {
|
||||
throw new BaseException(
|
||||
`Component "${getTypeNameForDebugging(parentComponent)}" has no route named "${routeName}".`);
|
||||
}
|
||||
|
||||
if (!isPresent(routeRecognizer.handler.componentType)) {
|
||||
var compInstruction = routeRecognizer.generateComponentPathValues(params);
|
||||
return new UnresolvedInstruction(() => {
|
||||
return routeRecognizer.handler.resolveComponentType().then(
|
||||
(_) => { return this._generate(linkParams, ancestorInstructions, _aux); });
|
||||
}, compInstruction['urlPath'], compInstruction['urlParams']);
|
||||
}
|
||||
|
||||
var componentInstruction = _aux ? componentRecognizer.generateAuxiliary(routeName, params) :
|
||||
componentRecognizer.generate(routeName, params);
|
||||
|
||||
|
||||
|
||||
var remaining = linkParams.slice(linkIndex + 1);
|
||||
|
||||
var instruction = new ResolvedInstruction(componentInstruction, null, auxInstructions);
|
||||
|
||||
// the component is sync
|
||||
if (isPresent(componentInstruction.componentType)) {
|
||||
// If the component is sync, we can generate resolved child route instructions
|
||||
// If not, we'll resolve the instructions at navigation time
|
||||
if (isPresent(componentInstruction) && isPresent(componentInstruction.componentType)) {
|
||||
let childInstruction: Instruction = null;
|
||||
if (linkIndex + 1 < linkParams.length) {
|
||||
let childAncestorComponents = ancestorInstructions.concat([instruction]);
|
||||
childInstruction = this._generate(remaining, childAncestorComponents);
|
||||
} else if (!componentInstruction.terminal) {
|
||||
// ... look for defaults
|
||||
childInstruction = this.generateDefault(componentInstruction.componentType);
|
||||
|
||||
if (isBlank(childInstruction)) {
|
||||
throw new BaseException(
|
||||
`Link "${ListWrapper.toJSON(linkParams)}" does not resolve to a terminal instruction.`);
|
||||
if (componentInstruction.terminal) {
|
||||
if (linkParamIndex >= linkParams.length) {
|
||||
// TODO: throw that there are extra link params beyond the terminal component
|
||||
}
|
||||
} else {
|
||||
let childAncestorComponents = ancestorInstructions.concat([instruction]);
|
||||
let remainingLinkParams = linkParams.slice(linkParamIndex);
|
||||
childInstruction = this._generate(remainingLinkParams, childAncestorComponents, null, false,
|
||||
_originalLink);
|
||||
}
|
||||
instruction.child = childInstruction;
|
||||
}
|
||||
|
@ -422,7 +457,6 @@ export class RouteRegistry {
|
|||
return null;
|
||||
}
|
||||
|
||||
|
||||
var defaultChild = null;
|
||||
if (isPresent(componentRecognizer.defaultRoute.handler.componentType)) {
|
||||
var componentInstruction = componentRecognizer.defaultRoute.generate({});
|
||||
|
|
|
@ -95,8 +95,6 @@ export class Router {
|
|||
throw new BaseException(`registerAuxOutlet expects to be called with an outlet with a name.`);
|
||||
}
|
||||
|
||||
// TODO...
|
||||
// what is the host of an aux route???
|
||||
var router = this.auxRouter(this.hostComponent);
|
||||
|
||||
this._auxRouters.set(outletName, router);
|
||||
|
@ -224,10 +222,12 @@ export class Router {
|
|||
/** @internal */
|
||||
_settleInstruction(instruction: Instruction): Promise<any> {
|
||||
return instruction.resolveComponent().then((_) => {
|
||||
instruction.component.reuse = false;
|
||||
|
||||
var unsettledInstructions: Array<Promise<any>> = [];
|
||||
|
||||
if (isPresent(instruction.component)) {
|
||||
instruction.component.reuse = false;
|
||||
}
|
||||
|
||||
if (isPresent(instruction.child)) {
|
||||
unsettledInstructions.push(this._settleInstruction(instruction.child));
|
||||
}
|
||||
|
@ -256,6 +256,9 @@ export class Router {
|
|||
if (isBlank(this._outlet)) {
|
||||
return _resolveToFalse;
|
||||
}
|
||||
if (isBlank(instruction.component)) {
|
||||
return _resolveToTrue;
|
||||
}
|
||||
return this._outlet.routerCanReuse(instruction.component)
|
||||
.then((result) => {
|
||||
instruction.component.reuse = result;
|
||||
|
@ -280,7 +283,7 @@ export class Router {
|
|||
if (isPresent(instruction)) {
|
||||
childInstruction = instruction.child;
|
||||
componentInstruction = instruction.component;
|
||||
reuse = instruction.component.reuse;
|
||||
reuse = isBlank(instruction.component) || instruction.component.reuse;
|
||||
}
|
||||
if (reuse) {
|
||||
next = _resolveToTrue;
|
||||
|
@ -304,8 +307,9 @@ export class Router {
|
|||
*/
|
||||
commit(instruction: Instruction, _skipLocationChange: boolean = false): Promise<any> {
|
||||
this._currentInstruction = instruction;
|
||||
|
||||
var next: Promise<any> = _resolveToTrue;
|
||||
if (isPresent(this._outlet)) {
|
||||
if (isPresent(this._outlet) && isPresent(instruction.component)) {
|
||||
var componentInstruction = instruction.component;
|
||||
if (componentInstruction.reuse) {
|
||||
next = this._outlet.reuse(componentInstruction);
|
||||
|
@ -381,15 +385,12 @@ export class Router {
|
|||
}
|
||||
|
||||
private _getAncestorInstructions(): Instruction[] {
|
||||
var ancestorComponents = [];
|
||||
var ancestorInstructions = [this._currentInstruction];
|
||||
var ancestorRouter: Router = this;
|
||||
while (isPresent(ancestorRouter.parent) &&
|
||||
isPresent(ancestorRouter.parent._currentInstruction)) {
|
||||
ancestorRouter = ancestorRouter.parent;
|
||||
ancestorComponents.unshift(ancestorRouter._currentInstruction);
|
||||
while (isPresent(ancestorRouter = ancestorRouter.parent)) {
|
||||
ancestorInstructions.unshift(ancestorRouter._currentInstruction);
|
||||
}
|
||||
|
||||
return ancestorComponents;
|
||||
return ancestorInstructions;
|
||||
}
|
||||
|
||||
|
||||
|
@ -505,6 +506,9 @@ class ChildRouter extends Router {
|
|||
function canActivateOne(nextInstruction: Instruction,
|
||||
prevInstruction: Instruction): Promise<boolean> {
|
||||
var next = _resolveToTrue;
|
||||
if (isBlank(nextInstruction.component)) {
|
||||
return next;
|
||||
}
|
||||
if (isPresent(nextInstruction.child)) {
|
||||
next = canActivateOne(nextInstruction.child,
|
||||
isPresent(prevInstruction) ? prevInstruction.child : null);
|
||||
|
|
|
@ -53,16 +53,24 @@ export class RouterLink {
|
|||
// the instruction passed to the router to navigate
|
||||
private _navigationInstruction: Instruction;
|
||||
|
||||
constructor(private _router: Router, private _location: Location) {}
|
||||
constructor(private _router: Router, private _location: Location) {
|
||||
// we need to update the link whenever a route changes to account for aux routes
|
||||
this._router.subscribe((_) => this._updateLink());
|
||||
}
|
||||
|
||||
// because auxiliary links take existing primary and auxiliary routes into account,
|
||||
// we need to update the link whenever params or other routes change.
|
||||
private _updateLink(): void {
|
||||
this._navigationInstruction = this._router.generate(this._routeParams);
|
||||
var navigationHref = this._navigationInstruction.toLinkUrl();
|
||||
this.visibleHref = this._location.prepareExternalUrl(navigationHref);
|
||||
}
|
||||
|
||||
get isRouteActive(): boolean { return this._router.isRouteActive(this._navigationInstruction); }
|
||||
|
||||
set routeParams(changes: any[]) {
|
||||
this._routeParams = changes;
|
||||
this._navigationInstruction = this._router.generate(this._routeParams);
|
||||
|
||||
var navigationHref = this._navigationInstruction.toLinkUrl();
|
||||
this.visibleHref = this._location.prepareExternalUrl(navigationHref);
|
||||
this._updateLink();
|
||||
}
|
||||
|
||||
onClick(): boolean {
|
||||
|
|
|
@ -12,7 +12,7 @@ import {registerSpecs} from './impl/async_route_spec_impl';
|
|||
export function main() {
|
||||
registerSpecs();
|
||||
|
||||
ddescribeRouter('async routes', () => {
|
||||
describeRouter('async routes', () => {
|
||||
describeWithout('children', () => {
|
||||
describeWith('route data', itShouldRoute);
|
||||
describeWithAndWithout('params', itShouldRoute);
|
||||
|
|
|
@ -1,145 +1,19 @@
|
|||
import {
|
||||
ComponentFixture,
|
||||
AsyncTestCompleter,
|
||||
TestComponentBuilder,
|
||||
beforeEach,
|
||||
ddescribe,
|
||||
xdescribe,
|
||||
describe,
|
||||
el,
|
||||
expect,
|
||||
iit,
|
||||
inject,
|
||||
beforeEachProviders,
|
||||
it,
|
||||
xit
|
||||
} from 'angular2/testing_internal';
|
||||
describeRouter,
|
||||
ddescribeRouter,
|
||||
describeWith,
|
||||
describeWithout,
|
||||
describeWithAndWithout,
|
||||
itShouldRoute
|
||||
} from './util';
|
||||
|
||||
import {provide, Component, Injector, Inject} from 'angular2/core';
|
||||
|
||||
import {Router, ROUTER_DIRECTIVES, RouteParams, RouteData, Location} from 'angular2/router';
|
||||
import {RouteConfig, Route, AuxRoute, Redirect} from 'angular2/src/router/route_config_decorator';
|
||||
|
||||
import {TEST_ROUTER_PROVIDERS, RootCmp, compile, clickOnElement, getHref} from './util';
|
||||
|
||||
function getLinkElement(rtc: ComponentFixture) {
|
||||
return rtc.debugElement.componentViewChildren[0].nativeElement;
|
||||
}
|
||||
|
||||
var cmpInstanceCount;
|
||||
var childCmpInstanceCount;
|
||||
import {registerSpecs} from './impl/aux_route_spec_impl';
|
||||
|
||||
export function main() {
|
||||
describe('auxiliary routes', () => {
|
||||
registerSpecs();
|
||||
|
||||
var tcb: TestComponentBuilder;
|
||||
var fixture: ComponentFixture;
|
||||
var rtr;
|
||||
|
||||
beforeEachProviders(() => TEST_ROUTER_PROVIDERS);
|
||||
|
||||
beforeEach(inject([TestComponentBuilder, Router], (tcBuilder, router) => {
|
||||
tcb = tcBuilder;
|
||||
rtr = router;
|
||||
childCmpInstanceCount = 0;
|
||||
cmpInstanceCount = 0;
|
||||
}));
|
||||
|
||||
it('should recognize and navigate from the URL', inject([AsyncTestCompleter], (async) => {
|
||||
compile(tcb, `main {<router-outlet></router-outlet>} | aux {<router-outlet name="modal"></router-outlet>}`)
|
||||
.then((rtc) => {fixture = rtc})
|
||||
.then((_) => rtr.config([
|
||||
new Route({path: '/hello', component: HelloCmp, name: 'Hello'}),
|
||||
new AuxRoute({path: '/modal', component: ModalCmp, name: 'Aux'})
|
||||
]))
|
||||
.then((_) => rtr.navigateByUrl('/hello(modal)'))
|
||||
.then((_) => {
|
||||
fixture.detectChanges();
|
||||
expect(fixture.debugElement.nativeElement).toHaveText('main {hello} | aux {modal}');
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should navigate via the link DSL', inject([AsyncTestCompleter], (async) => {
|
||||
compile(tcb, `main {<router-outlet></router-outlet>} | aux {<router-outlet name="modal"></router-outlet>}`)
|
||||
.then((rtc) => {fixture = rtc})
|
||||
.then((_) => rtr.config([
|
||||
new Route({path: '/hello', component: HelloCmp, name: 'Hello'}),
|
||||
new AuxRoute({path: '/modal', component: ModalCmp, name: 'Modal'})
|
||||
]))
|
||||
.then((_) => rtr.navigate(['/Hello', ['Modal']]))
|
||||
.then((_) => {
|
||||
fixture.detectChanges();
|
||||
expect(fixture.debugElement.nativeElement).toHaveText('main {hello} | aux {modal}');
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should generate a link URL', inject([AsyncTestCompleter], (async) => {
|
||||
compile(
|
||||
tcb,
|
||||
`<a [routerLink]="['/Hello', ['Modal']]">open modal</a> | main {<router-outlet></router-outlet>} | aux {<router-outlet name="modal"></router-outlet>}`)
|
||||
.then((rtc) => {fixture = rtc})
|
||||
.then((_) => rtr.config([
|
||||
new Route({path: '/hello', component: HelloCmp, name: 'Hello'}),
|
||||
new AuxRoute({path: '/modal', component: ModalCmp, name: 'Modal'})
|
||||
]))
|
||||
.then((_) => {
|
||||
fixture.detectChanges();
|
||||
expect(getHref(getLinkElement(fixture))).toEqual('/hello(modal)');
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should navigate from a link click',
|
||||
inject([AsyncTestCompleter, Location], (async, location) => {
|
||||
compile(
|
||||
tcb,
|
||||
`<a [routerLink]="['/Hello', ['Modal']]">open modal</a> | main {<router-outlet></router-outlet>} | aux {<router-outlet name="modal"></router-outlet>}`)
|
||||
.then((rtc) => {fixture = rtc})
|
||||
.then((_) => rtr.config([
|
||||
new Route({path: '/hello', component: HelloCmp, name: 'Hello'}),
|
||||
new AuxRoute({path: '/modal', component: ModalCmp, name: 'Modal'})
|
||||
]))
|
||||
.then((_) => {
|
||||
fixture.detectChanges();
|
||||
expect(fixture.debugElement.nativeElement)
|
||||
.toHaveText('open modal | main {} | aux {}');
|
||||
|
||||
rtr.subscribe((_) => {
|
||||
fixture.detectChanges();
|
||||
expect(fixture.debugElement.nativeElement)
|
||||
.toHaveText('open modal | main {hello} | aux {modal}');
|
||||
expect(location.urlChanges).toEqual(['/hello(modal)']);
|
||||
async.done();
|
||||
});
|
||||
|
||||
clickOnElement(getLinkElement(fixture));
|
||||
});
|
||||
}));
|
||||
describeRouter('aux routes', () => {
|
||||
itShouldRoute();
|
||||
describeWith('a primary route', itShouldRoute);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@Component({selector: 'hello-cmp', template: `{{greeting}}`})
|
||||
class HelloCmp {
|
||||
greeting: string;
|
||||
constructor() { this.greeting = 'hello'; }
|
||||
}
|
||||
|
||||
@Component({selector: 'modal-cmp', template: `modal`})
|
||||
class ModalCmp {
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'aux-cmp',
|
||||
template: 'main {<router-outlet></router-outlet>} | ' +
|
||||
'aux {<router-outlet name="modal"></router-outlet>}',
|
||||
directives: [ROUTER_DIRECTIVES],
|
||||
})
|
||||
@RouteConfig([
|
||||
new Route({path: '/hello', component: HelloCmp, name: 'Hello'}),
|
||||
new AuxRoute({path: '/modal', component: ModalCmp, name: 'Aux'})
|
||||
])
|
||||
class AuxCmp {
|
||||
}
|
||||
|
|
|
@ -0,0 +1,247 @@
|
|||
import {
|
||||
ComponentFixture,
|
||||
AsyncTestCompleter,
|
||||
TestComponentBuilder,
|
||||
beforeEach,
|
||||
ddescribe,
|
||||
xdescribe,
|
||||
describe,
|
||||
el,
|
||||
expect,
|
||||
iit,
|
||||
inject,
|
||||
beforeEachProviders,
|
||||
it,
|
||||
xit
|
||||
} from 'angular2/testing_internal';
|
||||
|
||||
import {provide, Component, Injector, Inject} from 'angular2/core';
|
||||
|
||||
import {Router, ROUTER_DIRECTIVES, RouteParams, RouteData, Location} from 'angular2/router';
|
||||
import {RouteConfig, Route, AuxRoute, Redirect} from 'angular2/src/router/route_config_decorator';
|
||||
|
||||
import {specs, compile, TEST_ROUTER_PROVIDERS, clickOnElement, getHref} from '../util';
|
||||
import {BaseException} from 'angular2/src/facade/exceptions';
|
||||
|
||||
function getLinkElement(rtc: ComponentFixture, linkIndex: number = 0) {
|
||||
return rtc.debugElement.componentViewChildren[linkIndex].nativeElement;
|
||||
}
|
||||
|
||||
function auxRoutes() {
|
||||
var tcb: TestComponentBuilder;
|
||||
var fixture: ComponentFixture;
|
||||
var rtr;
|
||||
|
||||
beforeEachProviders(() => TEST_ROUTER_PROVIDERS);
|
||||
|
||||
beforeEach(inject([TestComponentBuilder, Router], (tcBuilder, router) => {
|
||||
tcb = tcBuilder;
|
||||
rtr = router;
|
||||
}));
|
||||
|
||||
it('should recognize and navigate from the URL', inject([AsyncTestCompleter], (async) => {
|
||||
compile(tcb, `main {<router-outlet></router-outlet>} | aux {<router-outlet name="modal"></router-outlet>}`)
|
||||
.then((rtc) => {fixture = rtc})
|
||||
.then((_) => rtr.config([
|
||||
new Route({path: '/hello', component: HelloCmp, name: 'Hello'}),
|
||||
new AuxRoute({path: '/modal', component: ModalCmp, name: 'Aux'})
|
||||
]))
|
||||
.then((_) => rtr.navigateByUrl('/(modal)'))
|
||||
.then((_) => {
|
||||
fixture.detectChanges();
|
||||
expect(fixture.debugElement.nativeElement).toHaveText('main {} | aux {modal}');
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should navigate via the link DSL', inject([AsyncTestCompleter], (async) => {
|
||||
compile(tcb, `main {<router-outlet></router-outlet>} | aux {<router-outlet name="modal"></router-outlet>}`)
|
||||
.then((rtc) => {fixture = rtc})
|
||||
.then((_) => rtr.config([
|
||||
new Route({path: '/hello', component: HelloCmp, name: 'Hello'}),
|
||||
new AuxRoute({path: '/modal', component: ModalCmp, name: 'Modal'})
|
||||
]))
|
||||
.then((_) => rtr.navigate(['/', ['Modal']]))
|
||||
.then((_) => {
|
||||
fixture.detectChanges();
|
||||
expect(fixture.debugElement.nativeElement).toHaveText('main {} | aux {modal}');
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should generate a link URL', inject([AsyncTestCompleter], (async) => {
|
||||
compile(
|
||||
tcb,
|
||||
`<a [routerLink]="['/', ['Modal']]">open modal</a> | main {<router-outlet></router-outlet>} | aux {<router-outlet name="modal"></router-outlet>}`)
|
||||
.then((rtc) => {fixture = rtc})
|
||||
.then((_) => rtr.config([
|
||||
new Route({path: '/hello', component: HelloCmp, name: 'Hello'}),
|
||||
new AuxRoute({path: '/modal', component: ModalCmp, name: 'Modal'})
|
||||
]))
|
||||
.then((_) => {
|
||||
fixture.detectChanges();
|
||||
expect(getHref(getLinkElement(fixture))).toEqual('/(modal)');
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should navigate from a link click',
|
||||
inject([AsyncTestCompleter, Location], (async, location) => {
|
||||
compile(
|
||||
tcb,
|
||||
`<a [routerLink]="['/', ['Modal']]">open modal</a> | <a [routerLink]="['/Hello']">hello</a> | main {<router-outlet></router-outlet>} | aux {<router-outlet name="modal"></router-outlet>}`)
|
||||
.then((rtc) => {fixture = rtc})
|
||||
.then((_) => rtr.config([
|
||||
new Route({path: '/hello', component: HelloCmp, name: 'Hello'}),
|
||||
new AuxRoute({path: '/modal', component: ModalCmp, name: 'Modal'})
|
||||
]))
|
||||
.then((_) => {
|
||||
fixture.detectChanges();
|
||||
expect(fixture.debugElement.nativeElement)
|
||||
.toHaveText('open modal | hello | main {} | aux {}');
|
||||
|
||||
var navCount = 0;
|
||||
|
||||
rtr.subscribe((_) => {
|
||||
navCount += 1;
|
||||
fixture.detectChanges();
|
||||
if (navCount == 1) {
|
||||
expect(fixture.debugElement.nativeElement)
|
||||
.toHaveText('open modal | hello | main {} | aux {modal}');
|
||||
expect(location.urlChanges).toEqual(['/(modal)']);
|
||||
expect(getHref(getLinkElement(fixture, 0))).toEqual('/(modal)');
|
||||
expect(getHref(getLinkElement(fixture, 1))).toEqual('/hello(modal)');
|
||||
|
||||
// click on primary route link
|
||||
clickOnElement(getLinkElement(fixture, 1));
|
||||
} else if (navCount == 2) {
|
||||
expect(fixture.debugElement.nativeElement)
|
||||
.toHaveText('open modal | hello | main {hello} | aux {modal}');
|
||||
expect(location.urlChanges).toEqual(['/(modal)', '/hello(modal)']);
|
||||
expect(getHref(getLinkElement(fixture, 0))).toEqual('/hello(modal)');
|
||||
expect(getHref(getLinkElement(fixture, 1))).toEqual('/hello(modal)');
|
||||
async.done();
|
||||
} else {
|
||||
throw new BaseException(`Unexpected route change #${navCount}`);
|
||||
}
|
||||
});
|
||||
|
||||
clickOnElement(getLinkElement(fixture));
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
function auxRoutesWithAPrimaryRoute() {
|
||||
var tcb: TestComponentBuilder;
|
||||
var fixture: ComponentFixture;
|
||||
var rtr;
|
||||
|
||||
beforeEachProviders(() => TEST_ROUTER_PROVIDERS);
|
||||
|
||||
beforeEach(inject([TestComponentBuilder, Router], (tcBuilder, router) => {
|
||||
tcb = tcBuilder;
|
||||
rtr = router;
|
||||
}));
|
||||
|
||||
it('should recognize and navigate from the URL', inject([AsyncTestCompleter], (async) => {
|
||||
compile(tcb, `main {<router-outlet></router-outlet>} | aux {<router-outlet name="modal"></router-outlet>}`)
|
||||
.then((rtc) => {fixture = rtc})
|
||||
.then((_) => rtr.config([
|
||||
new Route({path: '/hello', component: HelloCmp, name: 'Hello'}),
|
||||
new AuxRoute({path: '/modal', component: ModalCmp, name: 'Aux'})
|
||||
]))
|
||||
.then((_) => rtr.navigateByUrl('/hello(modal)'))
|
||||
.then((_) => {
|
||||
fixture.detectChanges();
|
||||
expect(fixture.debugElement.nativeElement).toHaveText('main {hello} | aux {modal}');
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should navigate via the link DSL', inject([AsyncTestCompleter], (async) => {
|
||||
compile(tcb, `main {<router-outlet></router-outlet>} | aux {<router-outlet name="modal"></router-outlet>}`)
|
||||
.then((rtc) => {fixture = rtc})
|
||||
.then((_) => rtr.config([
|
||||
new Route({path: '/hello', component: HelloCmp, name: 'Hello'}),
|
||||
new AuxRoute({path: '/modal', component: ModalCmp, name: 'Modal'})
|
||||
]))
|
||||
.then((_) => rtr.navigate(['/Hello', ['Modal']]))
|
||||
.then((_) => {
|
||||
fixture.detectChanges();
|
||||
expect(fixture.debugElement.nativeElement).toHaveText('main {hello} | aux {modal}');
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should generate a link URL', inject([AsyncTestCompleter], (async) => {
|
||||
compile(
|
||||
tcb,
|
||||
`<a [routerLink]="['/Hello', ['Modal']]">open modal</a> | main {<router-outlet></router-outlet>} | aux {<router-outlet name="modal"></router-outlet>}`)
|
||||
.then((rtc) => {fixture = rtc})
|
||||
.then((_) => rtr.config([
|
||||
new Route({path: '/hello', component: HelloCmp, name: 'Hello'}),
|
||||
new AuxRoute({path: '/modal', component: ModalCmp, name: 'Modal'})
|
||||
]))
|
||||
.then((_) => {
|
||||
fixture.detectChanges();
|
||||
expect(getHref(getLinkElement(fixture))).toEqual('/hello(modal)');
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should navigate from a link click',
|
||||
inject([AsyncTestCompleter, Location], (async, location) => {
|
||||
compile(
|
||||
tcb,
|
||||
`<a [routerLink]="['/Hello', ['Modal']]">open modal</a> | main {<router-outlet></router-outlet>} | aux {<router-outlet name="modal"></router-outlet>}`)
|
||||
.then((rtc) => {fixture = rtc})
|
||||
.then((_) => rtr.config([
|
||||
new Route({path: '/hello', component: HelloCmp, name: 'Hello'}),
|
||||
new AuxRoute({path: '/modal', component: ModalCmp, name: 'Modal'})
|
||||
]))
|
||||
.then((_) => {
|
||||
fixture.detectChanges();
|
||||
expect(fixture.debugElement.nativeElement).toHaveText('open modal | main {} | aux {}');
|
||||
|
||||
rtr.subscribe((_) => {
|
||||
fixture.detectChanges();
|
||||
expect(fixture.debugElement.nativeElement)
|
||||
.toHaveText('open modal | main {hello} | aux {modal}');
|
||||
expect(location.urlChanges).toEqual(['/hello(modal)']);
|
||||
async.done();
|
||||
});
|
||||
|
||||
clickOnElement(getLinkElement(fixture));
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
export function registerSpecs() {
|
||||
specs['auxRoutes'] = auxRoutes;
|
||||
specs['auxRoutesWithAPrimaryRoute'] = auxRoutesWithAPrimaryRoute;
|
||||
}
|
||||
|
||||
|
||||
@Component({selector: 'hello-cmp', template: `{{greeting}}`})
|
||||
class HelloCmp {
|
||||
greeting: string;
|
||||
constructor() { this.greeting = 'hello'; }
|
||||
}
|
||||
|
||||
@Component({selector: 'modal-cmp', template: `modal`})
|
||||
class ModalCmp {
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'aux-cmp',
|
||||
template: 'main {<router-outlet></router-outlet>} | ' +
|
||||
'aux {<router-outlet name="modal"></router-outlet>}',
|
||||
directives: [ROUTER_DIRECTIVES],
|
||||
})
|
||||
@RouteConfig([
|
||||
new Route({path: '/hello', component: HelloCmp, name: 'Hello'}),
|
||||
new AuxRoute({path: '/modal', component: ModalCmp, name: 'Aux'})
|
||||
])
|
||||
class AuxCmp {
|
||||
}
|
|
@ -325,7 +325,7 @@ export function main() {
|
|||
}));
|
||||
|
||||
|
||||
describe("router link dsl", () => {
|
||||
describe('router link dsl', () => {
|
||||
it('should generate link hrefs with params', inject([AsyncTestCompleter], (async) => {
|
||||
compile('<a href="hello" [routerLink]="route:./User(name: name)">{{name}}</a>')
|
||||
.then((_) => router.config(
|
||||
|
|
|
@ -77,7 +77,11 @@ export var specs = {};
|
|||
export function describeRouter(description: string, fn: Function, exclusive = false): void {
|
||||
var specName = descriptionToSpecName(description);
|
||||
specNameBuilder.push(specName);
|
||||
describe(description, fn);
|
||||
if (exclusive) {
|
||||
ddescribe(description, fn);
|
||||
} else {
|
||||
describe(description, fn);
|
||||
}
|
||||
specNameBuilder.pop();
|
||||
}
|
||||
|
||||
|
|
|
@ -47,9 +47,9 @@ export function main() {
|
|||
var instr = registry.generate(['FirstCmp', 'SecondCmp'], []);
|
||||
expect(stringifyInstruction(instr)).toEqual('first/second');
|
||||
|
||||
expect(stringifyInstruction(registry.generate(['SecondCmp'], [instr])))
|
||||
expect(stringifyInstruction(registry.generate(['SecondCmp'], [instr, instr.child])))
|
||||
.toEqual('first/second');
|
||||
expect(stringifyInstruction(registry.generate(['./SecondCmp'], [instr])))
|
||||
expect(stringifyInstruction(registry.generate(['./SecondCmp'], [instr, instr.child])))
|
||||
.toEqual('first/second');
|
||||
});
|
||||
|
||||
|
|
Loading…
Reference in New Issue