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