feat(router): support links with just auxiliary routes

Closes #5930
This commit is contained in:
Brian Ford 2015-12-07 10:51:01 -08:00
parent 909e70bd61
commit 2a2f9a9a19
12 changed files with 476 additions and 295 deletions

View File

@ -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);
},

View File

@ -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;
}

View File

@ -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 */

View File

@ -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({});

View File

@ -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);

View File

@ -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 {

View File

@ -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);

View File

@ -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 {
}

View File

@ -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 {
}

View File

@ -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(

View File

@ -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();
}

View File

@ -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');
});