parent
							
								
									96e34c1d36
								
							
						
					
					
						commit
						ac6227e434
					
				| @ -19,7 +19,8 @@ export * from './src/router/route_config_decorator'; | ||||
| export * from './src/router/route_definition'; | ||||
| export {OnActivate, OnDeactivate, OnReuse, CanDeactivate, CanReuse} from './src/router/interfaces'; | ||||
| export {CanActivate} from './src/router/lifecycle_annotations'; | ||||
| export {Instruction} from './src/router/instruction'; | ||||
| export {Instruction, ComponentInstruction} from './src/router/instruction'; | ||||
| export {Url} from './src/router/url_parser'; | ||||
| 
 | ||||
| import {LocationStrategy} from './src/router/location_strategy'; | ||||
| import {HTML5LocationStrategy} from './src/router/html5_location_strategy'; | ||||
|  | ||||
| @ -1,19 +0,0 @@ | ||||
| import {isPresent} from 'angular2/src/facade/lang'; | ||||
| 
 | ||||
| export function parseAndAssignParamString(splitToken: string, paramString: string, | ||||
|                                           keyValueMap: StringMap<string, string>): void { | ||||
|   var first = paramString[0]; | ||||
|   if (first == '?' || first == ';') { | ||||
|     paramString = paramString.substring(1); | ||||
|   } | ||||
| 
 | ||||
|   paramString.split(splitToken) | ||||
|       .forEach((entry) => { | ||||
|         var tuple = entry.split('='); | ||||
|         var key = tuple[0]; | ||||
|         if (!isPresent(keyValueMap[key])) { | ||||
|           var value = tuple.length > 1 ? tuple[1] : true; | ||||
|           keyValueMap[key] = value; | ||||
|         } | ||||
|       }); | ||||
| } | ||||
| @ -6,9 +6,11 @@ import { | ||||
|   List, | ||||
|   ListWrapper | ||||
| } from 'angular2/src/facade/collection'; | ||||
| import {isPresent, isBlank, normalizeBlank} from 'angular2/src/facade/lang'; | ||||
| import {isPresent, isBlank, normalizeBlank, Type} from 'angular2/src/facade/lang'; | ||||
| import {Promise} from 'angular2/src/facade/async'; | ||||
| 
 | ||||
| import {PathRecognizer} from './path_recognizer'; | ||||
| import {Url} from './url_parser'; | ||||
| 
 | ||||
| export class RouteParams { | ||||
|   constructor(public params: StringMap<string, string>) {} | ||||
| @ -18,34 +20,82 @@ export class RouteParams { | ||||
| 
 | ||||
| 
 | ||||
| /** | ||||
|  * An `Instruction` represents the component hierarchy of the application based on a given route | ||||
|  * `Instruction` is a tree of `ComponentInstructions`, with all the information needed | ||||
|  * to transition each component in the app to a given route, including all auxiliary routes. | ||||
|  * | ||||
|  * This is a public API. | ||||
|  */ | ||||
| export class Instruction { | ||||
|   // "capturedUrl" is the part of the URL captured by this instruction
 | ||||
|   // "accumulatedUrl" is the part of the URL captured by this instruction and all children
 | ||||
|   accumulatedUrl: string; | ||||
|   constructor(public component: ComponentInstruction, public child: Instruction, | ||||
|               public auxInstruction: StringMap<string, Instruction>) {} | ||||
| 
 | ||||
|   replaceChild(child: Instruction): Instruction { | ||||
|     return new Instruction(this.component, child, this.auxInstruction); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Represents a partially completed instruction during recognition that only has the | ||||
|  * primary (non-aux) route instructions matched. | ||||
|  * | ||||
|  * `PrimaryInstruction` is an internal class used by `RouteRecognizer` while it's | ||||
|  * figuring out where to navigate. | ||||
|  */ | ||||
| export class PrimaryInstruction { | ||||
|   constructor(public component: ComponentInstruction, public child: PrimaryInstruction, | ||||
|               public auxUrls: List<Url>) {} | ||||
| } | ||||
| 
 | ||||
| export function stringifyInstruction(instruction: Instruction): string { | ||||
|   var params = instruction.component.urlParams.length > 0 ? | ||||
|                    ('?' + instruction.component.urlParams.join('&')) : | ||||
|                    ''; | ||||
| 
 | ||||
|   return instruction.component.urlPath + stringifyAux(instruction) + | ||||
|          stringifyPrimary(instruction.child) + params; | ||||
| } | ||||
| 
 | ||||
| function stringifyPrimary(instruction: Instruction): string { | ||||
|   if (isBlank(instruction)) { | ||||
|     return ''; | ||||
|   } | ||||
|   var params = instruction.component.urlParams.length > 0 ? | ||||
|                    (';' + instruction.component.urlParams.join(';')) : | ||||
|                    ''; | ||||
|   return '/' + instruction.component.urlPath + params + stringifyAux(instruction) + | ||||
|          stringifyPrimary(instruction.child); | ||||
| } | ||||
| 
 | ||||
| function stringifyAux(instruction: Instruction): string { | ||||
|   var routes = []; | ||||
|   StringMapWrapper.forEach(instruction.auxInstruction, (auxInstruction, _) => { | ||||
|     routes.push(stringifyPrimary(auxInstruction)); | ||||
|   }); | ||||
|   if (routes.length > 0) { | ||||
|     return '(' + routes.join('//') + ')'; | ||||
|   } | ||||
|   return ''; | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| /** | ||||
|  * A `ComponentInstruction` represents the route state for a single component. An `Instruction` is | ||||
|  * composed of a tree of these `ComponentInstruction`s. | ||||
|  * | ||||
|  * `ComponentInstructions` is a public API. Instances of `ComponentInstruction` are passed | ||||
|  * to route lifecycle hooks, like {@link CanActivate}. | ||||
|  */ | ||||
| export class ComponentInstruction { | ||||
|   reuse: boolean = false; | ||||
|   specificity: number; | ||||
| 
 | ||||
|   constructor(public component: any, public capturedUrl: string, | ||||
|               private _recognizer: PathRecognizer, public child: Instruction = null, | ||||
|               private _params: StringMap<string, any> = null) { | ||||
|     this.accumulatedUrl = capturedUrl; | ||||
|     this.specificity = _recognizer.specificity; | ||||
|     if (isPresent(child)) { | ||||
|       this.child = child; | ||||
|       this.specificity += child.specificity; | ||||
|       var childUrl = child.accumulatedUrl; | ||||
|       if (isPresent(childUrl)) { | ||||
|         this.accumulatedUrl += childUrl; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|   constructor(public urlPath: string, public urlParams: List<string>, | ||||
|               private _recognizer: PathRecognizer, public params: StringMap<string, any> = null) {} | ||||
| 
 | ||||
|   params(): StringMap<string, string> { | ||||
|     if (isBlank(this._params)) { | ||||
|       this._params = this._recognizer.parseParams(this.capturedUrl); | ||||
|     } | ||||
|     return this._params; | ||||
|   } | ||||
|   get componentType() { return this._recognizer.handler.componentType; } | ||||
| 
 | ||||
|   resolveComponentType(): Promise<Type> { return this._recognizer.handler.resolveComponentType(); } | ||||
| 
 | ||||
|   get specificity() { return this._recognizer.specificity; } | ||||
| 
 | ||||
|   get terminal() { return this._recognizer.terminal; } | ||||
| } | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| import {Instruction} from './instruction'; | ||||
| import {ComponentInstruction} from './instruction'; | ||||
| import {global} from 'angular2/src/facade/lang'; | ||||
| 
 | ||||
| // This is here only so that after TS transpilation the file is not empty.
 | ||||
| @ -11,33 +11,33 @@ var __ignore_me = global; | ||||
|  * Defines route lifecycle method [onActivate] | ||||
|  */ | ||||
| export interface OnActivate { | ||||
|   onActivate(nextInstruction: Instruction, prevInstruction: Instruction): any; | ||||
|   onActivate(nextInstruction: ComponentInstruction, prevInstruction: ComponentInstruction): any; | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Defines route lifecycle method [onReuse] | ||||
|  */ | ||||
| export interface OnReuse { | ||||
|   onReuse(nextInstruction: Instruction, prevInstruction: Instruction): any; | ||||
|   onReuse(nextInstruction: ComponentInstruction, prevInstruction: ComponentInstruction): any; | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Defines route lifecycle method [onDeactivate] | ||||
|  */ | ||||
| export interface OnDeactivate { | ||||
|   onDeactivate(nextInstruction: Instruction, prevInstruction: Instruction): any; | ||||
|   onDeactivate(nextInstruction: ComponentInstruction, prevInstruction: ComponentInstruction): any; | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Defines route lifecycle method [canReuse] | ||||
|  */ | ||||
| export interface CanReuse { | ||||
|   canReuse(nextInstruction: Instruction, prevInstruction: Instruction): any; | ||||
|   canReuse(nextInstruction: ComponentInstruction, prevInstruction: ComponentInstruction): any; | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Defines route lifecycle method [canDeactivate] | ||||
|  */ | ||||
| export interface CanDeactivate { | ||||
|   canDeactivate(nextInstruction: Instruction, prevInstruction: Instruction): any; | ||||
|   canDeactivate(nextInstruction: ComponentInstruction, prevInstruction: ComponentInstruction): any; | ||||
| } | ||||
|  | ||||
| @ -6,7 +6,7 @@ | ||||
| import {makeDecorator} from 'angular2/src/util/decorators'; | ||||
| import {CanActivate as CanActivateAnnotation} from './lifecycle_annotations_impl'; | ||||
| import {Promise} from 'angular2/src/facade/async'; | ||||
| import {Instruction} from 'angular2/src/router/instruction'; | ||||
| import {ComponentInstruction} from 'angular2/src/router/instruction'; | ||||
| 
 | ||||
| export { | ||||
|   canReuse, | ||||
| @ -17,5 +17,5 @@ export { | ||||
| } from './lifecycle_annotations_impl'; | ||||
| 
 | ||||
| export var CanActivate: | ||||
|     (hook: (next: Instruction, prev: Instruction) => Promise<boolean>| boolean) => ClassDecorator = | ||||
|     (hook: (next: ComponentInstruction, prev: ComponentInstruction) => Promise<boolean>| boolean) => ClassDecorator = | ||||
|         makeDecorator(CanActivateAnnotation); | ||||
|  | ||||
| @ -7,7 +7,6 @@ import { | ||||
|   isBlank, | ||||
|   BaseException | ||||
| } from 'angular2/src/facade/lang'; | ||||
| import {Promise, PromiseWrapper} from 'angular2/src/facade/async'; | ||||
| import { | ||||
|   Map, | ||||
|   MapWrapper, | ||||
| @ -17,21 +16,14 @@ import { | ||||
|   ListWrapper | ||||
| } from 'angular2/src/facade/collection'; | ||||
| import {IMPLEMENTS} from 'angular2/src/facade/lang'; | ||||
| import {parseAndAssignParamString} from 'angular2/src/router/helpers'; | ||||
| import {escapeRegex} from './url'; | ||||
| 
 | ||||
| import {RouteHandler} from './route_handler'; | ||||
| import {Url, RootUrl, serializeParams} from './url_parser'; | ||||
| import {ComponentInstruction} from './instruction'; | ||||
| 
 | ||||
| // TODO(jeffbcross): implement as interface when ts2dart adds support:
 | ||||
| // https://github.com/angular/ts2dart/issues/173
 | ||||
| export class Segment { | ||||
|   name: string; | ||||
|   regex: string; | ||||
|   generate(params: TouchMap): string { return ''; } | ||||
| } | ||||
| 
 | ||||
| class TouchMap { | ||||
|   map: StringMap<string, string> = StringMapWrapper.create(); | ||||
|   keys: StringMap<string, boolean> = StringMapWrapper.create(); | ||||
| export class TouchMap { | ||||
|   map: StringMap<string, string> = {}; | ||||
|   keys: StringMap<string, boolean> = {}; | ||||
| 
 | ||||
|   constructor(map: StringMap<string, any>) { | ||||
|     if (isPresent(map)) { | ||||
| @ -63,31 +55,28 @@ function normalizeString(obj: any): string { | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| class ContinuationSegment extends Segment {} | ||||
| export interface Segment { | ||||
|   name: string; | ||||
|   generate(params: TouchMap): string; | ||||
|   match(path: string): boolean; | ||||
| } | ||||
| 
 | ||||
| class StaticSegment extends Segment { | ||||
|   regex: string; | ||||
| class ContinuationSegment implements Segment { | ||||
|   name: string = ''; | ||||
| 
 | ||||
|   constructor(public string: string) { | ||||
|     super(); | ||||
|     this.regex = escapeRegex(string); | ||||
| 
 | ||||
|     // we add this property so that the route matcher still sees
 | ||||
|     // this segment as a valid path even if do not use the matrix
 | ||||
|     // parameters
 | ||||
|     this.regex += '(;[^\/]+)?'; | ||||
|   generate(params: TouchMap): string { return ''; } | ||||
|   match(path: string): boolean { return true; } | ||||
| } | ||||
| 
 | ||||
|   generate(params: TouchMap): string { return this.string; } | ||||
| class StaticSegment implements Segment { | ||||
|   name: string = ''; | ||||
|   constructor(public path: string) {} | ||||
|   match(path: string): boolean { return path == this.path; } | ||||
|   generate(params: TouchMap): string { return this.path; } | ||||
| } | ||||
| 
 | ||||
| @IMPLEMENTS(Segment) | ||||
| class DynamicSegment { | ||||
|   regex: string = "([^/]+)"; | ||||
| 
 | ||||
| class DynamicSegment implements Segment { | ||||
|   constructor(public name: string) {} | ||||
| 
 | ||||
|   match(path: string): boolean { return true; } | ||||
|   generate(params: TouchMap): string { | ||||
|     if (!StringMapWrapper.contains(params.map, this.name)) { | ||||
|       throw new BaseException( | ||||
| @ -98,11 +87,9 @@ class DynamicSegment { | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| class StarSegment { | ||||
|   regex: string = "(.+)"; | ||||
| 
 | ||||
| class StarSegment implements Segment { | ||||
|   constructor(public name: string) {} | ||||
| 
 | ||||
|   match(path: string): boolean { return true; } | ||||
|   generate(params: TouchMap): string { return normalizeString(params.get(this.name)); } | ||||
| } | ||||
| 
 | ||||
| @ -150,7 +137,7 @@ function parsePathString(route: string): StringMap<string, any> { | ||||
|         throw new BaseException(`Unexpected "..." before the end of the path for "${route}".`); | ||||
|       } | ||||
|       results.push(new ContinuationSegment()); | ||||
|     } else if (segment.length > 0) { | ||||
|     } else { | ||||
|       results.push(new StaticSegment(segment)); | ||||
|       specificity += 100 * (100 - i); | ||||
|     } | ||||
| @ -161,6 +148,23 @@ function parsePathString(route: string): StringMap<string, any> { | ||||
|   return result; | ||||
| } | ||||
| 
 | ||||
| // this function is used to determine whether a route config path like `/foo/:id` collides with
 | ||||
| // `/foo/:name`
 | ||||
| function pathDslHash(segments: List<Segment>): string { | ||||
|   return segments.map((segment) => { | ||||
|                    if (segment instanceof StarSegment) { | ||||
|                      return '*'; | ||||
|                    } else if (segment instanceof ContinuationSegment) { | ||||
|                      return '...'; | ||||
|                    } else if (segment instanceof DynamicSegment) { | ||||
|                      return ':'; | ||||
|                    } else if (segment instanceof StaticSegment) { | ||||
|                      return segment.path; | ||||
|                    } | ||||
|                  }) | ||||
|       .join('/'); | ||||
| } | ||||
| 
 | ||||
| function splitBySlash(url: string): List<string> { | ||||
|   return url.split('/'); | ||||
| } | ||||
| @ -178,125 +182,106 @@ function assertPath(path: string) { | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| export class PathMatch { | ||||
|   constructor(public instruction: ComponentInstruction, public remaining: Url, | ||||
|               public remainingAux: List<Url>) {} | ||||
| } | ||||
| 
 | ||||
| // represents something like '/foo/:bar'
 | ||||
| export class PathRecognizer { | ||||
|   segments: List<Segment>; | ||||
|   regex: RegExp; | ||||
|   private _segments: List<Segment>; | ||||
|   specificity: number; | ||||
|   terminal: boolean = true; | ||||
|   hash: string; | ||||
| 
 | ||||
|   static matrixRegex: RegExp = RegExpWrapper.create('^(.*\/[^\/]+?)(;[^\/]+)?\/?$'); | ||||
|   static queryRegex: RegExp = RegExpWrapper.create('^(.*\/[^\/]+?)(\\?[^\/]+)?$'); | ||||
|   // TODO: cache component instruction instances by params and by ParsedUrl instance
 | ||||
| 
 | ||||
|   constructor(public path: string, public handler: RouteHandler, public isRoot: boolean = false) { | ||||
|   constructor(public path: string, public handler: RouteHandler) { | ||||
|     assertPath(path); | ||||
|     var parsed = parsePathString(path); | ||||
|     var specificity = parsed['specificity']; | ||||
|     var segments = parsed['segments']; | ||||
|     var regexString = '^'; | ||||
| 
 | ||||
|     ListWrapper.forEach(segments, (segment) => { | ||||
|     this._segments = parsed['segments']; | ||||
|     this.specificity = parsed['specificity']; | ||||
|     this.hash = pathDslHash(this._segments); | ||||
| 
 | ||||
|     var lastSegment = this._segments[this._segments.length - 1]; | ||||
|     this.terminal = !(lastSegment instanceof ContinuationSegment); | ||||
|   } | ||||
| 
 | ||||
| 
 | ||||
|   recognize(beginningSegment: Url): PathMatch { | ||||
|     var nextSegment = beginningSegment; | ||||
|     var currentSegment: Url; | ||||
|     var positionalParams = {}; | ||||
|     var captured = []; | ||||
| 
 | ||||
|     for (var i = 0; i < this._segments.length; i += 1) { | ||||
|       if (isBlank(nextSegment)) { | ||||
|         return null; | ||||
|       } | ||||
|       currentSegment = nextSegment; | ||||
| 
 | ||||
|       var segment = this._segments[i]; | ||||
| 
 | ||||
|       if (segment instanceof ContinuationSegment) { | ||||
|         this.terminal = false; | ||||
|       } else { | ||||
|         regexString += '/' + segment.regex; | ||||
|       } | ||||
|     }); | ||||
| 
 | ||||
|     if (this.terminal) { | ||||
|       regexString += '$'; | ||||
|         break; | ||||
|       } | ||||
| 
 | ||||
|     this.regex = RegExpWrapper.create(regexString); | ||||
|     this.segments = segments; | ||||
|     this.specificity = specificity; | ||||
|       captured.push(currentSegment.path); | ||||
| 
 | ||||
|       // the star segment consumes all of the remaining URL, including matrix params
 | ||||
|       if (segment instanceof StarSegment) { | ||||
|         positionalParams[segment.name] = currentSegment.toString(); | ||||
|         nextSegment = null; | ||||
|         break; | ||||
|       } | ||||
| 
 | ||||
|   parseParams(url: string): StringMap<string, string> { | ||||
|     // the last segment is always the star one since it's terminal
 | ||||
|     var segmentsLimit = this.segments.length - 1; | ||||
|     var containsStarSegment = | ||||
|         segmentsLimit >= 0 && this.segments[segmentsLimit] instanceof StarSegment; | ||||
| 
 | ||||
|     var paramsString, useQueryString = this.isRoot && this.terminal; | ||||
|     if (!containsStarSegment) { | ||||
|       var matches = RegExpWrapper.firstMatch( | ||||
|           useQueryString ? PathRecognizer.queryRegex : PathRecognizer.matrixRegex, url); | ||||
|       if (isPresent(matches)) { | ||||
|         url = matches[1]; | ||||
|         paramsString = matches[2]; | ||||
|       if (segment instanceof DynamicSegment) { | ||||
|         positionalParams[segment.name] = currentSegment.path; | ||||
|       } else if (!segment.match(currentSegment.path)) { | ||||
|         return null; | ||||
|       } | ||||
| 
 | ||||
|       url = StringWrapper.replaceAll(url, /(;[^\/]+)(?=(\/|$))/g, ''); | ||||
|       nextSegment = currentSegment.child; | ||||
|     } | ||||
| 
 | ||||
|     var params = StringMapWrapper.create(); | ||||
|     var urlPart = url; | ||||
| 
 | ||||
|     for (var i = 0; i <= segmentsLimit; i++) { | ||||
|       var segment = this.segments[i]; | ||||
|       if (segment instanceof ContinuationSegment) { | ||||
|         continue; | ||||
|     if (this.terminal && isPresent(nextSegment)) { | ||||
|       return null; | ||||
|     } | ||||
| 
 | ||||
|       var match = RegExpWrapper.firstMatch(RegExpWrapper.create('/' + segment.regex), urlPart); | ||||
|       urlPart = StringWrapper.substring(urlPart, match[0].length); | ||||
|       if (segment.name.length > 0) { | ||||
|         params[segment.name] = match[1]; | ||||
|       } | ||||
|     var urlPath = captured.join('/'); | ||||
| 
 | ||||
|     // If this is the root component, read query params. Otherwise, read matrix params.
 | ||||
|     var paramsSegment = beginningSegment instanceof RootUrl ? beginningSegment : currentSegment; | ||||
| 
 | ||||
| 
 | ||||
|     var allParams = isPresent(paramsSegment.params) ? | ||||
|                         StringMapWrapper.merge(paramsSegment.params, positionalParams) : | ||||
|                         positionalParams; | ||||
|     var urlParams = serializeParams(paramsSegment.params); | ||||
| 
 | ||||
|     var instruction = new ComponentInstruction(urlPath, urlParams, this, allParams); | ||||
| 
 | ||||
|     return new PathMatch(instruction, nextSegment, currentSegment.auxiliary); | ||||
|   } | ||||
| 
 | ||||
|     if (isPresent(paramsString) && paramsString.length > 0) { | ||||
|       var expectedStartingValue = useQueryString ? '?' : ';'; | ||||
|       if (paramsString[0] == expectedStartingValue) { | ||||
|         parseAndAssignParamString(expectedStartingValue, paramsString, params); | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     return params; | ||||
|   } | ||||
| 
 | ||||
|   generate(params: StringMap<string, any>): string { | ||||
|   generate(params: StringMap<string, any>): ComponentInstruction { | ||||
|     var paramTokens = new TouchMap(params); | ||||
|     var applyLeadingSlash = false; | ||||
|     var useQueryString = this.isRoot && this.terminal; | ||||
| 
 | ||||
|     var url = ''; | ||||
|     for (var i = 0; i < this.segments.length; i++) { | ||||
|       let segment = this.segments[i]; | ||||
|       let s = segment.generate(paramTokens); | ||||
|       applyLeadingSlash = applyLeadingSlash || (segment instanceof ContinuationSegment); | ||||
|     var path = []; | ||||
| 
 | ||||
|       if (s.length > 0) { | ||||
|         url += (i > 0 ? '/' : '') + s; | ||||
|     for (var i = 0; i < this._segments.length; i++) { | ||||
|       let segment = this._segments[i]; | ||||
|       if (!(segment instanceof ContinuationSegment)) { | ||||
|         path.push(segment.generate(paramTokens)); | ||||
|       } | ||||
|     } | ||||
|     var urlPath = path.join('/'); | ||||
| 
 | ||||
|     var unusedParams = paramTokens.getUnused(); | ||||
|     if (!StringMapWrapper.isEmpty(unusedParams)) { | ||||
|       url += useQueryString ? '?' : ';'; | ||||
|       var paramToken = useQueryString ? '&' : ';'; | ||||
|       var i = 0; | ||||
|       StringMapWrapper.forEach(unusedParams, (value, key) => { | ||||
|         if (i++ > 0) { | ||||
|           url += paramToken; | ||||
|         } | ||||
|         url += key; | ||||
|         if (!isPresent(value) && useQueryString) { | ||||
|           value = 'true'; | ||||
|         } | ||||
|         if (isPresent(value)) { | ||||
|           url += '=' + value; | ||||
|         } | ||||
|       }); | ||||
|     } | ||||
|     var nonPositionalParams = paramTokens.getUnused(); | ||||
|     var urlParams = serializeParams(nonPositionalParams); | ||||
| 
 | ||||
|     if (applyLeadingSlash) { | ||||
|       url += '/'; | ||||
|     return new ComponentInstruction(urlPath, urlParams, this, params); | ||||
|   } | ||||
| 
 | ||||
|     return url; | ||||
|   } | ||||
| 
 | ||||
|   resolveComponentType(): Promise<any> { return this.handler.resolveComponentType(); } | ||||
| } | ||||
|  | ||||
| @ -2,6 +2,6 @@ import {RouteConfig as RouteConfigAnnotation, RouteDefinition} from './route_con | ||||
| import {makeDecorator} from 'angular2/src/util/decorators'; | ||||
| import {List} from 'angular2/src/facade/collection'; | ||||
| 
 | ||||
| export {Route, Redirect, AsyncRoute, RouteDefinition} from './route_config_impl'; | ||||
| export {Route, Redirect, AuxRoute, AsyncRoute, RouteDefinition} from './route_config_impl'; | ||||
| export var RouteConfig: (configs: List<RouteDefinition>) => ClassDecorator = | ||||
|     makeDecorator(RouteConfigAnnotation); | ||||
|  | ||||
| @ -8,7 +8,7 @@ export {RouteDefinition} from './route_definition'; | ||||
|  * | ||||
|  * Supported keys: | ||||
|  * - `path` (required) | ||||
|  * - `component`,  `redirectTo` (requires exactly one of these) | ||||
|  * - `component`, `loader`,  `redirectTo` (requires exactly one of these) | ||||
|  * - `as` (optional) | ||||
|  */ | ||||
| @CONST() | ||||
| @ -34,6 +34,21 @@ export class Route implements RouteDefinition { | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| @CONST() | ||||
| export class AuxRoute implements RouteDefinition { | ||||
|   path: string; | ||||
|   component: Type; | ||||
|   as: string; | ||||
|   // added next two properties to work around https://github.com/Microsoft/TypeScript/issues/4107
 | ||||
|   loader: Function = null; | ||||
|   redirectTo: string = null; | ||||
|   constructor({path, component, as}: {path: string, component: Type, as?: string}) { | ||||
|     this.path = path; | ||||
|     this.component = component; | ||||
|     this.as = as; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| @CONST() | ||||
| export class AsyncRoute implements RouteDefinition { | ||||
|   path: string; | ||||
| @ -51,6 +66,8 @@ export class Redirect implements RouteDefinition { | ||||
|   path: string; | ||||
|   redirectTo: string; | ||||
|   as: string = null; | ||||
|   // added next property to work around https://github.com/Microsoft/TypeScript/issues/4107
 | ||||
|   loader: Function = null; | ||||
|   constructor({path, redirectTo}: {path: string, redirectTo: string}) { | ||||
|     this.path = path; | ||||
|     this.redirectTo = redirectTo; | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| import {AsyncRoute, Route, Redirect, RouteDefinition} from './route_config_decorator'; | ||||
| import {AsyncRoute, AuxRoute, Route, Redirect, RouteDefinition} from './route_config_decorator'; | ||||
| import {ComponentDefinition} from './route_definition'; | ||||
| import {Type, BaseException} from 'angular2/src/facade/lang'; | ||||
| 
 | ||||
| @ -6,13 +6,14 @@ import {Type, BaseException} from 'angular2/src/facade/lang'; | ||||
|  * Given a JS Object that represents... returns a corresponding Route, AsyncRoute, or Redirect | ||||
|  */ | ||||
| export function normalizeRouteConfig(config: RouteDefinition): RouteDefinition { | ||||
|   if (config instanceof Route || config instanceof Redirect || config instanceof AsyncRoute) { | ||||
|   if (config instanceof Route || config instanceof Redirect || config instanceof AsyncRoute || | ||||
|       config instanceof AuxRoute) { | ||||
|     return <RouteDefinition>config; | ||||
|   } | ||||
| 
 | ||||
|   if ((!config.component) == (!config.redirectTo)) { | ||||
|     throw new BaseException( | ||||
|         `Route config should contain exactly one 'component', or 'redirectTo' property`); | ||||
|         `Route config should contain exactly one "component", "loader", or "redirectTo" property.`); | ||||
|   } | ||||
|   if (config.component) { | ||||
|     if (typeof config.component == 'object') { | ||||
| @ -28,7 +29,7 @@ export function normalizeRouteConfig(config: RouteDefinition): RouteDefinition { | ||||
|             {path: config.path, loader: componentDefinitionObject.loader, as: config.as}); | ||||
|       } else { | ||||
|         throw new BaseException( | ||||
|             `Invalid component type '${componentDefinitionObject.type}'. Valid types are "constructor" and "loader".`); | ||||
|             `Invalid component type "${componentDefinitionObject.type}". Valid types are "constructor" and "loader".`); | ||||
|       } | ||||
|     } | ||||
|     return new Route(<{ | ||||
|  | ||||
| @ -6,7 +6,8 @@ import { | ||||
|   isPresent, | ||||
|   isType, | ||||
|   isStringMap, | ||||
|   BaseException | ||||
|   BaseException, | ||||
|   Type | ||||
| } from 'angular2/src/facade/lang'; | ||||
| import { | ||||
|   Map, | ||||
| @ -17,12 +18,13 @@ import { | ||||
|   StringMapWrapper | ||||
| } from 'angular2/src/facade/collection'; | ||||
| 
 | ||||
| import {PathRecognizer} from './path_recognizer'; | ||||
| import {RouteHandler} from './route_handler'; | ||||
| import {Route, AsyncRoute, Redirect, RouteDefinition} from './route_config_impl'; | ||||
| import {PathRecognizer, PathMatch} from './path_recognizer'; | ||||
| import {Route, AsyncRoute, AuxRoute, Redirect, RouteDefinition} from './route_config_impl'; | ||||
| import {AsyncRouteHandler} from './async_route_handler'; | ||||
| import {SyncRouteHandler} from './sync_route_handler'; | ||||
| import {parseAndAssignParamString} from 'angular2/src/router/helpers'; | ||||
| import {Url} from './url_parser'; | ||||
| import {ComponentInstruction} from './instruction'; | ||||
| 
 | ||||
| 
 | ||||
| /** | ||||
|  * `RouteRecognizer` is responsible for recognizing routes for a single component. | ||||
| @ -31,30 +33,45 @@ import {parseAndAssignParamString} from 'angular2/src/router/helpers'; | ||||
|  */ | ||||
| export class RouteRecognizer { | ||||
|   names: Map<string, PathRecognizer> = new Map(); | ||||
|   redirects: Map<string, string> = new Map(); | ||||
|   matchers: Map<RegExp, PathRecognizer> = new Map(); | ||||
| 
 | ||||
|   constructor(public isRoot: boolean = false) {} | ||||
|   auxRoutes: Map<string, PathRecognizer> = new Map(); | ||||
| 
 | ||||
|   // TODO: optimize this into a trie
 | ||||
|   matchers: List<PathRecognizer> = []; | ||||
| 
 | ||||
|   // TODO: optimize this into a trie
 | ||||
|   redirects: List<Redirector> = []; | ||||
| 
 | ||||
|   config(config: RouteDefinition): boolean { | ||||
|     var handler; | ||||
| 
 | ||||
|     if (config instanceof AuxRoute) { | ||||
|       handler = new SyncRouteHandler(config.component); | ||||
|       let path = config.path.startsWith('/') ? config.path.substring(1) : config.path; | ||||
|       var recognizer = new PathRecognizer(config.path, handler); | ||||
|       this.auxRoutes.set(path, recognizer); | ||||
|       return recognizer.terminal; | ||||
|     } | ||||
|     if (config instanceof Redirect) { | ||||
|       let path = config.path == '/' ? '' : config.path; | ||||
|       this.redirects.set(path, config.redirectTo); | ||||
|       this.redirects.push(new Redirector(config.path, config.redirectTo)); | ||||
|       return true; | ||||
|     } else if (config instanceof Route) { | ||||
|     } | ||||
| 
 | ||||
|     if (config instanceof Route) { | ||||
|       handler = new SyncRouteHandler(config.component); | ||||
|     } else if (config instanceof AsyncRoute) { | ||||
|       handler = new AsyncRouteHandler(config.loader); | ||||
|     } | ||||
|     var recognizer = new PathRecognizer(config.path, handler, this.isRoot); | ||||
|     MapWrapper.forEach(this.matchers, (matcher, _) => { | ||||
|       if (recognizer.regex.toString() == matcher.regex.toString()) { | ||||
|     var recognizer = new PathRecognizer(config.path, handler); | ||||
| 
 | ||||
|     this.matchers.forEach((matcher) => { | ||||
|       if (recognizer.hash == matcher.hash) { | ||||
|         throw new BaseException( | ||||
|             `Configuration '${config.path}' conflicts with existing route '${matcher.path}'`); | ||||
|       } | ||||
|     }); | ||||
|     this.matchers.set(recognizer.regex, recognizer); | ||||
| 
 | ||||
|     this.matchers.push(recognizer); | ||||
|     if (isPresent(config.as)) { | ||||
|       this.names.set(config.as, recognizer); | ||||
|     } | ||||
| @ -66,102 +83,87 @@ export class RouteRecognizer { | ||||
|    * Given a URL, returns a list of `RouteMatch`es, which are partial recognitions for some route. | ||||
|    * | ||||
|    */ | ||||
|   recognize(url: string): List<RouteMatch> { | ||||
|   recognize(urlParse: Url): List<PathMatch> { | ||||
|     var solutions = []; | ||||
|     if (url.length > 0 && url[url.length - 1] == '/') { | ||||
|       url = url.substring(0, url.length - 1); | ||||
|     } | ||||
| 
 | ||||
|     MapWrapper.forEach(this.redirects, (target, path) => { | ||||
|       // "/" redirect case
 | ||||
|       if (path == '/' || path == '') { | ||||
|         if (path == url) { | ||||
|           url = target; | ||||
|         } | ||||
|       } else if (url.startsWith(path)) { | ||||
|         url = target + url.substring(path.length); | ||||
|       } | ||||
|     }); | ||||
|     urlParse = this._redirect(urlParse); | ||||
| 
 | ||||
|     var queryParams = StringMapWrapper.create(); | ||||
|     var queryString = ''; | ||||
|     var queryIndex = url.indexOf('?'); | ||||
|     if (queryIndex >= 0) { | ||||
|       queryString = url.substring(queryIndex + 1); | ||||
|       url = url.substring(0, queryIndex); | ||||
|     } | ||||
|     if (this.isRoot && queryString.length > 0) { | ||||
|       parseAndAssignParamString('&', queryString, queryParams); | ||||
|     } | ||||
|     this.matchers.forEach((pathRecognizer: PathRecognizer) => { | ||||
|       var pathMatch = pathRecognizer.recognize(urlParse); | ||||
| 
 | ||||
|     MapWrapper.forEach(this.matchers, (pathRecognizer, regex) => { | ||||
|       var match; | ||||
|       if (isPresent(match = RegExpWrapper.firstMatch(regex, url))) { | ||||
|         var matchedUrl = '/'; | ||||
|         var unmatchedUrl = ''; | ||||
|         if (url != '/') { | ||||
|           matchedUrl = match[0]; | ||||
|           unmatchedUrl = url.substring(match[0].length); | ||||
|         } | ||||
|         var params = null; | ||||
|         if (pathRecognizer.terminal && !StringMapWrapper.isEmpty(queryParams)) { | ||||
|           params = queryParams; | ||||
|           matchedUrl += '?' + queryString; | ||||
|         } | ||||
|         solutions.push(new RouteMatch(pathRecognizer, matchedUrl, unmatchedUrl, params)); | ||||
|       if (isPresent(pathMatch)) { | ||||
|         solutions.push(pathMatch); | ||||
|       } | ||||
|     }); | ||||
| 
 | ||||
|     return solutions; | ||||
|   } | ||||
| 
 | ||||
|   _redirect(urlParse: Url): Url { | ||||
|     for (var i = 0; i < this.redirects.length; i += 1) { | ||||
|       let redirector = this.redirects[i]; | ||||
|       var redirectedUrl = redirector.redirect(urlParse); | ||||
|       if (isPresent(redirectedUrl)) { | ||||
|         return redirectedUrl; | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     return urlParse; | ||||
|   } | ||||
| 
 | ||||
|   recognizeAuxiliary(urlParse: Url): PathMatch { | ||||
|     var pathRecognizer = this.auxRoutes.get(urlParse.path); | ||||
|     if (isBlank(pathRecognizer)) { | ||||
|       return null; | ||||
|     } | ||||
|     return pathRecognizer.recognize(urlParse); | ||||
|   } | ||||
| 
 | ||||
|   hasRoute(name: string): boolean { return this.names.has(name); } | ||||
| 
 | ||||
|   generate(name: string, params: any): StringMap<string, any> { | ||||
|   generate(name: string, params: any): ComponentInstruction { | ||||
|     var pathRecognizer: PathRecognizer = this.names.get(name); | ||||
|     if (isBlank(pathRecognizer)) { | ||||
|       return null; | ||||
|     } | ||||
|     var url = pathRecognizer.generate(params); | ||||
|     return {url, 'nextComponent': pathRecognizer.handler.componentType}; | ||||
|     return pathRecognizer.generate(params); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| export class RouteMatch { | ||||
|   private _params: StringMap<string, any>; | ||||
|   private _paramsParsed: boolean = false; | ||||
| export class Redirector { | ||||
|   segments: List<string> = []; | ||||
|   toSegments: List<string> = []; | ||||
| 
 | ||||
|   constructor(public recognizer: PathRecognizer, public matchedUrl: string, | ||||
|               public unmatchedUrl: string, p: StringMap<string, any> = null) { | ||||
|     this._params = isPresent(p) ? p : StringMapWrapper.create(); | ||||
|   constructor(path: string, redirectTo: string) { | ||||
|     if (path.startsWith('/')) { | ||||
|       path = path.substring(1); | ||||
|     } | ||||
|     this.segments = path.split('/'); | ||||
|     if (redirectTo.startsWith('/')) { | ||||
|       redirectTo = redirectTo.substring(1); | ||||
|     } | ||||
|     this.toSegments = redirectTo.split('/'); | ||||
|   } | ||||
| 
 | ||||
|   params(): StringMap<string, any> { | ||||
|     if (!this._paramsParsed) { | ||||
|       this._paramsParsed = true; | ||||
|       StringMapWrapper.forEach(this.recognizer.parseParams(this.matchedUrl), | ||||
|                                (value, key) => { StringMapWrapper.set(this._params, key, value); }); | ||||
|   /** | ||||
|    * Returns `null` or a `ParsedUrl` representing the new path to match | ||||
|    */ | ||||
|   redirect(urlParse: Url): Url { | ||||
|     for (var i = 0; i < this.segments.length; i += 1) { | ||||
|       if (isBlank(urlParse)) { | ||||
|         return null; | ||||
|       } | ||||
|     return this._params; | ||||
|       let segment = this.segments[i]; | ||||
|       if (segment != urlParse.path) { | ||||
|         return null; | ||||
|       } | ||||
|       urlParse = urlParse.child; | ||||
|     } | ||||
| 
 | ||||
| function configObjToHandler(config: any): RouteHandler { | ||||
|   if (isType(config)) { | ||||
|     return new SyncRouteHandler(config); | ||||
|   } else if (isStringMap(config)) { | ||||
|     if (isBlank(config['type'])) { | ||||
|       throw new BaseException( | ||||
|           `Component declaration when provided as a map should include a 'type' property`); | ||||
|     for (var i = this.toSegments.length - 1; i >= 0; i -= 1) { | ||||
|       let segment = this.toSegments[i]; | ||||
|       urlParse = new Url(segment, urlParse); | ||||
|     } | ||||
|     var componentType = config['type']; | ||||
|     if (componentType == 'constructor') { | ||||
|       return new SyncRouteHandler(config['constructor']); | ||||
|     } else if (componentType == 'loader') { | ||||
|       return new AsyncRouteHandler(config['loader']); | ||||
|     } else { | ||||
|       throw new BaseException(`oops`); | ||||
|     return urlParse; | ||||
|   } | ||||
| } | ||||
|   throw new BaseException(`Unexpected component "${config}".`); | ||||
| } | ||||
|  | ||||
| @ -1,5 +1,6 @@ | ||||
| import {RouteRecognizer, RouteMatch} from './route_recognizer'; | ||||
| import {Instruction} from './instruction'; | ||||
| import {PathMatch} from './path_recognizer'; | ||||
| import {RouteRecognizer} from './route_recognizer'; | ||||
| import {Instruction, ComponentInstruction, PrimaryInstruction} from './instruction'; | ||||
| import { | ||||
|   List, | ||||
|   ListWrapper, | ||||
| @ -24,6 +25,9 @@ import {RouteConfig, AsyncRoute, Route, Redirect, RouteDefinition} from './route | ||||
| import {reflector} from 'angular2/src/reflection/reflection'; | ||||
| import {Injectable} from 'angular2/di'; | ||||
| import {normalizeRouteConfig} from './route_config_nomalizer'; | ||||
| import {parser, Url} from './url_parser'; | ||||
| 
 | ||||
| var _resolveToNull = PromiseWrapper.resolve(null); | ||||
| 
 | ||||
| /** | ||||
|  * The RouteRegistry holds route configurations for each component in an Angular app. | ||||
| @ -37,13 +41,13 @@ export class RouteRegistry { | ||||
|   /** | ||||
|    * Given a component and a configuration object, add the route to this registry | ||||
|    */ | ||||
|   config(parentComponent: any, config: RouteDefinition, isRootLevelRoute: boolean = false): void { | ||||
|   config(parentComponent: any, config: RouteDefinition): void { | ||||
|     config = normalizeRouteConfig(config); | ||||
| 
 | ||||
|     var recognizer: RouteRecognizer = this._rules.get(parentComponent); | ||||
| 
 | ||||
|     if (isBlank(recognizer)) { | ||||
|       recognizer = new RouteRecognizer(isRootLevelRoute); | ||||
|       recognizer = new RouteRecognizer(); | ||||
|       this._rules.set(parentComponent, recognizer); | ||||
|     } | ||||
| 
 | ||||
| @ -61,7 +65,7 @@ export class RouteRegistry { | ||||
|   /** | ||||
|    * Reads the annotations of a component and configures the registry based on them | ||||
|    */ | ||||
|   configFromComponent(component: any, isRootComponent: boolean = false): void { | ||||
|   configFromComponent(component: any): void { | ||||
|     if (!isType(component)) { | ||||
|       return; | ||||
|     } | ||||
| @ -77,8 +81,7 @@ export class RouteRegistry { | ||||
|         var annotation = annotations[i]; | ||||
| 
 | ||||
|         if (annotation instanceof RouteConfig) { | ||||
|           ListWrapper.forEach(annotation.configs, | ||||
|                               (config) => this.config(component, config, isRootComponent)); | ||||
|           ListWrapper.forEach(annotation.configs, (config) => this.config(component, config)); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
| @ -90,52 +93,88 @@ export class RouteRegistry { | ||||
|    * the application into the state specified by the url | ||||
|    */ | ||||
|   recognize(url: string, parentComponent: any): Promise<Instruction> { | ||||
|     var parsedUrl = parser.parse(url); | ||||
|     return this._recognize(parsedUrl, parentComponent); | ||||
|   } | ||||
| 
 | ||||
|   private _recognize(parsedUrl: Url, parentComponent): Promise<Instruction> { | ||||
|     return this._recognizePrimaryRoute(parsedUrl, parentComponent) | ||||
|         .then((instruction: PrimaryInstruction) => | ||||
|                   this._completeAuxiliaryRouteMatches(instruction, parentComponent)); | ||||
|   } | ||||
| 
 | ||||
|   private _recognizePrimaryRoute(parsedUrl: Url, parentComponent): Promise<PrimaryInstruction> { | ||||
|     var componentRecognizer = this._rules.get(parentComponent); | ||||
|     if (isBlank(componentRecognizer)) { | ||||
|       return PromiseWrapper.resolve(null); | ||||
|     } | ||||
| 
 | ||||
|     // Matches some beginning part of the given URL
 | ||||
|     var possibleMatches = componentRecognizer.recognize(url); | ||||
|     var possibleMatches = componentRecognizer.recognize(parsedUrl); | ||||
| 
 | ||||
|     var matchPromises = | ||||
|         ListWrapper.map(possibleMatches, (candidate) => this._completeRouteMatch(candidate)); | ||||
|         ListWrapper.map(possibleMatches, (candidate) => this._completePrimaryRouteMatch(candidate)); | ||||
| 
 | ||||
|     return PromiseWrapper.all(matchPromises) | ||||
|         .then((solutions: List<Instruction>) => { | ||||
|           // remove nulls
 | ||||
|           var fullSolutions = ListWrapper.filter(solutions, (solution) => isPresent(solution)); | ||||
| 
 | ||||
|           if (fullSolutions.length > 0) { | ||||
|             return mostSpecific(fullSolutions); | ||||
|     return PromiseWrapper.all(matchPromises).then(mostSpecific); | ||||
|   } | ||||
| 
 | ||||
|   private _completePrimaryRouteMatch(partialMatch: PathMatch): Promise<PrimaryInstruction> { | ||||
|     var instruction = partialMatch.instruction; | ||||
|     return instruction.resolveComponentType().then((componentType) => { | ||||
|       this.configFromComponent(componentType); | ||||
| 
 | ||||
|       if (isBlank(partialMatch.remaining)) { | ||||
|         if (instruction.terminal) { | ||||
|           return new PrimaryInstruction(instruction, null, partialMatch.remainingAux); | ||||
|         } else { | ||||
|           return null; | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
|       return this._recognizePrimaryRoute(partialMatch.remaining, componentType) | ||||
|           .then((childInstruction) => { | ||||
|             if (isBlank(childInstruction)) { | ||||
|               return null; | ||||
|             } else { | ||||
|               return new PrimaryInstruction(instruction, childInstruction, | ||||
|                                             partialMatch.remainingAux); | ||||
|             } | ||||
|           }); | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
| 
 | ||||
|   _completeRouteMatch(partialMatch: RouteMatch): Promise<Instruction> { | ||||
|     var recognizer = partialMatch.recognizer; | ||||
|     var handler = recognizer.handler; | ||||
|     return handler.resolveComponentType().then((componentType) => { | ||||
|       this.configFromComponent(componentType); | ||||
| 
 | ||||
|       if (partialMatch.unmatchedUrl.length == 0) { | ||||
|         if (recognizer.terminal) { | ||||
|           return new Instruction(componentType, partialMatch.matchedUrl, recognizer, null, | ||||
|                                  partialMatch.params()); | ||||
|         } else { | ||||
|           return null; | ||||
|         } | ||||
|   private _completeAuxiliaryRouteMatches(instruction: PrimaryInstruction, | ||||
|                                          parentComponent: any): Promise<Instruction> { | ||||
|     if (isBlank(instruction)) { | ||||
|       return _resolveToNull; | ||||
|     } | ||||
| 
 | ||||
|       return this.recognize(partialMatch.unmatchedUrl, componentType) | ||||
|           .then(childInstruction => { | ||||
|             if (isBlank(childInstruction)) { | ||||
|               return null; | ||||
|             } else { | ||||
|               return new Instruction(componentType, partialMatch.matchedUrl, recognizer, | ||||
|                                      childInstruction); | ||||
|     var componentRecognizer = this._rules.get(parentComponent); | ||||
|     var auxInstructions = {}; | ||||
| 
 | ||||
|     var promises = instruction.auxUrls.map((auxSegment: Url) => { | ||||
|       var match = componentRecognizer.recognizeAuxiliary(auxSegment); | ||||
|       if (isBlank(match)) { | ||||
|         return _resolveToNull; | ||||
|       } | ||||
|       return this._completePrimaryRouteMatch(match).then((auxInstruction: PrimaryInstruction) => { | ||||
|         if (isPresent(auxInstruction)) { | ||||
|           return this._completeAuxiliaryRouteMatches(auxInstruction, parentComponent) | ||||
|               .then((finishedAuxRoute: Instruction) => { | ||||
|                 auxInstructions[auxSegment.path] = finishedAuxRoute; | ||||
|               }); | ||||
|         } | ||||
|       }); | ||||
|     }); | ||||
|     return PromiseWrapper.all(promises).then((_) => { | ||||
|       if (isBlank(instruction.child)) { | ||||
|         return new Instruction(instruction.component, null, auxInstructions); | ||||
|       } | ||||
|       return this._completeAuxiliaryRouteMatches(instruction.child, | ||||
|                                                  instruction.component.componentType) | ||||
|           .then((completeChild) => { | ||||
|             return new Instruction(instruction.component, completeChild, auxInstructions); | ||||
|           }); | ||||
|     }); | ||||
|   } | ||||
| @ -144,9 +183,10 @@ export class RouteRegistry { | ||||
|    * Given a normalized list with component names and params like: `['user', {id: 3 }]` | ||||
|    * generates a url with a leading slash relative to the provided `parentComponent`. | ||||
|    */ | ||||
|   generate(linkParams: List<any>, parentComponent: any): string { | ||||
|     let url = ''; | ||||
|   generate(linkParams: List<any>, parentComponent: any): Instruction { | ||||
|     let segments = []; | ||||
|     let componentCursor = parentComponent; | ||||
| 
 | ||||
|     for (let i = 0; i < linkParams.length; i += 1) { | ||||
|       let segment = linkParams[i]; | ||||
|       if (isBlank(componentCursor)) { | ||||
| @ -172,15 +212,22 @@ export class RouteRegistry { | ||||
|             `Component "${getTypeNameForDebugging(componentCursor)}" has no route config.`); | ||||
|       } | ||||
|       var response = componentRecognizer.generate(segment, params); | ||||
| 
 | ||||
|       if (isBlank(response)) { | ||||
|         throw new BaseException( | ||||
|             `Component "${getTypeNameForDebugging(componentCursor)}" has no route named "${segment}".`); | ||||
|       } | ||||
|       url += response['url']; | ||||
|       componentCursor = response['nextComponent']; | ||||
|       segments.push(response); | ||||
|       componentCursor = response.componentType; | ||||
|     } | ||||
| 
 | ||||
|     return url; | ||||
|     var instruction = null; | ||||
| 
 | ||||
|     while (segments.length > 0) { | ||||
|       instruction = new Instruction(segments.pop(), instruction, {}); | ||||
|     } | ||||
| 
 | ||||
|     return instruction; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| @ -188,11 +235,17 @@ export class RouteRegistry { | ||||
| /* | ||||
|  * Given a list of instructions, returns the most specific instruction | ||||
|  */ | ||||
| function mostSpecific(instructions: List<Instruction>): Instruction { | ||||
| function mostSpecific(instructions: List<PrimaryInstruction>): PrimaryInstruction { | ||||
|   if (instructions.length == 0) { | ||||
|     return null; | ||||
|   } | ||||
|   var mostSpecificSolution = instructions[0]; | ||||
|   for (var solutionIndex = 1; solutionIndex < instructions.length; solutionIndex++) { | ||||
|     var solution = instructions[solutionIndex]; | ||||
|     if (solution.specificity > mostSpecificSolution.specificity) { | ||||
|     var solution: PrimaryInstruction = instructions[solutionIndex]; | ||||
|     if (isBlank(solution)) { | ||||
|       continue; | ||||
|     } | ||||
|     if (solution.component.specificity > mostSpecificSolution.component.specificity) { | ||||
|       mostSpecificSolution = solution; | ||||
|     } | ||||
|   } | ||||
|  | ||||
| @ -12,7 +12,7 @@ import { | ||||
| 
 | ||||
| import {RouteRegistry} from './route_registry'; | ||||
| import {Pipeline} from './pipeline'; | ||||
| import {Instruction} from './instruction'; | ||||
| import {ComponentInstruction, Instruction, stringifyInstruction} from './instruction'; | ||||
| import {RouterOutlet} from './router_outlet'; | ||||
| import {Location} from './location'; | ||||
| import {getCanActivateHook} from './route_lifecycle_reflector'; | ||||
| @ -45,10 +45,9 @@ export class Router { | ||||
|   private _currentInstruction: Instruction = null; | ||||
|   private _currentNavigation: Promise<any> = _resolveToTrue; | ||||
|   private _outlet: RouterOutlet = null; | ||||
|   private _auxOutlets: Map<string, RouterOutlet> = new Map(); | ||||
|   private _subject: EventEmitter = new 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
 | ||||
|   constructor(public registry: RouteRegistry, public _pipeline: Pipeline, public parent: Router, | ||||
|               public hostComponent: any) {} | ||||
| 
 | ||||
| @ -65,8 +64,11 @@ export class Router { | ||||
|    * you're writing a reusable component. | ||||
|    */ | ||||
|   registerOutlet(outlet: RouterOutlet): Promise<boolean> { | ||||
|     // TODO: sibling routes
 | ||||
|     if (isPresent(outlet.name)) { | ||||
|       this._auxOutlets.set(outlet.name, outlet); | ||||
|     } else { | ||||
|       this._outlet = outlet; | ||||
|     } | ||||
|     if (isPresent(this._currentInstruction)) { | ||||
|       return outlet.commit(this._currentInstruction); | ||||
|     } | ||||
| @ -87,9 +89,8 @@ export class Router { | ||||
|    * ``` | ||||
|    */ | ||||
|   config(definitions: List<RouteDefinition>): Promise<any> { | ||||
|     definitions.forEach((routeDefinition) => { | ||||
|       this.registry.config(this.hostComponent, routeDefinition, this instanceof RootRouter); | ||||
|     }); | ||||
|     definitions.forEach( | ||||
|         (routeDefinition) => { this.registry.config(this.hostComponent, routeDefinition); }); | ||||
|     return this.renavigate(); | ||||
|   } | ||||
| 
 | ||||
| @ -104,29 +105,49 @@ export class Router { | ||||
|     return this._currentNavigation = this._currentNavigation.then((_) => { | ||||
|       this.lastNavigationAttempt = url; | ||||
|       this._startNavigating(); | ||||
|       return this._afterPromiseFinishNavigating(this.recognize(url).then((matchedInstruction) => { | ||||
|         if (isBlank(matchedInstruction)) { | ||||
|       return this._afterPromiseFinishNavigating(this.recognize(url).then((instruction) => { | ||||
|         if (isBlank(instruction)) { | ||||
|           return false; | ||||
|         } | ||||
|         return this._reuse(matchedInstruction) | ||||
|             .then((_) => this._canActivate(matchedInstruction)) | ||||
|         return this._navigate(instruction, _skipLocationChange); | ||||
|       })); | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
| 
 | ||||
|   /** | ||||
|    * Navigate via the provided instruction. Returns a promise that resolves when navigation is | ||||
|    * complete. | ||||
|    */ | ||||
|   navigateInstruction(instruction: Instruction, | ||||
|                       _skipLocationChange: boolean = false): Promise<any> { | ||||
|     if (isBlank(instruction)) { | ||||
|       return _resolveToFalse; | ||||
|     } | ||||
|     return this._currentNavigation = this._currentNavigation.then((_) => { | ||||
|       this._startNavigating(); | ||||
|       return this._afterPromiseFinishNavigating(this._navigate(instruction, _skipLocationChange)); | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   _navigate(instruction: Instruction, _skipLocationChange: boolean): Promise<any> { | ||||
|     return this._reuse(instruction) | ||||
|         .then((_) => this._canActivate(instruction)) | ||||
|         .then((result) => { | ||||
|           if (!result) { | ||||
|             return false; | ||||
|           } | ||||
|               return this._canDeactivate(matchedInstruction) | ||||
|           return this._canDeactivate(instruction) | ||||
|               .then((result) => { | ||||
|                 if (result) { | ||||
|                       return this.commit(matchedInstruction, _skipLocationChange) | ||||
|                   return this.commit(instruction, _skipLocationChange) | ||||
|                       .then((_) => { | ||||
|                             this._emitNavigationFinish(matchedInstruction.accumulatedUrl); | ||||
|                         this._emitNavigationFinish(stringifyInstruction(instruction)); | ||||
|                         return true; | ||||
|                       }); | ||||
|                 } | ||||
|               }); | ||||
|         }); | ||||
|       })); | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   private _emitNavigationFinish(url): void { ObservableWrapper.callNext(this._subject, url); } | ||||
| @ -138,21 +159,20 @@ export class Router { | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   _reuse(instruction): Promise<any> { | ||||
|   _reuse(instruction: Instruction): Promise<any> { | ||||
|     if (isBlank(this._outlet)) { | ||||
|       return _resolveToFalse; | ||||
|     } | ||||
|     return this._outlet.canReuse(instruction) | ||||
|         .then((result) => { | ||||
|           instruction.reuse = result; | ||||
|           if (isPresent(this._outlet.childRouter) && isPresent(instruction.child)) { | ||||
|             return this._outlet.childRouter._reuse(instruction.child); | ||||
|           } | ||||
|         }); | ||||
|   } | ||||
| 
 | ||||
|   private _canActivate(instruction: Instruction): Promise<boolean> { | ||||
|     return canActivateOne(instruction, this._currentInstruction); | ||||
|   private _canActivate(nextInstruction: Instruction): Promise<boolean> { | ||||
|     return canActivateOne(nextInstruction, this._currentInstruction); | ||||
|   } | ||||
| 
 | ||||
|   private _canDeactivate(instruction: Instruction): Promise<boolean> { | ||||
| @ -160,11 +180,12 @@ export class Router { | ||||
|       return _resolveToTrue; | ||||
|     } | ||||
|     var next: Promise<boolean>; | ||||
|     if (isPresent(instruction) && instruction.reuse) { | ||||
|     if (isPresent(instruction) && instruction.component.reuse) { | ||||
|       next = _resolveToTrue; | ||||
|     } else { | ||||
|       next = this._outlet.canDeactivate(instruction); | ||||
|     } | ||||
|     // TODO: aux route lifecycle hooks
 | ||||
|     return next.then((result) => { | ||||
|       if (result == false) { | ||||
|         return false; | ||||
| @ -182,10 +203,14 @@ export class Router { | ||||
|    */ | ||||
|   commit(instruction: Instruction, _skipLocationChange: boolean = false): Promise<any> { | ||||
|     this._currentInstruction = instruction; | ||||
|     var next = _resolveToTrue; | ||||
|     if (isPresent(this._outlet)) { | ||||
|       return this._outlet.commit(instruction); | ||||
|       next = this._outlet.commit(instruction); | ||||
|     } | ||||
|     return _resolveToTrue; | ||||
|     var promises = []; | ||||
|     MapWrapper.forEach(this._auxOutlets, | ||||
|                        (outlet, _) => { promises.push(outlet.commit(instruction)); }); | ||||
|     return next.then((_) => PromiseWrapper.all(promises)); | ||||
|   } | ||||
| 
 | ||||
| 
 | ||||
| @ -237,7 +262,7 @@ export class Router { | ||||
|    * Generate a URL from a component name and optional map of parameters. The URL is relative to the | ||||
|    * app's base href. | ||||
|    */ | ||||
|   generate(linkParams: List<any>): string { | ||||
|   generate(linkParams: List<any>): Instruction { | ||||
|     let normalizedLinkParams = splitAndFlattenLinkParams(linkParams); | ||||
| 
 | ||||
|     var first = ListWrapper.first(normalizedLinkParams); | ||||
| @ -275,11 +300,22 @@ export class Router { | ||||
|       throw new BaseException(msg); | ||||
|     } | ||||
| 
 | ||||
|     let url = ''; | ||||
|     if (isPresent(router.parent) && isPresent(router.parent._currentInstruction)) { | ||||
|       url = router.parent._currentInstruction.capturedUrl; | ||||
|     // TODO: structural cloning and whatnot
 | ||||
| 
 | ||||
|     var url = []; | ||||
|     var parent = router.parent; | ||||
|     while (isPresent(parent)) { | ||||
|       url.unshift(parent._currentInstruction); | ||||
|       parent = parent.parent; | ||||
|     } | ||||
|     return url + '/' + this.registry.generate(rest, router.hostComponent); | ||||
| 
 | ||||
|     var nextInstruction = this.registry.generate(rest, router.hostComponent); | ||||
| 
 | ||||
|     while (url.length > 0) { | ||||
|       nextInstruction = url.pop().replaceChild(nextInstruction); | ||||
|     } | ||||
| 
 | ||||
|     return nextInstruction; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| @ -291,14 +327,19 @@ export class RootRouter extends Router { | ||||
|     super(registry, pipeline, null, hostComponent); | ||||
|     this._location = location; | ||||
|     this._location.subscribe((change) => this.navigate(change['url'], isPresent(change['pop']))); | ||||
|     this.registry.configFromComponent(hostComponent, true); | ||||
| 
 | ||||
|     this.registry.configFromComponent(hostComponent); | ||||
|     this.navigate(location.path()); | ||||
|   } | ||||
| 
 | ||||
|   commit(instruction: Instruction, _skipLocationChange: boolean = false): Promise<any> { | ||||
|     var emitUrl = stringifyInstruction(instruction); | ||||
|     if (emitUrl.length > 0) { | ||||
|       emitUrl = '/' + emitUrl; | ||||
|     } | ||||
|     var promise = super.commit(instruction); | ||||
|     if (!_skipLocationChange) { | ||||
|       promise = promise.then((_) => { this._location.go(instruction.accumulatedUrl); }); | ||||
|       promise = promise.then((_) => { this._location.go(emitUrl); }); | ||||
|     } | ||||
|     return promise; | ||||
|   } | ||||
| @ -315,6 +356,12 @@ class ChildRouter extends Router { | ||||
|     // Delegate navigation to the root router
 | ||||
|     return this.parent.navigate(url, _skipLocationChange); | ||||
|   } | ||||
| 
 | ||||
|   navigateInstruction(instruction: Instruction, | ||||
|                       _skipLocationChange: boolean = false): Promise<any> { | ||||
|     // Delegate navigation to the root router
 | ||||
|     return this.parent.navigateInstruction(instruction, _skipLocationChange); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| /* | ||||
| @ -332,22 +379,24 @@ function splitAndFlattenLinkParams(linkParams: List<any>): List<any> { | ||||
|   }, []); | ||||
| } | ||||
| 
 | ||||
| function canActivateOne(nextInstruction, currentInstruction): Promise<boolean> { | ||||
| function canActivateOne(nextInstruction: Instruction, prevInstruction: Instruction): | ||||
|     Promise<boolean> { | ||||
|   var next = _resolveToTrue; | ||||
|   if (isPresent(nextInstruction.child)) { | ||||
|     next = canActivateOne(nextInstruction.child, | ||||
|                           isPresent(currentInstruction) ? currentInstruction.child : null); | ||||
|                           isPresent(prevInstruction) ? prevInstruction.child : null); | ||||
|   } | ||||
|   return next.then((res) => { | ||||
|     if (res == false) { | ||||
|   return next.then((result) => { | ||||
|     if (result == false) { | ||||
|       return false; | ||||
|     } | ||||
|     if (nextInstruction.reuse) { | ||||
|     if (nextInstruction.component.reuse) { | ||||
|       return true; | ||||
|     } | ||||
|     var hook = getCanActivateHook(nextInstruction.component); | ||||
|     var hook = getCanActivateHook(nextInstruction.component.componentType); | ||||
|     if (isPresent(hook)) { | ||||
|       return hook(nextInstruction, currentInstruction); | ||||
|       return hook(nextInstruction.component, | ||||
|                   isPresent(prevInstruction) ? prevInstruction.component : null); | ||||
|     } | ||||
|     return true; | ||||
|   }); | ||||
|  | ||||
| @ -3,6 +3,7 @@ import {List, StringMap, StringMapWrapper} from 'angular2/src/facade/collection' | ||||
| 
 | ||||
| import {Router} from './router'; | ||||
| import {Location} from './location'; | ||||
| import {Instruction, stringifyInstruction} from './instruction'; | ||||
| 
 | ||||
| /** | ||||
|  * The RouterLink directive lets you link to specific parts of your app. | ||||
| @ -43,19 +44,23 @@ export class RouterLink { | ||||
| 
 | ||||
|   // the url displayed on the anchor element.
 | ||||
|   visibleHref: string; | ||||
|   // the url passed to the router navigation.
 | ||||
|   _navigationHref: string; | ||||
| 
 | ||||
|   // the instruction passed to the router to navigate
 | ||||
|   private _navigationInstruction: Instruction; | ||||
| 
 | ||||
|   constructor(private _router: Router, private _location: Location) {} | ||||
| 
 | ||||
|   set routeParams(changes: List<any>) { | ||||
|     this._routeParams = changes; | ||||
|     this._navigationHref = this._router.generate(this._routeParams); | ||||
|     this.visibleHref = this._location.normalizeAbsolutely(this._navigationHref); | ||||
|     this._navigationInstruction = this._router.generate(this._routeParams); | ||||
| 
 | ||||
|     // TODO: is this the right spot for this?
 | ||||
|     var navigationHref = '/' + stringifyInstruction(this._navigationInstruction); | ||||
|     this.visibleHref = this._location.normalizeAbsolutely(navigationHref); | ||||
|   } | ||||
| 
 | ||||
|   onClick(): boolean { | ||||
|     this._router.navigate(this._navigationHref); | ||||
|     this._router.navigateInstruction(this._navigationInstruction); | ||||
|     return false; | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -7,7 +7,7 @@ import {DynamicComponentLoader, ComponentRef, ElementRef} from 'angular2/core'; | ||||
| import {Injector, bind, Dependency, undefinedValue} from 'angular2/di'; | ||||
| 
 | ||||
| import * as routerMod from './router'; | ||||
| import {Instruction, RouteParams} from './instruction'; | ||||
| import {Instruction, ComponentInstruction, RouteParams} from './instruction'; | ||||
| import * as hookMod from './lifecycle_annotations'; | ||||
| import {hasLifecycleHook} from './route_lifecycle_reflector'; | ||||
| 
 | ||||
| @ -23,16 +23,16 @@ import {hasLifecycleHook} from './route_lifecycle_reflector'; | ||||
| @Directive({selector: 'router-outlet'}) | ||||
| export class RouterOutlet { | ||||
|   childRouter: routerMod.Router = null; | ||||
|   name: string = null; | ||||
| 
 | ||||
|   private _componentRef: ComponentRef = null; | ||||
|   private _currentInstruction: Instruction = null; | ||||
|   private _currentInstruction: ComponentInstruction = null; | ||||
| 
 | ||||
|   constructor(private _elementRef: ElementRef, private _loader: DynamicComponentLoader, | ||||
|               private _parentRouter: routerMod.Router, @Attribute('name') nameAttr: string) { | ||||
|     // TODO: reintroduce with new // sibling routes
 | ||||
|     // if (isBlank(nameAttr)) {
 | ||||
|     //  nameAttr = 'default';
 | ||||
|     //}
 | ||||
|     if (isPresent(nameAttr)) { | ||||
|       this.name = nameAttr; | ||||
|     } | ||||
|     this._parentRouter.registerOutlet(this); | ||||
|   } | ||||
| 
 | ||||
| @ -40,15 +40,28 @@ export class RouterOutlet { | ||||
|    * Given an instruction, update the contents of this outlet. | ||||
|    */ | ||||
|   commit(instruction: Instruction): Promise<any> { | ||||
|     instruction = this._getInstruction(instruction); | ||||
|     var componentInstruction = instruction.component; | ||||
|     if (isBlank(componentInstruction)) { | ||||
|       return PromiseWrapper.resolve(true); | ||||
|     } | ||||
|     var next; | ||||
|     if (instruction.reuse) { | ||||
|       next = this._reuse(instruction); | ||||
|     if (componentInstruction.reuse) { | ||||
|       next = this._reuse(componentInstruction); | ||||
|     } else { | ||||
|       next = this.deactivate(instruction).then((_) => this._activate(instruction)); | ||||
|       next = this.deactivate(instruction).then((_) => this._activate(componentInstruction)); | ||||
|     } | ||||
|     return next.then((_) => this._commitChild(instruction)); | ||||
|   } | ||||
| 
 | ||||
|   private _getInstruction(instruction: Instruction): Instruction { | ||||
|     if (isPresent(this.name)) { | ||||
|       return instruction.auxInstruction[this.name]; | ||||
|     } else { | ||||
|       return instruction; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   private _commitChild(instruction: Instruction): Promise<any> { | ||||
|     if (isPresent(this.childRouter)) { | ||||
|       return this.childRouter.commit(instruction.child); | ||||
| @ -57,20 +70,21 @@ export class RouterOutlet { | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   private _activate(instruction: Instruction): Promise<any> { | ||||
|   private _activate(instruction: ComponentInstruction): Promise<any> { | ||||
|     var previousInstruction = this._currentInstruction; | ||||
|     this._currentInstruction = instruction; | ||||
|     this.childRouter = this._parentRouter.childRouter(instruction.component); | ||||
|     var componentType = instruction.componentType; | ||||
|     this.childRouter = this._parentRouter.childRouter(componentType); | ||||
| 
 | ||||
|     var bindings = Injector.resolve([ | ||||
|       bind(RouteParams) | ||||
|           .toValue(new RouteParams(instruction.params())), | ||||
|           .toValue(new RouteParams(instruction.params)), | ||||
|       bind(routerMod.Router).toValue(this.childRouter) | ||||
|     ]); | ||||
|     return this._loader.loadNextToLocation(instruction.component, this._elementRef, bindings) | ||||
|     return this._loader.loadNextToLocation(componentType, this._elementRef, bindings) | ||||
|         .then((componentRef) => { | ||||
|           this._componentRef = componentRef; | ||||
|           if (hasLifecycleHook(hookMod.onActivate, instruction.component)) { | ||||
|           if (hasLifecycleHook(hookMod.onActivate, componentType)) { | ||||
|             return this._componentRef.instance.onActivate(instruction, previousInstruction); | ||||
|           } | ||||
|         }); | ||||
| @ -84,9 +98,11 @@ export class RouterOutlet { | ||||
|     if (isBlank(this._currentInstruction)) { | ||||
|       return PromiseWrapper.resolve(true); | ||||
|     } | ||||
|     if (hasLifecycleHook(hookMod.canDeactivate, this._currentInstruction.component)) { | ||||
|       return PromiseWrapper.resolve( | ||||
|           this._componentRef.instance.canDeactivate(nextInstruction, this._currentInstruction)); | ||||
|     var outletInstruction = this._getInstruction(nextInstruction); | ||||
|     if (hasLifecycleHook(hookMod.canDeactivate, this._currentInstruction.componentType)) { | ||||
|       return PromiseWrapper.resolve(this._componentRef.instance.canDeactivate( | ||||
|           isPresent(outletInstruction) ? outletInstruction.component : null, | ||||
|           this._currentInstruction)); | ||||
|     } | ||||
|     return PromiseWrapper.resolve(true); | ||||
|   } | ||||
| @ -97,24 +113,34 @@ export class RouterOutlet { | ||||
|    */ | ||||
|   canReuse(nextInstruction: Instruction): Promise<boolean> { | ||||
|     var result; | ||||
| 
 | ||||
|     var outletInstruction = this._getInstruction(nextInstruction); | ||||
|     var componentInstruction = outletInstruction.component; | ||||
| 
 | ||||
|     if (isBlank(this._currentInstruction) || | ||||
|         this._currentInstruction.component != nextInstruction.component) { | ||||
|         this._currentInstruction.componentType != componentInstruction.componentType) { | ||||
|       result = false; | ||||
|     } else if (hasLifecycleHook(hookMod.canReuse, this._currentInstruction.component)) { | ||||
|       result = this._componentRef.instance.canReuse(nextInstruction, this._currentInstruction); | ||||
|     } else if (hasLifecycleHook(hookMod.canReuse, this._currentInstruction.componentType)) { | ||||
|       result = this._componentRef.instance.canReuse(componentInstruction, this._currentInstruction); | ||||
|     } else { | ||||
|       result = nextInstruction == this._currentInstruction || | ||||
|                StringMapWrapper.equals(nextInstruction.params(), this._currentInstruction.params()); | ||||
|       result = | ||||
|           componentInstruction == this._currentInstruction || | ||||
|           (isPresent(componentInstruction.params) && isPresent(this._currentInstruction.params) && | ||||
|            StringMapWrapper.equals(componentInstruction.params, this._currentInstruction.params)); | ||||
|     } | ||||
|     return PromiseWrapper.resolve(result); | ||||
|     return PromiseWrapper.resolve(result).then((result) => { | ||||
|       // TODO: this is a hack
 | ||||
|       componentInstruction.reuse = result; | ||||
|       return result; | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
| 
 | ||||
|   private _reuse(instruction): Promise<any> { | ||||
|   private _reuse(instruction: ComponentInstruction): Promise<any> { | ||||
|     var previousInstruction = this._currentInstruction; | ||||
|     this._currentInstruction = instruction; | ||||
|     return PromiseWrapper.resolve( | ||||
|         hasLifecycleHook(hookMod.onReuse, this._currentInstruction.component) ? | ||||
|         hasLifecycleHook(hookMod.onReuse, this._currentInstruction.componentType) ? | ||||
|             this._componentRef.instance.onReuse(instruction, previousInstruction) : | ||||
|             true); | ||||
|   } | ||||
| @ -122,14 +148,16 @@ export class RouterOutlet { | ||||
| 
 | ||||
| 
 | ||||
|   deactivate(nextInstruction: Instruction): Promise<any> { | ||||
|     var outletInstruction = this._getInstruction(nextInstruction); | ||||
|     var componentInstruction = isPresent(outletInstruction) ? outletInstruction.component : null; | ||||
|     return (isPresent(this.childRouter) ? | ||||
|                 this.childRouter.deactivate(isPresent(nextInstruction) ? nextInstruction.child : | ||||
|                 this.childRouter.deactivate(isPresent(outletInstruction) ? outletInstruction.child : | ||||
|                                                                            null) : | ||||
|                 PromiseWrapper.resolve(true)) | ||||
|         .then((_) => { | ||||
|           if (isPresent(this._componentRef) && isPresent(this._currentInstruction) && | ||||
|               hasLifecycleHook(hookMod.onDeactivate, this._currentInstruction.component)) { | ||||
|             return this._componentRef.instance.onDeactivate(nextInstruction, | ||||
|               hasLifecycleHook(hookMod.onDeactivate, this._currentInstruction.componentType)) { | ||||
|             return this._componentRef.instance.onDeactivate(componentInstruction, | ||||
|                                                             this._currentInstruction); | ||||
|           } | ||||
|         }) | ||||
|  | ||||
| @ -1,9 +0,0 @@ | ||||
| import {RegExpWrapper, StringWrapper} from 'angular2/src/facade/lang'; | ||||
| 
 | ||||
| var specialCharacters = ['/', '.', '*', '+', '?', '|', '(', ')', '[', ']', '{', '}', '\\']; | ||||
| 
 | ||||
| var escapeRe = RegExpWrapper.create('(\\' + specialCharacters.join('|\\') + ')', 'g'); | ||||
| 
 | ||||
| export function escapeRegex(string: string): string { | ||||
|   return StringWrapper.replaceAllMapped(string, escapeRe, (match) => { return "\\" + match; }); | ||||
| } | ||||
							
								
								
									
										210
									
								
								modules/angular2/src/router/url_parser.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										210
									
								
								modules/angular2/src/router/url_parser.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,210 @@ | ||||
| import {List, StringMap, StringMapWrapper} from 'angular2/src/facade/collection'; | ||||
| import { | ||||
|   isPresent, | ||||
|   isBlank, | ||||
|   BaseException, | ||||
|   RegExpWrapper, | ||||
|   CONST_EXPR | ||||
| } from 'angular2/src/facade/lang'; | ||||
| 
 | ||||
| 
 | ||||
| export class Url { | ||||
|   constructor(public path: string, public child: Url = null, | ||||
|               public auxiliary: List<Url> = CONST_EXPR([]), | ||||
|               public params: StringMap<string, any> = null) {} | ||||
| 
 | ||||
|   toString(): string { | ||||
|     return this.path + this._matrixParamsToString() + this._auxToString() + this._childString(); | ||||
|   } | ||||
| 
 | ||||
|   segmentToString(): string { return this.path + this._matrixParamsToString(); } | ||||
| 
 | ||||
|   _auxToString(): string { | ||||
|     return this.auxiliary.length > 0 ? | ||||
|                ('(' + this.auxiliary.map(sibling => sibling.toString()).join('//') + ')') : | ||||
|                ''; | ||||
|   } | ||||
| 
 | ||||
|   private _matrixParamsToString(): string { | ||||
|     if (isBlank(this.params)) { | ||||
|       return ''; | ||||
|     } | ||||
| 
 | ||||
|     return ';' + serializeParams(this.params).join(';'); | ||||
|   } | ||||
| 
 | ||||
|   _childString(): string { return isPresent(this.child) ? ('/' + this.child.toString()) : ''; } | ||||
| } | ||||
| 
 | ||||
| export class RootUrl extends Url { | ||||
|   constructor(path: string, child: Url = null, auxiliary: List<Url> = CONST_EXPR([]), | ||||
|               params: StringMap<string, any> = null) { | ||||
|     super(path, child, auxiliary, params); | ||||
|   } | ||||
| 
 | ||||
|   toString(): string { | ||||
|     return this.path + this._auxToString() + this._childString() + this._queryParamsToString(); | ||||
|   } | ||||
| 
 | ||||
|   segmentToString(): string { return this.path + this._queryParamsToString(); } | ||||
| 
 | ||||
|   private _queryParamsToString(): string { | ||||
|     if (isBlank(this.params)) { | ||||
|       return ''; | ||||
|     } | ||||
| 
 | ||||
|     return '?' + serializeParams(this.params).join('&'); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| var SEGMENT_RE = RegExpWrapper.create('^[^\\/\\(\\)\\?;=&]+'); | ||||
| function matchUrlSegment(str: string): string { | ||||
|   var match = RegExpWrapper.firstMatch(SEGMENT_RE, str); | ||||
|   return isPresent(match) ? match[0] : null; | ||||
| } | ||||
| 
 | ||||
| export class UrlParser { | ||||
|   private remaining: string; | ||||
| 
 | ||||
|   peekStartsWith(str: string): boolean { return this.remaining.startsWith(str); } | ||||
| 
 | ||||
|   capture(str: string): void { | ||||
|     if (!this.remaining.startsWith(str)) { | ||||
|       throw new BaseException(`Expected "${str}".`); | ||||
|     } | ||||
|     this.remaining = this.remaining.substring(str.length); | ||||
|   } | ||||
| 
 | ||||
|   parse(url: string): Url { | ||||
|     this.remaining = url; | ||||
|     if (url == '' || url == '/') { | ||||
|       return new Url(''); | ||||
|     } | ||||
|     return this.parseRoot(); | ||||
|   } | ||||
| 
 | ||||
|   // segment + (aux segments) + (query params)
 | ||||
|   parseRoot(): Url { | ||||
|     if (this.peekStartsWith('/')) { | ||||
|       this.capture('/'); | ||||
|     } | ||||
|     var path = matchUrlSegment(this.remaining); | ||||
|     this.capture(path); | ||||
| 
 | ||||
|     var aux = []; | ||||
|     if (this.peekStartsWith('(')) { | ||||
|       aux = this.parseAuxiliaryRoutes(); | ||||
|     } | ||||
|     if (this.peekStartsWith(';')) { | ||||
|       // TODO: should these params just be dropped?
 | ||||
|       this.parseMatrixParams(); | ||||
|     } | ||||
|     var child = null; | ||||
|     if (this.peekStartsWith('/') && !this.peekStartsWith('//')) { | ||||
|       this.capture('/'); | ||||
|       child = this.parseSegment(); | ||||
|     } | ||||
|     var queryParams = null; | ||||
|     if (this.peekStartsWith('?')) { | ||||
|       queryParams = this.parseQueryParams(); | ||||
|     } | ||||
|     return new RootUrl(path, child, aux, queryParams); | ||||
|   } | ||||
| 
 | ||||
|   // segment + (matrix params) + (aux segments)
 | ||||
|   parseSegment(): Url { | ||||
|     if (this.remaining.length == 0) { | ||||
|       return null; | ||||
|     } | ||||
|     if (this.peekStartsWith('/')) { | ||||
|       this.capture('/'); | ||||
|     } | ||||
|     var path = matchUrlSegment(this.remaining); | ||||
|     this.capture(path); | ||||
| 
 | ||||
|     var matrixParams = null; | ||||
|     if (this.peekStartsWith(';')) { | ||||
|       matrixParams = this.parseMatrixParams(); | ||||
|     } | ||||
|     var aux = []; | ||||
|     if (this.peekStartsWith('(')) { | ||||
|       aux = this.parseAuxiliaryRoutes(); | ||||
|     } | ||||
|     var child = null; | ||||
|     if (this.peekStartsWith('/') && !this.peekStartsWith('//')) { | ||||
|       this.capture('/'); | ||||
|       child = this.parseSegment(); | ||||
|     } | ||||
|     return new Url(path, child, aux, matrixParams); | ||||
|   } | ||||
| 
 | ||||
|   parseQueryParams(): StringMap<string, any> { | ||||
|     var params = {}; | ||||
|     this.capture('?'); | ||||
|     this.parseParam(params); | ||||
|     while (this.remaining.length > 0 && this.peekStartsWith('&')) { | ||||
|       this.capture('&'); | ||||
|       this.parseParam(params); | ||||
|     } | ||||
|     return params; | ||||
|   } | ||||
| 
 | ||||
|   parseMatrixParams(): StringMap<string, any> { | ||||
|     var params = {}; | ||||
|     while (this.remaining.length > 0 && this.peekStartsWith(';')) { | ||||
|       this.capture(';'); | ||||
|       this.parseParam(params); | ||||
|     } | ||||
|     return params; | ||||
|   } | ||||
| 
 | ||||
|   parseParam(params: StringMap<string, any>): void { | ||||
|     var key = matchUrlSegment(this.remaining); | ||||
|     if (isBlank(key)) { | ||||
|       return; | ||||
|     } | ||||
|     this.capture(key); | ||||
|     var value: any = true; | ||||
|     if (this.peekStartsWith('=')) { | ||||
|       this.capture('='); | ||||
|       var valueMatch = matchUrlSegment(this.remaining); | ||||
|       if (isPresent(valueMatch)) { | ||||
|         value = valueMatch; | ||||
|         this.capture(value); | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     params[key] = value; | ||||
|   } | ||||
| 
 | ||||
|   parseAuxiliaryRoutes(): List<Url> { | ||||
|     var routes = []; | ||||
|     this.capture('('); | ||||
| 
 | ||||
|     while (!this.peekStartsWith(')') && this.remaining.length > 0) { | ||||
|       routes.push(this.parseSegment()); | ||||
|       if (this.peekStartsWith('//')) { | ||||
|         this.capture('//'); | ||||
|       } | ||||
|     } | ||||
|     this.capture(')'); | ||||
| 
 | ||||
|     return routes; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| export var parser = new UrlParser(); | ||||
| 
 | ||||
| export function serializeParams(paramMap: StringMap<string, any>): List<string> { | ||||
|   var params = []; | ||||
|   if (isPresent(paramMap)) { | ||||
|     StringMapWrapper.forEach(paramMap, (value, key) => { | ||||
|       if (value == true) { | ||||
|         params.push(key); | ||||
|       } else { | ||||
|         params.push(key + '=' + value); | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
|   return params; | ||||
| } | ||||
| @ -30,7 +30,13 @@ import { | ||||
| import {RootRouter} from 'angular2/src/router/router'; | ||||
| import {Pipeline} from 'angular2/src/router/pipeline'; | ||||
| import {Router, RouterOutlet, RouterLink, RouteParams} from 'angular2/router'; | ||||
| import {RouteConfig, Route, AsyncRoute, Redirect} from 'angular2/src/router/route_config_decorator'; | ||||
| import { | ||||
|   RouteConfig, | ||||
|   Route, | ||||
|   AuxRoute, | ||||
|   AsyncRoute, | ||||
|   Redirect | ||||
| } from 'angular2/src/router/route_config_decorator'; | ||||
| 
 | ||||
| import {DOM} from 'angular2/src/dom/dom_adapter'; | ||||
| 
 | ||||
| @ -45,10 +51,12 @@ import { | ||||
|   CanReuse | ||||
| } from 'angular2/src/router/interfaces'; | ||||
| import {CanActivate} from 'angular2/src/router/lifecycle_annotations'; | ||||
| import {Instruction} from 'angular2/src/router/instruction'; | ||||
| import {ComponentInstruction} from 'angular2/src/router/instruction'; | ||||
| import {DirectiveResolver} from 'angular2/src/core/compiler/directive_resolver'; | ||||
| 
 | ||||
| var cmpInstanceCount, log, eventBus; | ||||
| var cmpInstanceCount; | ||||
| var log: List<string>; | ||||
| var eventBus: EventEmitter; | ||||
| var completer: PromiseCompleter<any>; | ||||
| 
 | ||||
| export function main() { | ||||
| @ -73,7 +81,7 @@ export function main() { | ||||
|       rtr = router; | ||||
|       location = loc; | ||||
|       cmpInstanceCount = 0; | ||||
|       log = ''; | ||||
|       log = []; | ||||
|       eventBus = new EventEmitter(); | ||||
|     })); | ||||
| 
 | ||||
| @ -207,7 +215,6 @@ export function main() { | ||||
|              }); | ||||
|        })); | ||||
| 
 | ||||
| 
 | ||||
|     it('should generate link hrefs from a child to its sibling', | ||||
|        inject([AsyncTestCompleter], (async) => { | ||||
|          compile() | ||||
| @ -247,6 +254,8 @@ export function main() { | ||||
|              }); | ||||
|        })); | ||||
| 
 | ||||
| 
 | ||||
|     describe('lifecycle hooks', () => { | ||||
|       it('should call the onActivate hook', inject([AsyncTestCompleter], (async) => { | ||||
|            compile() | ||||
|                .then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})])) | ||||
| @ -254,7 +263,7 @@ export function main() { | ||||
|                .then((_) => { | ||||
|                  rootTC.detectChanges(); | ||||
|                  expect(rootTC.nativeElement).toHaveText('activate cmp'); | ||||
|                expect(log).toEqual('activate: null -> /on-activate;'); | ||||
|                  expect(log).toEqual(['activate: null -> /on-activate']); | ||||
|                  async.done(); | ||||
|                }); | ||||
|          })); | ||||
| @ -273,8 +282,10 @@ export function main() { | ||||
|                      .then((_) => { | ||||
|                        rootTC.detectChanges(); | ||||
|                        expect(rootTC.nativeElement).toHaveText('parent {activate cmp}'); | ||||
|                      expect(log).toEqual( | ||||
|                          'parent activate: null -> /parent-activate/child-activate;activate: null -> /child-activate;'); | ||||
|                        expect(log).toEqual([ | ||||
|                          'parent activate: null -> /parent-activate', | ||||
|                          'activate: null -> /child-activate' | ||||
|                        ]); | ||||
|                        async.done(); | ||||
|                      }); | ||||
|                }); | ||||
| @ -288,7 +299,7 @@ export function main() { | ||||
|                .then((_) => { | ||||
|                  rootTC.detectChanges(); | ||||
|                  expect(rootTC.nativeElement).toHaveText('A'); | ||||
|                expect(log).toEqual('deactivate: /on-deactivate -> /a;'); | ||||
|                  expect(log).toEqual(['deactivate: /on-deactivate -> /a']); | ||||
|                  async.done(); | ||||
|                }); | ||||
|          })); | ||||
| @ -309,28 +320,30 @@ export function main() { | ||||
|                  rtr.navigate('/a').then((_) => { | ||||
|                    rootTC.detectChanges(); | ||||
|                    expect(rootTC.nativeElement).toHaveText('A'); | ||||
|                  expect(log).toEqual( | ||||
|                      'deactivate: /child-deactivate -> null;parent deactivate: /parent-deactivate/child-deactivate -> /a;'); | ||||
|                    expect(log).toEqual([ | ||||
|                      'deactivate: /child-deactivate -> null', | ||||
|                      'parent deactivate: /parent-deactivate -> /a' | ||||
|                    ]); | ||||
|                    async.done(); | ||||
|                  }); | ||||
|                }); | ||||
|          })); | ||||
| 
 | ||||
|     it('should reuse a component when the canReuse hook returns false', | ||||
|       it('should reuse a component when the canReuse hook returns true', | ||||
|          inject([AsyncTestCompleter], (async) => { | ||||
|            compile() | ||||
|                .then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})])) | ||||
|                .then((_) => rtr.navigate('/on-reuse/1/a')) | ||||
|                .then((_) => { | ||||
|                  rootTC.detectChanges(); | ||||
|                expect(log).toEqual(''); | ||||
|                  expect(log).toEqual([]); | ||||
|                  expect(rootTC.nativeElement).toHaveText('reuse {A}'); | ||||
|                  expect(cmpInstanceCount).toBe(1); | ||||
|                }) | ||||
|                .then((_) => rtr.navigate('/on-reuse/2/b')) | ||||
|                .then((_) => { | ||||
|                  rootTC.detectChanges(); | ||||
|                expect(log).toEqual('reuse: /on-reuse/1/a -> /on-reuse/2/b;'); | ||||
|                  expect(log).toEqual(['reuse: /on-reuse/1 -> /on-reuse/2']); | ||||
|                  expect(rootTC.nativeElement).toHaveText('reuse {B}'); | ||||
|                  expect(cmpInstanceCount).toBe(1); | ||||
|                  async.done(); | ||||
| @ -345,20 +358,21 @@ export function main() { | ||||
|                .then((_) => rtr.navigate('/never-reuse/1/a')) | ||||
|                .then((_) => { | ||||
|                  rootTC.detectChanges(); | ||||
|                expect(log).toEqual(''); | ||||
|                  expect(log).toEqual([]); | ||||
|                  expect(rootTC.nativeElement).toHaveText('reuse {A}'); | ||||
|                  expect(cmpInstanceCount).toBe(1); | ||||
|                }) | ||||
|                .then((_) => rtr.navigate('/never-reuse/2/b')) | ||||
|                .then((_) => { | ||||
|                  rootTC.detectChanges(); | ||||
|                expect(log).toEqual(''); | ||||
|                  expect(log).toEqual([]); | ||||
|                  expect(rootTC.nativeElement).toHaveText('reuse {B}'); | ||||
|                  expect(cmpInstanceCount).toBe(2); | ||||
|                  async.done(); | ||||
|                }); | ||||
|          })); | ||||
| 
 | ||||
| 
 | ||||
|       it('should navigate when canActivate returns true', inject([AsyncTestCompleter], (async) => { | ||||
|            compile() | ||||
|                .then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})])) | ||||
| @ -372,7 +386,7 @@ export function main() { | ||||
|                      .then((_) => { | ||||
|                        rootTC.detectChanges(); | ||||
|                        expect(rootTC.nativeElement).toHaveText('canActivate {A}'); | ||||
|                      expect(log).toEqual('canActivate: null -> /can-activate/a;'); | ||||
|                        expect(log).toEqual(['canActivate: null -> /can-activate']); | ||||
|                        async.done(); | ||||
|                      }); | ||||
|                }); | ||||
| @ -392,7 +406,7 @@ export function main() { | ||||
|                      .then((_) => { | ||||
|                        rootTC.detectChanges(); | ||||
|                        expect(rootTC.nativeElement).toHaveText(''); | ||||
|                      expect(log).toEqual('canActivate: null -> /can-activate/a;'); | ||||
|                        expect(log).toEqual(['canActivate: null -> /can-activate']); | ||||
|                        async.done(); | ||||
|                      }); | ||||
|                }); | ||||
| @ -406,7 +420,7 @@ export function main() { | ||||
|                .then((_) => { | ||||
|                  rootTC.detectChanges(); | ||||
|                  expect(rootTC.nativeElement).toHaveText('canDeactivate {A}'); | ||||
|                expect(log).toEqual(''); | ||||
|                  expect(log).toEqual([]); | ||||
| 
 | ||||
|                  ObservableWrapper.subscribe<string>(eventBus, (ev) => { | ||||
|                    if (ev.startsWith('canDeactivate')) { | ||||
| @ -417,7 +431,7 @@ export function main() { | ||||
|                  rtr.navigate('/a').then((_) => { | ||||
|                    rootTC.detectChanges(); | ||||
|                    expect(rootTC.nativeElement).toHaveText('A'); | ||||
|                  expect(log).toEqual('canDeactivate: /can-deactivate/a -> /a;'); | ||||
|                    expect(log).toEqual(['canDeactivate: /can-deactivate -> /a']); | ||||
|                    async.done(); | ||||
|                  }); | ||||
|                }); | ||||
| @ -431,7 +445,7 @@ export function main() { | ||||
|                .then((_) => { | ||||
|                  rootTC.detectChanges(); | ||||
|                  expect(rootTC.nativeElement).toHaveText('canDeactivate {A}'); | ||||
|                expect(log).toEqual(''); | ||||
|                  expect(log).toEqual([]); | ||||
| 
 | ||||
|                  ObservableWrapper.subscribe<string>(eventBus, (ev) => { | ||||
|                    if (ev.startsWith('canDeactivate')) { | ||||
| @ -442,7 +456,7 @@ export function main() { | ||||
|                  rtr.navigate('/a').then((_) => { | ||||
|                    rootTC.detectChanges(); | ||||
|                    expect(rootTC.nativeElement).toHaveText('canDeactivate {A}'); | ||||
|                  expect(log).toEqual('canDeactivate: /can-deactivate/a -> /a;'); | ||||
|                    expect(log).toEqual(['canDeactivate: /can-deactivate -> /a']); | ||||
|                    async.done(); | ||||
|                  }); | ||||
|                }); | ||||
| @ -455,19 +469,23 @@ export function main() { | ||||
|                .then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})])) | ||||
|                .then((_) => rtr.navigate('/activation-hooks/child')) | ||||
|                .then((_) => { | ||||
|                expect(log).toEqual('canActivate child: null -> /child;' + | ||||
|                                    'canActivate parent: null -> /activation-hooks/child;' + | ||||
|                                    'onActivate parent: null -> /activation-hooks/child;' + | ||||
|                                    'onActivate child: null -> /child;'); | ||||
|                  expect(log).toEqual([ | ||||
|                    'canActivate child: null -> /child', | ||||
|                    'canActivate parent: null -> /activation-hooks', | ||||
|                    'onActivate parent: null -> /activation-hooks', | ||||
|                    'onActivate child: null -> /child' | ||||
|                  ]); | ||||
| 
 | ||||
|                log = ''; | ||||
|                  log = []; | ||||
|                  return rtr.navigate('/a'); | ||||
|                }) | ||||
|                .then((_) => { | ||||
|                expect(log).toEqual('canDeactivate parent: /activation-hooks/child -> /a;' + | ||||
|                                    'canDeactivate child: /child -> null;' + | ||||
|                                    'onDeactivate child: /child -> null;' + | ||||
|                                    'onDeactivate parent: /activation-hooks/child -> /a;'); | ||||
|                  expect(log).toEqual([ | ||||
|                    'canDeactivate parent: /activation-hooks -> /a', | ||||
|                    'canDeactivate child: /child -> null', | ||||
|                    'onDeactivate child: /child -> null', | ||||
|                    'onDeactivate parent: /activation-hooks -> /a' | ||||
|                  ]); | ||||
|                  async.done(); | ||||
|                }); | ||||
|          })); | ||||
| @ -477,8 +495,8 @@ export function main() { | ||||
|                .then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})])) | ||||
|                .then((_) => rtr.navigate('/reuse-hooks/1')) | ||||
|                .then((_) => { | ||||
|                expect(log).toEqual('canActivate: null -> /reuse-hooks/1;' + | ||||
|                                    'onActivate: null -> /reuse-hooks/1;'); | ||||
|                  expect(log).toEqual( | ||||
|                      ['canActivate: null -> /reuse-hooks/1', 'onActivate: null -> /reuse-hooks/1']); | ||||
| 
 | ||||
|                  ObservableWrapper.subscribe<string>(eventBus, (ev) => { | ||||
|                    if (ev.startsWith('canReuse')) { | ||||
| @ -486,12 +504,15 @@ export function main() { | ||||
|                    } | ||||
|                  }); | ||||
| 
 | ||||
|                log = ''; | ||||
| 
 | ||||
|                  log = []; | ||||
|                  return rtr.navigate('/reuse-hooks/2'); | ||||
|                }) | ||||
|                .then((_) => { | ||||
|                expect(log).toEqual('canReuse: /reuse-hooks/1 -> /reuse-hooks/2;' + | ||||
|                                    'onReuse: /reuse-hooks/1 -> /reuse-hooks/2;'); | ||||
|                  expect(log).toEqual([ | ||||
|                    'canReuse: /reuse-hooks/1 -> /reuse-hooks/2', | ||||
|                    'onReuse: /reuse-hooks/1 -> /reuse-hooks/2' | ||||
|                  ]); | ||||
|                  async.done(); | ||||
|                }); | ||||
|          })); | ||||
| @ -501,8 +522,8 @@ export function main() { | ||||
|                .then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})])) | ||||
|                .then((_) => rtr.navigate('/reuse-hooks/1')) | ||||
|                .then((_) => { | ||||
|                expect(log).toEqual('canActivate: null -> /reuse-hooks/1;' + | ||||
|                                    'onActivate: null -> /reuse-hooks/1;'); | ||||
|                  expect(log).toEqual( | ||||
|                      ['canActivate: null -> /reuse-hooks/1', 'onActivate: null -> /reuse-hooks/1']); | ||||
| 
 | ||||
|                  ObservableWrapper.subscribe<string>(eventBus, (ev) => { | ||||
|                    if (ev.startsWith('canReuse')) { | ||||
| @ -510,19 +531,23 @@ export function main() { | ||||
|                    } | ||||
|                  }); | ||||
| 
 | ||||
|                log = ''; | ||||
|                  log = []; | ||||
|                  return rtr.navigate('/reuse-hooks/2'); | ||||
|                }) | ||||
|                .then((_) => { | ||||
|                expect(log).toEqual('canReuse: /reuse-hooks/1 -> /reuse-hooks/2;' + | ||||
|                                    'canActivate: /reuse-hooks/1 -> /reuse-hooks/2;' + | ||||
|                                    'canDeactivate: /reuse-hooks/1 -> /reuse-hooks/2;' + | ||||
|                                    'onDeactivate: /reuse-hooks/1 -> /reuse-hooks/2;' + | ||||
|                                    'onActivate: /reuse-hooks/1 -> /reuse-hooks/2;'); | ||||
|                  expect(log).toEqual([ | ||||
|                    'canReuse: /reuse-hooks/1 -> /reuse-hooks/2', | ||||
|                    'canActivate: /reuse-hooks/1 -> /reuse-hooks/2', | ||||
|                    'canDeactivate: /reuse-hooks/1 -> /reuse-hooks/2', | ||||
|                    'onDeactivate: /reuse-hooks/1 -> /reuse-hooks/2', | ||||
|                    'onActivate: /reuse-hooks/1 -> /reuse-hooks/2' | ||||
|                  ]); | ||||
|                  async.done(); | ||||
|                }); | ||||
|          })); | ||||
| 
 | ||||
|     }); | ||||
| 
 | ||||
|     describe('when clicked', () => { | ||||
| 
 | ||||
|       var clickOnElement = function(view) { | ||||
| @ -572,6 +597,19 @@ export function main() { | ||||
|                }); | ||||
|          })); | ||||
|     }); | ||||
| 
 | ||||
|     describe('auxillary routes', () => { | ||||
|       it('should recognize a simple case', inject([AsyncTestCompleter], (async) => { | ||||
|            compile() | ||||
|                .then((_) => rtr.config([new Route({path: '/...', component: AuxCmp})])) | ||||
|                .then((_) => rtr.navigate('/hello(modal)')) | ||||
|                .then((_) => { | ||||
|                  rootTC.detectChanges(); | ||||
|                  expect(rootTC.nativeElement).toHaveText('main {hello} | aux {modal}'); | ||||
|                  async.done(); | ||||
|                }); | ||||
|          })); | ||||
|     }); | ||||
|   }); | ||||
| } | ||||
| 
 | ||||
| @ -657,24 +695,26 @@ class MyComp { | ||||
|   name; | ||||
| } | ||||
| 
 | ||||
| function logHook(name: string, next: Instruction, prev: Instruction) { | ||||
|   var message = name + ': ' + (isPresent(prev) ? prev.accumulatedUrl : 'null') + ' -> ' + | ||||
|                 (isPresent(next) ? next.accumulatedUrl : 'null') + ';'; | ||||
|   log += message; | ||||
| function logHook(name: string, next: ComponentInstruction, prev: ComponentInstruction) { | ||||
|   var message = name + ': ' + (isPresent(prev) ? ('/' + prev.urlPath) : 'null') + ' -> ' + | ||||
|                 (isPresent(next) ? ('/' + next.urlPath) : 'null'); | ||||
|   log.push(message); | ||||
|   ObservableWrapper.callNext(eventBus, message); | ||||
| } | ||||
| 
 | ||||
| @Component({selector: 'activate-cmp'}) | ||||
| @View({template: 'activate cmp'}) | ||||
| class ActivateCmp implements OnActivate { | ||||
|   onActivate(next: Instruction, prev: Instruction) { logHook('activate', next, prev); } | ||||
|   onActivate(next: ComponentInstruction, prev: ComponentInstruction) { | ||||
|     logHook('activate', next, prev); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| @Component({selector: 'parent-activate-cmp'}) | ||||
| @View({template: `parent {<router-outlet></router-outlet>}`, directives: [RouterOutlet]}) | ||||
| @RouteConfig([new Route({path: '/child-activate', component: ActivateCmp})]) | ||||
| class ParentActivateCmp implements OnActivate { | ||||
|   onActivate(next: Instruction, prev: Instruction): Promise<any> { | ||||
|   onActivate(next: ComponentInstruction, prev: ComponentInstruction): Promise<any> { | ||||
|     completer = PromiseWrapper.completer(); | ||||
|     logHook('parent activate', next, prev); | ||||
|     return completer.promise; | ||||
| @ -684,13 +724,15 @@ class ParentActivateCmp implements OnActivate { | ||||
| @Component({selector: 'deactivate-cmp'}) | ||||
| @View({template: 'deactivate cmp'}) | ||||
| class DeactivateCmp implements OnDeactivate { | ||||
|   onDeactivate(next: Instruction, prev: Instruction) { logHook('deactivate', next, prev); } | ||||
|   onDeactivate(next: ComponentInstruction, prev: ComponentInstruction) { | ||||
|     logHook('deactivate', next, prev); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| @Component({selector: 'deactivate-cmp'}) | ||||
| @View({template: 'deactivate cmp'}) | ||||
| class WaitDeactivateCmp implements OnDeactivate { | ||||
|   onDeactivate(next: Instruction, prev: Instruction): Promise<any> { | ||||
|   onDeactivate(next: ComponentInstruction, prev: ComponentInstruction): Promise<any> { | ||||
|     completer = PromiseWrapper.completer(); | ||||
|     logHook('deactivate', next, prev); | ||||
|     return completer.promise; | ||||
| @ -701,7 +743,9 @@ class WaitDeactivateCmp implements OnDeactivate { | ||||
| @View({template: `parent {<router-outlet></router-outlet>}`, directives: [RouterOutlet]}) | ||||
| @RouteConfig([new Route({path: '/child-deactivate', component: WaitDeactivateCmp})]) | ||||
| class ParentDeactivateCmp implements OnDeactivate { | ||||
|   onDeactivate(next: Instruction, prev: Instruction) { logHook('parent deactivate', next, prev); } | ||||
|   onDeactivate(next: ComponentInstruction, prev: ComponentInstruction) { | ||||
|     logHook('parent deactivate', next, prev); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| @Component({selector: 'reuse-cmp'}) | ||||
| @ -709,8 +753,8 @@ class ParentDeactivateCmp implements OnDeactivate { | ||||
| @RouteConfig([new Route({path: '/a', component: A}), new Route({path: '/b', component: B})]) | ||||
| class ReuseCmp implements OnReuse, CanReuse { | ||||
|   constructor() { cmpInstanceCount += 1; } | ||||
|   canReuse(next: Instruction, prev: Instruction) { return true; } | ||||
|   onReuse(next: Instruction, prev: Instruction) { logHook('reuse', next, prev); } | ||||
|   canReuse(next: ComponentInstruction, prev: ComponentInstruction) { return true; } | ||||
|   onReuse(next: ComponentInstruction, prev: ComponentInstruction) { logHook('reuse', next, prev); } | ||||
| } | ||||
| 
 | ||||
| @Component({selector: 'never-reuse-cmp'}) | ||||
| @ -718,8 +762,8 @@ class ReuseCmp implements OnReuse, CanReuse { | ||||
| @RouteConfig([new Route({path: '/a', component: A}), new Route({path: '/b', component: B})]) | ||||
| class NeverReuseCmp implements OnReuse, CanReuse { | ||||
|   constructor() { cmpInstanceCount += 1; } | ||||
|   canReuse(next: Instruction, prev: Instruction) { return false; } | ||||
|   onReuse(next: Instruction, prev: Instruction) { logHook('reuse', next, prev); } | ||||
|   canReuse(next: ComponentInstruction, prev: ComponentInstruction) { return false; } | ||||
|   onReuse(next: ComponentInstruction, prev: ComponentInstruction) { logHook('reuse', next, prev); } | ||||
| } | ||||
| 
 | ||||
| @Component({selector: 'can-activate-cmp'}) | ||||
| @ -727,7 +771,7 @@ class NeverReuseCmp implements OnReuse, CanReuse { | ||||
| @RouteConfig([new Route({path: '/a', component: A}), new Route({path: '/b', component: B})]) | ||||
| @CanActivate(CanActivateCmp.canActivate) | ||||
| class CanActivateCmp { | ||||
|   static canActivate(next: Instruction, prev: Instruction) { | ||||
|   static canActivate(next: ComponentInstruction, prev: ComponentInstruction): Promise<boolean> { | ||||
|     completer = PromiseWrapper.completer(); | ||||
|     logHook('canActivate', next, prev); | ||||
|     return completer.promise; | ||||
| @ -738,7 +782,7 @@ class CanActivateCmp { | ||||
| @View({template: `canDeactivate {<router-outlet></router-outlet>}`, directives: [RouterOutlet]}) | ||||
| @RouteConfig([new Route({path: '/a', component: A}), new Route({path: '/b', component: B})]) | ||||
| class CanDeactivateCmp implements CanDeactivate { | ||||
|   canDeactivate(next: Instruction, prev: Instruction) { | ||||
|   canDeactivate(next: ComponentInstruction, prev: ComponentInstruction): Promise<boolean> { | ||||
|     completer = PromiseWrapper.completer(); | ||||
|     logHook('canDeactivate', next, prev); | ||||
|     return completer.promise; | ||||
| @ -749,19 +793,23 @@ class CanDeactivateCmp implements CanDeactivate { | ||||
| @View({template: `child`}) | ||||
| @CanActivate(AllHooksChildCmp.canActivate) | ||||
| class AllHooksChildCmp implements CanDeactivate, OnDeactivate, OnActivate { | ||||
|   canDeactivate(next: Instruction, prev: Instruction) { | ||||
|   canDeactivate(next: ComponentInstruction, prev: ComponentInstruction) { | ||||
|     logHook('canDeactivate child', next, prev); | ||||
|     return true; | ||||
|   } | ||||
| 
 | ||||
|   onDeactivate(next: Instruction, prev: Instruction) { logHook('onDeactivate child', next, prev); } | ||||
|   onDeactivate(next: ComponentInstruction, prev: ComponentInstruction) { | ||||
|     logHook('onDeactivate child', next, prev); | ||||
|   } | ||||
| 
 | ||||
|   static canActivate(next: Instruction, prev: Instruction) { | ||||
|   static canActivate(next: ComponentInstruction, prev: ComponentInstruction) { | ||||
|     logHook('canActivate child', next, prev); | ||||
|     return true; | ||||
|   } | ||||
| 
 | ||||
|   onActivate(next: Instruction, prev: Instruction) { logHook('onActivate child', next, prev); } | ||||
|   onActivate(next: ComponentInstruction, prev: ComponentInstruction) { | ||||
|     logHook('onActivate child', next, prev); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| @Component({selector: 'all-hooks-parent-cmp'}) | ||||
| @ -769,46 +817,56 @@ class AllHooksChildCmp implements CanDeactivate, OnDeactivate, OnActivate { | ||||
| @RouteConfig([new Route({path: '/child', component: AllHooksChildCmp})]) | ||||
| @CanActivate(AllHooksParentCmp.canActivate) | ||||
| class AllHooksParentCmp implements CanDeactivate, OnDeactivate, OnActivate { | ||||
|   canDeactivate(next: Instruction, prev: Instruction) { | ||||
|   canDeactivate(next: ComponentInstruction, prev: ComponentInstruction) { | ||||
|     logHook('canDeactivate parent', next, prev); | ||||
|     return true; | ||||
|   } | ||||
| 
 | ||||
|   onDeactivate(next: Instruction, prev: Instruction) { logHook('onDeactivate parent', next, prev); } | ||||
|   onDeactivate(next: ComponentInstruction, prev: ComponentInstruction) { | ||||
|     logHook('onDeactivate parent', next, prev); | ||||
|   } | ||||
| 
 | ||||
|   static canActivate(next: Instruction, prev: Instruction) { | ||||
|   static canActivate(next: ComponentInstruction, prev: ComponentInstruction) { | ||||
|     logHook('canActivate parent', next, prev); | ||||
|     return true; | ||||
|   } | ||||
| 
 | ||||
|   onActivate(next: Instruction, prev: Instruction) { logHook('onActivate parent', next, prev); } | ||||
|   onActivate(next: ComponentInstruction, prev: ComponentInstruction) { | ||||
|     logHook('onActivate parent', next, prev); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| @Component({selector: 'reuse-hooks-cmp'}) | ||||
| @View({template: 'reuse hooks cmp'}) | ||||
| @CanActivate(ReuseHooksCmp.canActivate) | ||||
| class ReuseHooksCmp implements OnActivate, OnReuse, OnDeactivate, CanReuse, CanDeactivate { | ||||
|   canReuse(next: Instruction, prev: Instruction): Promise<any> { | ||||
|   canReuse(next: ComponentInstruction, prev: ComponentInstruction): Promise<any> { | ||||
|     completer = PromiseWrapper.completer(); | ||||
|     logHook('canReuse', next, prev); | ||||
|     return completer.promise; | ||||
|   } | ||||
| 
 | ||||
|   onReuse(next: Instruction, prev: Instruction) { logHook('onReuse', next, prev); } | ||||
|   onReuse(next: ComponentInstruction, prev: ComponentInstruction) { | ||||
|     logHook('onReuse', next, prev); | ||||
|   } | ||||
| 
 | ||||
|   canDeactivate(next: Instruction, prev: Instruction) { | ||||
|   canDeactivate(next: ComponentInstruction, prev: ComponentInstruction) { | ||||
|     logHook('canDeactivate', next, prev); | ||||
|     return true; | ||||
|   } | ||||
| 
 | ||||
|   onDeactivate(next: Instruction, prev: Instruction) { logHook('onDeactivate', next, prev); } | ||||
|   onDeactivate(next: ComponentInstruction, prev: ComponentInstruction) { | ||||
|     logHook('onDeactivate', next, prev); | ||||
|   } | ||||
| 
 | ||||
|   static canActivate(next: Instruction, prev: Instruction) { | ||||
|   static canActivate(next: ComponentInstruction, prev: ComponentInstruction) { | ||||
|     logHook('canActivate', next, prev); | ||||
|     return true; | ||||
|   } | ||||
| 
 | ||||
|   onActivate(next: Instruction, prev: Instruction) { logHook('onActivate', next, prev); } | ||||
|   onActivate(next: ComponentInstruction, prev: ComponentInstruction) { | ||||
|     logHook('onActivate', next, prev); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| @Component({selector: 'lifecycle-cmp'}) | ||||
| @ -828,3 +886,21 @@ class ReuseHooksCmp implements OnActivate, OnReuse, OnDeactivate, CanReuse, CanD | ||||
| ]) | ||||
| class LifecycleCmp { | ||||
| } | ||||
| 
 | ||||
| @Component({selector: 'modal-cmp'}) | ||||
| @View({template: "modal"}) | ||||
| class ModalCmp { | ||||
| } | ||||
| 
 | ||||
| @Component({selector: 'aux-cmp'}) | ||||
| @View({ | ||||
|   template: | ||||
|       `main {<router-outlet></router-outlet>} | aux {<router-outlet name="modal"></router-outlet>}`, | ||||
|   directives: [RouterOutlet] | ||||
| }) | ||||
| @RouteConfig([ | ||||
|   new Route({path: '/hello', component: HelloCmp}), | ||||
|   new AuxRoute({path: '/modal', component: ModalCmp}) | ||||
| ]) | ||||
| class AuxCmp { | ||||
| } | ||||
|  | ||||
| @ -11,6 +11,7 @@ import { | ||||
| } from 'angular2/test_lib'; | ||||
| 
 | ||||
| import {PathRecognizer} from 'angular2/src/router/path_recognizer'; | ||||
| import {parser, Url, RootUrl} from 'angular2/src/router/url_parser'; | ||||
| import {SyncRouteHandler} from 'angular2/src/router/sync_route_handler'; | ||||
| 
 | ||||
| class DummyClass { | ||||
| @ -41,64 +42,59 @@ export function main() { | ||||
| 
 | ||||
|     describe('querystring params', () => { | ||||
|       it('should parse querystring params so long as the recognizer is a root', () => { | ||||
|         var rec = new PathRecognizer('/hello/there', mockRouteHandler, true); | ||||
|         var params = rec.parseParams('/hello/there?name=igor'); | ||||
|         expect(params).toEqual({'name': 'igor'}); | ||||
|         var rec = new PathRecognizer('/hello/there', mockRouteHandler); | ||||
|         var url = parser.parse('/hello/there?name=igor'); | ||||
|         var match = rec.recognize(url); | ||||
|         expect(match.instruction.params).toEqual({'name': 'igor'}); | ||||
|       }); | ||||
| 
 | ||||
|       it('should return a combined map of parameters with the param expected in the URL path', | ||||
|          () => { | ||||
|            var rec = new PathRecognizer('/hello/:name', mockRouteHandler, true); | ||||
|            var params = rec.parseParams('/hello/paul?topic=success'); | ||||
|            expect(params).toEqual({'name': 'paul', 'topic': 'success'}); | ||||
|            var rec = new PathRecognizer('/hello/:name', mockRouteHandler); | ||||
|            var url = parser.parse('/hello/paul?topic=success'); | ||||
|            var match = rec.recognize(url); | ||||
|            expect(match.instruction.params).toEqual({'name': 'paul', 'topic': 'success'}); | ||||
|          }); | ||||
|     }); | ||||
| 
 | ||||
|     describe('matrix params', () => { | ||||
|       it('should recognize a trailing matrix value on a path value and assign it to the params return value', | ||||
|          () => { | ||||
|       it('should be parsed along with dynamic paths', () => { | ||||
|         var rec = new PathRecognizer('/hello/:id', mockRouteHandler); | ||||
|            var params = rec.parseParams('/hello/matias;key=value'); | ||||
| 
 | ||||
|            expect(params['id']).toEqual('matias'); | ||||
|            expect(params['key']).toEqual('value'); | ||||
|         var url = new Url('hello', new Url('matias', null, null, {'key': 'value'})); | ||||
|         var match = rec.recognize(url); | ||||
|         expect(match.instruction.params).toEqual({'id': 'matias', 'key': 'value'}); | ||||
|       }); | ||||
| 
 | ||||
|       it('should recognize and parse multiple matrix params separated by a colon value', () => { | ||||
|         var rec = new PathRecognizer('/jello/:sid', mockRouteHandler); | ||||
|         var params = rec.parseParams('/jello/man;color=red;height=20'); | ||||
| 
 | ||||
|         expect(params['sid']).toEqual('man'); | ||||
|         expect(params['color']).toEqual('red'); | ||||
|         expect(params['height']).toEqual('20'); | ||||
|       it('should be parsed on a static path', () => { | ||||
|         var rec = new PathRecognizer('/person', mockRouteHandler); | ||||
|         var url = new Url('person', null, null, {'name': 'dave'}); | ||||
|         var match = rec.recognize(url); | ||||
|         expect(match.instruction.params).toEqual({'name': 'dave'}); | ||||
|       }); | ||||
| 
 | ||||
|       it('should recognize a matrix param value on a static path value', () => { | ||||
|         var rec = new PathRecognizer('/static/man', mockRouteHandler); | ||||
|         var params = rec.parseParams('/static/man;name=dave'); | ||||
|         expect(params['name']).toEqual('dave'); | ||||
|       }); | ||||
| 
 | ||||
|       it('should not parse matrix params when a wildcard segment is used', () => { | ||||
|       it('should be ignored on a wildcard segment', () => { | ||||
|         var rec = new PathRecognizer('/wild/*everything', mockRouteHandler); | ||||
|         var params = rec.parseParams('/wild/super;variable=value'); | ||||
|         expect(params['everything']).toEqual('super;variable=value'); | ||||
|         var url = parser.parse('/wild/super;variable=value'); | ||||
|         var match = rec.recognize(url); | ||||
|         expect(match.instruction.params).toEqual({'everything': 'super;variable=value'}); | ||||
|       }); | ||||
| 
 | ||||
|       it('should set matrix param values to true when no value is present within the path string', | ||||
|          () => { | ||||
|       it('should set matrix param values to true when no value is present', () => { | ||||
|         var rec = new PathRecognizer('/path', mockRouteHandler); | ||||
|            var params = rec.parseParams('/path;one;two;three=3'); | ||||
|            expect(params['one']).toEqual(true); | ||||
|            expect(params['two']).toEqual(true); | ||||
|            expect(params['three']).toEqual('3'); | ||||
|         var url = new Url('path', null, null, {'one': true, 'two': true, 'three': '3'}); | ||||
|         var match = rec.recognize(url); | ||||
|         expect(match.instruction.params).toEqual({'one': true, 'two': true, 'three': '3'}); | ||||
|       }); | ||||
| 
 | ||||
|       it('should ignore earlier instances of matrix params and only consider the ones at the end of the path', | ||||
|          () => { | ||||
|       it('should be parsed on the final segment of the path', () => { | ||||
|         var rec = new PathRecognizer('/one/two/three', mockRouteHandler); | ||||
|            var params = rec.parseParams('/one;a=1/two;b=2/three;c=3'); | ||||
|            expect(params).toEqual({'c': '3'}); | ||||
| 
 | ||||
|         var three = new Url('three', null, null, {'c': '3'}); | ||||
|         var two = new Url('two', three, null, {'b': '2'}); | ||||
|         var one = new Url('one', two, null, {'a': '1'}); | ||||
| 
 | ||||
|         var match = rec.recognize(one); | ||||
|         expect(match.instruction.params).toEqual({'c': '3'}); | ||||
|       }); | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
| @ -98,7 +98,31 @@ export function main() { | ||||
|              }); | ||||
|        })); | ||||
| 
 | ||||
|     // TODO: test apps with wrong configs
 | ||||
|     it('should throw if a config is missing a target', | ||||
|        inject( | ||||
|            [AsyncTestCompleter], | ||||
|            (async) => { | ||||
|                bootstrap(WrongConfigCmp, testBindings) | ||||
|                    .catch((e) => { | ||||
|                      expect(e.originalException) | ||||
|                          .toContainError( | ||||
|                              'Route config should contain exactly one "component", "loader", or "redirectTo" property.'); | ||||
|                      async.done(); | ||||
|                      return null; | ||||
|                    })})); | ||||
| 
 | ||||
|     it('should throw if a config has an invalid component type', | ||||
|        inject( | ||||
|            [AsyncTestCompleter], | ||||
|            (async) => { | ||||
|                bootstrap(WrongComponentTypeCmp, testBindings) | ||||
|                    .catch((e) => { | ||||
|                      expect(e.originalException) | ||||
|                          .toContainError( | ||||
|                              'Invalid component type "intentionallyWrongComponentType". Valid types are "constructor" and "loader".'); | ||||
|                      async.done(); | ||||
|                      return null; | ||||
|                    })})); | ||||
|   }); | ||||
| } | ||||
| 
 | ||||
| @ -149,3 +173,17 @@ class ParentCmp { | ||||
| class HierarchyAppCmp { | ||||
|   constructor(public router: Router, public location: LocationStrategy) {} | ||||
| } | ||||
| 
 | ||||
| @Component({selector: 'app-cmp'}) | ||||
| @View({template: `root { <router-outlet></router-outlet> }`, directives: routerDirectives}) | ||||
| @RouteConfig([{path: '/hello'}]) | ||||
| class WrongConfigCmp { | ||||
| } | ||||
| 
 | ||||
| @Component({selector: 'app-cmp'}) | ||||
| @View({template: `root { <router-outlet></router-outlet> }`, directives: routerDirectives}) | ||||
| @RouteConfig([ | ||||
|   {path: '/hello', component: {type: 'intentionallyWrongComponentType', constructor: HelloCmp}}, | ||||
| ]) | ||||
| class WrongComponentTypeCmp { | ||||
| } | ||||
|  | ||||
| @ -12,9 +12,11 @@ import { | ||||
| 
 | ||||
| import {Map, StringMap, StringMapWrapper} from 'angular2/src/facade/collection'; | ||||
| 
 | ||||
| import {RouteRecognizer, RouteMatch} from 'angular2/src/router/route_recognizer'; | ||||
| import {RouteRecognizer} from 'angular2/src/router/route_recognizer'; | ||||
| import {ComponentInstruction} from 'angular2/src/router/instruction'; | ||||
| 
 | ||||
| import {Route, Redirect} from 'angular2/src/router/route_config_decorator'; | ||||
| import {parser} from 'angular2/src/router/url_parser'; | ||||
| 
 | ||||
| export function main() { | ||||
|   describe('RouteRecognizer', () => { | ||||
| @ -25,31 +27,31 @@ export function main() { | ||||
| 
 | ||||
|     it('should recognize a static segment', () => { | ||||
|       recognizer.config(new Route({path: '/test', component: DummyCmpA})); | ||||
|       var solution = recognizer.recognize('/test')[0]; | ||||
|       var solution = recognize(recognizer, '/test'); | ||||
|       expect(getComponentType(solution)).toEqual(DummyCmpA); | ||||
|     }); | ||||
| 
 | ||||
| 
 | ||||
|     it('should recognize a single slash', () => { | ||||
|       recognizer.config(new Route({path: '/', component: DummyCmpA})); | ||||
|       var solution = recognizer.recognize('/')[0]; | ||||
|       var solution = recognize(recognizer, '/'); | ||||
|       expect(getComponentType(solution)).toEqual(DummyCmpA); | ||||
|     }); | ||||
| 
 | ||||
| 
 | ||||
|     it('should recognize a dynamic segment', () => { | ||||
|       recognizer.config(new Route({path: '/user/:name', component: DummyCmpA})); | ||||
|       var solution = recognizer.recognize('/user/brian')[0]; | ||||
|       var solution = recognize(recognizer, '/user/brian'); | ||||
|       expect(getComponentType(solution)).toEqual(DummyCmpA); | ||||
|       expect(solution.params()).toEqual({'name': 'brian'}); | ||||
|       expect(solution.params).toEqual({'name': 'brian'}); | ||||
|     }); | ||||
| 
 | ||||
| 
 | ||||
|     it('should recognize a star segment', () => { | ||||
|       recognizer.config(new Route({path: '/first/*rest', component: DummyCmpA})); | ||||
|       var solution = recognizer.recognize('/first/second/third')[0]; | ||||
|       var solution = recognize(recognizer, '/first/second/third'); | ||||
|       expect(getComponentType(solution)).toEqual(DummyCmpA); | ||||
|       expect(solution.params()).toEqual({'rest': 'second/third'}); | ||||
|       expect(solution.params).toEqual({'rest': 'second/third'}); | ||||
|     }); | ||||
| 
 | ||||
| 
 | ||||
| @ -70,235 +72,105 @@ export function main() { | ||||
| 
 | ||||
| 
 | ||||
|     it('should recognize redirects', () => { | ||||
|       recognizer.config(new Redirect({path: '/a', redirectTo: '/b'})); | ||||
|       recognizer.config(new Route({path: '/b', component: DummyCmpA})); | ||||
|       var solutions = recognizer.recognize('/a'); | ||||
|       expect(solutions.length).toBe(1); | ||||
| 
 | ||||
|       var solution = solutions[0]; | ||||
|       recognizer.config(new Redirect({path: '/a', redirectTo: 'b'})); | ||||
|       var solution = recognize(recognizer, '/a'); | ||||
|       expect(getComponentType(solution)).toEqual(DummyCmpA); | ||||
|       expect(solution.matchedUrl).toEqual('/b'); | ||||
|       expect(solution.urlPath).toEqual('b'); | ||||
|     }); | ||||
| 
 | ||||
| 
 | ||||
|     it('should not perform root URL redirect on a non-root route', () => { | ||||
|       recognizer.config(new Redirect({path: '/', redirectTo: '/foo'})); | ||||
|       recognizer.config(new Route({path: '/bar', component: DummyCmpA})); | ||||
|       var solutions = recognizer.recognize('/bar'); | ||||
|       expect(solutions.length).toBe(1); | ||||
| 
 | ||||
|       var solution = solutions[0]; | ||||
|       expect(getComponentType(solution)).toEqual(DummyCmpA); | ||||
|       expect(solution.matchedUrl).toEqual('/bar'); | ||||
|       var solution = recognize(recognizer, '/bar'); | ||||
|       expect(solution.componentType).toEqual(DummyCmpA); | ||||
|       expect(solution.urlPath).toEqual('bar'); | ||||
|     }); | ||||
| 
 | ||||
|     it('should perform a root URL redirect when only a slash or an empty string is being processed', | ||||
|        () => { | ||||
| 
 | ||||
|     it('should perform a root URL redirect only for root routes', () => { | ||||
|       recognizer.config(new Redirect({path: '/', redirectTo: '/matias'})); | ||||
|       recognizer.config(new Route({path: '/matias', component: DummyCmpA})); | ||||
|       recognizer.config(new Route({path: '/fatias', component: DummyCmpA})); | ||||
| 
 | ||||
|          var solutions; | ||||
|       var solution; | ||||
| 
 | ||||
|          solutions = recognizer.recognize('/'); | ||||
|          expect(solutions[0].matchedUrl).toBe('/matias'); | ||||
|       solution = recognize(recognizer, '/'); | ||||
|       expect(solution.urlPath).toEqual('matias'); | ||||
| 
 | ||||
|          solutions = recognizer.recognize('/fatias'); | ||||
|          expect(solutions[0].matchedUrl).toBe('/fatias'); | ||||
|       solution = recognize(recognizer, '/fatias'); | ||||
|       expect(solution.urlPath).toEqual('fatias'); | ||||
| 
 | ||||
|          solutions = recognizer.recognize(''); | ||||
|          expect(solutions[0].matchedUrl).toBe('/matias'); | ||||
|       solution = recognize(recognizer, ''); | ||||
|       expect(solution.urlPath).toEqual('matias'); | ||||
|     }); | ||||
| 
 | ||||
| 
 | ||||
|     it('should generate URLs with params', () => { | ||||
|       recognizer.config(new Route({path: '/app/user/:name', component: DummyCmpA, as: 'user'})); | ||||
|       expect(recognizer.generate('user', {'name': 'misko'})['url']).toEqual('app/user/misko'); | ||||
|       var instruction = recognizer.generate('user', {'name': 'misko'}); | ||||
|       expect(instruction.urlPath).toEqual('app/user/misko'); | ||||
|     }); | ||||
| 
 | ||||
| 
 | ||||
|     it('should generate URLs with numeric params', () => { | ||||
|       recognizer.config(new Route({path: '/app/page/:number', component: DummyCmpA, as: 'page'})); | ||||
|       expect(recognizer.generate('page', {'number': 42})['url']).toEqual('app/page/42'); | ||||
|       expect(recognizer.generate('page', {'number': 42}).urlPath).toEqual('app/page/42'); | ||||
|     }); | ||||
| 
 | ||||
| 
 | ||||
|     it('should throw in the absence of required params URLs', () => { | ||||
|       recognizer.config(new Route({path: 'app/user/:name', component: DummyCmpA, as: 'user'})); | ||||
|       expect(() => recognizer.generate('user', {})['url']) | ||||
|       expect(() => recognizer.generate('user', {})) | ||||
|           .toThrowError('Route generator for \'name\' was not included in parameters passed.'); | ||||
|     }); | ||||
| 
 | ||||
|     describe('querystring params', () => { | ||||
|       it('should recognize querystring parameters within the URL path', () => { | ||||
|         var recognizer = new RouteRecognizer(true); | ||||
|         recognizer.config(new Route({path: 'profile/:name', component: DummyCmpA, as: 'user'})); | ||||
| 
 | ||||
|         var solution = recognizer.recognize('/profile/matsko?comments=all')[0]; | ||||
|         var params = solution.params(); | ||||
|         expect(params['name']).toEqual('matsko'); | ||||
|         expect(params['comments']).toEqual('all'); | ||||
|     describe('params', () => { | ||||
|       it('should recognize parameters within the URL path', () => { | ||||
|         recognizer.config(new Route({path: 'profile/:name', component: DummyCmpA, as: 'user'})); | ||||
|         var solution = recognize(recognizer, '/profile/matsko?comments=all'); | ||||
|         expect(solution.params).toEqual({'name': 'matsko', 'comments': 'all'}); | ||||
|       }); | ||||
| 
 | ||||
| 
 | ||||
|       it('should generate and populate the given static-based route with querystring params', | ||||
|          () => { | ||||
|            var recognizer = new RouteRecognizer(true); | ||||
|            recognizer.config( | ||||
|                new Route({path: 'forum/featured', component: DummyCmpA, as: 'forum-page'})); | ||||
| 
 | ||||
|            var params = StringMapWrapper.create(); | ||||
|            params['start'] = 10; | ||||
|            params['end'] = 100; | ||||
|            var params = {'start': 10, 'end': 100}; | ||||
| 
 | ||||
|            var result = recognizer.generate('forum-page', params); | ||||
|            expect(result['url']).toEqual('forum/featured?start=10&end=100'); | ||||
|            expect(result.urlPath).toEqual('forum/featured'); | ||||
|            expect(result.urlParams).toEqual(['start=10', 'end=100']); | ||||
|          }); | ||||
| 
 | ||||
|       it('should place a higher priority on actual route params incase the same params are defined in the querystring', | ||||
|          () => { | ||||
|            var recognizer = new RouteRecognizer(true); | ||||
| 
 | ||||
|       it('should prefer positional params over query params', () => { | ||||
|         recognizer.config(new Route({path: 'profile/:name', component: DummyCmpA, as: 'user'})); | ||||
| 
 | ||||
|            var solution = recognizer.recognize('/profile/yegor?name=igor')[0]; | ||||
|            var params = solution.params(); | ||||
|            expect(params['name']).toEqual('yegor'); | ||||
|         var solution = recognize(recognizer, '/profile/yegor?name=igor'); | ||||
|         expect(solution.params).toEqual({'name': 'yegor'}); | ||||
|       }); | ||||
| 
 | ||||
|       it('should strip out any occurences of matrix params when querystring params are allowed', | ||||
|          () => { | ||||
|            var recognizer = new RouteRecognizer(true); | ||||
|            recognizer.config(new Route({path: '/home', component: DummyCmpA, as: 'user'})); | ||||
| 
 | ||||
|            var solution = recognizer.recognize('/home;showAll=true;limit=100?showAll=false')[0]; | ||||
|            var params = solution.params(); | ||||
| 
 | ||||
|            expect(params['showAll']).toEqual('false'); | ||||
|            expect(params['limit']).toBeFalsy(); | ||||
|          }); | ||||
| 
 | ||||
|       it('should strip out any occurences of matrix params as input data', () => { | ||||
|         var recognizer = new RouteRecognizer(true); | ||||
|       it('should ignore matrix params for the top-level component', () => { | ||||
|         recognizer.config(new Route({path: '/home/:subject', component: DummyCmpA, as: 'user'})); | ||||
| 
 | ||||
|         var solution = recognizer.recognize('/home/zero;one=1?two=2')[0]; | ||||
|         var params = solution.params(); | ||||
| 
 | ||||
|         expect(params['subject']).toEqual('zero'); | ||||
|         expect(params['one']).toBeFalsy(); | ||||
|         expect(params['two']).toEqual('2'); | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
|     describe('matrix params', () => { | ||||
|       it('should recognize matrix parameters within the URL path', () => { | ||||
|         var recognizer = new RouteRecognizer(); | ||||
|         recognizer.config(new Route({path: 'profile/:name', component: DummyCmpA, as: 'user'})); | ||||
| 
 | ||||
|         var solution = recognizer.recognize('/profile/matsko;comments=all')[0]; | ||||
|         var params = solution.params(); | ||||
|         expect(params['name']).toEqual('matsko'); | ||||
|         expect(params['comments']).toEqual('all'); | ||||
|       }); | ||||
| 
 | ||||
|       it('should recognize multiple matrix params and set parameters that contain no value to true', | ||||
|          () => { | ||||
|            var recognizer = new RouteRecognizer(); | ||||
|            recognizer.config(new Route({path: '/profile/hello', component: DummyCmpA, as: 'user'})); | ||||
| 
 | ||||
|            var solution = | ||||
|                recognizer.recognize('/profile/hello;modal;showAll=true;hideAll=false')[0]; | ||||
|            var params = solution.params(); | ||||
| 
 | ||||
|            expect(params['modal']).toEqual(true); | ||||
|            expect(params['showAll']).toEqual('true'); | ||||
|            expect(params['hideAll']).toEqual('false'); | ||||
|          }); | ||||
| 
 | ||||
|       it('should only consider the matrix parameters at the end of the path handler', () => { | ||||
|         var recognizer = new RouteRecognizer(); | ||||
|         recognizer.config(new Route({path: '/profile/hi/:name', component: DummyCmpA, as: 'user'})); | ||||
| 
 | ||||
|         var solution = recognizer.recognize('/profile;a=1/hi;b=2;c=3/william;d=4')[0]; | ||||
|         var params = solution.params(); | ||||
| 
 | ||||
|         expect(params).toEqual({'name': 'william', 'd': '4'}); | ||||
|       }); | ||||
| 
 | ||||
|       it('should generate and populate the given static-based route with matrix params', () => { | ||||
|         var recognizer = new RouteRecognizer(); | ||||
|         recognizer.config( | ||||
|             new Route({path: 'forum/featured', component: DummyCmpA, as: 'forum-page'})); | ||||
| 
 | ||||
|         var params = StringMapWrapper.create(); | ||||
|         params['start'] = 10; | ||||
|         params['end'] = 100; | ||||
| 
 | ||||
|         var result = recognizer.generate('forum-page', params); | ||||
|         expect(result['url']).toEqual('forum/featured;start=10;end=100'); | ||||
|       }); | ||||
| 
 | ||||
|       it('should generate and populate the given dynamic-based route with matrix params', () => { | ||||
|         var recognizer = new RouteRecognizer(); | ||||
|         recognizer.config( | ||||
|             new Route({path: 'forum/:topic', component: DummyCmpA, as: 'forum-page'})); | ||||
| 
 | ||||
|         var params = StringMapWrapper.create(); | ||||
|         params['topic'] = 'crazy'; | ||||
|         params['total-posts'] = 100; | ||||
|         params['moreDetail'] = null; | ||||
| 
 | ||||
|         var result = recognizer.generate('forum-page', params); | ||||
|         expect(result['url']).toEqual('forum/crazy;total-posts=100;moreDetail'); | ||||
|       }); | ||||
| 
 | ||||
|       it('should not apply any matrix params if a dynamic route segment takes up the slot when a path is generated', | ||||
|          () => { | ||||
|            var recognizer = new RouteRecognizer(); | ||||
|            recognizer.config( | ||||
|                new Route({path: 'hello/:name', component: DummyCmpA, as: 'profile-page'})); | ||||
| 
 | ||||
|            var params = StringMapWrapper.create(); | ||||
|            params['name'] = 'matsko'; | ||||
| 
 | ||||
|            var result = recognizer.generate('profile-page', params); | ||||
|            expect(result['url']).toEqual('hello/matsko'); | ||||
|          }); | ||||
| 
 | ||||
|       it('should place a higher priority on actual route params incase the same params are defined in the matrix params string', | ||||
|          () => { | ||||
|            var recognizer = new RouteRecognizer(); | ||||
|            recognizer.config(new Route({path: 'profile/:name', component: DummyCmpA, as: 'user'})); | ||||
| 
 | ||||
|            var solution = recognizer.recognize('/profile/yegor;name=igor')[0]; | ||||
|            var params = solution.params(); | ||||
|            expect(params['name']).toEqual('yegor'); | ||||
|          }); | ||||
| 
 | ||||
|       it('should strip out any occurences of querystring params when matrix params are allowed', | ||||
|          () => { | ||||
|            var recognizer = new RouteRecognizer(); | ||||
|            recognizer.config(new Route({path: '/home', component: DummyCmpA, as: 'user'})); | ||||
| 
 | ||||
|            var solution = recognizer.recognize('/home;limit=100?limit=1000&showAll=true')[0]; | ||||
|            var params = solution.params(); | ||||
| 
 | ||||
|            expect(params['showAll']).toBeFalsy(); | ||||
|            expect(params['limit']).toEqual('100'); | ||||
|          }); | ||||
| 
 | ||||
|       it('should strip out any occurences of matrix params as input data', () => { | ||||
|         var recognizer = new RouteRecognizer(); | ||||
|         recognizer.config(new Route({path: '/home/:subject', component: DummyCmpA, as: 'user'})); | ||||
| 
 | ||||
|         var solution = recognizer.recognize('/home/zero;one=1?two=2')[0]; | ||||
|         var params = solution.params(); | ||||
| 
 | ||||
|         expect(params['subject']).toEqual('zero'); | ||||
|         expect(params['one']).toEqual('1'); | ||||
|         expect(params['two']).toBeFalsy(); | ||||
|         var solution = recognize(recognizer, '/home;sort=asc/zero;one=1?two=2'); | ||||
|         expect(solution.params).toEqual({'subject': 'zero', 'two': '2'}); | ||||
|       }); | ||||
|     }); | ||||
|   }); | ||||
| } | ||||
| 
 | ||||
| function getComponentType(routeMatch: RouteMatch): any { | ||||
|   return routeMatch.recognizer.handler.componentType; | ||||
| function recognize(recognizer: RouteRecognizer, url: string): ComponentInstruction { | ||||
|   return recognizer.recognize(parser.parse(url))[0].instruction; | ||||
| } | ||||
| 
 | ||||
| function getComponentType(routeMatch: ComponentInstruction): any { | ||||
|   return routeMatch.componentType; | ||||
| } | ||||
| 
 | ||||
| class DummyCmpA {} | ||||
|  | ||||
| @ -14,6 +14,7 @@ import {Promise, PromiseWrapper} from 'angular2/src/facade/async'; | ||||
| 
 | ||||
| import {RouteRegistry} from 'angular2/src/router/route_registry'; | ||||
| import {RouteConfig, Route, AsyncRoute} from 'angular2/src/router/route_config_decorator'; | ||||
| import {stringifyInstruction} from 'angular2/src/router/instruction'; | ||||
| 
 | ||||
| export function main() { | ||||
|   describe('RouteRegistry', () => { | ||||
| @ -27,7 +28,7 @@ export function main() { | ||||
| 
 | ||||
|          registry.recognize('/test', RootHostCmp) | ||||
|              .then((instruction) => { | ||||
|                expect(instruction.component).toBe(DummyCmpB); | ||||
|                expect(instruction.component.componentType).toBe(DummyCmpB); | ||||
|                async.done(); | ||||
|              }); | ||||
|        })); | ||||
| @ -36,8 +37,10 @@ export function main() { | ||||
|       registry.config(RootHostCmp, | ||||
|                       new Route({path: '/first/...', component: DummyParentCmp, as: 'firstCmp'})); | ||||
| 
 | ||||
|       expect(registry.generate(['firstCmp', 'secondCmp'], RootHostCmp)).toEqual('first/second'); | ||||
|       expect(registry.generate(['secondCmp'], DummyParentCmp)).toEqual('second'); | ||||
|       expect(stringifyInstruction(registry.generate(['firstCmp', 'secondCmp'], RootHostCmp))) | ||||
|           .toEqual('first/second'); | ||||
|       expect(stringifyInstruction(registry.generate(['secondCmp'], DummyParentCmp))) | ||||
|           .toEqual('second'); | ||||
|     }); | ||||
| 
 | ||||
|     it('should generate URLs with params', () => { | ||||
| @ -45,8 +48,8 @@ export function main() { | ||||
|           RootHostCmp, | ||||
|           new Route({path: '/first/:param/...', component: DummyParentParamCmp, as: 'firstCmp'})); | ||||
| 
 | ||||
|       var url = | ||||
|           registry.generate(['firstCmp', {param: 'one'}, 'secondCmp', {param: 'two'}], RootHostCmp); | ||||
|       var url = stringifyInstruction(registry.generate( | ||||
|           ['firstCmp', {param: 'one'}, 'secondCmp', {param: 'two'}], RootHostCmp)); | ||||
|       expect(url).toEqual('first/one/second/two'); | ||||
|     }); | ||||
| 
 | ||||
| @ -61,7 +64,8 @@ export function main() { | ||||
| 
 | ||||
|          registry.recognize('/first/second', RootHostCmp) | ||||
|              .then((_) => { | ||||
|                expect(registry.generate(['firstCmp', 'secondCmp'], RootHostCmp)) | ||||
|                expect( | ||||
|                    stringifyInstruction(registry.generate(['firstCmp', 'secondCmp'], RootHostCmp))) | ||||
|                    .toEqual('first/second'); | ||||
|                async.done(); | ||||
|              }); | ||||
| @ -73,14 +77,13 @@ export function main() { | ||||
|           .toThrowError('Component "RootHostCmp" has no route config.'); | ||||
|     }); | ||||
| 
 | ||||
| 
 | ||||
|     it('should prefer static segments to dynamic', inject([AsyncTestCompleter], (async) => { | ||||
|          registry.config(RootHostCmp, new Route({path: '/:site', component: DummyCmpB})); | ||||
|          registry.config(RootHostCmp, new Route({path: '/home', component: DummyCmpA})); | ||||
| 
 | ||||
|          registry.recognize('/home', RootHostCmp) | ||||
|              .then((instruction) => { | ||||
|                expect(instruction.component).toBe(DummyCmpA); | ||||
|                expect(instruction.component.componentType).toBe(DummyCmpA); | ||||
|                async.done(); | ||||
|              }); | ||||
|        })); | ||||
| @ -91,7 +94,7 @@ export function main() { | ||||
| 
 | ||||
|          registry.recognize('/home', RootHostCmp) | ||||
|              .then((instruction) => { | ||||
|                expect(instruction.component).toBe(DummyCmpA); | ||||
|                expect(instruction.component.componentType).toBe(DummyCmpA); | ||||
|                async.done(); | ||||
|              }); | ||||
|        })); | ||||
| @ -102,7 +105,7 @@ export function main() { | ||||
| 
 | ||||
|          registry.recognize('/some/path', RootHostCmp) | ||||
|              .then((instruction) => { | ||||
|                expect(instruction.component).toBe(DummyCmpA); | ||||
|                expect(instruction.component.componentType).toBe(DummyCmpA); | ||||
|                async.done(); | ||||
|              }); | ||||
|        })); | ||||
| @ -113,7 +116,7 @@ export function main() { | ||||
| 
 | ||||
|          registry.recognize('/first/second', RootHostCmp) | ||||
|              .then((instruction) => { | ||||
|                expect(instruction.component).toBe(DummyCmpA); | ||||
|                expect(instruction.component.componentType).toBe(DummyCmpA); | ||||
|                async.done(); | ||||
|              }); | ||||
|        })); | ||||
| @ -127,7 +130,7 @@ export function main() { | ||||
| 
 | ||||
|          registry.recognize('/first/second/third', RootHostCmp) | ||||
|              .then((instruction) => { | ||||
|                expect(instruction.component).toBe(DummyCmpB); | ||||
|                expect(instruction.component.componentType).toBe(DummyCmpB); | ||||
|                async.done(); | ||||
|              }); | ||||
|        })); | ||||
| @ -137,8 +140,8 @@ export function main() { | ||||
| 
 | ||||
|          registry.recognize('/first/second', RootHostCmp) | ||||
|              .then((instruction) => { | ||||
|                expect(instruction.component).toBe(DummyParentCmp); | ||||
|                expect(instruction.child.component).toBe(DummyCmpB); | ||||
|                expect(instruction.component.componentType).toBe(DummyParentCmp); | ||||
|                expect(instruction.child.component.componentType).toBe(DummyCmpB); | ||||
|                async.done(); | ||||
|              }); | ||||
|        })); | ||||
| @ -149,8 +152,8 @@ export function main() { | ||||
| 
 | ||||
|          registry.recognize('/first/second', RootHostCmp) | ||||
|              .then((instruction) => { | ||||
|                expect(instruction.component).toBe(DummyAsyncCmp); | ||||
|                expect(instruction.child.component).toBe(DummyCmpB); | ||||
|                expect(instruction.component.componentType).toBe(DummyAsyncCmp); | ||||
|                expect(instruction.child.component.componentType).toBe(DummyCmpB); | ||||
|                async.done(); | ||||
|              }); | ||||
|        })); | ||||
| @ -162,28 +165,12 @@ export function main() { | ||||
| 
 | ||||
|          registry.recognize('/first/second', RootHostCmp) | ||||
|              .then((instruction) => { | ||||
|                expect(instruction.component).toBe(DummyParentCmp); | ||||
|                expect(instruction.child.component).toBe(DummyCmpB); | ||||
|                expect(instruction.component.componentType).toBe(DummyParentCmp); | ||||
|                expect(instruction.child.component.componentType).toBe(DummyCmpB); | ||||
|                async.done(); | ||||
|              }); | ||||
|        })); | ||||
| 
 | ||||
|     // TODO: not sure what to do with these tests
 | ||||
|     // 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\'');
 | ||||
|     //});
 | ||||
| 
 | ||||
|     it('should throw when a parent config is missing the `...` suffix any of its children add routes', | ||||
|        () => { | ||||
|          expect(() => | ||||
| @ -198,6 +185,40 @@ export function main() { | ||||
|           .toThrowError('Unexpected "..." before the end of the path for "home/.../fun/".'); | ||||
|     }); | ||||
| 
 | ||||
|     it('should match matrix params on child components and query params on the root component', | ||||
|        inject([AsyncTestCompleter], (async) => { | ||||
|          registry.config(RootHostCmp, new Route({path: '/first/...', component: DummyParentCmp})); | ||||
| 
 | ||||
|          registry.recognize('/first/second;filter=odd?comments=all', RootHostCmp) | ||||
|              .then((instruction) => { | ||||
|                expect(instruction.component.componentType).toBe(DummyParentCmp); | ||||
|                expect(instruction.component.params).toEqual({'comments': 'all'}); | ||||
| 
 | ||||
|                expect(instruction.child.component.componentType).toBe(DummyCmpB); | ||||
|                expect(instruction.child.component.params).toEqual({'filter': 'odd'}); | ||||
|                async.done(); | ||||
|              }); | ||||
|        })); | ||||
| 
 | ||||
|     it('should generate URLs with matrix and query params', () => { | ||||
|       registry.config( | ||||
|           RootHostCmp, | ||||
|           new Route({path: '/first/:param/...', component: DummyParentParamCmp, as: 'firstCmp'})); | ||||
| 
 | ||||
|       var url = stringifyInstruction(registry.generate( | ||||
|           [ | ||||
|             'firstCmp', | ||||
|             {param: 'one', query: 'cats'}, | ||||
|             'secondCmp', | ||||
|             { | ||||
|               param: 'two', | ||||
|               sort: 'asc', | ||||
|             } | ||||
|           ], | ||||
|           RootHostCmp)); | ||||
|       expect(url).toEqual('first/one/second/two;sort=asc?query=cats'); | ||||
|     }); | ||||
| 
 | ||||
|   }); | ||||
| } | ||||
| 
 | ||||
|  | ||||
| @ -23,14 +23,13 @@ import {IMPLEMENTS} from 'angular2/src/facade/lang'; | ||||
| import {bind, Component, View} from 'angular2/angular2'; | ||||
| 
 | ||||
| import {Location, Router, RouterLink} from 'angular2/router'; | ||||
| import {Instruction, ComponentInstruction} from 'angular2/src/router/instruction'; | ||||
| 
 | ||||
| import { | ||||
|   DOM | ||||
| } from 'angular2/src/dom/dom_adapter' | ||||
| import {DOM} from 'angular2/src/dom/dom_adapter'; | ||||
| 
 | ||||
| var dummyInstruction = new Instruction(new ComponentInstruction('detail', [], null), null, {}); | ||||
| 
 | ||||
|     export function | ||||
|     main() { | ||||
| export function main() { | ||||
|   describe('router-link directive', function() { | ||||
| 
 | ||||
|     beforeEachBindings( | ||||
| @ -59,7 +58,7 @@ import { | ||||
|                testComponent.detectChanges(); | ||||
|                // TODO: shouldn't this be just 'click' rather than '^click'?
 | ||||
|                testComponent.query(By.css('a')).triggerEventHandler('^click', {}); | ||||
|                expect(router.spy("navigate")).toHaveBeenCalledWith('/detail'); | ||||
|                expect(router.spy('navigateInstruction')).toHaveBeenCalledWith(dummyInstruction); | ||||
|                async.done(); | ||||
|              }); | ||||
|        })); | ||||
| @ -100,7 +99,7 @@ class DummyRouter extends SpyObject { | ||||
| 
 | ||||
| function makeDummyRouter() { | ||||
|   var dr = new DummyRouter(); | ||||
|   dr.spy('generate').andCallFake((routeParams) => routeParams.join('=')); | ||||
|   dr.spy('navigate'); | ||||
|   dr.spy('generate').andCallFake((routeParams) => dummyInstruction); | ||||
|   dr.spy('navigateInstruction'); | ||||
|   return dr; | ||||
| } | ||||
|  | ||||
| @ -20,6 +20,7 @@ import {Pipeline} from 'angular2/src/router/pipeline'; | ||||
| import {RouterOutlet} from 'angular2/src/router/router_outlet'; | ||||
| import {SpyLocation} from 'angular2/src/mock/location_mock'; | ||||
| import {Location} from 'angular2/src/router/location'; | ||||
| import {stringifyInstruction} from 'angular2/src/router/instruction'; | ||||
| 
 | ||||
| import {RouteRegistry} from 'angular2/src/router/route_registry'; | ||||
| import {RouteConfig, Route} from 'angular2/src/router/route_config_decorator'; | ||||
| @ -125,51 +126,53 @@ export function main() { | ||||
|     it('should generate URLs from the root component when the path starts with /', () => { | ||||
|       router.config([new Route({path: '/first/...', component: DummyParentComp, as: 'firstCmp'})]); | ||||
| 
 | ||||
|       expect(router.generate(['/firstCmp', 'secondCmp'])).toEqual('/first/second'); | ||||
|       expect(router.generate(['/firstCmp', 'secondCmp'])).toEqual('/first/second'); | ||||
|       expect(router.generate(['/firstCmp/secondCmp'])).toEqual('/first/second'); | ||||
|       var instruction = router.generate(['/firstCmp', 'secondCmp']); | ||||
|       expect(stringifyInstruction(instruction)).toEqual('first/second'); | ||||
| 
 | ||||
|       instruction = router.generate(['/firstCmp/secondCmp']); | ||||
|       expect(stringifyInstruction(instruction)).toEqual('first/second'); | ||||
|     }); | ||||
| 
 | ||||
|     describe('querstring params', () => { | ||||
|       it('should only apply querystring params if the given URL is on the root router and is terminal', | ||||
|          () => { | ||||
|            router.config([ | ||||
|              new Route({path: '/hi/how/are/you', component: DummyComponent, as: 'greeting-url'}) | ||||
|            ]); | ||||
|     describe('query string params', () => { | ||||
|       it('should use query string params for the root route', () => { | ||||
|         router.config( | ||||
|             [new Route({path: '/hi/how/are/you', component: DummyComponent, as: 'greeting-url'})]); | ||||
| 
 | ||||
|            var path = router.generate(['/greeting-url', {'name': 'brad'}]); | ||||
|            expect(path).toEqual('/hi/how/are/you?name=brad'); | ||||
|         var instruction = router.generate(['/greeting-url', {'name': 'brad'}]); | ||||
|         var path = stringifyInstruction(instruction); | ||||
|         expect(path).toEqual('hi/how/are/you?name=brad'); | ||||
|       }); | ||||
| 
 | ||||
|       it('should use parameters that are not apart of the route definition as querystring params', | ||||
|       it('should serialize parameters that are not part of the route definition as query string params', | ||||
|          () => { | ||||
|            router.config( | ||||
|                [new Route({path: '/one/two/:three', component: DummyComponent, as: 'number-url'})]); | ||||
| 
 | ||||
|            var path = router.generate(['/number-url', {'three': 'three', 'four': 'four'}]); | ||||
|            expect(path).toEqual('/one/two/three?four=four'); | ||||
|            var instruction = router.generate(['/number-url', {'three': 'three', 'four': 'four'}]); | ||||
|            var path = stringifyInstruction(instruction); | ||||
|            expect(path).toEqual('one/two/three?four=four'); | ||||
|          }); | ||||
|     }); | ||||
| 
 | ||||
|     describe('matrix params', () => { | ||||
|       it('should apply inline matrix params for each router path within the generated URL', () => { | ||||
|       it('should generate matrix params for each non-root component', () => { | ||||
|         router.config( | ||||
|             [new Route({path: '/first/...', component: DummyParentComp, as: 'firstCmp'})]); | ||||
| 
 | ||||
|         var path = | ||||
|         var instruction = | ||||
|             router.generate(['/firstCmp', {'key': 'value'}, 'secondCmp', {'project': 'angular'}]); | ||||
|         expect(path).toEqual('/first;key=value/second;project=angular'); | ||||
|         var path = stringifyInstruction(instruction); | ||||
|         expect(path).toEqual('first/second;project=angular?key=value'); | ||||
|       }); | ||||
| 
 | ||||
|       it('should apply inline matrix params for each router path within the generated URL and also include named params', | ||||
|          () => { | ||||
|            router.config([ | ||||
|              new Route({path: '/first/:token/...', component: DummyParentComp, as: 'firstCmp'}) | ||||
|            ]); | ||||
|       it('should work with named params', () => { | ||||
|         router.config( | ||||
|             [new Route({path: '/first/:token/...', component: DummyParentComp, as: 'firstCmp'})]); | ||||
| 
 | ||||
|            var path = | ||||
|         var instruction = | ||||
|             router.generate(['/firstCmp', {'token': 'min'}, 'secondCmp', {'author': 'max'}]); | ||||
|            expect(path).toEqual('/first/min/second;author=max'); | ||||
|         var path = stringifyInstruction(instruction); | ||||
|         expect(path).toEqual('first/min/second;author=max'); | ||||
|       }); | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
							
								
								
									
										118
									
								
								modules/angular2/test/router/url_parser_spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										118
									
								
								modules/angular2/test/router/url_parser_spec.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,118 @@ | ||||
| import { | ||||
|   AsyncTestCompleter, | ||||
|   describe, | ||||
|   it, | ||||
|   iit, | ||||
|   ddescribe, | ||||
|   expect, | ||||
|   inject, | ||||
|   beforeEach, | ||||
|   SpyObject | ||||
| } from 'angular2/test_lib'; | ||||
| 
 | ||||
| import {UrlParser, Url} from 'angular2/src/router/url_parser'; | ||||
| 
 | ||||
| 
 | ||||
| export function main() { | ||||
|   describe('ParsedUrl', () => { | ||||
|     var urlParser; | ||||
| 
 | ||||
|     beforeEach(() => { urlParser = new UrlParser(); }); | ||||
| 
 | ||||
|     it('should work in a simple case', () => { | ||||
|       var url = urlParser.parse('hello/there'); | ||||
|       expect(url.toString()).toEqual('hello/there'); | ||||
|     }); | ||||
| 
 | ||||
|     it('should remove the leading slash', () => { | ||||
|       var url = urlParser.parse('/hello/there'); | ||||
|       expect(url.toString()).toEqual('hello/there'); | ||||
|     }); | ||||
| 
 | ||||
|     it('should work with a single aux route', () => { | ||||
|       var url = urlParser.parse('hello/there(a)'); | ||||
|       expect(url.toString()).toEqual('hello/there(a)'); | ||||
|     }); | ||||
| 
 | ||||
|     it('should work with multiple aux routes', () => { | ||||
|       var url = urlParser.parse('hello/there(a//b)'); | ||||
|       expect(url.toString()).toEqual('hello/there(a//b)'); | ||||
|     }); | ||||
| 
 | ||||
|     it('should work with children after an aux route', () => { | ||||
|       var url = urlParser.parse('hello/there(a//b)/c/d'); | ||||
|       expect(url.toString()).toEqual('hello/there(a//b)/c/d'); | ||||
|     }); | ||||
| 
 | ||||
|     it('should work when aux routes have children', () => { | ||||
|       var url = urlParser.parse('hello(aa/bb//bb/cc)'); | ||||
|       expect(url.toString()).toEqual('hello(aa/bb//bb/cc)'); | ||||
|     }); | ||||
| 
 | ||||
|     it('should parse an aux route with an aux route', () => { | ||||
|       var url = urlParser.parse('hello(aa(bb))'); | ||||
|       expect(url.toString()).toEqual('hello(aa(bb))'); | ||||
|     }); | ||||
| 
 | ||||
|     it('should simplify an empty aux route definition', () => { | ||||
|       var url = urlParser.parse('hello()/there'); | ||||
|       expect(url.toString()).toEqual('hello/there'); | ||||
|     }); | ||||
| 
 | ||||
|     it('should parse a key-value matrix param', () => { | ||||
|       var url = urlParser.parse('hello/friend;name=bob'); | ||||
|       expect(url.toString()).toEqual('hello/friend;name=bob'); | ||||
|     }); | ||||
| 
 | ||||
|     it('should parse multiple key-value matrix params', () => { | ||||
|       var url = urlParser.parse('hello/there;greeting=hi;whats=up'); | ||||
|       expect(url.toString()).toEqual('hello/there;greeting=hi;whats=up'); | ||||
|     }); | ||||
| 
 | ||||
|     it('should ignore matrix params on the first segment', () => { | ||||
|       var url = urlParser.parse('profile;a=1/hi'); | ||||
|       expect(url.toString()).toEqual('profile/hi'); | ||||
|     }); | ||||
| 
 | ||||
|     it('should parse a key-only matrix param', () => { | ||||
|       var url = urlParser.parse('hello/there;hi'); | ||||
|       expect(url.toString()).toEqual('hello/there;hi'); | ||||
|     }); | ||||
| 
 | ||||
|     it('should parse a key-value query param', () => { | ||||
|       var url = urlParser.parse('hello/friend?name=bob'); | ||||
|       expect(url.toString()).toEqual('hello/friend?name=bob'); | ||||
|     }); | ||||
| 
 | ||||
|     it('should parse multiple key-value query params', () => { | ||||
|       var url = urlParser.parse('hello/there?greeting=hi&whats=up'); | ||||
|       expect(url.params).toEqual({'greeting': 'hi', 'whats': 'up'}); | ||||
|       expect(url.toString()).toEqual('hello/there?greeting=hi&whats=up'); | ||||
|     }); | ||||
| 
 | ||||
|     it('should parse a key-only matrix param', () => { | ||||
|       var url = urlParser.parse('hello/there?hi'); | ||||
|       expect(url.toString()).toEqual('hello/there?hi'); | ||||
|     }); | ||||
| 
 | ||||
|     it('should parse a route with matrix and query params', () => { | ||||
|       var url = urlParser.parse('hello/there;sort=asc;unfiltered?hi&friend=true'); | ||||
|       expect(url.toString()).toEqual('hello/there;sort=asc;unfiltered?hi&friend=true'); | ||||
|     }); | ||||
| 
 | ||||
|     it('should parse a route with matrix params and aux routes', () => { | ||||
|       var url = urlParser.parse('hello/there;sort=asc(modal)'); | ||||
|       expect(url.toString()).toEqual('hello/there;sort=asc(modal)'); | ||||
|     }); | ||||
| 
 | ||||
|     it('should parse an aux route with matrix params', () => { | ||||
|       var url = urlParser.parse('hello/there(modal;sort=asc)'); | ||||
|       expect(url.toString()).toEqual('hello/there(modal;sort=asc)'); | ||||
|     }); | ||||
| 
 | ||||
|     it('should parse a route with matrix params, aux routes, and query params', () => { | ||||
|       var url = urlParser.parse('hello/there;sort=asc(modal)?friend=true'); | ||||
|       expect(url.toString()).toEqual('hello/there;sort=asc(modal)?friend=true'); | ||||
|     }); | ||||
|   }); | ||||
| } | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user