parent
							
								
									96e34c1d36
								
							
						
					
					
						commit
						ac6227e434
					
				| @ -19,7 +19,8 @@ export * from './src/router/route_config_decorator'; | |||||||
| export * from './src/router/route_definition'; | export * from './src/router/route_definition'; | ||||||
| export {OnActivate, OnDeactivate, OnReuse, CanDeactivate, CanReuse} from './src/router/interfaces'; | export {OnActivate, OnDeactivate, OnReuse, CanDeactivate, CanReuse} from './src/router/interfaces'; | ||||||
| export {CanActivate} from './src/router/lifecycle_annotations'; | 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 {LocationStrategy} from './src/router/location_strategy'; | ||||||
| import {HTML5LocationStrategy} from './src/router/html5_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, |   List, | ||||||
|   ListWrapper |   ListWrapper | ||||||
| } from 'angular2/src/facade/collection'; | } 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 {PathRecognizer} from './path_recognizer'; | ||||||
|  | import {Url} from './url_parser'; | ||||||
| 
 | 
 | ||||||
| export class RouteParams { | export class RouteParams { | ||||||
|   constructor(public params: StringMap<string, string>) {} |   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 { | export class Instruction { | ||||||
|   // "capturedUrl" is the part of the URL captured by this instruction
 |   constructor(public component: ComponentInstruction, public child: Instruction, | ||||||
|   // "accumulatedUrl" is the part of the URL captured by this instruction and all children
 |               public auxInstruction: StringMap<string, Instruction>) {} | ||||||
|   accumulatedUrl: string; |  | ||||||
|   reuse: boolean = false; |  | ||||||
|   specificity: number; |  | ||||||
| 
 | 
 | ||||||
|   constructor(public component: any, public capturedUrl: string, |   replaceChild(child: Instruction): Instruction { | ||||||
|               private _recognizer: PathRecognizer, public child: Instruction = null, |     return new Instruction(this.component, child, this.auxInstruction); | ||||||
|               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; |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   params(): StringMap<string, string> { |  | ||||||
|     if (isBlank(this._params)) { |  | ||||||
|       this._params = this._recognizer.parseParams(this.capturedUrl); |  | ||||||
|     } |  | ||||||
|     return this._params; |  | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * 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; | ||||||
|  | 
 | ||||||
|  |   constructor(public urlPath: string, public urlParams: List<string>, | ||||||
|  |               private _recognizer: PathRecognizer, public params: StringMap<string, any> = null) {} | ||||||
|  | 
 | ||||||
|  |   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'; | import {global} from 'angular2/src/facade/lang'; | ||||||
| 
 | 
 | ||||||
| // This is here only so that after TS transpilation the file is not empty.
 | // 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] |  * Defines route lifecycle method [onActivate] | ||||||
|  */ |  */ | ||||||
| export interface OnActivate { | export interface OnActivate { | ||||||
|   onActivate(nextInstruction: Instruction, prevInstruction: Instruction): any; |   onActivate(nextInstruction: ComponentInstruction, prevInstruction: ComponentInstruction): any; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Defines route lifecycle method [onReuse] |  * Defines route lifecycle method [onReuse] | ||||||
|  */ |  */ | ||||||
| export interface OnReuse { | export interface OnReuse { | ||||||
|   onReuse(nextInstruction: Instruction, prevInstruction: Instruction): any; |   onReuse(nextInstruction: ComponentInstruction, prevInstruction: ComponentInstruction): any; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Defines route lifecycle method [onDeactivate] |  * Defines route lifecycle method [onDeactivate] | ||||||
|  */ |  */ | ||||||
| export interface OnDeactivate { | export interface OnDeactivate { | ||||||
|   onDeactivate(nextInstruction: Instruction, prevInstruction: Instruction): any; |   onDeactivate(nextInstruction: ComponentInstruction, prevInstruction: ComponentInstruction): any; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Defines route lifecycle method [canReuse] |  * Defines route lifecycle method [canReuse] | ||||||
|  */ |  */ | ||||||
| export interface CanReuse { | export interface CanReuse { | ||||||
|   canReuse(nextInstruction: Instruction, prevInstruction: Instruction): any; |   canReuse(nextInstruction: ComponentInstruction, prevInstruction: ComponentInstruction): any; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Defines route lifecycle method [canDeactivate] |  * Defines route lifecycle method [canDeactivate] | ||||||
|  */ |  */ | ||||||
| export interface 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 {makeDecorator} from 'angular2/src/util/decorators'; | ||||||
| import {CanActivate as CanActivateAnnotation} from './lifecycle_annotations_impl'; | import {CanActivate as CanActivateAnnotation} from './lifecycle_annotations_impl'; | ||||||
| import {Promise} from 'angular2/src/facade/async'; | import {Promise} from 'angular2/src/facade/async'; | ||||||
| import {Instruction} from 'angular2/src/router/instruction'; | import {ComponentInstruction} from 'angular2/src/router/instruction'; | ||||||
| 
 | 
 | ||||||
| export { | export { | ||||||
|   canReuse, |   canReuse, | ||||||
| @ -17,5 +17,5 @@ export { | |||||||
| } from './lifecycle_annotations_impl'; | } from './lifecycle_annotations_impl'; | ||||||
| 
 | 
 | ||||||
| export var CanActivate: | export var CanActivate: | ||||||
|     (hook: (next: Instruction, prev: Instruction) => Promise<boolean>| boolean) => ClassDecorator = |     (hook: (next: ComponentInstruction, prev: ComponentInstruction) => Promise<boolean>| boolean) => ClassDecorator = | ||||||
|         makeDecorator(CanActivateAnnotation); |         makeDecorator(CanActivateAnnotation); | ||||||
|  | |||||||
| @ -7,7 +7,6 @@ import { | |||||||
|   isBlank, |   isBlank, | ||||||
|   BaseException |   BaseException | ||||||
| } from 'angular2/src/facade/lang'; | } from 'angular2/src/facade/lang'; | ||||||
| import {Promise, PromiseWrapper} from 'angular2/src/facade/async'; |  | ||||||
| import { | import { | ||||||
|   Map, |   Map, | ||||||
|   MapWrapper, |   MapWrapper, | ||||||
| @ -17,21 +16,14 @@ import { | |||||||
|   ListWrapper |   ListWrapper | ||||||
| } from 'angular2/src/facade/collection'; | } from 'angular2/src/facade/collection'; | ||||||
| import {IMPLEMENTS} from 'angular2/src/facade/lang'; | import {IMPLEMENTS} from 'angular2/src/facade/lang'; | ||||||
| import {parseAndAssignParamString} from 'angular2/src/router/helpers'; | 
 | ||||||
| import {escapeRegex} from './url'; |  | ||||||
| import {RouteHandler} from './route_handler'; | 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:
 | export class TouchMap { | ||||||
| // https://github.com/angular/ts2dart/issues/173
 |   map: StringMap<string, string> = {}; | ||||||
| export class Segment { |   keys: StringMap<string, boolean> = {}; | ||||||
|   name: string; |  | ||||||
|   regex: string; |  | ||||||
|   generate(params: TouchMap): string { return ''; } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| class TouchMap { |  | ||||||
|   map: StringMap<string, string> = StringMapWrapper.create(); |  | ||||||
|   keys: StringMap<string, boolean> = StringMapWrapper.create(); |  | ||||||
| 
 | 
 | ||||||
|   constructor(map: StringMap<string, any>) { |   constructor(map: StringMap<string, any>) { | ||||||
|     if (isPresent(map)) { |     if (isPresent(map)) { | ||||||
| @ -63,31 +55,28 @@ function normalizeString(obj: any): string { | |||||||
|   } |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| class ContinuationSegment extends Segment {} | export interface Segment { | ||||||
| 
 |   name: string; | ||||||
| class StaticSegment extends Segment { |   generate(params: TouchMap): string; | ||||||
|   regex: string; |   match(path: string): boolean; | ||||||
|   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 this.string; } |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @IMPLEMENTS(Segment) | class ContinuationSegment implements Segment { | ||||||
| class DynamicSegment { |   name: string = ''; | ||||||
|   regex: string = "([^/]+)"; |   generate(params: TouchMap): string { return ''; } | ||||||
|  |   match(path: string): boolean { return true; } | ||||||
|  | } | ||||||
| 
 | 
 | ||||||
|  | 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; } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | class DynamicSegment implements Segment { | ||||||
|   constructor(public name: string) {} |   constructor(public name: string) {} | ||||||
| 
 |   match(path: string): boolean { return true; } | ||||||
|   generate(params: TouchMap): string { |   generate(params: TouchMap): string { | ||||||
|     if (!StringMapWrapper.contains(params.map, this.name)) { |     if (!StringMapWrapper.contains(params.map, this.name)) { | ||||||
|       throw new BaseException( |       throw new BaseException( | ||||||
| @ -98,11 +87,9 @@ class DynamicSegment { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class StarSegment { | class StarSegment implements Segment { | ||||||
|   regex: string = "(.+)"; |  | ||||||
| 
 |  | ||||||
|   constructor(public name: string) {} |   constructor(public name: string) {} | ||||||
| 
 |   match(path: string): boolean { return true; } | ||||||
|   generate(params: TouchMap): string { return normalizeString(params.get(this.name)); } |   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}".`); |         throw new BaseException(`Unexpected "..." before the end of the path for "${route}".`); | ||||||
|       } |       } | ||||||
|       results.push(new ContinuationSegment()); |       results.push(new ContinuationSegment()); | ||||||
|     } else if (segment.length > 0) { |     } else { | ||||||
|       results.push(new StaticSegment(segment)); |       results.push(new StaticSegment(segment)); | ||||||
|       specificity += 100 * (100 - i); |       specificity += 100 * (100 - i); | ||||||
|     } |     } | ||||||
| @ -161,6 +148,23 @@ function parsePathString(route: string): StringMap<string, any> { | |||||||
|   return result; |   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> { | function splitBySlash(url: string): List<string> { | ||||||
|   return url.split('/'); |   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'
 | // represents something like '/foo/:bar'
 | ||||||
| export class PathRecognizer { | export class PathRecognizer { | ||||||
|   segments: List<Segment>; |   private _segments: List<Segment>; | ||||||
|   regex: RegExp; |  | ||||||
|   specificity: number; |   specificity: number; | ||||||
|   terminal: boolean = true; |   terminal: boolean = true; | ||||||
|  |   hash: string; | ||||||
| 
 | 
 | ||||||
|   static matrixRegex: RegExp = RegExpWrapper.create('^(.*\/[^\/]+?)(;[^\/]+)?\/?$'); |   // TODO: cache component instruction instances by params and by ParsedUrl instance
 | ||||||
|   static queryRegex: RegExp = RegExpWrapper.create('^(.*\/[^\/]+?)(\\?[^\/]+)?$'); |  | ||||||
| 
 | 
 | ||||||
|   constructor(public path: string, public handler: RouteHandler, public isRoot: boolean = false) { |   constructor(public path: string, public handler: RouteHandler) { | ||||||
|     assertPath(path); |     assertPath(path); | ||||||
|     var parsed = parsePathString(path); |     var parsed = parsePathString(path); | ||||||
|     var specificity = parsed['specificity']; |  | ||||||
|     var segments = parsed['segments']; |  | ||||||
|     var regexString = '^'; |  | ||||||
| 
 | 
 | ||||||
|     ListWrapper.forEach(segments, (segment) => { |     this._segments = parsed['segments']; | ||||||
|       if (segment instanceof ContinuationSegment) { |     this.specificity = parsed['specificity']; | ||||||
|         this.terminal = false; |     this.hash = pathDslHash(this._segments); | ||||||
|       } else { |  | ||||||
|         regexString += '/' + segment.regex; |  | ||||||
|       } |  | ||||||
|     }); |  | ||||||
| 
 | 
 | ||||||
|     if (this.terminal) { |     var lastSegment = this._segments[this._segments.length - 1]; | ||||||
|       regexString += '$'; |     this.terminal = !(lastSegment instanceof ContinuationSegment); | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     this.regex = RegExpWrapper.create(regexString); |  | ||||||
|     this.segments = segments; |  | ||||||
|     this.specificity = specificity; |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   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; |   recognize(beginningSegment: Url): PathMatch { | ||||||
|     if (!containsStarSegment) { |     var nextSegment = beginningSegment; | ||||||
|       var matches = RegExpWrapper.firstMatch( |     var currentSegment: Url; | ||||||
|           useQueryString ? PathRecognizer.queryRegex : PathRecognizer.matrixRegex, url); |     var positionalParams = {}; | ||||||
|       if (isPresent(matches)) { |     var captured = []; | ||||||
|         url = matches[1]; | 
 | ||||||
|         paramsString = matches[2]; |     for (var i = 0; i < this._segments.length; i += 1) { | ||||||
|  |       if (isBlank(nextSegment)) { | ||||||
|  |         return null; | ||||||
|       } |       } | ||||||
|  |       currentSegment = nextSegment; | ||||||
| 
 | 
 | ||||||
|       url = StringWrapper.replaceAll(url, /(;[^\/]+)(?=(\/|$))/g, ''); |       var segment = this._segments[i]; | ||||||
|     } |  | ||||||
| 
 | 
 | ||||||
|     var params = StringMapWrapper.create(); |  | ||||||
|     var urlPart = url; |  | ||||||
| 
 |  | ||||||
|     for (var i = 0; i <= segmentsLimit; i++) { |  | ||||||
|       var segment = this.segments[i]; |  | ||||||
|       if (segment instanceof ContinuationSegment) { |       if (segment instanceof ContinuationSegment) { | ||||||
|         continue; |         break; | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|       var match = RegExpWrapper.firstMatch(RegExpWrapper.create('/' + segment.regex), urlPart); |       captured.push(currentSegment.path); | ||||||
|       urlPart = StringWrapper.substring(urlPart, match[0].length); | 
 | ||||||
|       if (segment.name.length > 0) { |       // the star segment consumes all of the remaining URL, including matrix params
 | ||||||
|         params[segment.name] = match[1]; |       if (segment instanceof StarSegment) { | ||||||
|  |         positionalParams[segment.name] = currentSegment.toString(); | ||||||
|  |         nextSegment = null; | ||||||
|  |         break; | ||||||
|       } |       } | ||||||
|  | 
 | ||||||
|  |       if (segment instanceof DynamicSegment) { | ||||||
|  |         positionalParams[segment.name] = currentSegment.path; | ||||||
|  |       } else if (!segment.match(currentSegment.path)) { | ||||||
|  |         return null; | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       nextSegment = currentSegment.child; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     if (isPresent(paramsString) && paramsString.length > 0) { |     if (this.terminal && isPresent(nextSegment)) { | ||||||
|       var expectedStartingValue = useQueryString ? '?' : ';'; |       return null; | ||||||
|       if (paramsString[0] == expectedStartingValue) { |  | ||||||
|         parseAndAssignParamString(expectedStartingValue, paramsString, params); |  | ||||||
|       } |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     return params; |     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); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   generate(params: StringMap<string, any>): string { | 
 | ||||||
|  |   generate(params: StringMap<string, any>): ComponentInstruction { | ||||||
|     var paramTokens = new TouchMap(params); |     var paramTokens = new TouchMap(params); | ||||||
|     var applyLeadingSlash = false; |  | ||||||
|     var useQueryString = this.isRoot && this.terminal; |  | ||||||
| 
 | 
 | ||||||
|     var url = ''; |     var path = []; | ||||||
|     for (var i = 0; i < this.segments.length; i++) { |  | ||||||
|       let segment = this.segments[i]; |  | ||||||
|       let s = segment.generate(paramTokens); |  | ||||||
|       applyLeadingSlash = applyLeadingSlash || (segment instanceof ContinuationSegment); |  | ||||||
| 
 | 
 | ||||||
|       if (s.length > 0) { |     for (var i = 0; i < this._segments.length; i++) { | ||||||
|         url += (i > 0 ? '/' : '') + s; |       let segment = this._segments[i]; | ||||||
|  |       if (!(segment instanceof ContinuationSegment)) { | ||||||
|  |         path.push(segment.generate(paramTokens)); | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|  |     var urlPath = path.join('/'); | ||||||
| 
 | 
 | ||||||
|     var unusedParams = paramTokens.getUnused(); |     var nonPositionalParams = paramTokens.getUnused(); | ||||||
|     if (!StringMapWrapper.isEmpty(unusedParams)) { |     var urlParams = serializeParams(nonPositionalParams); | ||||||
|       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; |  | ||||||
|         } |  | ||||||
|       }); |  | ||||||
|     } |  | ||||||
| 
 | 
 | ||||||
|     if (applyLeadingSlash) { |     return new ComponentInstruction(urlPath, urlParams, this, params); | ||||||
|       url += '/'; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     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 {makeDecorator} from 'angular2/src/util/decorators'; | ||||||
| import {List} from 'angular2/src/facade/collection'; | 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 = | export var RouteConfig: (configs: List<RouteDefinition>) => ClassDecorator = | ||||||
|     makeDecorator(RouteConfigAnnotation); |     makeDecorator(RouteConfigAnnotation); | ||||||
|  | |||||||
| @ -8,7 +8,7 @@ export {RouteDefinition} from './route_definition'; | |||||||
|  * |  * | ||||||
|  * Supported keys: |  * Supported keys: | ||||||
|  * - `path` (required) |  * - `path` (required) | ||||||
|  * - `component`,  `redirectTo` (requires exactly one of these) |  * - `component`, `loader`,  `redirectTo` (requires exactly one of these) | ||||||
|  * - `as` (optional) |  * - `as` (optional) | ||||||
|  */ |  */ | ||||||
| @CONST() | @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() | @CONST() | ||||||
| export class AsyncRoute implements RouteDefinition { | export class AsyncRoute implements RouteDefinition { | ||||||
|   path: string; |   path: string; | ||||||
| @ -51,6 +66,8 @@ export class Redirect implements RouteDefinition { | |||||||
|   path: string; |   path: string; | ||||||
|   redirectTo: string; |   redirectTo: string; | ||||||
|   as: string = null; |   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}) { |   constructor({path, redirectTo}: {path: string, redirectTo: string}) { | ||||||
|     this.path = path; |     this.path = path; | ||||||
|     this.redirectTo = redirectTo; |     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 {ComponentDefinition} from './route_definition'; | ||||||
| import {Type, BaseException} from 'angular2/src/facade/lang'; | 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 |  * Given a JS Object that represents... returns a corresponding Route, AsyncRoute, or Redirect | ||||||
|  */ |  */ | ||||||
| export function normalizeRouteConfig(config: RouteDefinition): RouteDefinition { | 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; |     return <RouteDefinition>config; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   if ((!config.component) == (!config.redirectTo)) { |   if ((!config.component) == (!config.redirectTo)) { | ||||||
|     throw new BaseException( |     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 (config.component) { | ||||||
|     if (typeof config.component == 'object') { |     if (typeof config.component == 'object') { | ||||||
| @ -28,7 +29,7 @@ export function normalizeRouteConfig(config: RouteDefinition): RouteDefinition { | |||||||
|             {path: config.path, loader: componentDefinitionObject.loader, as: config.as}); |             {path: config.path, loader: componentDefinitionObject.loader, as: config.as}); | ||||||
|       } else { |       } else { | ||||||
|         throw new BaseException( |         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(<{ |     return new Route(<{ | ||||||
|  | |||||||
| @ -6,7 +6,8 @@ import { | |||||||
|   isPresent, |   isPresent, | ||||||
|   isType, |   isType, | ||||||
|   isStringMap, |   isStringMap, | ||||||
|   BaseException |   BaseException, | ||||||
|  |   Type | ||||||
| } from 'angular2/src/facade/lang'; | } from 'angular2/src/facade/lang'; | ||||||
| import { | import { | ||||||
|   Map, |   Map, | ||||||
| @ -17,12 +18,13 @@ import { | |||||||
|   StringMapWrapper |   StringMapWrapper | ||||||
| } from 'angular2/src/facade/collection'; | } from 'angular2/src/facade/collection'; | ||||||
| 
 | 
 | ||||||
| import {PathRecognizer} from './path_recognizer'; | import {PathRecognizer, PathMatch} from './path_recognizer'; | ||||||
| import {RouteHandler} from './route_handler'; | import {Route, AsyncRoute, AuxRoute, Redirect, RouteDefinition} from './route_config_impl'; | ||||||
| import {Route, AsyncRoute, Redirect, RouteDefinition} from './route_config_impl'; |  | ||||||
| import {AsyncRouteHandler} from './async_route_handler'; | import {AsyncRouteHandler} from './async_route_handler'; | ||||||
| import {SyncRouteHandler} from './sync_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. |  * `RouteRecognizer` is responsible for recognizing routes for a single component. | ||||||
| @ -31,30 +33,45 @@ import {parseAndAssignParamString} from 'angular2/src/router/helpers'; | |||||||
|  */ |  */ | ||||||
| export class RouteRecognizer { | export class RouteRecognizer { | ||||||
|   names: Map<string, PathRecognizer> = new Map(); |   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 { |   config(config: RouteDefinition): boolean { | ||||||
|     var handler; |     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) { |     if (config instanceof Redirect) { | ||||||
|       let path = config.path == '/' ? '' : config.path; |       this.redirects.push(new Redirector(config.path, config.redirectTo)); | ||||||
|       this.redirects.set(path, config.redirectTo); |  | ||||||
|       return true; |       return true; | ||||||
|     } else if (config instanceof Route) { |     } | ||||||
|  | 
 | ||||||
|  |     if (config instanceof Route) { | ||||||
|       handler = new SyncRouteHandler(config.component); |       handler = new SyncRouteHandler(config.component); | ||||||
|     } else if (config instanceof AsyncRoute) { |     } else if (config instanceof AsyncRoute) { | ||||||
|       handler = new AsyncRouteHandler(config.loader); |       handler = new AsyncRouteHandler(config.loader); | ||||||
|     } |     } | ||||||
|     var recognizer = new PathRecognizer(config.path, handler, this.isRoot); |     var recognizer = new PathRecognizer(config.path, handler); | ||||||
|     MapWrapper.forEach(this.matchers, (matcher, _) => { | 
 | ||||||
|       if (recognizer.regex.toString() == matcher.regex.toString()) { |     this.matchers.forEach((matcher) => { | ||||||
|  |       if (recognizer.hash == matcher.hash) { | ||||||
|         throw new BaseException( |         throw new BaseException( | ||||||
|             `Configuration '${config.path}' conflicts with existing route '${matcher.path}'`); |             `Configuration '${config.path}' conflicts with existing route '${matcher.path}'`); | ||||||
|       } |       } | ||||||
|     }); |     }); | ||||||
|     this.matchers.set(recognizer.regex, recognizer); | 
 | ||||||
|  |     this.matchers.push(recognizer); | ||||||
|     if (isPresent(config.as)) { |     if (isPresent(config.as)) { | ||||||
|       this.names.set(config.as, recognizer); |       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. |    * 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 = []; |     var solutions = []; | ||||||
|     if (url.length > 0 && url[url.length - 1] == '/') { |  | ||||||
|       url = url.substring(0, url.length - 1); |  | ||||||
|     } |  | ||||||
| 
 | 
 | ||||||
|     MapWrapper.forEach(this.redirects, (target, path) => { |     urlParse = this._redirect(urlParse); | ||||||
|       // "/" redirect case
 |  | ||||||
|       if (path == '/' || path == '') { |  | ||||||
|         if (path == url) { |  | ||||||
|           url = target; |  | ||||||
|         } |  | ||||||
|       } else if (url.startsWith(path)) { |  | ||||||
|         url = target + url.substring(path.length); |  | ||||||
|       } |  | ||||||
|     }); |  | ||||||
| 
 | 
 | ||||||
|     var queryParams = StringMapWrapper.create(); |     this.matchers.forEach((pathRecognizer: PathRecognizer) => { | ||||||
|     var queryString = ''; |       var pathMatch = pathRecognizer.recognize(urlParse); | ||||||
|     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); |  | ||||||
|     } |  | ||||||
| 
 | 
 | ||||||
|     MapWrapper.forEach(this.matchers, (pathRecognizer, regex) => { |       if (isPresent(pathMatch)) { | ||||||
|       var match; |         solutions.push(pathMatch); | ||||||
|       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)); |  | ||||||
|       } |       } | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     return solutions; |     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); } |   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); |     var pathRecognizer: PathRecognizer = this.names.get(name); | ||||||
|     if (isBlank(pathRecognizer)) { |     if (isBlank(pathRecognizer)) { | ||||||
|       return null; |       return null; | ||||||
|     } |     } | ||||||
|     var url = pathRecognizer.generate(params); |     return pathRecognizer.generate(params); | ||||||
|     return {url, 'nextComponent': pathRecognizer.handler.componentType}; |  | ||||||
|   } |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export class RouteMatch { | export class Redirector { | ||||||
|   private _params: StringMap<string, any>; |   segments: List<string> = []; | ||||||
|   private _paramsParsed: boolean = false; |   toSegments: List<string> = []; | ||||||
| 
 | 
 | ||||||
|   constructor(public recognizer: PathRecognizer, public matchedUrl: string, |   constructor(path: string, redirectTo: string) { | ||||||
|               public unmatchedUrl: string, p: StringMap<string, any> = null) { |     if (path.startsWith('/')) { | ||||||
|     this._params = isPresent(p) ? p : StringMapWrapper.create(); |       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) { |    * Returns `null` or a `ParsedUrl` representing the new path to match | ||||||
|       this._paramsParsed = true; |    */ | ||||||
|       StringMapWrapper.forEach(this.recognizer.parseParams(this.matchedUrl), |   redirect(urlParse: Url): Url { | ||||||
|                                (value, key) => { StringMapWrapper.set(this._params, key, value); }); |     for (var i = 0; i < this.segments.length; i += 1) { | ||||||
|  |       if (isBlank(urlParse)) { | ||||||
|  |         return null; | ||||||
|  |       } | ||||||
|  |       let segment = this.segments[i]; | ||||||
|  |       if (segment != urlParse.path) { | ||||||
|  |         return null; | ||||||
|  |       } | ||||||
|  |       urlParse = urlParse.child; | ||||||
|     } |     } | ||||||
|     return this._params; | 
 | ||||||
|  |     for (var i = this.toSegments.length - 1; i >= 0; i -= 1) { | ||||||
|  |       let segment = this.toSegments[i]; | ||||||
|  |       urlParse = new Url(segment, urlParse); | ||||||
|  |     } | ||||||
|  |     return urlParse; | ||||||
|   } |   } | ||||||
| } | } | ||||||
| 
 |  | ||||||
| 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`); |  | ||||||
|     } |  | ||||||
|     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`); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|   throw new BaseException(`Unexpected component "${config}".`); |  | ||||||
| } |  | ||||||
|  | |||||||
| @ -1,5 +1,6 @@ | |||||||
| import {RouteRecognizer, RouteMatch} from './route_recognizer'; | import {PathMatch} from './path_recognizer'; | ||||||
| import {Instruction} from './instruction'; | import {RouteRecognizer} from './route_recognizer'; | ||||||
|  | import {Instruction, ComponentInstruction, PrimaryInstruction} from './instruction'; | ||||||
| import { | import { | ||||||
|   List, |   List, | ||||||
|   ListWrapper, |   ListWrapper, | ||||||
| @ -24,6 +25,9 @@ import {RouteConfig, AsyncRoute, Route, Redirect, RouteDefinition} from './route | |||||||
| import {reflector} from 'angular2/src/reflection/reflection'; | import {reflector} from 'angular2/src/reflection/reflection'; | ||||||
| import {Injectable} from 'angular2/di'; | import {Injectable} from 'angular2/di'; | ||||||
| import {normalizeRouteConfig} from './route_config_nomalizer'; | 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. |  * 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 |    * 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); |     config = normalizeRouteConfig(config); | ||||||
| 
 | 
 | ||||||
|     var recognizer: RouteRecognizer = this._rules.get(parentComponent); |     var recognizer: RouteRecognizer = this._rules.get(parentComponent); | ||||||
| 
 | 
 | ||||||
|     if (isBlank(recognizer)) { |     if (isBlank(recognizer)) { | ||||||
|       recognizer = new RouteRecognizer(isRootLevelRoute); |       recognizer = new RouteRecognizer(); | ||||||
|       this._rules.set(parentComponent, recognizer); |       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 |    * 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)) { |     if (!isType(component)) { | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
| @ -77,8 +81,7 @@ export class RouteRegistry { | |||||||
|         var annotation = annotations[i]; |         var annotation = annotations[i]; | ||||||
| 
 | 
 | ||||||
|         if (annotation instanceof RouteConfig) { |         if (annotation instanceof RouteConfig) { | ||||||
|           ListWrapper.forEach(annotation.configs, |           ListWrapper.forEach(annotation.configs, (config) => this.config(component, config)); | ||||||
|                               (config) => this.config(component, config, isRootComponent)); |  | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
| @ -90,63 +93,100 @@ export class RouteRegistry { | |||||||
|    * the application into the state specified by the url |    * the application into the state specified by the url | ||||||
|    */ |    */ | ||||||
|   recognize(url: string, parentComponent: any): Promise<Instruction> { |   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); |     var componentRecognizer = this._rules.get(parentComponent); | ||||||
|     if (isBlank(componentRecognizer)) { |     if (isBlank(componentRecognizer)) { | ||||||
|       return PromiseWrapper.resolve(null); |       return PromiseWrapper.resolve(null); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     // Matches some beginning part of the given URL
 |     // Matches some beginning part of the given URL
 | ||||||
|     var possibleMatches = componentRecognizer.recognize(url); |     var possibleMatches = componentRecognizer.recognize(parsedUrl); | ||||||
|  | 
 | ||||||
|     var matchPromises = |     var matchPromises = | ||||||
|         ListWrapper.map(possibleMatches, (candidate) => this._completeRouteMatch(candidate)); |         ListWrapper.map(possibleMatches, (candidate) => this._completePrimaryRouteMatch(candidate)); | ||||||
| 
 | 
 | ||||||
|     return PromiseWrapper.all(matchPromises) |     return PromiseWrapper.all(matchPromises).then(mostSpecific); | ||||||
|         .then((solutions: List<Instruction>) => { |  | ||||||
|           // remove nulls
 |  | ||||||
|           var fullSolutions = ListWrapper.filter(solutions, (solution) => isPresent(solution)); |  | ||||||
| 
 |  | ||||||
|           if (fullSolutions.length > 0) { |  | ||||||
|             return mostSpecific(fullSolutions); |  | ||||||
|           } |  | ||||||
|           return null; |  | ||||||
|         }); |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
| 
 |   private _completePrimaryRouteMatch(partialMatch: PathMatch): Promise<PrimaryInstruction> { | ||||||
|   _completeRouteMatch(partialMatch: RouteMatch): Promise<Instruction> { |     var instruction = partialMatch.instruction; | ||||||
|     var recognizer = partialMatch.recognizer; |     return instruction.resolveComponentType().then((componentType) => { | ||||||
|     var handler = recognizer.handler; |  | ||||||
|     return handler.resolveComponentType().then((componentType) => { |  | ||||||
|       this.configFromComponent(componentType); |       this.configFromComponent(componentType); | ||||||
| 
 | 
 | ||||||
|       if (partialMatch.unmatchedUrl.length == 0) { |       if (isBlank(partialMatch.remaining)) { | ||||||
|         if (recognizer.terminal) { |         if (instruction.terminal) { | ||||||
|           return new Instruction(componentType, partialMatch.matchedUrl, recognizer, null, |           return new PrimaryInstruction(instruction, null, partialMatch.remainingAux); | ||||||
|                                  partialMatch.params()); |  | ||||||
|         } else { |         } else { | ||||||
|           return null; |           return null; | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|       return this.recognize(partialMatch.unmatchedUrl, componentType) |       return this._recognizePrimaryRoute(partialMatch.remaining, componentType) | ||||||
|           .then(childInstruction => { |           .then((childInstruction) => { | ||||||
|             if (isBlank(childInstruction)) { |             if (isBlank(childInstruction)) { | ||||||
|               return null; |               return null; | ||||||
|             } else { |             } else { | ||||||
|               return new Instruction(componentType, partialMatch.matchedUrl, recognizer, |               return new PrimaryInstruction(instruction, childInstruction, | ||||||
|                                      childInstruction); |                                             partialMatch.remainingAux); | ||||||
|             } |             } | ||||||
|           }); |           }); | ||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
|  |   private _completeAuxiliaryRouteMatches(instruction: PrimaryInstruction, | ||||||
|  |                                          parentComponent: any): Promise<Instruction> { | ||||||
|  |     if (isBlank(instruction)) { | ||||||
|  |       return _resolveToNull; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     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); | ||||||
|  |           }); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   /** |   /** | ||||||
|    * Given a normalized list with component names and params like: `['user', {id: 3 }]` |    * 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`. |    * generates a url with a leading slash relative to the provided `parentComponent`. | ||||||
|    */ |    */ | ||||||
|   generate(linkParams: List<any>, parentComponent: any): string { |   generate(linkParams: List<any>, parentComponent: any): Instruction { | ||||||
|     let url = ''; |     let segments = []; | ||||||
|     let componentCursor = parentComponent; |     let componentCursor = parentComponent; | ||||||
|  | 
 | ||||||
|     for (let i = 0; i < linkParams.length; i += 1) { |     for (let i = 0; i < linkParams.length; i += 1) { | ||||||
|       let segment = linkParams[i]; |       let segment = linkParams[i]; | ||||||
|       if (isBlank(componentCursor)) { |       if (isBlank(componentCursor)) { | ||||||
| @ -172,15 +212,22 @@ export class RouteRegistry { | |||||||
|             `Component "${getTypeNameForDebugging(componentCursor)}" has no route config.`); |             `Component "${getTypeNameForDebugging(componentCursor)}" has no route config.`); | ||||||
|       } |       } | ||||||
|       var response = componentRecognizer.generate(segment, params); |       var response = componentRecognizer.generate(segment, params); | ||||||
|  | 
 | ||||||
|       if (isBlank(response)) { |       if (isBlank(response)) { | ||||||
|         throw new BaseException( |         throw new BaseException( | ||||||
|             `Component "${getTypeNameForDebugging(componentCursor)}" has no route named "${segment}".`); |             `Component "${getTypeNameForDebugging(componentCursor)}" has no route named "${segment}".`); | ||||||
|       } |       } | ||||||
|       url += response['url']; |       segments.push(response); | ||||||
|       componentCursor = response['nextComponent']; |       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 |  * 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]; |   var mostSpecificSolution = instructions[0]; | ||||||
|   for (var solutionIndex = 1; solutionIndex < instructions.length; solutionIndex++) { |   for (var solutionIndex = 1; solutionIndex < instructions.length; solutionIndex++) { | ||||||
|     var solution = instructions[solutionIndex]; |     var solution: PrimaryInstruction = instructions[solutionIndex]; | ||||||
|     if (solution.specificity > mostSpecificSolution.specificity) { |     if (isBlank(solution)) { | ||||||
|  |       continue; | ||||||
|  |     } | ||||||
|  |     if (solution.component.specificity > mostSpecificSolution.component.specificity) { | ||||||
|       mostSpecificSolution = solution; |       mostSpecificSolution = solution; | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  | |||||||
| @ -12,7 +12,7 @@ import { | |||||||
| 
 | 
 | ||||||
| import {RouteRegistry} from './route_registry'; | import {RouteRegistry} from './route_registry'; | ||||||
| import {Pipeline} from './pipeline'; | import {Pipeline} from './pipeline'; | ||||||
| import {Instruction} from './instruction'; | import {ComponentInstruction, Instruction, stringifyInstruction} from './instruction'; | ||||||
| import {RouterOutlet} from './router_outlet'; | import {RouterOutlet} from './router_outlet'; | ||||||
| import {Location} from './location'; | import {Location} from './location'; | ||||||
| import {getCanActivateHook} from './route_lifecycle_reflector'; | import {getCanActivateHook} from './route_lifecycle_reflector'; | ||||||
| @ -45,10 +45,9 @@ export class Router { | |||||||
|   private _currentInstruction: Instruction = null; |   private _currentInstruction: Instruction = null; | ||||||
|   private _currentNavigation: Promise<any> = _resolveToTrue; |   private _currentNavigation: Promise<any> = _resolveToTrue; | ||||||
|   private _outlet: RouterOutlet = null; |   private _outlet: RouterOutlet = null; | ||||||
|  |   private _auxOutlets: Map<string, RouterOutlet> = new Map(); | ||||||
|   private _subject: EventEmitter = new EventEmitter(); |   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, |   constructor(public registry: RouteRegistry, public _pipeline: Pipeline, public parent: Router, | ||||||
|               public hostComponent: any) {} |               public hostComponent: any) {} | ||||||
| 
 | 
 | ||||||
| @ -65,8 +64,11 @@ export class Router { | |||||||
|    * you're writing a reusable component. |    * you're writing a reusable component. | ||||||
|    */ |    */ | ||||||
|   registerOutlet(outlet: RouterOutlet): Promise<boolean> { |   registerOutlet(outlet: RouterOutlet): Promise<boolean> { | ||||||
|     // TODO: sibling routes
 |     if (isPresent(outlet.name)) { | ||||||
|     this._outlet = outlet; |       this._auxOutlets.set(outlet.name, outlet); | ||||||
|  |     } else { | ||||||
|  |       this._outlet = outlet; | ||||||
|  |     } | ||||||
|     if (isPresent(this._currentInstruction)) { |     if (isPresent(this._currentInstruction)) { | ||||||
|       return outlet.commit(this._currentInstruction); |       return outlet.commit(this._currentInstruction); | ||||||
|     } |     } | ||||||
| @ -87,9 +89,8 @@ export class Router { | |||||||
|    * ``` |    * ``` | ||||||
|    */ |    */ | ||||||
|   config(definitions: List<RouteDefinition>): Promise<any> { |   config(definitions: List<RouteDefinition>): Promise<any> { | ||||||
|     definitions.forEach((routeDefinition) => { |     definitions.forEach( | ||||||
|       this.registry.config(this.hostComponent, routeDefinition, this instanceof RootRouter); |         (routeDefinition) => { this.registry.config(this.hostComponent, routeDefinition); }); | ||||||
|     }); |  | ||||||
|     return this.renavigate(); |     return this.renavigate(); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
| @ -104,31 +105,51 @@ export class Router { | |||||||
|     return this._currentNavigation = this._currentNavigation.then((_) => { |     return this._currentNavigation = this._currentNavigation.then((_) => { | ||||||
|       this.lastNavigationAttempt = url; |       this.lastNavigationAttempt = url; | ||||||
|       this._startNavigating(); |       this._startNavigating(); | ||||||
|       return this._afterPromiseFinishNavigating(this.recognize(url).then((matchedInstruction) => { |       return this._afterPromiseFinishNavigating(this.recognize(url).then((instruction) => { | ||||||
|         if (isBlank(matchedInstruction)) { |         if (isBlank(instruction)) { | ||||||
|           return false; |           return false; | ||||||
|         } |         } | ||||||
|         return this._reuse(matchedInstruction) |         return this._navigate(instruction, _skipLocationChange); | ||||||
|             .then((_) => this._canActivate(matchedInstruction)) |  | ||||||
|             .then((result) => { |  | ||||||
|               if (!result) { |  | ||||||
|                 return false; |  | ||||||
|               } |  | ||||||
|               return this._canDeactivate(matchedInstruction) |  | ||||||
|                   .then((result) => { |  | ||||||
|                     if (result) { |  | ||||||
|                       return this.commit(matchedInstruction, _skipLocationChange) |  | ||||||
|                           .then((_) => { |  | ||||||
|                             this._emitNavigationFinish(matchedInstruction.accumulatedUrl); |  | ||||||
|                             return true; |  | ||||||
|                           }); |  | ||||||
|                     } |  | ||||||
|                   }); |  | ||||||
|             }); |  | ||||||
|       })); |       })); | ||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * 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(instruction) | ||||||
|  |               .then((result) => { | ||||||
|  |                 if (result) { | ||||||
|  |                   return this.commit(instruction, _skipLocationChange) | ||||||
|  |                       .then((_) => { | ||||||
|  |                         this._emitNavigationFinish(stringifyInstruction(instruction)); | ||||||
|  |                         return true; | ||||||
|  |                       }); | ||||||
|  |                 } | ||||||
|  |               }); | ||||||
|  |         }); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   private _emitNavigationFinish(url): void { ObservableWrapper.callNext(this._subject, url); } |   private _emitNavigationFinish(url): void { ObservableWrapper.callNext(this._subject, url); } | ||||||
| 
 | 
 | ||||||
|   private _afterPromiseFinishNavigating(promise: Promise<any>): Promise<any> { |   private _afterPromiseFinishNavigating(promise: Promise<any>): Promise<any> { | ||||||
| @ -138,21 +159,20 @@ export class Router { | |||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   _reuse(instruction): Promise<any> { |   _reuse(instruction: Instruction): Promise<any> { | ||||||
|     if (isBlank(this._outlet)) { |     if (isBlank(this._outlet)) { | ||||||
|       return _resolveToFalse; |       return _resolveToFalse; | ||||||
|     } |     } | ||||||
|     return this._outlet.canReuse(instruction) |     return this._outlet.canReuse(instruction) | ||||||
|         .then((result) => { |         .then((result) => { | ||||||
|           instruction.reuse = result; |  | ||||||
|           if (isPresent(this._outlet.childRouter) && isPresent(instruction.child)) { |           if (isPresent(this._outlet.childRouter) && isPresent(instruction.child)) { | ||||||
|             return this._outlet.childRouter._reuse(instruction.child); |             return this._outlet.childRouter._reuse(instruction.child); | ||||||
|           } |           } | ||||||
|         }); |         }); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   private _canActivate(instruction: Instruction): Promise<boolean> { |   private _canActivate(nextInstruction: Instruction): Promise<boolean> { | ||||||
|     return canActivateOne(instruction, this._currentInstruction); |     return canActivateOne(nextInstruction, this._currentInstruction); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   private _canDeactivate(instruction: Instruction): Promise<boolean> { |   private _canDeactivate(instruction: Instruction): Promise<boolean> { | ||||||
| @ -160,11 +180,12 @@ export class Router { | |||||||
|       return _resolveToTrue; |       return _resolveToTrue; | ||||||
|     } |     } | ||||||
|     var next: Promise<boolean>; |     var next: Promise<boolean>; | ||||||
|     if (isPresent(instruction) && instruction.reuse) { |     if (isPresent(instruction) && instruction.component.reuse) { | ||||||
|       next = _resolveToTrue; |       next = _resolveToTrue; | ||||||
|     } else { |     } else { | ||||||
|       next = this._outlet.canDeactivate(instruction); |       next = this._outlet.canDeactivate(instruction); | ||||||
|     } |     } | ||||||
|  |     // TODO: aux route lifecycle hooks
 | ||||||
|     return next.then((result) => { |     return next.then((result) => { | ||||||
|       if (result == false) { |       if (result == false) { | ||||||
|         return false; |         return false; | ||||||
| @ -182,10 +203,14 @@ export class Router { | |||||||
|    */ |    */ | ||||||
|   commit(instruction: Instruction, _skipLocationChange: boolean = false): Promise<any> { |   commit(instruction: Instruction, _skipLocationChange: boolean = false): Promise<any> { | ||||||
|     this._currentInstruction = instruction; |     this._currentInstruction = instruction; | ||||||
|  |     var next = _resolveToTrue; | ||||||
|     if (isPresent(this._outlet)) { |     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 |    * Generate a URL from a component name and optional map of parameters. The URL is relative to the | ||||||
|    * app's base href. |    * app's base href. | ||||||
|    */ |    */ | ||||||
|   generate(linkParams: List<any>): string { |   generate(linkParams: List<any>): Instruction { | ||||||
|     let normalizedLinkParams = splitAndFlattenLinkParams(linkParams); |     let normalizedLinkParams = splitAndFlattenLinkParams(linkParams); | ||||||
| 
 | 
 | ||||||
|     var first = ListWrapper.first(normalizedLinkParams); |     var first = ListWrapper.first(normalizedLinkParams); | ||||||
| @ -275,11 +300,22 @@ export class Router { | |||||||
|       throw new BaseException(msg); |       throw new BaseException(msg); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     let url = ''; |     // TODO: structural cloning and whatnot
 | ||||||
|     if (isPresent(router.parent) && isPresent(router.parent._currentInstruction)) { | 
 | ||||||
|       url = router.parent._currentInstruction.capturedUrl; |     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); |     super(registry, pipeline, null, hostComponent); | ||||||
|     this._location = location; |     this._location = location; | ||||||
|     this._location.subscribe((change) => this.navigate(change['url'], isPresent(change['pop']))); |     this._location.subscribe((change) => this.navigate(change['url'], isPresent(change['pop']))); | ||||||
|     this.registry.configFromComponent(hostComponent, true); | 
 | ||||||
|  |     this.registry.configFromComponent(hostComponent); | ||||||
|     this.navigate(location.path()); |     this.navigate(location.path()); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   commit(instruction: Instruction, _skipLocationChange: boolean = false): Promise<any> { |   commit(instruction: Instruction, _skipLocationChange: boolean = false): Promise<any> { | ||||||
|  |     var emitUrl = stringifyInstruction(instruction); | ||||||
|  |     if (emitUrl.length > 0) { | ||||||
|  |       emitUrl = '/' + emitUrl; | ||||||
|  |     } | ||||||
|     var promise = super.commit(instruction); |     var promise = super.commit(instruction); | ||||||
|     if (!_skipLocationChange) { |     if (!_skipLocationChange) { | ||||||
|       promise = promise.then((_) => { this._location.go(instruction.accumulatedUrl); }); |       promise = promise.then((_) => { this._location.go(emitUrl); }); | ||||||
|     } |     } | ||||||
|     return promise; |     return promise; | ||||||
|   } |   } | ||||||
| @ -315,6 +356,12 @@ class ChildRouter extends Router { | |||||||
|     // Delegate navigation to the root router
 |     // Delegate navigation to the root router
 | ||||||
|     return this.parent.navigate(url, _skipLocationChange); |     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; |   var next = _resolveToTrue; | ||||||
|   if (isPresent(nextInstruction.child)) { |   if (isPresent(nextInstruction.child)) { | ||||||
|     next = canActivateOne(nextInstruction.child, |     next = canActivateOne(nextInstruction.child, | ||||||
|                           isPresent(currentInstruction) ? currentInstruction.child : null); |                           isPresent(prevInstruction) ? prevInstruction.child : null); | ||||||
|   } |   } | ||||||
|   return next.then((res) => { |   return next.then((result) => { | ||||||
|     if (res == false) { |     if (result == false) { | ||||||
|       return false; |       return false; | ||||||
|     } |     } | ||||||
|     if (nextInstruction.reuse) { |     if (nextInstruction.component.reuse) { | ||||||
|       return true; |       return true; | ||||||
|     } |     } | ||||||
|     var hook = getCanActivateHook(nextInstruction.component); |     var hook = getCanActivateHook(nextInstruction.component.componentType); | ||||||
|     if (isPresent(hook)) { |     if (isPresent(hook)) { | ||||||
|       return hook(nextInstruction, currentInstruction); |       return hook(nextInstruction.component, | ||||||
|  |                   isPresent(prevInstruction) ? prevInstruction.component : null); | ||||||
|     } |     } | ||||||
|     return true; |     return true; | ||||||
|   }); |   }); | ||||||
|  | |||||||
| @ -3,6 +3,7 @@ import {List, StringMap, StringMapWrapper} from 'angular2/src/facade/collection' | |||||||
| 
 | 
 | ||||||
| import {Router} from './router'; | import {Router} from './router'; | ||||||
| import {Location} from './location'; | import {Location} from './location'; | ||||||
|  | import {Instruction, stringifyInstruction} from './instruction'; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * The RouterLink directive lets you link to specific parts of your app. |  * 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.
 |   // the url displayed on the anchor element.
 | ||||||
|   visibleHref: string; |   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) {} |   constructor(private _router: Router, private _location: Location) {} | ||||||
| 
 | 
 | ||||||
|   set routeParams(changes: List<any>) { |   set routeParams(changes: List<any>) { | ||||||
|     this._routeParams = changes; |     this._routeParams = changes; | ||||||
|     this._navigationHref = this._router.generate(this._routeParams); |     this._navigationInstruction = this._router.generate(this._routeParams); | ||||||
|     this.visibleHref = this._location.normalizeAbsolutely(this._navigationHref); | 
 | ||||||
|  |     // TODO: is this the right spot for this?
 | ||||||
|  |     var navigationHref = '/' + stringifyInstruction(this._navigationInstruction); | ||||||
|  |     this.visibleHref = this._location.normalizeAbsolutely(navigationHref); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   onClick(): boolean { |   onClick(): boolean { | ||||||
|     this._router.navigate(this._navigationHref); |     this._router.navigateInstruction(this._navigationInstruction); | ||||||
|     return false; |     return false; | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  | |||||||
| @ -7,7 +7,7 @@ import {DynamicComponentLoader, ComponentRef, ElementRef} from 'angular2/core'; | |||||||
| import {Injector, bind, Dependency, undefinedValue} from 'angular2/di'; | import {Injector, bind, Dependency, undefinedValue} from 'angular2/di'; | ||||||
| 
 | 
 | ||||||
| import * as routerMod from './router'; | import * as routerMod from './router'; | ||||||
| import {Instruction, RouteParams} from './instruction'; | import {Instruction, ComponentInstruction, RouteParams} from './instruction'; | ||||||
| import * as hookMod from './lifecycle_annotations'; | import * as hookMod from './lifecycle_annotations'; | ||||||
| import {hasLifecycleHook} from './route_lifecycle_reflector'; | import {hasLifecycleHook} from './route_lifecycle_reflector'; | ||||||
| 
 | 
 | ||||||
| @ -23,16 +23,16 @@ import {hasLifecycleHook} from './route_lifecycle_reflector'; | |||||||
| @Directive({selector: 'router-outlet'}) | @Directive({selector: 'router-outlet'}) | ||||||
| export class RouterOutlet { | export class RouterOutlet { | ||||||
|   childRouter: routerMod.Router = null; |   childRouter: routerMod.Router = null; | ||||||
|  |   name: string = null; | ||||||
| 
 | 
 | ||||||
|   private _componentRef: ComponentRef = null; |   private _componentRef: ComponentRef = null; | ||||||
|   private _currentInstruction: Instruction = null; |   private _currentInstruction: ComponentInstruction = null; | ||||||
| 
 | 
 | ||||||
|   constructor(private _elementRef: ElementRef, private _loader: DynamicComponentLoader, |   constructor(private _elementRef: ElementRef, private _loader: DynamicComponentLoader, | ||||||
|               private _parentRouter: routerMod.Router, @Attribute('name') nameAttr: string) { |               private _parentRouter: routerMod.Router, @Attribute('name') nameAttr: string) { | ||||||
|     // TODO: reintroduce with new // sibling routes
 |     if (isPresent(nameAttr)) { | ||||||
|     // if (isBlank(nameAttr)) {
 |       this.name = nameAttr; | ||||||
|     //  nameAttr = 'default';
 |     } | ||||||
|     //}
 |  | ||||||
|     this._parentRouter.registerOutlet(this); |     this._parentRouter.registerOutlet(this); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
| @ -40,15 +40,28 @@ export class RouterOutlet { | |||||||
|    * Given an instruction, update the contents of this outlet. |    * Given an instruction, update the contents of this outlet. | ||||||
|    */ |    */ | ||||||
|   commit(instruction: Instruction): Promise<any> { |   commit(instruction: Instruction): Promise<any> { | ||||||
|  |     instruction = this._getInstruction(instruction); | ||||||
|  |     var componentInstruction = instruction.component; | ||||||
|  |     if (isBlank(componentInstruction)) { | ||||||
|  |       return PromiseWrapper.resolve(true); | ||||||
|  |     } | ||||||
|     var next; |     var next; | ||||||
|     if (instruction.reuse) { |     if (componentInstruction.reuse) { | ||||||
|       next = this._reuse(instruction); |       next = this._reuse(componentInstruction); | ||||||
|     } else { |     } else { | ||||||
|       next = this.deactivate(instruction).then((_) => this._activate(instruction)); |       next = this.deactivate(instruction).then((_) => this._activate(componentInstruction)); | ||||||
|     } |     } | ||||||
|     return next.then((_) => this._commitChild(instruction)); |     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> { |   private _commitChild(instruction: Instruction): Promise<any> { | ||||||
|     if (isPresent(this.childRouter)) { |     if (isPresent(this.childRouter)) { | ||||||
|       return this.childRouter.commit(instruction.child); |       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; |     var previousInstruction = this._currentInstruction; | ||||||
|     this._currentInstruction = instruction; |     this._currentInstruction = instruction; | ||||||
|     this.childRouter = this._parentRouter.childRouter(instruction.component); |     var componentType = instruction.componentType; | ||||||
|  |     this.childRouter = this._parentRouter.childRouter(componentType); | ||||||
| 
 | 
 | ||||||
|     var bindings = Injector.resolve([ |     var bindings = Injector.resolve([ | ||||||
|       bind(RouteParams) |       bind(RouteParams) | ||||||
|           .toValue(new RouteParams(instruction.params())), |           .toValue(new RouteParams(instruction.params)), | ||||||
|       bind(routerMod.Router).toValue(this.childRouter) |       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) => { |         .then((componentRef) => { | ||||||
|           this._componentRef = componentRef; |           this._componentRef = componentRef; | ||||||
|           if (hasLifecycleHook(hookMod.onActivate, instruction.component)) { |           if (hasLifecycleHook(hookMod.onActivate, componentType)) { | ||||||
|             return this._componentRef.instance.onActivate(instruction, previousInstruction); |             return this._componentRef.instance.onActivate(instruction, previousInstruction); | ||||||
|           } |           } | ||||||
|         }); |         }); | ||||||
| @ -84,9 +98,11 @@ export class RouterOutlet { | |||||||
|     if (isBlank(this._currentInstruction)) { |     if (isBlank(this._currentInstruction)) { | ||||||
|       return PromiseWrapper.resolve(true); |       return PromiseWrapper.resolve(true); | ||||||
|     } |     } | ||||||
|     if (hasLifecycleHook(hookMod.canDeactivate, this._currentInstruction.component)) { |     var outletInstruction = this._getInstruction(nextInstruction); | ||||||
|       return PromiseWrapper.resolve( |     if (hasLifecycleHook(hookMod.canDeactivate, this._currentInstruction.componentType)) { | ||||||
|           this._componentRef.instance.canDeactivate(nextInstruction, this._currentInstruction)); |       return PromiseWrapper.resolve(this._componentRef.instance.canDeactivate( | ||||||
|  |           isPresent(outletInstruction) ? outletInstruction.component : null, | ||||||
|  |           this._currentInstruction)); | ||||||
|     } |     } | ||||||
|     return PromiseWrapper.resolve(true); |     return PromiseWrapper.resolve(true); | ||||||
|   } |   } | ||||||
| @ -97,24 +113,34 @@ export class RouterOutlet { | |||||||
|    */ |    */ | ||||||
|   canReuse(nextInstruction: Instruction): Promise<boolean> { |   canReuse(nextInstruction: Instruction): Promise<boolean> { | ||||||
|     var result; |     var result; | ||||||
|  | 
 | ||||||
|  |     var outletInstruction = this._getInstruction(nextInstruction); | ||||||
|  |     var componentInstruction = outletInstruction.component; | ||||||
|  | 
 | ||||||
|     if (isBlank(this._currentInstruction) || |     if (isBlank(this._currentInstruction) || | ||||||
|         this._currentInstruction.component != nextInstruction.component) { |         this._currentInstruction.componentType != componentInstruction.componentType) { | ||||||
|       result = false; |       result = false; | ||||||
|     } else if (hasLifecycleHook(hookMod.canReuse, this._currentInstruction.component)) { |     } else if (hasLifecycleHook(hookMod.canReuse, this._currentInstruction.componentType)) { | ||||||
|       result = this._componentRef.instance.canReuse(nextInstruction, this._currentInstruction); |       result = this._componentRef.instance.canReuse(componentInstruction, this._currentInstruction); | ||||||
|     } else { |     } else { | ||||||
|       result = nextInstruction == this._currentInstruction || |       result = | ||||||
|                StringMapWrapper.equals(nextInstruction.params(), this._currentInstruction.params()); |           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; |     var previousInstruction = this._currentInstruction; | ||||||
|     this._currentInstruction = instruction; |     this._currentInstruction = instruction; | ||||||
|     return PromiseWrapper.resolve( |     return PromiseWrapper.resolve( | ||||||
|         hasLifecycleHook(hookMod.onReuse, this._currentInstruction.component) ? |         hasLifecycleHook(hookMod.onReuse, this._currentInstruction.componentType) ? | ||||||
|             this._componentRef.instance.onReuse(instruction, previousInstruction) : |             this._componentRef.instance.onReuse(instruction, previousInstruction) : | ||||||
|             true); |             true); | ||||||
|   } |   } | ||||||
| @ -122,14 +148,16 @@ export class RouterOutlet { | |||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|   deactivate(nextInstruction: Instruction): Promise<any> { |   deactivate(nextInstruction: Instruction): Promise<any> { | ||||||
|  |     var outletInstruction = this._getInstruction(nextInstruction); | ||||||
|  |     var componentInstruction = isPresent(outletInstruction) ? outletInstruction.component : null; | ||||||
|     return (isPresent(this.childRouter) ? |     return (isPresent(this.childRouter) ? | ||||||
|                 this.childRouter.deactivate(isPresent(nextInstruction) ? nextInstruction.child : |                 this.childRouter.deactivate(isPresent(outletInstruction) ? outletInstruction.child : | ||||||
|                                                                          null) : |                                                                            null) : | ||||||
|                 PromiseWrapper.resolve(true)) |                 PromiseWrapper.resolve(true)) | ||||||
|         .then((_) => { |         .then((_) => { | ||||||
|           if (isPresent(this._componentRef) && isPresent(this._currentInstruction) && |           if (isPresent(this._componentRef) && isPresent(this._currentInstruction) && | ||||||
|               hasLifecycleHook(hookMod.onDeactivate, this._currentInstruction.component)) { |               hasLifecycleHook(hookMod.onDeactivate, this._currentInstruction.componentType)) { | ||||||
|             return this._componentRef.instance.onDeactivate(nextInstruction, |             return this._componentRef.instance.onDeactivate(componentInstruction, | ||||||
|                                                             this._currentInstruction); |                                                             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 {RootRouter} from 'angular2/src/router/router'; | ||||||
| import {Pipeline} from 'angular2/src/router/pipeline'; | import {Pipeline} from 'angular2/src/router/pipeline'; | ||||||
| import {Router, RouterOutlet, RouterLink, RouteParams} from 'angular2/router'; | 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'; | import {DOM} from 'angular2/src/dom/dom_adapter'; | ||||||
| 
 | 
 | ||||||
| @ -45,10 +51,12 @@ import { | |||||||
|   CanReuse |   CanReuse | ||||||
| } from 'angular2/src/router/interfaces'; | } from 'angular2/src/router/interfaces'; | ||||||
| import {CanActivate} from 'angular2/src/router/lifecycle_annotations'; | 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'; | 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>; | var completer: PromiseCompleter<any>; | ||||||
| 
 | 
 | ||||||
| export function main() { | export function main() { | ||||||
| @ -73,7 +81,7 @@ export function main() { | |||||||
|       rtr = router; |       rtr = router; | ||||||
|       location = loc; |       location = loc; | ||||||
|       cmpInstanceCount = 0; |       cmpInstanceCount = 0; | ||||||
|       log = ''; |       log = []; | ||||||
|       eventBus = new EventEmitter(); |       eventBus = new EventEmitter(); | ||||||
|     })); |     })); | ||||||
| 
 | 
 | ||||||
| @ -207,7 +215,6 @@ export function main() { | |||||||
|              }); |              }); | ||||||
|        })); |        })); | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|     it('should generate link hrefs from a child to its sibling', |     it('should generate link hrefs from a child to its sibling', | ||||||
|        inject([AsyncTestCompleter], (async) => { |        inject([AsyncTestCompleter], (async) => { | ||||||
|          compile() |          compile() | ||||||
| @ -247,281 +254,299 @@ export function main() { | |||||||
|              }); |              }); | ||||||
|        })); |        })); | ||||||
| 
 | 
 | ||||||
|     it('should call the onActivate hook', inject([AsyncTestCompleter], (async) => { |  | ||||||
|          compile() |  | ||||||
|              .then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})])) |  | ||||||
|              .then((_) => rtr.navigate('/on-activate')) |  | ||||||
|              .then((_) => { |  | ||||||
|                rootTC.detectChanges(); |  | ||||||
|                expect(rootTC.nativeElement).toHaveText('activate cmp'); |  | ||||||
|                expect(log).toEqual('activate: null -> /on-activate;'); |  | ||||||
|                async.done(); |  | ||||||
|              }); |  | ||||||
|        })); |  | ||||||
| 
 | 
 | ||||||
|     it('should wait for a parent component\'s onActivate hook to resolve before calling its child\'s', |     describe('lifecycle hooks', () => { | ||||||
|        inject([AsyncTestCompleter], (async) => { |       it('should call the onActivate hook', inject([AsyncTestCompleter], (async) => { | ||||||
|          compile() |            compile() | ||||||
|              .then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})])) |                .then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})])) | ||||||
|              .then((_) => { |                .then((_) => rtr.navigate('/on-activate')) | ||||||
|                ObservableWrapper.subscribe<string>(eventBus, (ev) => { |                .then((_) => { | ||||||
|                  if (ev.startsWith('parent activate')) { |                  rootTC.detectChanges(); | ||||||
|                    completer.resolve(true); |                  expect(rootTC.nativeElement).toHaveText('activate cmp'); | ||||||
|                  } |                  expect(log).toEqual(['activate: null -> /on-activate']); | ||||||
|  |                  async.done(); | ||||||
|                }); |                }); | ||||||
|                rtr.navigate('/parent-activate/child-activate') |          })); | ||||||
|                    .then((_) => { | 
 | ||||||
|  |       it('should wait for a parent component\'s onActivate hook to resolve before calling its child\'s', | ||||||
|  |          inject([AsyncTestCompleter], (async) => { | ||||||
|  |            compile() | ||||||
|  |                .then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})])) | ||||||
|  |                .then((_) => { | ||||||
|  |                  ObservableWrapper.subscribe<string>(eventBus, (ev) => { | ||||||
|  |                    if (ev.startsWith('parent activate')) { | ||||||
|  |                      completer.resolve(true); | ||||||
|  |                    } | ||||||
|  |                  }); | ||||||
|  |                  rtr.navigate('/parent-activate/child-activate') | ||||||
|  |                      .then((_) => { | ||||||
|  |                        rootTC.detectChanges(); | ||||||
|  |                        expect(rootTC.nativeElement).toHaveText('parent {activate cmp}'); | ||||||
|  |                        expect(log).toEqual([ | ||||||
|  |                          'parent activate: null -> /parent-activate', | ||||||
|  |                          'activate: null -> /child-activate' | ||||||
|  |                        ]); | ||||||
|  |                        async.done(); | ||||||
|  |                      }); | ||||||
|  |                }); | ||||||
|  |          })); | ||||||
|  | 
 | ||||||
|  |       it('should call the onDeactivate hook', inject([AsyncTestCompleter], (async) => { | ||||||
|  |            compile() | ||||||
|  |                .then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})])) | ||||||
|  |                .then((_) => rtr.navigate('/on-deactivate')) | ||||||
|  |                .then((_) => rtr.navigate('/a')) | ||||||
|  |                .then((_) => { | ||||||
|  |                  rootTC.detectChanges(); | ||||||
|  |                  expect(rootTC.nativeElement).toHaveText('A'); | ||||||
|  |                  expect(log).toEqual(['deactivate: /on-deactivate -> /a']); | ||||||
|  |                  async.done(); | ||||||
|  |                }); | ||||||
|  |          })); | ||||||
|  | 
 | ||||||
|  |       it('should wait for a child component\'s onDeactivate hook to resolve before calling its parent\'s', | ||||||
|  |          inject([AsyncTestCompleter], (async) => { | ||||||
|  |            compile() | ||||||
|  |                .then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})])) | ||||||
|  |                .then((_) => rtr.navigate('/parent-deactivate/child-deactivate')) | ||||||
|  |                .then((_) => { | ||||||
|  |                  ObservableWrapper.subscribe<string>(eventBus, (ev) => { | ||||||
|  |                    if (ev.startsWith('deactivate')) { | ||||||
|  |                      completer.resolve(true); | ||||||
|                      rootTC.detectChanges(); |                      rootTC.detectChanges(); | ||||||
|                      expect(rootTC.nativeElement).toHaveText('parent {activate cmp}'); |                      expect(rootTC.nativeElement).toHaveText('parent {deactivate cmp}'); | ||||||
|                      expect(log).toEqual( |                    } | ||||||
|                          'parent activate: null -> /parent-activate/child-activate;activate: null -> /child-activate;'); |                  }); | ||||||
|                      async.done(); |                  rtr.navigate('/a').then((_) => { | ||||||
|                    }); |  | ||||||
|              }); |  | ||||||
|        })); |  | ||||||
| 
 |  | ||||||
|     it('should call the onDeactivate hook', inject([AsyncTestCompleter], (async) => { |  | ||||||
|          compile() |  | ||||||
|              .then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})])) |  | ||||||
|              .then((_) => rtr.navigate('/on-deactivate')) |  | ||||||
|              .then((_) => rtr.navigate('/a')) |  | ||||||
|              .then((_) => { |  | ||||||
|                rootTC.detectChanges(); |  | ||||||
|                expect(rootTC.nativeElement).toHaveText('A'); |  | ||||||
|                expect(log).toEqual('deactivate: /on-deactivate -> /a;'); |  | ||||||
|                async.done(); |  | ||||||
|              }); |  | ||||||
|        })); |  | ||||||
| 
 |  | ||||||
|     it('should wait for a child component\'s onDeactivate hook to resolve before calling its parent\'s', |  | ||||||
|        inject([AsyncTestCompleter], (async) => { |  | ||||||
|          compile() |  | ||||||
|              .then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})])) |  | ||||||
|              .then((_) => rtr.navigate('/parent-deactivate/child-deactivate')) |  | ||||||
|              .then((_) => { |  | ||||||
|                ObservableWrapper.subscribe<string>(eventBus, (ev) => { |  | ||||||
|                  if (ev.startsWith('deactivate')) { |  | ||||||
|                    completer.resolve(true); |  | ||||||
|                    rootTC.detectChanges(); |                    rootTC.detectChanges(); | ||||||
|                    expect(rootTC.nativeElement).toHaveText('parent {deactivate cmp}'); |                    expect(rootTC.nativeElement).toHaveText('A'); | ||||||
|                  } |                    expect(log).toEqual([ | ||||||
|  |                      'deactivate: /child-deactivate -> null', | ||||||
|  |                      'parent deactivate: /parent-deactivate -> /a' | ||||||
|  |                    ]); | ||||||
|  |                    async.done(); | ||||||
|  |                  }); | ||||||
|                }); |                }); | ||||||
|                rtr.navigate('/a').then((_) => { |          })); | ||||||
|  | 
 | ||||||
|  |       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(); |                  rootTC.detectChanges(); | ||||||
|                  expect(rootTC.nativeElement).toHaveText('A'); |                  expect(log).toEqual([]); | ||||||
|                  expect(log).toEqual( |                  expect(rootTC.nativeElement).toHaveText('reuse {A}'); | ||||||
|                      'deactivate: /child-deactivate -> null;parent deactivate: /parent-deactivate/child-deactivate -> /a;'); |                  expect(cmpInstanceCount).toBe(1); | ||||||
|  |                }) | ||||||
|  |                .then((_) => rtr.navigate('/on-reuse/2/b')) | ||||||
|  |                .then((_) => { | ||||||
|  |                  rootTC.detectChanges(); | ||||||
|  |                  expect(log).toEqual(['reuse: /on-reuse/1 -> /on-reuse/2']); | ||||||
|  |                  expect(rootTC.nativeElement).toHaveText('reuse {B}'); | ||||||
|  |                  expect(cmpInstanceCount).toBe(1); | ||||||
|                  async.done(); |                  async.done(); | ||||||
|                }); |                }); | ||||||
|              }); |          })); | ||||||
|        })); |  | ||||||
| 
 |  | ||||||
|     it('should reuse a component when the canReuse hook returns false', |  | ||||||
|        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(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(rootTC.nativeElement).toHaveText('reuse {B}'); |  | ||||||
|                expect(cmpInstanceCount).toBe(1); |  | ||||||
|                async.done(); |  | ||||||
|              }); |  | ||||||
|        })); |  | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|     it('should not reuse a component when the canReuse hook returns false', |       it('should not reuse a component when the canReuse hook returns false', | ||||||
|        inject([AsyncTestCompleter], (async) => { |          inject([AsyncTestCompleter], (async) => { | ||||||
|          compile() |            compile() | ||||||
|              .then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})])) |                .then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})])) | ||||||
|              .then((_) => rtr.navigate('/never-reuse/1/a')) |                .then((_) => rtr.navigate('/never-reuse/1/a')) | ||||||
|              .then((_) => { |                .then((_) => { | ||||||
|                rootTC.detectChanges(); |  | ||||||
|                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(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})])) |  | ||||||
|              .then((_) => { |  | ||||||
|                ObservableWrapper.subscribe<string>(eventBus, (ev) => { |  | ||||||
|                  if (ev.startsWith('canActivate')) { |  | ||||||
|                    completer.resolve(true); |  | ||||||
|                  } |  | ||||||
|                }); |  | ||||||
|                rtr.navigate('/can-activate/a') |  | ||||||
|                    .then((_) => { |  | ||||||
|                      rootTC.detectChanges(); |  | ||||||
|                      expect(rootTC.nativeElement).toHaveText('canActivate {A}'); |  | ||||||
|                      expect(log).toEqual('canActivate: null -> /can-activate/a;'); |  | ||||||
|                      async.done(); |  | ||||||
|                    }); |  | ||||||
|              }); |  | ||||||
|        })); |  | ||||||
| 
 |  | ||||||
|     it('should not navigate when canActivate returns false', |  | ||||||
|        inject([AsyncTestCompleter], (async) => { |  | ||||||
|          compile() |  | ||||||
|              .then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})])) |  | ||||||
|              .then((_) => { |  | ||||||
|                ObservableWrapper.subscribe<string>(eventBus, (ev) => { |  | ||||||
|                  if (ev.startsWith('canActivate')) { |  | ||||||
|                    completer.resolve(false); |  | ||||||
|                  } |  | ||||||
|                }); |  | ||||||
|                rtr.navigate('/can-activate/a') |  | ||||||
|                    .then((_) => { |  | ||||||
|                      rootTC.detectChanges(); |  | ||||||
|                      expect(rootTC.nativeElement).toHaveText(''); |  | ||||||
|                      expect(log).toEqual('canActivate: null -> /can-activate/a;'); |  | ||||||
|                      async.done(); |  | ||||||
|                    }); |  | ||||||
|              }); |  | ||||||
|        })); |  | ||||||
| 
 |  | ||||||
|     it('should navigate away when canDeactivate returns true', |  | ||||||
|        inject([AsyncTestCompleter], (async) => { |  | ||||||
|          compile() |  | ||||||
|              .then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})])) |  | ||||||
|              .then((_) => rtr.navigate('/can-deactivate/a')) |  | ||||||
|              .then((_) => { |  | ||||||
|                rootTC.detectChanges(); |  | ||||||
|                expect(rootTC.nativeElement).toHaveText('canDeactivate {A}'); |  | ||||||
|                expect(log).toEqual(''); |  | ||||||
| 
 |  | ||||||
|                ObservableWrapper.subscribe<string>(eventBus, (ev) => { |  | ||||||
|                  if (ev.startsWith('canDeactivate')) { |  | ||||||
|                    completer.resolve(true); |  | ||||||
|                  } |  | ||||||
|                }); |  | ||||||
| 
 |  | ||||||
|                rtr.navigate('/a').then((_) => { |  | ||||||
|                  rootTC.detectChanges(); |                  rootTC.detectChanges(); | ||||||
|                  expect(rootTC.nativeElement).toHaveText('A'); |                  expect(log).toEqual([]); | ||||||
|                  expect(log).toEqual('canDeactivate: /can-deactivate/a -> /a;'); |                  expect(rootTC.nativeElement).toHaveText('reuse {A}'); | ||||||
|  |                  expect(cmpInstanceCount).toBe(1); | ||||||
|  |                }) | ||||||
|  |                .then((_) => rtr.navigate('/never-reuse/2/b')) | ||||||
|  |                .then((_) => { | ||||||
|  |                  rootTC.detectChanges(); | ||||||
|  |                  expect(log).toEqual([]); | ||||||
|  |                  expect(rootTC.nativeElement).toHaveText('reuse {B}'); | ||||||
|  |                  expect(cmpInstanceCount).toBe(2); | ||||||
|                  async.done(); |                  async.done(); | ||||||
|                }); |                }); | ||||||
|              }); |          })); | ||||||
|        })); |  | ||||||
| 
 | 
 | ||||||
|     it('should not navigate away when canDeactivate returns false', |  | ||||||
|        inject([AsyncTestCompleter], (async) => { |  | ||||||
|          compile() |  | ||||||
|              .then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})])) |  | ||||||
|              .then((_) => rtr.navigate('/can-deactivate/a')) |  | ||||||
|              .then((_) => { |  | ||||||
|                rootTC.detectChanges(); |  | ||||||
|                expect(rootTC.nativeElement).toHaveText('canDeactivate {A}'); |  | ||||||
|                expect(log).toEqual(''); |  | ||||||
| 
 | 
 | ||||||
|                ObservableWrapper.subscribe<string>(eventBus, (ev) => { |       it('should navigate when canActivate returns true', inject([AsyncTestCompleter], (async) => { | ||||||
|                  if (ev.startsWith('canDeactivate')) { |            compile() | ||||||
|                    completer.resolve(false); |                .then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})])) | ||||||
|                  } |                .then((_) => { | ||||||
|  |                  ObservableWrapper.subscribe<string>(eventBus, (ev) => { | ||||||
|  |                    if (ev.startsWith('canActivate')) { | ||||||
|  |                      completer.resolve(true); | ||||||
|  |                    } | ||||||
|  |                  }); | ||||||
|  |                  rtr.navigate('/can-activate/a') | ||||||
|  |                      .then((_) => { | ||||||
|  |                        rootTC.detectChanges(); | ||||||
|  |                        expect(rootTC.nativeElement).toHaveText('canActivate {A}'); | ||||||
|  |                        expect(log).toEqual(['canActivate: null -> /can-activate']); | ||||||
|  |                        async.done(); | ||||||
|  |                      }); | ||||||
|                }); |                }); | ||||||
|  |          })); | ||||||
| 
 | 
 | ||||||
|                rtr.navigate('/a').then((_) => { |       it('should not navigate when canActivate returns false', | ||||||
|  |          inject([AsyncTestCompleter], (async) => { | ||||||
|  |            compile() | ||||||
|  |                .then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})])) | ||||||
|  |                .then((_) => { | ||||||
|  |                  ObservableWrapper.subscribe<string>(eventBus, (ev) => { | ||||||
|  |                    if (ev.startsWith('canActivate')) { | ||||||
|  |                      completer.resolve(false); | ||||||
|  |                    } | ||||||
|  |                  }); | ||||||
|  |                  rtr.navigate('/can-activate/a') | ||||||
|  |                      .then((_) => { | ||||||
|  |                        rootTC.detectChanges(); | ||||||
|  |                        expect(rootTC.nativeElement).toHaveText(''); | ||||||
|  |                        expect(log).toEqual(['canActivate: null -> /can-activate']); | ||||||
|  |                        async.done(); | ||||||
|  |                      }); | ||||||
|  |                }); | ||||||
|  |          })); | ||||||
|  | 
 | ||||||
|  |       it('should navigate away when canDeactivate returns true', | ||||||
|  |          inject([AsyncTestCompleter], (async) => { | ||||||
|  |            compile() | ||||||
|  |                .then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})])) | ||||||
|  |                .then((_) => rtr.navigate('/can-deactivate/a')) | ||||||
|  |                .then((_) => { | ||||||
|                  rootTC.detectChanges(); |                  rootTC.detectChanges(); | ||||||
|                  expect(rootTC.nativeElement).toHaveText('canDeactivate {A}'); |                  expect(rootTC.nativeElement).toHaveText('canDeactivate {A}'); | ||||||
|                  expect(log).toEqual('canDeactivate: /can-deactivate/a -> /a;'); |                  expect(log).toEqual([]); | ||||||
|  | 
 | ||||||
|  |                  ObservableWrapper.subscribe<string>(eventBus, (ev) => { | ||||||
|  |                    if (ev.startsWith('canDeactivate')) { | ||||||
|  |                      completer.resolve(true); | ||||||
|  |                    } | ||||||
|  |                  }); | ||||||
|  | 
 | ||||||
|  |                  rtr.navigate('/a').then((_) => { | ||||||
|  |                    rootTC.detectChanges(); | ||||||
|  |                    expect(rootTC.nativeElement).toHaveText('A'); | ||||||
|  |                    expect(log).toEqual(['canDeactivate: /can-deactivate -> /a']); | ||||||
|  |                    async.done(); | ||||||
|  |                  }); | ||||||
|  |                }); | ||||||
|  |          })); | ||||||
|  | 
 | ||||||
|  |       it('should not navigate away when canDeactivate returns false', | ||||||
|  |          inject([AsyncTestCompleter], (async) => { | ||||||
|  |            compile() | ||||||
|  |                .then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})])) | ||||||
|  |                .then((_) => rtr.navigate('/can-deactivate/a')) | ||||||
|  |                .then((_) => { | ||||||
|  |                  rootTC.detectChanges(); | ||||||
|  |                  expect(rootTC.nativeElement).toHaveText('canDeactivate {A}'); | ||||||
|  |                  expect(log).toEqual([]); | ||||||
|  | 
 | ||||||
|  |                  ObservableWrapper.subscribe<string>(eventBus, (ev) => { | ||||||
|  |                    if (ev.startsWith('canDeactivate')) { | ||||||
|  |                      completer.resolve(false); | ||||||
|  |                    } | ||||||
|  |                  }); | ||||||
|  | 
 | ||||||
|  |                  rtr.navigate('/a').then((_) => { | ||||||
|  |                    rootTC.detectChanges(); | ||||||
|  |                    expect(rootTC.nativeElement).toHaveText('canDeactivate {A}'); | ||||||
|  |                    expect(log).toEqual(['canDeactivate: /can-deactivate -> /a']); | ||||||
|  |                    async.done(); | ||||||
|  |                  }); | ||||||
|  |                }); | ||||||
|  |          })); | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |       it('should run activation and deactivation hooks in the correct order', | ||||||
|  |          inject([AsyncTestCompleter], (async) => { | ||||||
|  |            compile() | ||||||
|  |                .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', | ||||||
|  |                    'onActivate parent: null -> /activation-hooks', | ||||||
|  |                    'onActivate child: null -> /child' | ||||||
|  |                  ]); | ||||||
|  | 
 | ||||||
|  |                  log = []; | ||||||
|  |                  return rtr.navigate('/a'); | ||||||
|  |                }) | ||||||
|  |                .then((_) => { | ||||||
|  |                  expect(log).toEqual([ | ||||||
|  |                    'canDeactivate parent: /activation-hooks -> /a', | ||||||
|  |                    'canDeactivate child: /child -> null', | ||||||
|  |                    'onDeactivate child: /child -> null', | ||||||
|  |                    'onDeactivate parent: /activation-hooks -> /a' | ||||||
|  |                  ]); | ||||||
|                  async.done(); |                  async.done(); | ||||||
|                }); |                }); | ||||||
|              }); |          })); | ||||||
|        })); | 
 | ||||||
|  |       it('should only run reuse hooks when reusing', inject([AsyncTestCompleter], (async) => { | ||||||
|  |            compile() | ||||||
|  |                .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']); | ||||||
|  | 
 | ||||||
|  |                  ObservableWrapper.subscribe<string>(eventBus, (ev) => { | ||||||
|  |                    if (ev.startsWith('canReuse')) { | ||||||
|  |                      completer.resolve(true); | ||||||
|  |                    } | ||||||
|  |                  }); | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|     it('should run activation and deactivation hooks in the correct order', |                  log = []; | ||||||
|        inject([AsyncTestCompleter], (async) => { |                  return rtr.navigate('/reuse-hooks/2'); | ||||||
|          compile() |                }) | ||||||
|              .then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})])) |                .then((_) => { | ||||||
|              .then((_) => rtr.navigate('/activation-hooks/child')) |                  expect(log).toEqual([ | ||||||
|              .then((_) => { |                    'canReuse: /reuse-hooks/1 -> /reuse-hooks/2', | ||||||
|                expect(log).toEqual('canActivate child: null -> /child;' + |                    'onReuse: /reuse-hooks/1 -> /reuse-hooks/2' | ||||||
|                                    'canActivate parent: null -> /activation-hooks/child;' + |                  ]); | ||||||
|                                    'onActivate parent: null -> /activation-hooks/child;' + |                  async.done(); | ||||||
|                                    'onActivate child: null -> /child;'); |  | ||||||
| 
 |  | ||||||
|                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;'); |  | ||||||
|                async.done(); |  | ||||||
|              }); |  | ||||||
|        })); |  | ||||||
| 
 |  | ||||||
|     it('should only run reuse hooks when reusing', inject([AsyncTestCompleter], (async) => { |  | ||||||
|          compile() |  | ||||||
|              .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;'); |  | ||||||
| 
 |  | ||||||
|                ObservableWrapper.subscribe<string>(eventBus, (ev) => { |  | ||||||
|                  if (ev.startsWith('canReuse')) { |  | ||||||
|                    completer.resolve(true); |  | ||||||
|                  } |  | ||||||
|                }); |                }); | ||||||
|  |          })); | ||||||
| 
 | 
 | ||||||
|                log = ''; |       it('should not run reuse hooks when not reusing', inject([AsyncTestCompleter], (async) => { | ||||||
|                return rtr.navigate('/reuse-hooks/2'); |            compile() | ||||||
|              }) |                .then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})])) | ||||||
|              .then((_) => { |                .then((_) => rtr.navigate('/reuse-hooks/1')) | ||||||
|                expect(log).toEqual('canReuse: /reuse-hooks/1 -> /reuse-hooks/2;' + |                .then((_) => { | ||||||
|                                    'onReuse: /reuse-hooks/1 -> /reuse-hooks/2;'); |                  expect(log).toEqual( | ||||||
|                async.done(); |                      ['canActivate: null -> /reuse-hooks/1', 'onActivate: null -> /reuse-hooks/1']); | ||||||
|              }); |  | ||||||
|        })); |  | ||||||
| 
 | 
 | ||||||
|     it('should not run reuse hooks when not reusing', inject([AsyncTestCompleter], (async) => { |                  ObservableWrapper.subscribe<string>(eventBus, (ev) => { | ||||||
|          compile() |                    if (ev.startsWith('canReuse')) { | ||||||
|              .then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})])) |                      completer.resolve(false); | ||||||
|              .then((_) => rtr.navigate('/reuse-hooks/1')) |                    } | ||||||
|              .then((_) => { |                  }); | ||||||
|                expect(log).toEqual('canActivate: null -> /reuse-hooks/1;' + |  | ||||||
|                                    'onActivate: null -> /reuse-hooks/1;'); |  | ||||||
| 
 | 
 | ||||||
|                ObservableWrapper.subscribe<string>(eventBus, (ev) => { |                  log = []; | ||||||
|                  if (ev.startsWith('canReuse')) { |                  return rtr.navigate('/reuse-hooks/2'); | ||||||
|                    completer.resolve(false); |                }) | ||||||
|                  } |                .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' | ||||||
|  |                  ]); | ||||||
|  |                  async.done(); | ||||||
|                }); |                }); | ||||||
|  |          })); | ||||||
| 
 | 
 | ||||||
|                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;'); |  | ||||||
|                async.done(); |  | ||||||
|              }); |  | ||||||
|        })); |  | ||||||
| 
 | 
 | ||||||
|     describe('when clicked', () => { |     describe('when clicked', () => { | ||||||
| 
 | 
 | ||||||
| @ -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; |   name; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| function logHook(name: string, next: Instruction, prev: Instruction) { | function logHook(name: string, next: ComponentInstruction, prev: ComponentInstruction) { | ||||||
|   var message = name + ': ' + (isPresent(prev) ? prev.accumulatedUrl : 'null') + ' -> ' + |   var message = name + ': ' + (isPresent(prev) ? ('/' + prev.urlPath) : 'null') + ' -> ' + | ||||||
|                 (isPresent(next) ? next.accumulatedUrl : 'null') + ';'; |                 (isPresent(next) ? ('/' + next.urlPath) : 'null'); | ||||||
|   log += message; |   log.push(message); | ||||||
|   ObservableWrapper.callNext(eventBus, message); |   ObservableWrapper.callNext(eventBus, message); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @Component({selector: 'activate-cmp'}) | @Component({selector: 'activate-cmp'}) | ||||||
| @View({template: 'activate cmp'}) | @View({template: 'activate cmp'}) | ||||||
| class ActivateCmp implements OnActivate { | 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'}) | @Component({selector: 'parent-activate-cmp'}) | ||||||
| @View({template: `parent {<router-outlet></router-outlet>}`, directives: [RouterOutlet]}) | @View({template: `parent {<router-outlet></router-outlet>}`, directives: [RouterOutlet]}) | ||||||
| @RouteConfig([new Route({path: '/child-activate', component: ActivateCmp})]) | @RouteConfig([new Route({path: '/child-activate', component: ActivateCmp})]) | ||||||
| class ParentActivateCmp implements OnActivate { | class ParentActivateCmp implements OnActivate { | ||||||
|   onActivate(next: Instruction, prev: Instruction): Promise<any> { |   onActivate(next: ComponentInstruction, prev: ComponentInstruction): Promise<any> { | ||||||
|     completer = PromiseWrapper.completer(); |     completer = PromiseWrapper.completer(); | ||||||
|     logHook('parent activate', next, prev); |     logHook('parent activate', next, prev); | ||||||
|     return completer.promise; |     return completer.promise; | ||||||
| @ -684,13 +724,15 @@ class ParentActivateCmp implements OnActivate { | |||||||
| @Component({selector: 'deactivate-cmp'}) | @Component({selector: 'deactivate-cmp'}) | ||||||
| @View({template: 'deactivate cmp'}) | @View({template: 'deactivate cmp'}) | ||||||
| class DeactivateCmp implements OnDeactivate { | 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'}) | @Component({selector: 'deactivate-cmp'}) | ||||||
| @View({template: 'deactivate cmp'}) | @View({template: 'deactivate cmp'}) | ||||||
| class WaitDeactivateCmp implements OnDeactivate { | class WaitDeactivateCmp implements OnDeactivate { | ||||||
|   onDeactivate(next: Instruction, prev: Instruction): Promise<any> { |   onDeactivate(next: ComponentInstruction, prev: ComponentInstruction): Promise<any> { | ||||||
|     completer = PromiseWrapper.completer(); |     completer = PromiseWrapper.completer(); | ||||||
|     logHook('deactivate', next, prev); |     logHook('deactivate', next, prev); | ||||||
|     return completer.promise; |     return completer.promise; | ||||||
| @ -701,7 +743,9 @@ class WaitDeactivateCmp implements OnDeactivate { | |||||||
| @View({template: `parent {<router-outlet></router-outlet>}`, directives: [RouterOutlet]}) | @View({template: `parent {<router-outlet></router-outlet>}`, directives: [RouterOutlet]}) | ||||||
| @RouteConfig([new Route({path: '/child-deactivate', component: WaitDeactivateCmp})]) | @RouteConfig([new Route({path: '/child-deactivate', component: WaitDeactivateCmp})]) | ||||||
| class ParentDeactivateCmp implements OnDeactivate { | 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'}) | @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})]) | @RouteConfig([new Route({path: '/a', component: A}), new Route({path: '/b', component: B})]) | ||||||
| class ReuseCmp implements OnReuse, CanReuse { | class ReuseCmp implements OnReuse, CanReuse { | ||||||
|   constructor() { cmpInstanceCount += 1; } |   constructor() { cmpInstanceCount += 1; } | ||||||
|   canReuse(next: Instruction, prev: Instruction) { return true; } |   canReuse(next: ComponentInstruction, prev: ComponentInstruction) { return true; } | ||||||
|   onReuse(next: Instruction, prev: Instruction) { logHook('reuse', next, prev); } |   onReuse(next: ComponentInstruction, prev: ComponentInstruction) { logHook('reuse', next, prev); } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @Component({selector: 'never-reuse-cmp'}) | @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})]) | @RouteConfig([new Route({path: '/a', component: A}), new Route({path: '/b', component: B})]) | ||||||
| class NeverReuseCmp implements OnReuse, CanReuse { | class NeverReuseCmp implements OnReuse, CanReuse { | ||||||
|   constructor() { cmpInstanceCount += 1; } |   constructor() { cmpInstanceCount += 1; } | ||||||
|   canReuse(next: Instruction, prev: Instruction) { return false; } |   canReuse(next: ComponentInstruction, prev: ComponentInstruction) { return false; } | ||||||
|   onReuse(next: Instruction, prev: Instruction) { logHook('reuse', next, prev); } |   onReuse(next: ComponentInstruction, prev: ComponentInstruction) { logHook('reuse', next, prev); } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @Component({selector: 'can-activate-cmp'}) | @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})]) | @RouteConfig([new Route({path: '/a', component: A}), new Route({path: '/b', component: B})]) | ||||||
| @CanActivate(CanActivateCmp.canActivate) | @CanActivate(CanActivateCmp.canActivate) | ||||||
| class CanActivateCmp { | class CanActivateCmp { | ||||||
|   static canActivate(next: Instruction, prev: Instruction) { |   static canActivate(next: ComponentInstruction, prev: ComponentInstruction): Promise<boolean> { | ||||||
|     completer = PromiseWrapper.completer(); |     completer = PromiseWrapper.completer(); | ||||||
|     logHook('canActivate', next, prev); |     logHook('canActivate', next, prev); | ||||||
|     return completer.promise; |     return completer.promise; | ||||||
| @ -738,7 +782,7 @@ class CanActivateCmp { | |||||||
| @View({template: `canDeactivate {<router-outlet></router-outlet>}`, directives: [RouterOutlet]}) | @View({template: `canDeactivate {<router-outlet></router-outlet>}`, directives: [RouterOutlet]}) | ||||||
| @RouteConfig([new Route({path: '/a', component: A}), new Route({path: '/b', component: B})]) | @RouteConfig([new Route({path: '/a', component: A}), new Route({path: '/b', component: B})]) | ||||||
| class CanDeactivateCmp implements CanDeactivate { | class CanDeactivateCmp implements CanDeactivate { | ||||||
|   canDeactivate(next: Instruction, prev: Instruction) { |   canDeactivate(next: ComponentInstruction, prev: ComponentInstruction): Promise<boolean> { | ||||||
|     completer = PromiseWrapper.completer(); |     completer = PromiseWrapper.completer(); | ||||||
|     logHook('canDeactivate', next, prev); |     logHook('canDeactivate', next, prev); | ||||||
|     return completer.promise; |     return completer.promise; | ||||||
| @ -749,19 +793,23 @@ class CanDeactivateCmp implements CanDeactivate { | |||||||
| @View({template: `child`}) | @View({template: `child`}) | ||||||
| @CanActivate(AllHooksChildCmp.canActivate) | @CanActivate(AllHooksChildCmp.canActivate) | ||||||
| class AllHooksChildCmp implements CanDeactivate, OnDeactivate, OnActivate { | class AllHooksChildCmp implements CanDeactivate, OnDeactivate, OnActivate { | ||||||
|   canDeactivate(next: Instruction, prev: Instruction) { |   canDeactivate(next: ComponentInstruction, prev: ComponentInstruction) { | ||||||
|     logHook('canDeactivate child', next, prev); |     logHook('canDeactivate child', next, prev); | ||||||
|     return true; |     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); |     logHook('canActivate child', next, prev); | ||||||
|     return true; |     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'}) | @Component({selector: 'all-hooks-parent-cmp'}) | ||||||
| @ -769,46 +817,56 @@ class AllHooksChildCmp implements CanDeactivate, OnDeactivate, OnActivate { | |||||||
| @RouteConfig([new Route({path: '/child', component: AllHooksChildCmp})]) | @RouteConfig([new Route({path: '/child', component: AllHooksChildCmp})]) | ||||||
| @CanActivate(AllHooksParentCmp.canActivate) | @CanActivate(AllHooksParentCmp.canActivate) | ||||||
| class AllHooksParentCmp implements CanDeactivate, OnDeactivate, OnActivate { | class AllHooksParentCmp implements CanDeactivate, OnDeactivate, OnActivate { | ||||||
|   canDeactivate(next: Instruction, prev: Instruction) { |   canDeactivate(next: ComponentInstruction, prev: ComponentInstruction) { | ||||||
|     logHook('canDeactivate parent', next, prev); |     logHook('canDeactivate parent', next, prev); | ||||||
|     return true; |     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); |     logHook('canActivate parent', next, prev); | ||||||
|     return true; |     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'}) | @Component({selector: 'reuse-hooks-cmp'}) | ||||||
| @View({template: 'reuse hooks cmp'}) | @View({template: 'reuse hooks cmp'}) | ||||||
| @CanActivate(ReuseHooksCmp.canActivate) | @CanActivate(ReuseHooksCmp.canActivate) | ||||||
| class ReuseHooksCmp implements OnActivate, OnReuse, OnDeactivate, CanReuse, CanDeactivate { | 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(); |     completer = PromiseWrapper.completer(); | ||||||
|     logHook('canReuse', next, prev); |     logHook('canReuse', next, prev); | ||||||
|     return completer.promise; |     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); |     logHook('canDeactivate', next, prev); | ||||||
|     return true; |     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); |     logHook('canActivate', next, prev); | ||||||
|     return true; |     return true; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   onActivate(next: Instruction, prev: Instruction) { logHook('onActivate', next, prev); } |   onActivate(next: ComponentInstruction, prev: ComponentInstruction) { | ||||||
|  |     logHook('onActivate', next, prev); | ||||||
|  |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @Component({selector: 'lifecycle-cmp'}) | @Component({selector: 'lifecycle-cmp'}) | ||||||
| @ -828,3 +886,21 @@ class ReuseHooksCmp implements OnActivate, OnReuse, OnDeactivate, CanReuse, CanD | |||||||
| ]) | ]) | ||||||
| class LifecycleCmp { | 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'; | } from 'angular2/test_lib'; | ||||||
| 
 | 
 | ||||||
| import {PathRecognizer} from 'angular2/src/router/path_recognizer'; | 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'; | import {SyncRouteHandler} from 'angular2/src/router/sync_route_handler'; | ||||||
| 
 | 
 | ||||||
| class DummyClass { | class DummyClass { | ||||||
| @ -41,65 +42,60 @@ export function main() { | |||||||
| 
 | 
 | ||||||
|     describe('querystring params', () => { |     describe('querystring params', () => { | ||||||
|       it('should parse querystring params so long as the recognizer is a root', () => { |       it('should parse querystring params so long as the recognizer is a root', () => { | ||||||
|         var rec = new PathRecognizer('/hello/there', mockRouteHandler, true); |         var rec = new PathRecognizer('/hello/there', mockRouteHandler); | ||||||
|         var params = rec.parseParams('/hello/there?name=igor'); |         var url = parser.parse('/hello/there?name=igor'); | ||||||
|         expect(params).toEqual({'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', |       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 rec = new PathRecognizer('/hello/:name', mockRouteHandler); | ||||||
|            var params = rec.parseParams('/hello/paul?topic=success'); |            var url = parser.parse('/hello/paul?topic=success'); | ||||||
|            expect(params).toEqual({'name': 'paul', 'topic': 'success'}); |            var match = rec.recognize(url); | ||||||
|  |            expect(match.instruction.params).toEqual({'name': 'paul', 'topic': 'success'}); | ||||||
|          }); |          }); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     describe('matrix params', () => { |     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 rec = new PathRecognizer('/hello/:id', mockRouteHandler); |         var url = new Url('hello', new Url('matias', null, null, {'key': 'value'})); | ||||||
|            var params = rec.parseParams('/hello/matias;key=value'); |         var match = rec.recognize(url); | ||||||
| 
 |         expect(match.instruction.params).toEqual({'id': 'matias', 'key': 'value'}); | ||||||
|            expect(params['id']).toEqual('matias'); |  | ||||||
|            expect(params['key']).toEqual('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 recognize a matrix param value on a static path value', () => { |       it('should be parsed on a static path', () => { | ||||||
|         var rec = new PathRecognizer('/static/man', mockRouteHandler); |         var rec = new PathRecognizer('/person', mockRouteHandler); | ||||||
|         var params = rec.parseParams('/static/man;name=dave'); |         var url = new Url('person', null, null, {'name': 'dave'}); | ||||||
|         expect(params['name']).toEqual('dave'); |         var match = rec.recognize(url); | ||||||
|  |         expect(match.instruction.params).toEqual({'name': '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 rec = new PathRecognizer('/wild/*everything', mockRouteHandler); | ||||||
|         var params = rec.parseParams('/wild/super;variable=value'); |         var url = parser.parse('/wild/super;variable=value'); | ||||||
|         expect(params['everything']).toEqual('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 rec = new PathRecognizer('/path', mockRouteHandler); |         var url = new Url('path', null, null, {'one': true, 'two': true, 'three': '3'}); | ||||||
|            var params = rec.parseParams('/path;one;two;three=3'); |         var match = rec.recognize(url); | ||||||
|            expect(params['one']).toEqual(true); |         expect(match.instruction.params).toEqual({'one': true, 'two': true, 'three': '3'}); | ||||||
|            expect(params['two']).toEqual(true); |       }); | ||||||
|            expect(params['three']).toEqual('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 rec = new PathRecognizer('/one/two/three', mockRouteHandler); | 
 | ||||||
|            var params = rec.parseParams('/one;a=1/two;b=2/three;c=3'); |         var three = new Url('three', null, null, {'c': '3'}); | ||||||
|            expect(params).toEqual({'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 { | class HierarchyAppCmp { | ||||||
|   constructor(public router: Router, public location: LocationStrategy) {} |   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 {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 {Route, Redirect} from 'angular2/src/router/route_config_decorator'; | ||||||
|  | import {parser} from 'angular2/src/router/url_parser'; | ||||||
| 
 | 
 | ||||||
| export function main() { | export function main() { | ||||||
|   describe('RouteRecognizer', () => { |   describe('RouteRecognizer', () => { | ||||||
| @ -25,31 +27,31 @@ export function main() { | |||||||
| 
 | 
 | ||||||
|     it('should recognize a static segment', () => { |     it('should recognize a static segment', () => { | ||||||
|       recognizer.config(new Route({path: '/test', component: DummyCmpA})); |       recognizer.config(new Route({path: '/test', component: DummyCmpA})); | ||||||
|       var solution = recognizer.recognize('/test')[0]; |       var solution = recognize(recognizer, '/test'); | ||||||
|       expect(getComponentType(solution)).toEqual(DummyCmpA); |       expect(getComponentType(solution)).toEqual(DummyCmpA); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|     it('should recognize a single slash', () => { |     it('should recognize a single slash', () => { | ||||||
|       recognizer.config(new Route({path: '/', component: DummyCmpA})); |       recognizer.config(new Route({path: '/', component: DummyCmpA})); | ||||||
|       var solution = recognizer.recognize('/')[0]; |       var solution = recognize(recognizer, '/'); | ||||||
|       expect(getComponentType(solution)).toEqual(DummyCmpA); |       expect(getComponentType(solution)).toEqual(DummyCmpA); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|     it('should recognize a dynamic segment', () => { |     it('should recognize a dynamic segment', () => { | ||||||
|       recognizer.config(new Route({path: '/user/:name', component: DummyCmpA})); |       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(getComponentType(solution)).toEqual(DummyCmpA); | ||||||
|       expect(solution.params()).toEqual({'name': 'brian'}); |       expect(solution.params).toEqual({'name': 'brian'}); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|     it('should recognize a star segment', () => { |     it('should recognize a star segment', () => { | ||||||
|       recognizer.config(new Route({path: '/first/*rest', component: DummyCmpA})); |       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(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', () => { |     it('should recognize redirects', () => { | ||||||
|       recognizer.config(new Redirect({path: '/a', redirectTo: '/b'})); |  | ||||||
|       recognizer.config(new Route({path: '/b', component: DummyCmpA})); |       recognizer.config(new Route({path: '/b', component: DummyCmpA})); | ||||||
|       var solutions = recognizer.recognize('/a'); |       recognizer.config(new Redirect({path: '/a', redirectTo: 'b'})); | ||||||
|       expect(solutions.length).toBe(1); |       var solution = recognize(recognizer, '/a'); | ||||||
| 
 |  | ||||||
|       var solution = solutions[0]; |  | ||||||
|       expect(getComponentType(solution)).toEqual(DummyCmpA); |       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', () => { |     it('should not perform root URL redirect on a non-root route', () => { | ||||||
|       recognizer.config(new Redirect({path: '/', redirectTo: '/foo'})); |       recognizer.config(new Redirect({path: '/', redirectTo: '/foo'})); | ||||||
|       recognizer.config(new Route({path: '/bar', component: DummyCmpA})); |       recognizer.config(new Route({path: '/bar', component: DummyCmpA})); | ||||||
|       var solutions = recognizer.recognize('/bar'); |       var solution = recognize(recognizer, '/bar'); | ||||||
|       expect(solutions.length).toBe(1); |       expect(solution.componentType).toEqual(DummyCmpA); | ||||||
| 
 |       expect(solution.urlPath).toEqual('bar'); | ||||||
|       var solution = solutions[0]; |  | ||||||
|       expect(getComponentType(solution)).toEqual(DummyCmpA); |  | ||||||
|       expect(solution.matchedUrl).toEqual('/bar'); |  | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     it('should perform a root URL redirect when only a slash or an empty string is being processed', |  | ||||||
|        () => { |  | ||||||
|          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; |     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})); | ||||||
| 
 | 
 | ||||||
|          solutions = recognizer.recognize('/'); |       var solution; | ||||||
|          expect(solutions[0].matchedUrl).toBe('/matias'); |  | ||||||
| 
 | 
 | ||||||
|          solutions = recognizer.recognize('/fatias'); |       solution = recognize(recognizer, '/'); | ||||||
|          expect(solutions[0].matchedUrl).toBe('/fatias'); |       expect(solution.urlPath).toEqual('matias'); | ||||||
|  | 
 | ||||||
|  |       solution = recognize(recognizer, '/fatias'); | ||||||
|  |       expect(solution.urlPath).toEqual('fatias'); | ||||||
|  | 
 | ||||||
|  |       solution = recognize(recognizer, ''); | ||||||
|  |       expect(solution.urlPath).toEqual('matias'); | ||||||
|  |     }); | ||||||
| 
 | 
 | ||||||
|          solutions = recognizer.recognize(''); |  | ||||||
|          expect(solutions[0].matchedUrl).toBe('/matias'); |  | ||||||
|        }); |  | ||||||
| 
 | 
 | ||||||
|     it('should generate URLs with params', () => { |     it('should generate URLs with params', () => { | ||||||
|       recognizer.config(new Route({path: '/app/user/:name', component: DummyCmpA, as: 'user'})); |       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', () => { |     it('should generate URLs with numeric params', () => { | ||||||
|       recognizer.config(new Route({path: '/app/page/:number', component: DummyCmpA, as: 'page'})); |       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', () => { |     it('should throw in the absence of required params URLs', () => { | ||||||
|       recognizer.config(new Route({path: 'app/user/:name', component: DummyCmpA, as: 'user'})); |       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.'); |           .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]; |     describe('params', () => { | ||||||
|         var params = solution.params(); |       it('should recognize parameters within the URL path', () => { | ||||||
|         expect(params['name']).toEqual('matsko'); |         recognizer.config(new Route({path: 'profile/:name', component: DummyCmpA, as: 'user'})); | ||||||
|         expect(params['comments']).toEqual('all'); |         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', |       it('should generate and populate the given static-based route with querystring params', | ||||||
|          () => { |          () => { | ||||||
|            var recognizer = new RouteRecognizer(true); |  | ||||||
|            recognizer.config( |            recognizer.config( | ||||||
|                new Route({path: 'forum/featured', component: DummyCmpA, as: 'forum-page'})); |                new Route({path: 'forum/featured', component: DummyCmpA, as: 'forum-page'})); | ||||||
| 
 | 
 | ||||||
|            var params = StringMapWrapper.create(); |            var params = {'start': 10, 'end': 100}; | ||||||
|            params['start'] = 10; |  | ||||||
|            params['end'] = 100; |  | ||||||
| 
 | 
 | ||||||
|            var result = recognizer.generate('forum-page', params); |            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); |  | ||||||
|            recognizer.config(new Route({path: 'profile/:name', component: DummyCmpA, as: 'user'})); |  | ||||||
| 
 | 
 | ||||||
|            var solution = recognizer.recognize('/profile/yegor?name=igor')[0]; |       it('should prefer positional params over query params', () => { | ||||||
|            var params = solution.params(); |  | ||||||
|            expect(params['name']).toEqual('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); |  | ||||||
|         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'})); |         recognizer.config(new Route({path: 'profile/:name', component: DummyCmpA, as: 'user'})); | ||||||
| 
 | 
 | ||||||
|         var solution = recognizer.recognize('/profile/matsko;comments=all')[0]; |         var solution = recognize(recognizer, '/profile/yegor?name=igor'); | ||||||
|         var params = solution.params(); |         expect(solution.params).toEqual({'name': 'yegor'}); | ||||||
|         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 = |       it('should ignore matrix params for the top-level component', () => { | ||||||
|                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'})); |         recognizer.config(new Route({path: '/home/:subject', component: DummyCmpA, as: 'user'})); | ||||||
| 
 |         var solution = recognize(recognizer, '/home;sort=asc/zero;one=1?two=2'); | ||||||
|         var solution = recognizer.recognize('/home/zero;one=1?two=2')[0]; |         expect(solution.params).toEqual({'subject': 'zero', 'two': '2'}); | ||||||
|         var params = solution.params(); |  | ||||||
| 
 |  | ||||||
|         expect(params['subject']).toEqual('zero'); |  | ||||||
|         expect(params['one']).toEqual('1'); |  | ||||||
|         expect(params['two']).toBeFalsy(); |  | ||||||
|       }); |       }); | ||||||
|     }); |     }); | ||||||
|   }); |   }); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| function getComponentType(routeMatch: RouteMatch): any { | function recognize(recognizer: RouteRecognizer, url: string): ComponentInstruction { | ||||||
|   return routeMatch.recognizer.handler.componentType; |   return recognizer.recognize(parser.parse(url))[0].instruction; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function getComponentType(routeMatch: ComponentInstruction): any { | ||||||
|  |   return routeMatch.componentType; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| class DummyCmpA {} | class DummyCmpA {} | ||||||
|  | |||||||
| @ -14,6 +14,7 @@ import {Promise, PromiseWrapper} from 'angular2/src/facade/async'; | |||||||
| 
 | 
 | ||||||
| import {RouteRegistry} from 'angular2/src/router/route_registry'; | import {RouteRegistry} from 'angular2/src/router/route_registry'; | ||||||
| import {RouteConfig, Route, AsyncRoute} from 'angular2/src/router/route_config_decorator'; | import {RouteConfig, Route, AsyncRoute} from 'angular2/src/router/route_config_decorator'; | ||||||
|  | import {stringifyInstruction} from 'angular2/src/router/instruction'; | ||||||
| 
 | 
 | ||||||
| export function main() { | export function main() { | ||||||
|   describe('RouteRegistry', () => { |   describe('RouteRegistry', () => { | ||||||
| @ -27,7 +28,7 @@ export function main() { | |||||||
| 
 | 
 | ||||||
|          registry.recognize('/test', RootHostCmp) |          registry.recognize('/test', RootHostCmp) | ||||||
|              .then((instruction) => { |              .then((instruction) => { | ||||||
|                expect(instruction.component).toBe(DummyCmpB); |                expect(instruction.component.componentType).toBe(DummyCmpB); | ||||||
|                async.done(); |                async.done(); | ||||||
|              }); |              }); | ||||||
|        })); |        })); | ||||||
| @ -36,8 +37,10 @@ export function main() { | |||||||
|       registry.config(RootHostCmp, |       registry.config(RootHostCmp, | ||||||
|                       new Route({path: '/first/...', component: DummyParentCmp, as: 'firstCmp'})); |                       new Route({path: '/first/...', component: DummyParentCmp, as: 'firstCmp'})); | ||||||
| 
 | 
 | ||||||
|       expect(registry.generate(['firstCmp', 'secondCmp'], RootHostCmp)).toEqual('first/second'); |       expect(stringifyInstruction(registry.generate(['firstCmp', 'secondCmp'], RootHostCmp))) | ||||||
|       expect(registry.generate(['secondCmp'], DummyParentCmp)).toEqual('second'); |           .toEqual('first/second'); | ||||||
|  |       expect(stringifyInstruction(registry.generate(['secondCmp'], DummyParentCmp))) | ||||||
|  |           .toEqual('second'); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     it('should generate URLs with params', () => { |     it('should generate URLs with params', () => { | ||||||
| @ -45,8 +48,8 @@ export function main() { | |||||||
|           RootHostCmp, |           RootHostCmp, | ||||||
|           new Route({path: '/first/:param/...', component: DummyParentParamCmp, as: 'firstCmp'})); |           new Route({path: '/first/:param/...', component: DummyParentParamCmp, as: 'firstCmp'})); | ||||||
| 
 | 
 | ||||||
|       var url = |       var url = stringifyInstruction(registry.generate( | ||||||
|           registry.generate(['firstCmp', {param: 'one'}, 'secondCmp', {param: 'two'}], RootHostCmp); |           ['firstCmp', {param: 'one'}, 'secondCmp', {param: 'two'}], RootHostCmp)); | ||||||
|       expect(url).toEqual('first/one/second/two'); |       expect(url).toEqual('first/one/second/two'); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
| @ -61,7 +64,8 @@ export function main() { | |||||||
| 
 | 
 | ||||||
|          registry.recognize('/first/second', RootHostCmp) |          registry.recognize('/first/second', RootHostCmp) | ||||||
|              .then((_) => { |              .then((_) => { | ||||||
|                expect(registry.generate(['firstCmp', 'secondCmp'], RootHostCmp)) |                expect( | ||||||
|  |                    stringifyInstruction(registry.generate(['firstCmp', 'secondCmp'], RootHostCmp))) | ||||||
|                    .toEqual('first/second'); |                    .toEqual('first/second'); | ||||||
|                async.done(); |                async.done(); | ||||||
|              }); |              }); | ||||||
| @ -73,14 +77,13 @@ export function main() { | |||||||
|           .toThrowError('Component "RootHostCmp" has no route config.'); |           .toThrowError('Component "RootHostCmp" has no route config.'); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|     it('should prefer static segments to dynamic', inject([AsyncTestCompleter], (async) => { |     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: '/:site', component: DummyCmpB})); | ||||||
|          registry.config(RootHostCmp, new Route({path: '/home', component: DummyCmpA})); |          registry.config(RootHostCmp, new Route({path: '/home', component: DummyCmpA})); | ||||||
| 
 | 
 | ||||||
|          registry.recognize('/home', RootHostCmp) |          registry.recognize('/home', RootHostCmp) | ||||||
|              .then((instruction) => { |              .then((instruction) => { | ||||||
|                expect(instruction.component).toBe(DummyCmpA); |                expect(instruction.component.componentType).toBe(DummyCmpA); | ||||||
|                async.done(); |                async.done(); | ||||||
|              }); |              }); | ||||||
|        })); |        })); | ||||||
| @ -91,7 +94,7 @@ export function main() { | |||||||
| 
 | 
 | ||||||
|          registry.recognize('/home', RootHostCmp) |          registry.recognize('/home', RootHostCmp) | ||||||
|              .then((instruction) => { |              .then((instruction) => { | ||||||
|                expect(instruction.component).toBe(DummyCmpA); |                expect(instruction.component.componentType).toBe(DummyCmpA); | ||||||
|                async.done(); |                async.done(); | ||||||
|              }); |              }); | ||||||
|        })); |        })); | ||||||
| @ -102,7 +105,7 @@ export function main() { | |||||||
| 
 | 
 | ||||||
|          registry.recognize('/some/path', RootHostCmp) |          registry.recognize('/some/path', RootHostCmp) | ||||||
|              .then((instruction) => { |              .then((instruction) => { | ||||||
|                expect(instruction.component).toBe(DummyCmpA); |                expect(instruction.component.componentType).toBe(DummyCmpA); | ||||||
|                async.done(); |                async.done(); | ||||||
|              }); |              }); | ||||||
|        })); |        })); | ||||||
| @ -113,7 +116,7 @@ export function main() { | |||||||
| 
 | 
 | ||||||
|          registry.recognize('/first/second', RootHostCmp) |          registry.recognize('/first/second', RootHostCmp) | ||||||
|              .then((instruction) => { |              .then((instruction) => { | ||||||
|                expect(instruction.component).toBe(DummyCmpA); |                expect(instruction.component.componentType).toBe(DummyCmpA); | ||||||
|                async.done(); |                async.done(); | ||||||
|              }); |              }); | ||||||
|        })); |        })); | ||||||
| @ -127,7 +130,7 @@ export function main() { | |||||||
| 
 | 
 | ||||||
|          registry.recognize('/first/second/third', RootHostCmp) |          registry.recognize('/first/second/third', RootHostCmp) | ||||||
|              .then((instruction) => { |              .then((instruction) => { | ||||||
|                expect(instruction.component).toBe(DummyCmpB); |                expect(instruction.component.componentType).toBe(DummyCmpB); | ||||||
|                async.done(); |                async.done(); | ||||||
|              }); |              }); | ||||||
|        })); |        })); | ||||||
| @ -137,8 +140,8 @@ export function main() { | |||||||
| 
 | 
 | ||||||
|          registry.recognize('/first/second', RootHostCmp) |          registry.recognize('/first/second', RootHostCmp) | ||||||
|              .then((instruction) => { |              .then((instruction) => { | ||||||
|                expect(instruction.component).toBe(DummyParentCmp); |                expect(instruction.component.componentType).toBe(DummyParentCmp); | ||||||
|                expect(instruction.child.component).toBe(DummyCmpB); |                expect(instruction.child.component.componentType).toBe(DummyCmpB); | ||||||
|                async.done(); |                async.done(); | ||||||
|              }); |              }); | ||||||
|        })); |        })); | ||||||
| @ -149,8 +152,8 @@ export function main() { | |||||||
| 
 | 
 | ||||||
|          registry.recognize('/first/second', RootHostCmp) |          registry.recognize('/first/second', RootHostCmp) | ||||||
|              .then((instruction) => { |              .then((instruction) => { | ||||||
|                expect(instruction.component).toBe(DummyAsyncCmp); |                expect(instruction.component.componentType).toBe(DummyAsyncCmp); | ||||||
|                expect(instruction.child.component).toBe(DummyCmpB); |                expect(instruction.child.component.componentType).toBe(DummyCmpB); | ||||||
|                async.done(); |                async.done(); | ||||||
|              }); |              }); | ||||||
|        })); |        })); | ||||||
| @ -162,28 +165,12 @@ export function main() { | |||||||
| 
 | 
 | ||||||
|          registry.recognize('/first/second', RootHostCmp) |          registry.recognize('/first/second', RootHostCmp) | ||||||
|              .then((instruction) => { |              .then((instruction) => { | ||||||
|                expect(instruction.component).toBe(DummyParentCmp); |                expect(instruction.component.componentType).toBe(DummyParentCmp); | ||||||
|                expect(instruction.child.component).toBe(DummyCmpB); |                expect(instruction.child.component.componentType).toBe(DummyCmpB); | ||||||
|                async.done(); |                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', |     it('should throw when a parent config is missing the `...` suffix any of its children add routes', | ||||||
|        () => { |        () => { | ||||||
|          expect(() => |          expect(() => | ||||||
| @ -198,6 +185,40 @@ export function main() { | |||||||
|           .toThrowError('Unexpected "..." before the end of the path for "home/.../fun/".'); |           .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 {bind, Component, View} from 'angular2/angular2'; | ||||||
| 
 | 
 | ||||||
| import {Location, Router, RouterLink} from 'angular2/router'; | import {Location, Router, RouterLink} from 'angular2/router'; | ||||||
|  | import {Instruction, ComponentInstruction} from 'angular2/src/router/instruction'; | ||||||
| 
 | 
 | ||||||
| import { | import {DOM} from 'angular2/src/dom/dom_adapter'; | ||||||
|   DOM |  | ||||||
| } from 'angular2/src/dom/dom_adapter' |  | ||||||
| 
 | 
 | ||||||
|  | var dummyInstruction = new Instruction(new ComponentInstruction('detail', [], null), null, {}); | ||||||
| 
 | 
 | ||||||
|     export function | export function main() { | ||||||
|     main() { |  | ||||||
|   describe('router-link directive', function() { |   describe('router-link directive', function() { | ||||||
| 
 | 
 | ||||||
|     beforeEachBindings( |     beforeEachBindings( | ||||||
| @ -59,7 +58,7 @@ import { | |||||||
|                testComponent.detectChanges(); |                testComponent.detectChanges(); | ||||||
|                // TODO: shouldn't this be just 'click' rather than '^click'?
 |                // TODO: shouldn't this be just 'click' rather than '^click'?
 | ||||||
|                testComponent.query(By.css('a')).triggerEventHandler('^click', {}); |                testComponent.query(By.css('a')).triggerEventHandler('^click', {}); | ||||||
|                expect(router.spy("navigate")).toHaveBeenCalledWith('/detail'); |                expect(router.spy('navigateInstruction')).toHaveBeenCalledWith(dummyInstruction); | ||||||
|                async.done(); |                async.done(); | ||||||
|              }); |              }); | ||||||
|        })); |        })); | ||||||
| @ -100,7 +99,7 @@ class DummyRouter extends SpyObject { | |||||||
| 
 | 
 | ||||||
| function makeDummyRouter() { | function makeDummyRouter() { | ||||||
|   var dr = new DummyRouter(); |   var dr = new DummyRouter(); | ||||||
|   dr.spy('generate').andCallFake((routeParams) => routeParams.join('=')); |   dr.spy('generate').andCallFake((routeParams) => dummyInstruction); | ||||||
|   dr.spy('navigate'); |   dr.spy('navigateInstruction'); | ||||||
|   return dr; |   return dr; | ||||||
| } | } | ||||||
|  | |||||||
| @ -20,6 +20,7 @@ import {Pipeline} from 'angular2/src/router/pipeline'; | |||||||
| import {RouterOutlet} from 'angular2/src/router/router_outlet'; | import {RouterOutlet} from 'angular2/src/router/router_outlet'; | ||||||
| import {SpyLocation} from 'angular2/src/mock/location_mock'; | import {SpyLocation} from 'angular2/src/mock/location_mock'; | ||||||
| import {Location} from 'angular2/src/router/location'; | import {Location} from 'angular2/src/router/location'; | ||||||
|  | import {stringifyInstruction} from 'angular2/src/router/instruction'; | ||||||
| 
 | 
 | ||||||
| import {RouteRegistry} from 'angular2/src/router/route_registry'; | import {RouteRegistry} from 'angular2/src/router/route_registry'; | ||||||
| import {RouteConfig, Route} from 'angular2/src/router/route_config_decorator'; | import {RouteConfig, Route} from 'angular2/src/router/route_config_decorator'; | ||||||
| @ -125,52 +126,54 @@ export function main() { | |||||||
|     it('should generate URLs from the root component when the path starts with /', () => { |     it('should generate URLs from the root component when the path starts with /', () => { | ||||||
|       router.config([new Route({path: '/first/...', component: DummyParentComp, as: 'firstCmp'})]); |       router.config([new Route({path: '/first/...', component: DummyParentComp, as: 'firstCmp'})]); | ||||||
| 
 | 
 | ||||||
|       expect(router.generate(['/firstCmp', 'secondCmp'])).toEqual('/first/second'); |       var instruction = router.generate(['/firstCmp', 'secondCmp']); | ||||||
|       expect(router.generate(['/firstCmp', 'secondCmp'])).toEqual('/first/second'); |       expect(stringifyInstruction(instruction)).toEqual('first/second'); | ||||||
|       expect(router.generate(['/firstCmp/secondCmp'])).toEqual('/first/second'); | 
 | ||||||
|  |       instruction = router.generate(['/firstCmp/secondCmp']); | ||||||
|  |       expect(stringifyInstruction(instruction)).toEqual('first/second'); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     describe('querstring params', () => { |     describe('query string params', () => { | ||||||
|       it('should only apply querystring params if the given URL is on the root router and is terminal', |       it('should use query string params for the root route', () => { | ||||||
|          () => { |         router.config( | ||||||
|            router.config([ |             [new Route({path: '/hi/how/are/you', component: DummyComponent, as: 'greeting-url'})]); | ||||||
|              new Route({path: '/hi/how/are/you', component: DummyComponent, as: 'greeting-url'}) |  | ||||||
|            ]); |  | ||||||
| 
 | 
 | ||||||
|            var path = router.generate(['/greeting-url', {'name': 'brad'}]); |         var instruction = router.generate(['/greeting-url', {'name': 'brad'}]); | ||||||
|            expect(path).toEqual('/hi/how/are/you?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( |            router.config( | ||||||
|                [new Route({path: '/one/two/:three', component: DummyComponent, as: 'number-url'})]); |                [new Route({path: '/one/two/:three', component: DummyComponent, as: 'number-url'})]); | ||||||
| 
 | 
 | ||||||
|            var path = router.generate(['/number-url', {'three': 'three', 'four': 'four'}]); |            var instruction = router.generate(['/number-url', {'three': 'three', 'four': 'four'}]); | ||||||
|            expect(path).toEqual('/one/two/three?four=four'); |            var path = stringifyInstruction(instruction); | ||||||
|  |            expect(path).toEqual('one/two/three?four=four'); | ||||||
|          }); |          }); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     describe('matrix params', () => { |     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( |         router.config( | ||||||
|             [new Route({path: '/first/...', component: DummyParentComp, as: 'firstCmp'})]); |             [new Route({path: '/first/...', component: DummyParentComp, as: 'firstCmp'})]); | ||||||
| 
 | 
 | ||||||
|         var path = |         var instruction = | ||||||
|             router.generate(['/firstCmp', {'key': 'value'}, 'secondCmp', {'project': 'angular'}]); |             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', |       it('should work with named params', () => { | ||||||
|          () => { |         router.config( | ||||||
|            router.config([ |             [new Route({path: '/first/:token/...', component: DummyParentComp, as: 'firstCmp'})]); | ||||||
|              new Route({path: '/first/:token/...', component: DummyParentComp, as: 'firstCmp'}) |  | ||||||
|            ]); |  | ||||||
| 
 | 
 | ||||||
|            var path = |         var instruction = | ||||||
|                router.generate(['/firstCmp', {'token': 'min'}, 'secondCmp', {'author': 'max'}]); |             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