From 2a2f9a9a196cf1502c3002f14bd42b13a4dcaa53 Mon Sep 17 00:00:00 2001 From: Brian Ford Date: Mon, 7 Dec 2015 10:51:01 -0800 Subject: [PATCH] feat(router): support links with just auxiliary routes Closes #5930 --- modules/angular1_router/lib/facades.es5 | 4 + .../src/router/component_recognizer.ts | 8 +- modules/angular2/src/router/instruction.ts | 6 +- modules/angular2/src/router/route_registry.ts | 294 ++++++++++-------- modules/angular2/src/router/router.ts | 30 +- modules/angular2/src/router/router_link.ts | 18 +- .../router/integration/async_route_spec.ts | 2 +- .../integration/auxiliary_route_spec.ts | 150 +-------- .../integration/impl/aux_route_spec_impl.ts | 247 +++++++++++++++ .../router/integration/router_link_spec.ts | 2 +- .../angular2/test/router/integration/util.ts | 6 +- .../test/router/route_registry_spec.ts | 4 +- 12 files changed, 476 insertions(+), 295 deletions(-) create mode 100644 modules/angular2/test/router/integration/impl/aux_route_spec_impl.ts diff --git a/modules/angular1_router/lib/facades.es5 b/modules/angular1_router/lib/facades.es5 index b82d198056..f419cdef5f 100644 --- a/modules/angular1_router/lib/facades.es5 +++ b/modules/angular1_router/lib/facades.es5 @@ -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); }, diff --git a/modules/angular2/src/router/component_recognizer.ts b/modules/angular2/src/router/component_recognizer.ts index 56730566fd..616680673c 100644 --- a/modules/angular2/src/router/component_recognizer.ts +++ b/modules/angular2/src/router/component_recognizer.ts @@ -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; } diff --git a/modules/angular2/src/router/instruction.ts b/modules/angular2/src/router/instruction.ts index be3e3d46f1..7178b4b742 100644 --- a/modules/angular2/src/router/instruction.ts +++ b/modules/angular2/src/router/instruction.ts @@ -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 */ diff --git a/modules/angular2/src/router/route_registry.ts b/modules/angular2/src/router/route_registry.ts index fd10fab80b..a897d4dcaa 100644 --- a/modules/angular2/src/router/route_registry.ts +++ b/modules/angular2/src/router/route_registry.ts @@ -144,20 +144,18 @@ export class RouteRegistry { */ recognize(url: string, ancestorInstructions: Instruction[]): Promise { 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 { - 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({}); diff --git a/modules/angular2/src/router/router.ts b/modules/angular2/src/router/router.ts index 032f3932c5..e90982e353 100644 --- a/modules/angular2/src/router/router.ts +++ b/modules/angular2/src/router/router.ts @@ -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 { return instruction.resolveComponent().then((_) => { - instruction.component.reuse = false; - var unsettledInstructions: Array> = []; + 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 { this._currentInstruction = instruction; + var next: Promise = _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 { var next = _resolveToTrue; + if (isBlank(nextInstruction.component)) { + return next; + } if (isPresent(nextInstruction.child)) { next = canActivateOne(nextInstruction.child, isPresent(prevInstruction) ? prevInstruction.child : null); diff --git a/modules/angular2/src/router/router_link.ts b/modules/angular2/src/router/router_link.ts index f228c4f7ae..bc51960bde 100644 --- a/modules/angular2/src/router/router_link.ts +++ b/modules/angular2/src/router/router_link.ts @@ -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 { diff --git a/modules/angular2/test/router/integration/async_route_spec.ts b/modules/angular2/test/router/integration/async_route_spec.ts index 40b182bfec..2706f52235 100644 --- a/modules/angular2/test/router/integration/async_route_spec.ts +++ b/modules/angular2/test/router/integration/async_route_spec.ts @@ -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); diff --git a/modules/angular2/test/router/integration/auxiliary_route_spec.ts b/modules/angular2/test/router/integration/auxiliary_route_spec.ts index 968acbe7d2..3bc7dd496a 100644 --- a/modules/angular2/test/router/integration/auxiliary_route_spec.ts +++ b/modules/angular2/test/router/integration/auxiliary_route_spec.ts @@ -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 {} | aux {}`) - .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 {} | aux {}`) - .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, - `open modal | main {} | aux {}`) - .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, - `open modal | main {} | aux {}`) - .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 {} | ' + - 'aux {}', - directives: [ROUTER_DIRECTIVES], -}) -@RouteConfig([ - new Route({path: '/hello', component: HelloCmp, name: 'Hello'}), - new AuxRoute({path: '/modal', component: ModalCmp, name: 'Aux'}) -]) -class AuxCmp { -} diff --git a/modules/angular2/test/router/integration/impl/aux_route_spec_impl.ts b/modules/angular2/test/router/integration/impl/aux_route_spec_impl.ts new file mode 100644 index 0000000000..eebe20cd0c --- /dev/null +++ b/modules/angular2/test/router/integration/impl/aux_route_spec_impl.ts @@ -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 {} | aux {}`) + .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 {} | aux {}`) + .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, + `open modal | main {} | aux {}`) + .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, + `open modal | hello | main {} | aux {}`) + .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 {} | aux {}`) + .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 {} | aux {}`) + .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, + `open modal | main {} | aux {}`) + .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, + `open modal | main {} | aux {}`) + .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 {} | ' + + 'aux {}', + directives: [ROUTER_DIRECTIVES], +}) +@RouteConfig([ + new Route({path: '/hello', component: HelloCmp, name: 'Hello'}), + new AuxRoute({path: '/modal', component: ModalCmp, name: 'Aux'}) +]) +class AuxCmp { +} diff --git a/modules/angular2/test/router/integration/router_link_spec.ts b/modules/angular2/test/router/integration/router_link_spec.ts index bf6d3b494e..86fad53f5b 100644 --- a/modules/angular2/test/router/integration/router_link_spec.ts +++ b/modules/angular2/test/router/integration/router_link_spec.ts @@ -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('{{name}}') .then((_) => router.config( diff --git a/modules/angular2/test/router/integration/util.ts b/modules/angular2/test/router/integration/util.ts index 5610d99b78..7789ebeb1a 100644 --- a/modules/angular2/test/router/integration/util.ts +++ b/modules/angular2/test/router/integration/util.ts @@ -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(); } diff --git a/modules/angular2/test/router/route_registry_spec.ts b/modules/angular2/test/router/route_registry_spec.ts index 423a88d264..5097afbdeb 100644 --- a/modules/angular2/test/router/route_registry_spec.ts +++ b/modules/angular2/test/router/route_registry_spec.ts @@ -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'); });