diff --git a/modules/angular1_router/build.js b/modules/angular1_router/build.js index c6bfada381..ddaacbaa67 100644 --- a/modules/angular1_router/build.js +++ b/modules/angular1_router/build.js @@ -6,13 +6,12 @@ var ts = require('typescript'); var files = [ 'lifecycle_annotations_impl.ts', 'url_parser.ts', - 'route_recognizer.ts', + 'path_recognizer.ts', 'route_config_impl.ts', 'async_route_handler.ts', 'sync_route_handler.ts', - 'component_recognizer.ts', + 'route_recognizer.ts', 'instruction.ts', - 'path_recognizer.ts', 'route_config_nomalizer.ts', 'route_lifecycle_reflector.ts', 'route_registry.ts', diff --git a/modules/angular1_router/lib/facades.es5 b/modules/angular1_router/lib/facades.es5 index b82d198056..f60f3b986d 100644 --- a/modules/angular1_router/lib/facades.es5 +++ b/modules/angular1_router/lib/facades.es5 @@ -173,10 +173,6 @@ var StringMapWrapper = { var List = Array; var ListWrapper = { - clear: function (l) { - l.length = 0; - }, - create: function () { return []; }, diff --git a/modules/angular1_router/src/module_template.js b/modules/angular1_router/src/module_template.js index 0e25db57a4..aeed65ea3e 100644 --- a/modules/angular1_router/src/module_template.js +++ b/modules/angular1_router/src/module_template.js @@ -31,9 +31,7 @@ function routerFactory($q, $location, $$directiveIntrospector, $browser, $rootSc // property in a route config exports.assertComponentExists = function () {}; - angular.stringifyInstruction = function (instruction) { - return instruction.toRootUrl(); - }; + angular.stringifyInstruction = exports.stringifyInstruction; var RouteRegistry = exports.RouteRegistry; var RootRouter = exports.RootRouter; diff --git a/modules/angular1_router/src/ng_route_shim.js b/modules/angular1_router/src/ng_route_shim.js index 5ba7aa4fe7..0d3f245b28 100644 --- a/modules/angular1_router/src/ng_route_shim.js +++ b/modules/angular1_router/src/ng_route_shim.js @@ -110,7 +110,7 @@ routeMap[path] = routeCopy; if (route.redirectTo) { - routeDefinition.redirectTo = [routeMap[route.redirectTo].name]; + routeDefinition.redirectTo = route.redirectTo; } else { if (routeCopy.controller && !routeCopy.controllerAs) { console.warn('Route for "' + path + '" should use "controllerAs".'); @@ -123,7 +123,7 @@ } routeDefinition.component = directiveName; - routeDefinition.name = route.name || upperCase(directiveName); + routeDefinition.as = upperCase(directiveName); var directiveController = routeCopy.controller; diff --git a/modules/angular1_router/test/integration/navigation_spec.js b/modules/angular1_router/test/integration/navigation_spec.js index 76b9490bab..1a82f197c6 100644 --- a/modules/angular1_router/test/integration/navigation_spec.js +++ b/modules/angular1_router/test/integration/navigation_spec.js @@ -113,7 +113,8 @@ describe('navigation', function () { }); - it('should work with recursive nested outlets', function () { + // TODO: fix this + xit('should work with recursive nested outlets', function () { registerComponent('recurCmp', { template: '
recur {
}
', $routeConfig: [ @@ -151,8 +152,8 @@ describe('navigation', function () { compile('
'); $router.config([ - { path: '/', redirectTo: ['/User'] }, - { path: '/user', component: 'userCmp', name: 'User' } + { path: '/', redirectTo: '/user' }, + { path: '/user', component: 'userCmp' } ]); $router.navigateByUrl('/'); @@ -166,15 +167,16 @@ describe('navigation', function () { registerComponent('childRouter', { template: '
inner {
}
', $routeConfig: [ - { path: '/new-child', component: 'oneCmp', name: 'NewChild'}, - { path: '/new-child-two', component: 'twoCmp', name: 'NewChildTwo'} + { path: '/old-child', redirectTo: '/new-child' }, + { path: '/new-child', component: 'oneCmp'}, + { path: '/old-child-two', redirectTo: '/new-child-two' }, + { path: '/new-child-two', component: 'twoCmp'} ] }); $router.config([ - { path: '/old-parent/old-child', redirectTo: ['/NewParent', 'NewChild'] }, - { path: '/old-parent/old-child-two', redirectTo: ['/NewParent', 'NewChildTwo'] }, - { path: '/new-parent/...', component: 'childRouter', name: 'NewParent' } + { path: '/old-parent', redirectTo: '/new-parent' }, + { path: '/new-parent/...', component: 'childRouter' } ]); compile('
'); diff --git a/modules/angular1_router/test/integration/shim_spec.js b/modules/angular1_router/test/integration/shim_spec.js index e2edffe552..f074bd50fd 100644 --- a/modules/angular1_router/test/integration/shim_spec.js +++ b/modules/angular1_router/test/integration/shim_spec.js @@ -139,12 +139,11 @@ describe('ngRoute shim', function () { it('should adapt routes with redirects', inject(function ($location) { $routeProvider - .when('/home', { - template: 'welcome home!', - name: 'Home' - }) .when('/', { redirectTo: '/home' + }) + .when('/home', { + template: 'welcome home!' }); $rootScope.$digest(); diff --git a/modules/angular2/src/router/async_route_handler.ts b/modules/angular2/src/router/async_route_handler.ts index a739352151..6e22218344 100644 --- a/modules/angular2/src/router/async_route_handler.ts +++ b/modules/angular2/src/router/async_route_handler.ts @@ -1,19 +1,13 @@ +import {RouteHandler} from './route_handler'; import {Promise, PromiseWrapper} from 'angular2/src/facade/async'; import {isPresent, Type} from 'angular2/src/facade/lang'; -import {RouteHandler} from './route_handler'; -import {RouteData, BLANK_ROUTE_DATA} from './instruction'; - - export class AsyncRouteHandler implements RouteHandler { /** @internal */ _resolvedComponent: Promise = null; componentType: Type; - public data: RouteData; - constructor(private _loader: Function, data: {[key: string]: any} = null) { - this.data = isPresent(data) ? new RouteData(data) : BLANK_ROUTE_DATA; - } + constructor(private _loader: Function, public data?: {[key: string]: any}) {} resolveComponentType(): Promise { if (isPresent(this._resolvedComponent)) { diff --git a/modules/angular2/src/router/component_recognizer.ts b/modules/angular2/src/router/component_recognizer.ts deleted file mode 100644 index 56730566fd..0000000000 --- a/modules/angular2/src/router/component_recognizer.ts +++ /dev/null @@ -1,157 +0,0 @@ -import {isBlank, isPresent} from 'angular2/src/facade/lang'; -import {BaseException, WrappedException} from 'angular2/src/facade/exceptions'; -import {Map, MapWrapper, ListWrapper, StringMapWrapper} from 'angular2/src/facade/collection'; -import {Promise, PromiseWrapper} from 'angular2/src/facade/async'; - -import { - AbstractRecognizer, - RouteRecognizer, - RedirectRecognizer, - RouteMatch -} from './route_recognizer'; -import {Route, AsyncRoute, AuxRoute, Redirect, RouteDefinition} from './route_config_impl'; -import {AsyncRouteHandler} from './async_route_handler'; -import {SyncRouteHandler} from './sync_route_handler'; -import {Url} from './url_parser'; -import {ComponentInstruction} from './instruction'; - - -/** - * `ComponentRecognizer` is responsible for recognizing routes for a single component. - * It is consumed by `RouteRegistry`, which knows how to recognize an entire hierarchy of - * components. - */ -export class ComponentRecognizer { - names = new Map(); - - // map from name to recognizer - auxNames = new Map(); - - // map from starting path to recognizer - auxRoutes = new Map(); - - // TODO: optimize this into a trie - matchers: AbstractRecognizer[] = []; - - defaultRoute: RouteRecognizer = null; - - /** - * returns whether or not the config is terminal - */ - config(config: RouteDefinition): boolean { - var handler; - - if (isPresent(config.name) && config.name[0].toUpperCase() != config.name[0]) { - var suggestedName = config.name[0].toUpperCase() + config.name.substring(1); - throw new BaseException( - `Route "${config.path}" with name "${config.name}" does not begin with an uppercase letter. Route names should be CamelCase like "${suggestedName}".`); - } - - if (config instanceof AuxRoute) { - handler = new SyncRouteHandler(config.component, config.data); - let path = config.path.startsWith('/') ? config.path.substring(1) : config.path; - var recognizer = new RouteRecognizer(config.path, handler); - this.auxRoutes.set(path, recognizer); - if (isPresent(config.name)) { - this.auxNames.set(config.name, recognizer); - } - return recognizer.terminal; - } - - var useAsDefault = false; - - if (config instanceof Redirect) { - let redirector = new RedirectRecognizer(config.path, config.redirectTo); - this._assertNoHashCollision(redirector.hash, config.path); - this.matchers.push(redirector); - return true; - } - - if (config instanceof Route) { - handler = new SyncRouteHandler(config.component, config.data); - useAsDefault = isPresent(config.useAsDefault) && config.useAsDefault; - } else if (config instanceof AsyncRoute) { - handler = new AsyncRouteHandler(config.loader, config.data); - useAsDefault = isPresent(config.useAsDefault) && config.useAsDefault; - } - var recognizer = new RouteRecognizer(config.path, handler); - - this._assertNoHashCollision(recognizer.hash, config.path); - - if (useAsDefault) { - if (isPresent(this.defaultRoute)) { - throw new BaseException(`Only one route can be default`); - } - this.defaultRoute = recognizer; - } - - this.matchers.push(recognizer); - if (isPresent(config.name)) { - this.names.set(config.name, recognizer); - } - return recognizer.terminal; - } - - - private _assertNoHashCollision(hash: string, path) { - this.matchers.forEach((matcher) => { - if (hash == matcher.hash) { - throw new BaseException( - `Configuration '${path}' conflicts with existing route '${matcher.path}'`); - } - }); - } - - - /** - * Given a URL, returns a list of `RouteMatch`es, which are partial recognitions for some route. - */ - recognize(urlParse: Url): Promise[] { - var solutions = []; - - this.matchers.forEach((routeRecognizer: AbstractRecognizer) => { - var pathMatch = routeRecognizer.recognize(urlParse); - - if (isPresent(pathMatch)) { - solutions.push(pathMatch); - } - }); - - return solutions; - } - - recognizeAuxiliary(urlParse: Url): Promise[] { - var routeRecognizer: RouteRecognizer = this.auxRoutes.get(urlParse.path); - if (isPresent(routeRecognizer)) { - return [routeRecognizer.recognize(urlParse)]; - } - - return [PromiseWrapper.resolve(null)]; - } - - hasRoute(name: string): boolean { return this.names.has(name); } - - componentLoaded(name: string): boolean { - return this.hasRoute(name) && isPresent(this.names.get(name).handler.componentType); - } - - loadComponent(name: string): Promise { - return this.names.get(name).handler.resolveComponentType(); - } - - generate(name: string, params: any): ComponentInstruction { - var pathRecognizer: RouteRecognizer = this.names.get(name); - if (isBlank(pathRecognizer)) { - return null; - } - return pathRecognizer.generate(params); - } - - generateAuxiliary(name: string, params: any): ComponentInstruction { - var pathRecognizer: RouteRecognizer = this.auxNames.get(name); - if (isBlank(pathRecognizer)) { - return null; - } - return pathRecognizer.generate(params); - } -} diff --git a/modules/angular2/src/router/instruction.ts b/modules/angular2/src/router/instruction.ts index 13e4e9f192..760394c65e 100644 --- a/modules/angular2/src/router/instruction.ts +++ b/modules/angular2/src/router/instruction.ts @@ -1,7 +1,10 @@ import {Map, MapWrapper, StringMapWrapper, ListWrapper} from 'angular2/src/facade/collection'; +import {unimplemented} from 'angular2/src/facade/exceptions'; import {isPresent, isBlank, normalizeBlank, Type, CONST_EXPR} from 'angular2/src/facade/lang'; -import {Promise, PromiseWrapper} from 'angular2/src/facade/async'; +import {Promise} from 'angular2/src/facade/async'; +import {PathRecognizer} from './path_recognizer'; +import {Url} from './url_parser'; /** * `RouteParams` is an immutable map of parameters for the given route @@ -74,7 +77,7 @@ export class RouteData { get(key: string): any { return normalizeBlank(StringMapWrapper.get(this.data, key)); } } -export var BLANK_ROUTE_DATA = new RouteData(); +var BLANK_ROUTE_DATA = new RouteData(); /** * `Instruction` is a tree of {@link ComponentInstruction}s with all the information needed @@ -103,184 +106,74 @@ export var BLANK_ROUTE_DATA = new RouteData(); * bootstrap(AppCmp, ROUTER_PROVIDERS); * ``` */ -export abstract class Instruction { - public component: ComponentInstruction; - public child: Instruction; - public auxInstruction: {[key: string]: Instruction} = {}; - - get urlPath(): string { return this.component.urlPath; } - - get urlParams(): string[] { return this.component.urlParams; } - - get specificity(): number { - var total = 0; - if (isPresent(this.component)) { - total += this.component.specificity; - } - if (isPresent(this.child)) { - total += this.child.specificity; - } - return total; - } - - abstract resolveComponent(): Promise; - - /** - * converts the instruction into a URL string - */ - toRootUrl(): string { return this.toUrlPath() + this.toUrlQuery(); } - - /** @internal */ - _toNonRootUrl(): string { - return this._stringifyPathMatrixAuxPrefixed() + - (isPresent(this.child) ? this.child._toNonRootUrl() : ''); - } - - toUrlQuery(): string { return this.urlParams.length > 0 ? ('?' + this.urlParams.join('&')) : ''; } +export class Instruction { + constructor(public component: ComponentInstruction, public child: Instruction, + public auxInstruction: {[key: string]: Instruction}) {} /** * Returns a new instruction that shares the state of the existing instruction, but with * the given child {@link Instruction} replacing the existing child. */ replaceChild(child: Instruction): Instruction { - return new ResolvedInstruction(this.component, child, this.auxInstruction); + return new Instruction(this.component, child, this.auxInstruction); } +} - /** - * If the final URL for the instruction is `` - */ - toUrlPath(): string { - return this.urlPath + this._stringifyAux() + - (isPresent(this.child) ? this.child._toNonRootUrl() : ''); +/** + * 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: Url[]) {} +} + +export function stringifyInstruction(instruction: Instruction): string { + return stringifyInstructionPath(instruction) + stringifyInstructionQuery(instruction); +} + +export function stringifyInstructionPath(instruction: Instruction): string { + return instruction.component.urlPath + stringifyAux(instruction) + + stringifyPrimaryPrefixed(instruction.child); +} + +export function stringifyInstructionQuery(instruction: Instruction): string { + return instruction.component.urlParams.length > 0 ? + ('?' + instruction.component.urlParams.join('&')) : + ''; +} + +function stringifyPrimaryPrefixed(instruction: Instruction): string { + var primary = stringifyPrimary(instruction); + if (primary.length > 0) { + primary = '/' + primary; } + return primary; +} - // default instructions override these - toLinkUrl(): string { - return this.urlPath + this._stringifyAux() + - (isPresent(this.child) ? this.child._toLinkUrl() : ''); - } - - // this is the non-root version (called recursively) - /** @internal */ - _toLinkUrl(): string { - return this._stringifyPathMatrixAuxPrefixed() + - (isPresent(this.child) ? this.child._toLinkUrl() : ''); - } - - /** @internal */ - _stringifyPathMatrixAuxPrefixed(): string { - var primary = this._stringifyPathMatrixAux(); - if (primary.length > 0) { - primary = '/' + primary; - } - return primary; - } - - /** @internal */ - _stringifyMatrixParams(): string { - return this.urlParams.length > 0 ? (';' + this.component.urlParams.join(';')) : ''; - } - - /** @internal */ - _stringifyPathMatrixAux(): string { - if (isBlank(this.component)) { - return ''; - } - return this.urlPath + this._stringifyMatrixParams() + this._stringifyAux(); - } - - /** @internal */ - _stringifyAux(): string { - var routes = []; - StringMapWrapper.forEach(this.auxInstruction, (auxInstruction, _) => { - routes.push(auxInstruction._stringifyPathMatrixAux()); - }); - if (routes.length > 0) { - return '(' + routes.join('//') + ')'; - } +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) + + stringifyPrimaryPrefixed(instruction.child); } - -/** - * a resolved instruction has an outlet instruction for itself, but maybe not for... - */ -export class ResolvedInstruction extends Instruction { - constructor(public component: ComponentInstruction, public child: Instruction, - public auxInstruction: {[key: string]: Instruction}) { - super(); - } - - resolveComponent(): Promise { - return PromiseWrapper.resolve(this.component); - } -} - - -/** - * Represents a resolved default route - */ -export class DefaultInstruction extends Instruction { - constructor(public component: ComponentInstruction, public child: DefaultInstruction) { super(); } - - resolveComponent(): Promise { - return PromiseWrapper.resolve(this.component); - } - - toLinkUrl(): string { return ''; } - - /** @internal */ - _toLinkUrl(): string { return ''; } -} - - -/** - * Represents a component that may need to do some redirection or lazy loading at a later time. - */ -export class UnresolvedInstruction extends Instruction { - constructor(private _resolver: () => Promise, private _urlPath: string = '', - private _urlParams: string[] = CONST_EXPR([])) { - super(); - } - - get urlPath(): string { - if (isPresent(this.component)) { - return this.component.urlPath; - } - if (isPresent(this._urlPath)) { - return this._urlPath; - } - return ''; - } - - get urlParams(): string[] { - if (isPresent(this.component)) { - return this.component.urlParams; - } - if (isPresent(this._urlParams)) { - return this._urlParams; - } - return []; - } - - resolveComponent(): Promise { - if (isPresent(this.component)) { - return PromiseWrapper.resolve(this.component); - } - return this._resolver().then((resolution: Instruction) => { - this.child = resolution.child; - return this.component = resolution.component; - }); - } -} - - -export class RedirectInstruction extends ResolvedInstruction { - constructor(component: ComponentInstruction, child: Instruction, - auxInstruction: {[key: string]: Instruction}) { - super(component, child, auxInstruction); +function stringifyAux(instruction: Instruction): string { + var routes = []; + StringMapWrapper.forEach(instruction.auxInstruction, (auxInstruction, _) => { + routes.push(stringifyPrimary(auxInstruction)); + }); + if (routes.length > 0) { + return '(' + routes.join('//') + ')'; } + return ''; } @@ -292,18 +185,67 @@ export class RedirectInstruction extends ResolvedInstruction { * to route lifecycle hooks, like {@link CanActivate}. * * `ComponentInstruction`s are [https://en.wikipedia.org/wiki/Hash_consing](hash consed). You should - * never construct one yourself with "new." Instead, rely on {@link Router/RouteRecognizer} to + * never construct one yourself with "new." Instead, rely on {@link Router/PathRecognizer} to * construct `ComponentInstruction`s. * * You should not modify this object. It should be treated as immutable. */ -export class ComponentInstruction { +export abstract class ComponentInstruction { reuse: boolean = false; - public routeData: RouteData; + public urlPath: string; + public urlParams: string[]; + public params: {[key: string]: any}; - constructor(public urlPath: string, public urlParams: string[], data: RouteData, - public componentType, public terminal: boolean, public specificity: number, - public params: {[key: string]: any} = null) { - this.routeData = isPresent(data) ? data : BLANK_ROUTE_DATA; - } + /** + * Returns the component type of the represented route, or `null` if this instruction + * hasn't been resolved. + */ + get componentType() { return unimplemented(); }; + + /** + * Returns a promise that will resolve to component type of the represented route. + * If this instruction references an {@link AsyncRoute}, the `loader` function of that route + * will run. + */ + abstract resolveComponentType(): Promise; + + /** + * Returns the specificity of the route associated with this `Instruction`. + */ + get specificity() { return unimplemented(); }; + + /** + * Returns `true` if the component type of this instruction has no child {@link RouteConfig}, + * or `false` if it does. + */ + get terminal() { return unimplemented(); }; + + /** + * Returns the route data of the given route that was specified in the {@link RouteDefinition}, + * or an empty object if no route data was specified. + */ + get routeData(): RouteData { return unimplemented(); }; +} + +export class ComponentInstruction_ extends ComponentInstruction { + private _routeData: RouteData; + + constructor(urlPath: string, urlParams: string[], private _recognizer: PathRecognizer, + params: {[key: string]: any} = null) { + super(); + this.urlPath = urlPath; + this.urlParams = urlParams; + this.params = params; + if (isPresent(this._recognizer.handler.data)) { + this._routeData = new RouteData(this._recognizer.handler.data); + } else { + this._routeData = BLANK_ROUTE_DATA; + } + } + + get componentType() { return this._recognizer.handler.componentType; } + resolveComponentType(): Promise { return this._recognizer.handler.resolveComponentType(); } + get specificity() { return this._recognizer.specificity; } + get terminal() { return this._recognizer.terminal; } + get routeData(): RouteData { return this._routeData; } } diff --git a/modules/angular2/src/router/path_recognizer.ts b/modules/angular2/src/router/path_recognizer.ts index 1e03e1271d..01e84e0cc0 100644 --- a/modules/angular2/src/router/path_recognizer.ts +++ b/modules/angular2/src/router/path_recognizer.ts @@ -7,9 +7,12 @@ import { isBlank } from 'angular2/src/facade/lang'; import {BaseException, WrappedException} from 'angular2/src/facade/exceptions'; + import {Map, MapWrapper, StringMapWrapper} from 'angular2/src/facade/collection'; +import {RouteHandler} from './route_handler'; import {Url, RootUrl, serializeParams} from './url_parser'; +import {ComponentInstruction, ComponentInstruction_} from './instruction'; class TouchMap { map: {[key: string]: string} = {}; @@ -30,7 +33,7 @@ class TouchMap { } getUnused(): {[key: string]: any} { - var unused: {[key: string]: any} = {}; + var unused: {[key: string]: any} = StringMapWrapper.create(); var keys = StringMapWrapper.keys(this.keys); keys.forEach(key => unused[key] = StringMapWrapper.get(this.map, key)); return unused; @@ -123,6 +126,7 @@ function parsePathString(route: string): {[key: string]: any} { results.push(new StarSegment(match[1])); } else if (segment == '...') { if (i < limit) { + // TODO (matsko): setup a proper error here ` throw new BaseException(`Unexpected "..." before the end of the path for "${route}".`); } results.push(new ContinuationSegment()); @@ -171,17 +175,23 @@ function assertPath(path: string) { } } +export class PathMatch { + constructor(public instruction: ComponentInstruction, public remaining: Url, + public remainingAux: Url[]) {} +} -/** - * Parses a URL string using a given matcher DSL, and generates URLs from param maps - */ +// represents something like '/foo/:bar' export class PathRecognizer { private _segments: Segment[]; specificity: number; terminal: boolean = true; hash: string; + private _cache: Map = new Map(); - constructor(public path: string) { + + // TODO: cache component instruction instances by params and by ParsedUrl instance + + constructor(public path: string, public handler: RouteHandler) { assertPath(path); var parsed = parsePathString(path); @@ -193,7 +203,8 @@ export class PathRecognizer { this.terminal = !(lastSegment instanceof ContinuationSegment); } - recognize(beginningSegment: Url): {[key: string]: any} { + + recognize(beginningSegment: Url): PathMatch { var nextSegment = beginningSegment; var currentSegment: Url; var positionalParams = {}; @@ -236,6 +247,7 @@ export class PathRecognizer { var urlPath = captured.join('/'); var auxiliary; + var instruction: ComponentInstruction; var urlParams; var allParams; if (isPresent(currentSegment)) { @@ -255,11 +267,12 @@ export class PathRecognizer { auxiliary = []; urlParams = []; } - return {urlPath, urlParams, allParams, auxiliary, nextSegment}; + instruction = this._getInstruction(urlPath, urlParams, this, allParams); + return new PathMatch(instruction, nextSegment, auxiliary); } - generate(params: {[key: string]: any}): {[key: string]: any} { + generate(params: {[key: string]: any}): ComponentInstruction { var paramTokens = new TouchMap(params); var path = []; @@ -275,6 +288,18 @@ export class PathRecognizer { var nonPositionalParams = paramTokens.getUnused(); var urlParams = serializeParams(nonPositionalParams); - return {urlPath, urlParams}; + return this._getInstruction(urlPath, urlParams, this, params); + } + + private _getInstruction(urlPath: string, urlParams: string[], _recognizer: PathRecognizer, + params: {[key: string]: any}): ComponentInstruction { + var hashKey = urlPath + '?' + urlParams.join('?'); + if (this._cache.has(hashKey)) { + return this._cache.get(hashKey); + } + var instruction = new ComponentInstruction_(urlPath, urlParams, _recognizer, params); + this._cache.set(hashKey, instruction); + + return instruction; } } diff --git a/modules/angular2/src/router/route_config_impl.ts b/modules/angular2/src/router/route_config_impl.ts index b52de354df..b56fb0103f 100644 --- a/modules/angular2/src/router/route_config_impl.ts +++ b/modules/angular2/src/router/route_config_impl.ts @@ -21,8 +21,6 @@ export class RouteConfig { * - `name` is an optional `CamelCase` string representing the name of the route. * - `data` is an optional property of any type representing arbitrary route metadata for the given * route. It is injectable via {@link RouteData}. - * - `useAsDefault` is a boolean value. If `true`, the child route will be navigated to if no child - * route is specified during the navigation. * * ### Example * ``` @@ -40,20 +38,16 @@ export class Route implements RouteDefinition { path: string; component: Type; name: string; - useAsDefault: boolean; // added next three properties to work around https://github.com/Microsoft/TypeScript/issues/4107 aux: string = null; loader: Function = null; - redirectTo: any[] = null; - constructor({path, component, name, data, useAsDefault}: { - path: string, - component: Type, name?: string, data?: {[key: string]: any}, useAsDefault?: boolean - }) { + redirectTo: string = null; + constructor({path, component, name, + data}: {path: string, component: Type, name?: string, data?: {[key: string]: any}}) { this.path = path; this.component = component; this.name = name; this.data = data; - this.useAsDefault = useAsDefault; } } @@ -86,8 +80,7 @@ export class AuxRoute implements RouteDefinition { // added next three properties to work around https://github.com/Microsoft/TypeScript/issues/4107 aux: string = null; loader: Function = null; - redirectTo: any[] = null; - useAsDefault: boolean = false; + redirectTo: string = null; constructor({path, component, name}: {path: string, component: Type, name?: string}) { this.path = path; this.component = component; @@ -105,8 +98,6 @@ export class AuxRoute implements RouteDefinition { * - `name` is an optional `CamelCase` string representing the name of the route. * - `data` is an optional property of any type representing arbitrary route metadata for the given * route. It is injectable via {@link RouteData}. - * - `useAsDefault` is a boolean value. If `true`, the child route will be navigated to if no child - * route is specified during the navigation. * * ### Example * ``` @@ -124,37 +115,31 @@ export class AsyncRoute implements RouteDefinition { path: string; loader: Function; name: string; - useAsDefault: boolean; aux: string = null; - constructor({path, loader, name, data, useAsDefault}: { - path: string, - loader: Function, name?: string, data?: {[key: string]: any}, useAsDefault?: boolean - }) { + constructor({path, loader, name, data}: + {path: string, loader: Function, name?: string, data?: {[key: string]: any}}) { this.path = path; this.loader = loader; this.name = name; this.data = data; - this.useAsDefault = useAsDefault; } } /** - * `Redirect` is a type of {@link RouteDefinition} used to route a path to a canonical route. + * `Redirect` is a type of {@link RouteDefinition} used to route a path to an asynchronously loaded + * component. * * It has the following properties: * - `path` is a string that uses the route matcher DSL. - * - `redirectTo` is an array representing the link DSL. - * - * Note that redirects **do not** affect how links are generated. For that, see the `useAsDefault` - * option. + * - `redirectTo` is a string representing the new URL to be matched against. * * ### Example * ``` * import {RouteConfig} from 'angular2/router'; * * @RouteConfig([ - * {path: '/', redirectTo: ['/Home'] }, - * {path: '/home', component: HomeCmp, name: 'Home'} + * {path: '/', redirectTo: '/home'}, + * {path: '/home', component: HomeCmp} * ]) * class MyApp {} * ``` @@ -162,14 +147,13 @@ export class AsyncRoute implements RouteDefinition { @CONST() export class Redirect implements RouteDefinition { path: string; - redirectTo: any[]; + redirectTo: string; name: string = null; // added next three properties to work around https://github.com/Microsoft/TypeScript/issues/4107 loader: Function = null; data: any = null; aux: string = null; - useAsDefault: boolean = false; - constructor({path, redirectTo}: {path: string, redirectTo: any[]}) { + constructor({path, redirectTo}: {path: string, redirectTo: string}) { this.path = path; this.redirectTo = redirectTo; } diff --git a/modules/angular2/src/router/route_config_nomalizer.dart b/modules/angular2/src/router/route_config_nomalizer.dart index 6fe053e62d..2d3005295b 100644 --- a/modules/angular2/src/router/route_config_nomalizer.dart +++ b/modules/angular2/src/router/route_config_nomalizer.dart @@ -1,22 +1,9 @@ library angular2.src.router.route_config_normalizer; import "route_config_decorator.dart"; -import "route_registry.dart"; import "package:angular2/src/facade/exceptions.dart" show BaseException; -RouteDefinition normalizeRouteConfig(RouteDefinition config, RouteRegistry registry) { - if (config is AsyncRoute) { - - configRegistryAndReturnType(componentType) { - registry.configFromComponent(componentType); - return componentType; - } - - loader() { - return config.loader().then(configRegistryAndReturnType); - } - return new AsyncRoute(path: config.path, loader: loader, name: config.name, data: config.data, useAsDefault: config.useAsDefault); - } +RouteDefinition normalizeRouteConfig(RouteDefinition config) { return config; } diff --git a/modules/angular2/src/router/route_config_nomalizer.ts b/modules/angular2/src/router/route_config_nomalizer.ts index 16518fa79f..8d0725dbab 100644 --- a/modules/angular2/src/router/route_config_nomalizer.ts +++ b/modules/angular2/src/router/route_config_nomalizer.ts @@ -2,29 +2,14 @@ import {AsyncRoute, AuxRoute, Route, Redirect, RouteDefinition} from './route_co import {ComponentDefinition} from './route_definition'; import {isType, Type} from 'angular2/src/facade/lang'; import {BaseException, WrappedException} from 'angular2/src/facade/exceptions'; -import {RouteRegistry} from './route_registry'; /** - * Given a JS Object that represents a route config, returns a corresponding Route, AsyncRoute, - * AuxRoute or Redirect object. - * - * Also wraps an AsyncRoute's loader function to add the loaded component's route config to the - * `RouteRegistry`. + * Given a JS Object that represents... returns a corresponding Route, AsyncRoute, or Redirect */ -export function normalizeRouteConfig(config: RouteDefinition, - registry: RouteRegistry): RouteDefinition { - if (config instanceof AsyncRoute) { - var wrappedLoader = wrapLoaderToReconfigureRegistry(config.loader, registry); - return new AsyncRoute({ - path: config.path, - loader: wrappedLoader, - name: config.name, - data: config.data, - useAsDefault: config.useAsDefault - }); - } - if (config instanceof Route || config instanceof Redirect || config instanceof AuxRoute) { +export function normalizeRouteConfig(config: RouteDefinition): RouteDefinition { + if (config instanceof Route || config instanceof Redirect || config instanceof AsyncRoute || + config instanceof AuxRoute) { return config; } @@ -39,13 +24,7 @@ export function normalizeRouteConfig(config: RouteDefinition, config.name = config.as; } if (config.loader) { - var wrappedLoader = wrapLoaderToReconfigureRegistry(config.loader, registry); - return new AsyncRoute({ - path: config.path, - loader: wrappedLoader, - name: config.name, - useAsDefault: config.useAsDefault - }); + return new AsyncRoute({path: config.path, loader: config.loader, name: config.name}); } if (config.aux) { return new AuxRoute({path: config.aux, component:config.component, name: config.name}); @@ -57,17 +36,11 @@ export function normalizeRouteConfig(config: RouteDefinition, return new Route({ path: config.path, component:componentDefinitionObject.constructor, - name: config.name, - data: config.data, - useAsDefault: config.useAsDefault + name: config.name }); } else if (componentDefinitionObject.type == 'loader') { - return new AsyncRoute({ - path: config.path, - loader: componentDefinitionObject.loader, - name: config.name, - useAsDefault: config.useAsDefault - }); + return new AsyncRoute( + {path: config.path, loader: componentDefinitionObject.loader, name: config.name}); } else { throw new BaseException( `Invalid component type "${componentDefinitionObject.type}". Valid types are "constructor" and "loader".`); @@ -77,8 +50,6 @@ export function normalizeRouteConfig(config: RouteDefinition, path: string; component: Type; name?: string; - data?: {[key: string]: any}; - useAsDefault?: boolean; }>config); } @@ -89,16 +60,6 @@ export function normalizeRouteConfig(config: RouteDefinition, return config; } - -function wrapLoaderToReconfigureRegistry(loader: Function, registry: RouteRegistry): Function { - return () => { - return loader().then((componentType) => { - registry.configFromComponent(componentType); - return componentType; - }); - }; -} - export function assertComponentExists(component: Type, path: string): void { if (!isType(component)) { throw new BaseException(`Component for route "${path}" is not defined, or is not a class.`); diff --git a/modules/angular2/src/router/route_definition.dart b/modules/angular2/src/router/route_definition.dart index 79c8b5672b..e5d0a22539 100644 --- a/modules/angular2/src/router/route_definition.dart +++ b/modules/angular2/src/router/route_definition.dart @@ -3,6 +3,5 @@ library angular2.src.router.route_definition; abstract class RouteDefinition { final String path; final String name; - final bool useAsDefault; - const RouteDefinition({this.path, this.name, this.useAsDefault : false}); + const RouteDefinition({this.path, this.name}); } diff --git a/modules/angular2/src/router/route_definition.ts b/modules/angular2/src/router/route_definition.ts index 7d38690ee1..ee1266143d 100644 --- a/modules/angular2/src/router/route_definition.ts +++ b/modules/angular2/src/router/route_definition.ts @@ -16,11 +16,10 @@ export interface RouteDefinition { aux?: string; component?: Type | ComponentDefinition; loader?: Function; - redirectTo?: any[]; + redirectTo?: string; as?: string; name?: string; data?: any; - useAsDefault?: boolean; } export interface ComponentDefinition { diff --git a/modules/angular2/src/router/route_handler.ts b/modules/angular2/src/router/route_handler.ts index 5971267ee8..54ca3b2ef0 100644 --- a/modules/angular2/src/router/route_handler.ts +++ b/modules/angular2/src/router/route_handler.ts @@ -1,9 +1,8 @@ import {Promise, PromiseWrapper} from 'angular2/src/facade/async'; import {Type} from 'angular2/src/facade/lang'; -import {RouteData} from './instruction'; export interface RouteHandler { componentType: Type; resolveComponentType(): Promise; - data: RouteData; + data?: {[key: string]: any}; } diff --git a/modules/angular2/src/router/route_recognizer.ts b/modules/angular2/src/router/route_recognizer.ts index fb0b8973e9..6511fadda7 100644 --- a/modules/angular2/src/router/route_recognizer.ts +++ b/modules/angular2/src/router/route_recognizer.ts @@ -1,119 +1,184 @@ -import {isPresent, isBlank} from 'angular2/src/facade/lang'; -import {BaseException} from 'angular2/src/facade/exceptions'; -import {PromiseWrapper, Promise} from 'angular2/src/facade/promise'; -import {Map} from 'angular2/src/facade/collection'; +import { + RegExp, + RegExpWrapper, + isBlank, + isPresent, + isType, + isStringMap, + Type +} from 'angular2/src/facade/lang'; +import {BaseException, WrappedException} from 'angular2/src/facade/exceptions'; +import {Map, MapWrapper, ListWrapper, StringMapWrapper} from 'angular2/src/facade/collection'; -import {RouteHandler} from './route_handler'; +import {PathRecognizer, PathMatch} from './path_recognizer'; +import {Route, AsyncRoute, AuxRoute, Redirect, RouteDefinition} from './route_config_impl'; +import {AsyncRouteHandler} from './async_route_handler'; +import {SyncRouteHandler} from './sync_route_handler'; import {Url} from './url_parser'; import {ComponentInstruction} from './instruction'; -import {PathRecognizer} from './path_recognizer'; -export abstract class RouteMatch {} +/** + * `RouteRecognizer` is responsible for recognizing routes for a single component. + * It is consumed by `RouteRegistry`, which knows how to recognize an entire hierarchy of + * components. + */ +export class RouteRecognizer { + names = new Map(); -export interface AbstractRecognizer { - hash: string; - path: string; - recognize(beginningSegment: Url): Promise; - generate(params: {[key: string]: any}): ComponentInstruction; -} + // map from name to recognizer + auxNames = new Map(); + + // map from starting path to recognizer + auxRoutes = new Map(); + + // TODO: optimize this into a trie + matchers: PathRecognizer[] = []; + + // TODO: optimize this into a trie + redirects: Redirector[] = []; + + config(config: RouteDefinition): boolean { + var handler; + + if (isPresent(config.name) && config.name[0].toUpperCase() != config.name[0]) { + var suggestedName = config.name[0].toUpperCase() + config.name.substring(1); + throw new BaseException( + `Route "${config.path}" with name "${config.name}" does not begin with an uppercase letter. Route names should be CamelCase like "${suggestedName}".`); + } + + if (config instanceof AuxRoute) { + handler = new SyncRouteHandler(config.component, config.data); + let path = config.path.startsWith('/') ? config.path.substring(1) : config.path; + var recognizer = new PathRecognizer(config.path, handler); + this.auxRoutes.set(path, recognizer); + if (isPresent(config.name)) { + this.auxNames.set(config.name, recognizer); + } + return recognizer.terminal; + } + + if (config instanceof Redirect) { + this.redirects.push(new Redirector(config.path, config.redirectTo)); + return true; + } + + if (config instanceof Route) { + handler = new SyncRouteHandler(config.component, config.data); + } else if (config instanceof AsyncRoute) { + handler = new AsyncRouteHandler(config.loader, config.data); + } + var recognizer = new PathRecognizer(config.path, handler); + + this.matchers.forEach((matcher) => { + if (recognizer.hash == matcher.hash) { + throw new BaseException( + `Configuration '${config.path}' conflicts with existing route '${matcher.path}'`); + } + }); + + this.matchers.push(recognizer); + if (isPresent(config.name)) { + this.names.set(config.name, recognizer); + } + return recognizer.terminal; + } -export class PathMatch extends RouteMatch { - constructor(public instruction: ComponentInstruction, public remaining: Url, - public remainingAux: Url[]) { - super(); + /** + * Given a URL, returns a list of `RouteMatch`es, which are partial recognitions for some route. + * + */ + recognize(urlParse: Url): PathMatch[] { + var solutions = []; + + urlParse = this._redirect(urlParse); + + this.matchers.forEach((pathRecognizer: PathRecognizer) => { + var pathMatch = pathRecognizer.recognize(urlParse); + + if (isPresent(pathMatch)) { + solutions.push(pathMatch); + } + }); + + return solutions; + } + + /** @internal */ + _redirect(urlParse: Url): Url { + for (var i = 0; i < this.redirects.length; i += 1) { + let redirector = this.redirects[i]; + var redirectedUrl = redirector.redirect(urlParse); + if (isPresent(redirectedUrl)) { + return redirectedUrl; + } + } + + return urlParse; + } + + recognizeAuxiliary(urlParse: Url): PathMatch { + var pathRecognizer = this.auxRoutes.get(urlParse.path); + if (isBlank(pathRecognizer)) { + return null; + } + return pathRecognizer.recognize(urlParse); + } + + hasRoute(name: string): boolean { return this.names.has(name); } + + generate(name: string, params: any): ComponentInstruction { + var pathRecognizer: PathRecognizer = this.names.get(name); + if (isBlank(pathRecognizer)) { + return null; + } + return pathRecognizer.generate(params); + } + + generateAuxiliary(name: string, params: any): ComponentInstruction { + var pathRecognizer: PathRecognizer = this.auxNames.get(name); + if (isBlank(pathRecognizer)) { + return null; + } + return pathRecognizer.generate(params); } } +export class Redirector { + segments: string[] = []; + toSegments: string[] = []; -export class RedirectMatch extends RouteMatch { - constructor(public redirectTo: any[], public specificity) { super(); } -} - -export class RedirectRecognizer implements AbstractRecognizer { - private _pathRecognizer: PathRecognizer; - public hash: string; - - constructor(public path: string, public redirectTo: any[]) { - this._pathRecognizer = new PathRecognizer(path); - this.hash = this._pathRecognizer.hash; + constructor(path: string, redirectTo: string) { + if (path.startsWith('/')) { + path = path.substring(1); + } + this.segments = path.split('/'); + if (redirectTo.startsWith('/')) { + redirectTo = redirectTo.substring(1); + } + this.toSegments = redirectTo.split('/'); } /** * Returns `null` or a `ParsedUrl` representing the new path to match */ - recognize(beginningSegment: Url): Promise { - var match = null; - if (isPresent(this._pathRecognizer.recognize(beginningSegment))) { - match = new RedirectMatch(this.redirectTo, this._pathRecognizer.specificity); + redirect(urlParse: Url): Url { + 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 PromiseWrapper.resolve(match); - } - generate(params: {[key: string]: any}): ComponentInstruction { - throw new BaseException(`Tried to generate a redirect.`); - } -} - - -// represents something like '/foo/:bar' -export class RouteRecognizer implements AbstractRecognizer { - specificity: number; - terminal: boolean = true; - hash: string; - - private _cache: Map = new Map(); - private _pathRecognizer: PathRecognizer; - - // TODO: cache component instruction instances by params and by ParsedUrl instance - - constructor(public path: string, public handler: RouteHandler) { - this._pathRecognizer = new PathRecognizer(path); - this.specificity = this._pathRecognizer.specificity; - this.hash = this._pathRecognizer.hash; - this.terminal = this._pathRecognizer.terminal; - } - - recognize(beginningSegment: Url): Promise { - var res = this._pathRecognizer.recognize(beginningSegment); - if (isBlank(res)) { - return null; - } - - return this.handler.resolveComponentType().then((_) => { - var componentInstruction = - this._getInstruction(res['urlPath'], res['urlParams'], res['allParams']); - return new PathMatch(componentInstruction, res['nextSegment'], res['auxiliary']); - }); - } - - generate(params: {[key: string]: any}): ComponentInstruction { - var generated = this._pathRecognizer.generate(params); - var urlPath = generated['urlPath']; - var urlParams = generated['urlParams']; - return this._getInstruction(urlPath, urlParams, params); - } - - generateComponentPathValues(params: {[key: string]: any}): {[key: string]: any} { - return this._pathRecognizer.generate(params); - } - - private _getInstruction(urlPath: string, urlParams: string[], - params: {[key: string]: any}): ComponentInstruction { - if (isBlank(this.handler.componentType)) { - throw new BaseException(`Tried to get instruction before the type was loaded.`); - } - - var hashKey = urlPath + '?' + urlParams.join('?'); - if (this._cache.has(hashKey)) { - return this._cache.get(hashKey); - } - var instruction = - new ComponentInstruction(urlPath, urlParams, this.handler.data, this.handler.componentType, - this.terminal, this.specificity, params); - this._cache.set(hashKey, instruction); - - return instruction; + for (var i = this.toSegments.length - 1; i >= 0; i -= 1) { + let segment = this.toSegments[i]; + urlParse = new Url(segment, urlParse); + } + return urlParse; } } diff --git a/modules/angular2/src/router/route_registry.ts b/modules/angular2/src/router/route_registry.ts index 716cecc84f..c132d363ab 100644 --- a/modules/angular2/src/router/route_registry.ts +++ b/modules/angular2/src/router/route_registry.ts @@ -1,3 +1,6 @@ +import {PathMatch} from './path_recognizer'; +import {RouteRecognizer} from './route_recognizer'; +import {Instruction, ComponentInstruction, PrimaryInstruction} from './instruction'; import {ListWrapper, Map, MapWrapper, StringMapWrapper} from 'angular2/src/facade/collection'; import {Promise, PromiseWrapper} from 'angular2/src/facade/async'; import { @@ -13,7 +16,6 @@ import { getTypeNameForDebugging } from 'angular2/src/facade/lang'; import {BaseException, WrappedException} from 'angular2/src/facade/exceptions'; -import {reflector} from 'angular2/src/core/reflection/reflection'; import { RouteConfig, AsyncRoute, @@ -22,16 +24,7 @@ import { Redirect, RouteDefinition } from './route_config_impl'; -import {PathMatch, RedirectMatch, RouteMatch} from './route_recognizer'; -import {ComponentRecognizer} from './component_recognizer'; -import { - Instruction, - ResolvedInstruction, - RedirectInstruction, - UnresolvedInstruction, - DefaultInstruction -} from './instruction'; - +import {reflector} from 'angular2/src/core/reflection/reflection'; import {Injectable} from 'angular2/angular2'; import {normalizeRouteConfig, assertComponentExists} from './route_config_nomalizer'; import {parser, Url, pathSegmentsToUrl} from './url_parser'; @@ -45,13 +38,13 @@ var _resolveToNull = PromiseWrapper.resolve(null); */ @Injectable() export class RouteRegistry { - private _rules = new Map(); + private _rules = new Map(); /** * Given a component and a configuration object, add the route to this registry */ config(parentComponent: any, config: RouteDefinition): void { - config = normalizeRouteConfig(config, this); + config = normalizeRouteConfig(config); // this is here because Dart type guard reasons if (config instanceof Route) { @@ -60,10 +53,10 @@ export class RouteRegistry { assertComponentExists(config.component, config.path); } - var recognizer: ComponentRecognizer = this._rules.get(parentComponent); + var recognizer: RouteRecognizer = this._rules.get(parentComponent); if (isBlank(recognizer)) { - recognizer = new ComponentRecognizer(); + recognizer = new RouteRecognizer(); this._rules.set(parentComponent, recognizer); } @@ -109,162 +102,102 @@ export class RouteRegistry { * Given a URL and a parent component, return the most specific instruction for navigating * the application into the state specified by the url */ - recognize(url: string, ancestorComponents: any[]): Promise { + recognize(url: string, parentComponent: any): Promise { var parsedUrl = parser.parse(url); - return this._recognize(parsedUrl, ancestorComponents); + return this._recognize(parsedUrl, parentComponent); } + private _recognize(parsedUrl: Url, parentComponent): Promise { + return this._recognizePrimaryRoute(parsedUrl, parentComponent) + .then((instruction: PrimaryInstruction) => + this._completeAuxiliaryRouteMatches(instruction, parentComponent)); + } - /** - * Recognizes all parent-child routes, but creates unresolved auxiliary routes - */ - - private _recognize(parsedUrl: Url, ancestorComponents: any[], - _aux = false): Promise { - var parentComponent = ancestorComponents[ancestorComponents.length - 1]; + private _recognizePrimaryRoute(parsedUrl: Url, parentComponent): Promise { var componentRecognizer = this._rules.get(parentComponent); if (isBlank(componentRecognizer)) { return _resolveToNull; } // Matches some beginning part of the given URL - var possibleMatches: Promise[] = - _aux ? componentRecognizer.recognizeAuxiliary(parsedUrl) : - componentRecognizer.recognize(parsedUrl); + var possibleMatches = componentRecognizer.recognize(parsedUrl); - var matchPromises: Promise[] = possibleMatches.map( - (candidate: Promise) => candidate.then((candidate: RouteMatch) => { - - if (candidate instanceof PathMatch) { - if (candidate.instruction.terminal) { - var unresolvedAux = - this._auxRoutesToUnresolved(candidate.remainingAux, parentComponent); - return new ResolvedInstruction(candidate.instruction, null, unresolvedAux); - } - - var newAncestorComponents = - ancestorComponents.concat([candidate.instruction.componentType]); - - return this._recognize(candidate.remaining, newAncestorComponents) - .then((childInstruction) => { - if (isBlank(childInstruction)) { - return null; - } - - // redirect instructions are already absolute - if (childInstruction instanceof RedirectInstruction) { - return childInstruction; - } - var unresolvedAux = - this._auxRoutesToUnresolved(candidate.remainingAux, parentComponent); - return new ResolvedInstruction(candidate.instruction, childInstruction, - unresolvedAux); - }); - } - - if (candidate instanceof RedirectMatch) { - var instruction = this.generate(candidate.redirectTo, ancestorComponents); - return new RedirectInstruction(instruction.component, instruction.child, - instruction.auxInstruction); - } - })); - - if ((isBlank(parsedUrl) || parsedUrl.path == '') && possibleMatches.length == 0) { - return PromiseWrapper.resolve(this.generateDefault(parentComponent)); - } + var matchPromises = + possibleMatches.map(candidate => this._completePrimaryRouteMatch(candidate)); return PromiseWrapper.all(matchPromises).then(mostSpecific); } - private _auxRoutesToUnresolved(auxRoutes: Url[], parentComponent): {[key: string]: Instruction} { - var unresolvedAuxInstructions: {[key: string]: Instruction} = {}; + private _completePrimaryRouteMatch(partialMatch: PathMatch): Promise { + var instruction = partialMatch.instruction; + return instruction.resolveComponentType().then((componentType) => { + this.configFromComponent(componentType); - auxRoutes.forEach((auxUrl: Url) => { - unresolvedAuxInstructions[auxUrl.path] = new UnresolvedInstruction( - () => { return this._recognize(auxUrl, [parentComponent], true); }); + if (instruction.terminal) { + return new PrimaryInstruction(instruction, null, partialMatch.remainingAux); + } + + return this._recognizePrimaryRoute(partialMatch.remaining, componentType) + .then((childInstruction) => { + if (isBlank(childInstruction)) { + return null; + } else { + return new PrimaryInstruction(instruction, childInstruction, + partialMatch.remainingAux); + } + }); }); - - return unresolvedAuxInstructions; } + private _completeAuxiliaryRouteMatches(instruction: PrimaryInstruction, + parentComponent: any): Promise { + if (isBlank(instruction)) { + return _resolveToNull; + } + + var componentRecognizer = this._rules.get(parentComponent); + var auxInstructions: {[key: string]: Instruction} = {}; + + 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 }]` * generates a url with a leading slash relative to the provided `parentComponent`. - * - * If the optional param `_aux` is `true`, then we generate starting at an auxiliary - * route boundary. */ - generate(linkParams: any[], ancestorComponents: any[], _aux = false): Instruction { - let parentComponent = ancestorComponents[ancestorComponents.length - 1]; - let grandparentComponent = - ancestorComponents.length > 1 ? ancestorComponents[ancestorComponents.length - 2] : null; - - let normalizedLinkParams = splitAndFlattenLinkParams(linkParams); - - var first = ListWrapper.first(normalizedLinkParams); - var rest = ListWrapper.slice(normalizedLinkParams, 1); - - // The first segment should be either '.' (generate from parent) or '' (generate from root). - // When we normalize above, we strip all the slashes, './' becomes '.' and '/' becomes ''. - if (first == '') { - var firstComponent = ancestorComponents[0]; - ListWrapper.clear(ancestorComponents); - ancestorComponents.push(firstComponent); - } else if (first == '..') { - // we already captured the first instance of "..", so we need to pop off an ancestor - ancestorComponents.pop(); - while (ListWrapper.first(rest) == '..') { - rest = ListWrapper.slice(rest, 1); - ancestorComponents.pop(); - if (ancestorComponents.length <= 0) { - throw new BaseException( - `Link "${ListWrapper.toJSON(linkParams)}" has too many "../" segments.`); - } - } - } else if (first != '.') { - // For a link with no leading `./`, `/`, or `../`, we look for a sibling and child. - // If both exist, we throw. Otherwise, we prefer whichever exists. - var childRouteExists = this.hasRoute(first, parentComponent); - var parentRouteExists = - isPresent(grandparentComponent) && this.hasRoute(first, grandparentComponent); - - if (parentRouteExists && childRouteExists) { - let msg = - `Link "${ListWrapper.toJSON(linkParams)}" is ambiguous, use "./" or "../" to disambiguate.`; - throw new BaseException(msg); - } - if (parentRouteExists) { - ancestorComponents.pop(); - } - rest = linkParams; - } - - if (rest[rest.length - 1] == '') { - rest.pop(); - } - - if (rest.length < 1) { - let msg = `Link "${ListWrapper.toJSON(linkParams)}" must include a route name.`; - throw new BaseException(msg); - } - - return this._generate(rest, ancestorComponents, _aux); - } - - - /* - * Internal helper that does not make any assertions about the beginning of the link DSL - */ - private _generate(linkParams: any[], ancestorComponents: any[], _aux = false): Instruction { - let parentComponent = ancestorComponents[ancestorComponents.length - 1]; - - if (linkParams.length == 0) { - return this.generateDefault(parentComponent); - } + generate(linkParams: any[], parentComponent: any, _aux = false): Instruction { let linkIndex = 0; let routeName = linkParams[linkIndex]; + // TODO: this is kind of odd but it makes existing assertions pass + if (isBlank(parentComponent)) { + throw new BaseException(`Could not find route named "${routeName}".`); + } + if (!isString(routeName)) { throw new BaseException(`Unexpected segment "${routeName}" in link DSL. Expected a string.`); } else if (routeName == '' || routeName == '.' || routeName == '..') { @@ -283,10 +216,7 @@ export class RouteRegistry { let auxInstructions: {[key: string]: Instruction} = {}; var nextSegment; while (linkIndex + 1 < linkParams.length && isArray(nextSegment = linkParams[linkIndex + 1])) { - let auxInstruction = this._generate(nextSegment, [parentComponent], true); - - // TODO: this will not work for aux routes with parameters or multiple segments - auxInstructions[auxInstruction.component.urlPath] = auxInstruction; + auxInstructions[nextSegment[0]] = this.generate(nextSegment, parentComponent, true); linkIndex += 1; } @@ -296,105 +226,74 @@ export class RouteRegistry { `Component "${getTypeNameForDebugging(parentComponent)}" has no route config.`); } - var routeRecognizer = - (_aux ? componentRecognizer.auxNames : componentRecognizer.names).get(routeName); + var componentInstruction = _aux ? componentRecognizer.generateAuxiliary(routeName, params) : + componentRecognizer.generate(routeName, params); - if (!isPresent(routeRecognizer)) { + if (isBlank(componentInstruction)) { throw new BaseException( `Component "${getTypeNameForDebugging(parentComponent)}" has no route named "${routeName}".`); } - if (!isPresent(routeRecognizer.handler.componentType)) { - var compInstruction = routeRecognizer.generateComponentPathValues(params); - return new UnresolvedInstruction(() => { - return routeRecognizer.handler.resolveComponentType().then( - (_) => { return this._generate(linkParams, ancestorComponents, _aux); }); - }, compInstruction['urlPath'], compInstruction['urlParams']); + var childInstruction = null; + if (linkIndex + 1 < linkParams.length) { + var remaining = linkParams.slice(linkIndex + 1); + childInstruction = this.generate(remaining, componentInstruction.componentType); + } else if (!componentInstruction.terminal) { + throw new BaseException( + `Link "${ListWrapper.toJSON(linkParams)}" does not resolve to a terminal or async instruction.`); } - var componentInstruction = _aux ? componentRecognizer.generateAuxiliary(routeName, params) : - componentRecognizer.generate(routeName, params); - - - var childInstruction: Instruction = null; - - var remaining = linkParams.slice(linkIndex + 1); - - // the component is sync - if (isPresent(componentInstruction.componentType)) { - if (linkIndex + 1 < linkParams.length) { - let childAncestorComponents = - ancestorComponents.concat([componentInstruction.componentType]); - childInstruction = this._generate(remaining, childAncestorComponents); - } else if (!componentInstruction.terminal) { - // ... look for defaults - childInstruction = this.generateDefault(componentInstruction.componentType); - - if (isBlank(childInstruction)) { - throw new BaseException( - `Link "${ListWrapper.toJSON(linkParams)}" does not resolve to a terminal instruction.`); - } - } - } - - return new ResolvedInstruction(componentInstruction, childInstruction, auxInstructions); + return new Instruction(componentInstruction, childInstruction, auxInstructions); } public hasRoute(name: string, parentComponent: any): boolean { - var componentRecognizer: ComponentRecognizer = this._rules.get(parentComponent); + var componentRecognizer: RouteRecognizer = this._rules.get(parentComponent); if (isBlank(componentRecognizer)) { return false; } return componentRecognizer.hasRoute(name); } - public generateDefault(componentCursor: Type): Instruction { + // if the child includes a redirect like : "/" -> "/something", + // we want to honor that redirection when creating the link + private _generateRedirects(componentCursor: Type): Instruction { if (isBlank(componentCursor)) { return null; } - var componentRecognizer = this._rules.get(componentCursor); - if (isBlank(componentRecognizer) || isBlank(componentRecognizer.defaultRoute)) { + if (isBlank(componentRecognizer)) { return null; } + for (let i = 0; i < componentRecognizer.redirects.length; i += 1) { + let redirect = componentRecognizer.redirects[i]; - var defaultChild = null; - if (isPresent(componentRecognizer.defaultRoute.handler.componentType)) { - var componentInstruction = componentRecognizer.defaultRoute.generate({}); - if (!componentRecognizer.defaultRoute.terminal) { - defaultChild = this.generateDefault(componentRecognizer.defaultRoute.handler.componentType); + // we only handle redirecting from an empty segment + if (redirect.segments.length == 1 && redirect.segments[0] == '') { + var toSegments = pathSegmentsToUrl(redirect.toSegments); + var matches = componentRecognizer.recognize(toSegments); + var primaryInstruction = + ListWrapper.maximum(matches, (match: PathMatch) => match.instruction.specificity); + + if (isPresent(primaryInstruction)) { + var child = this._generateRedirects(primaryInstruction.instruction.componentType); + return new Instruction(primaryInstruction.instruction, child, {}); + } + return null; } - return new DefaultInstruction(componentInstruction, defaultChild); } - return new UnresolvedInstruction(() => { - return componentRecognizer.defaultRoute.handler.resolveComponentType().then( - () => this.generateDefault(componentCursor)); - }); + return null; } } -/* - * Given: ['/a/b', {c: 2}] - * Returns: ['', 'a', 'b', {c: 2}] - */ -function splitAndFlattenLinkParams(linkParams: any[]): any[] { - return linkParams.reduce((accumulation: any[], item) => { - if (isString(item)) { - let strItem: string = item; - return accumulation.concat(strItem.split('/')); - } - accumulation.push(item); - return accumulation; - }, []); -} /* * Given a list of instructions, returns the most specific instruction */ -function mostSpecific(instructions: Instruction[]): Instruction { - return ListWrapper.maximum(instructions, (instruction: Instruction) => instruction.specificity); +function mostSpecific(instructions: PrimaryInstruction[]): PrimaryInstruction { + return ListWrapper.maximum( + instructions, (instruction: PrimaryInstruction) => instruction.component.specificity); } function assertTerminalComponent(component, path) { diff --git a/modules/angular2/src/router/router.ts b/modules/angular2/src/router/router.ts index fbf9701ae3..5eb4a31ec9 100644 --- a/modules/angular2/src/router/router.ts +++ b/modules/angular2/src/router/router.ts @@ -6,6 +6,9 @@ import {RouteRegistry} from './route_registry'; import { ComponentInstruction, Instruction, + stringifyInstruction, + stringifyInstructionPath, + stringifyInstructionQuery } from './instruction'; import {RouterOutlet} from './router_outlet'; import {Location} from './location'; @@ -209,7 +212,7 @@ export class Router { if (result) { return this.commit(instruction, _skipLocationChange) .then((_) => { - this._emitNavigationFinish(instruction.toRootUrl()); + this._emitNavigationFinish(stringifyInstruction(instruction)); return true; }); } @@ -217,20 +220,25 @@ export class Router { }); } + // TODO(btford): it'd be nice to remove this method as part of cleaning up the traversal logic + // Since refactoring `Router.generate` to return an instruction rather than a string, it's not + // guaranteed that the `componentType`s for the terminal async routes have been loaded by the time + // we begin navigation. The method below simply traverses instructions and resolves any components + // for which `componentType` is not present /** @internal */ _settleInstruction(instruction: Instruction): Promise { - return instruction.resolveComponent().then((_) => { - var unsettledInstructions: Array> = []; - - if (isPresent(instruction.child)) { - unsettledInstructions.push(this._settleInstruction(instruction.child)); - } - - StringMapWrapper.forEach(instruction.auxInstruction, (instruction, _) => { - unsettledInstructions.push(this._settleInstruction(instruction)); - }); - return PromiseWrapper.all(unsettledInstructions); + var unsettledInstructions: Array> = []; + if (isBlank(instruction.component.componentType)) { + unsettledInstructions.push(instruction.component.resolveComponentType().then( + (type: Type) => { this.registry.configFromComponent(type); })); + } + if (isPresent(instruction.child)) { + unsettledInstructions.push(this._settleInstruction(instruction.child)); + } + StringMapWrapper.forEach(instruction.auxInstruction, (instruction, _) => { + unsettledInstructions.push(this._settleInstruction(instruction)); }); + return PromiseWrapper.all(unsettledInstructions); } private _emitNavigationFinish(url): void { ObservableWrapper.callEmit(this._subject, url); } @@ -370,22 +378,7 @@ export class Router { * Given a URL, returns an instruction representing the component graph */ recognize(url: string): Promise { - var ancestorComponents = this._getAncestorComponents(); - return this.registry.recognize(url, ancestorComponents); - } - - /** - * get all the host components for this and - */ - private _getAncestorComponents(): any[] { - var ancestorComponents = []; - var ancestorRouter = this; - do { - ancestorComponents.unshift(ancestorRouter.hostComponent); - ancestorRouter = ancestorRouter.parent; - } while (isPresent(ancestorRouter)); - - return ancestorComponents; + return this.registry.recognize(url, this.hostComponent); } @@ -406,27 +399,67 @@ export class Router { * app's base href. */ generate(linkParams: any[]): Instruction { - var ancestorComponents = this._getAncestorComponents(); - var startingNumberOfAncestors = ancestorComponents.length; + let normalizedLinkParams = splitAndFlattenLinkParams(linkParams); - var nextInstruction = this.registry.generate(linkParams, ancestorComponents); - if (isBlank(nextInstruction)) { - return null; - } + var first = ListWrapper.first(normalizedLinkParams); + var rest = ListWrapper.slice(normalizedLinkParams, 1); - var parentInstructionsToClone = startingNumberOfAncestors - ancestorComponents.length; + var router = this; - var router = this.parent; - for (var i = 0; i < parentInstructionsToClone; i++) { - if (isBlank(router)) { - break; + // The first segment should be either '.' (generate from parent) or '' (generate from root). + // When we normalize above, we strip all the slashes, './' becomes '.' and '/' becomes ''. + if (first == '') { + while (isPresent(router.parent)) { + router = router.parent; } + } else if (first == '..') { router = router.parent; + while (ListWrapper.first(rest) == '..') { + rest = ListWrapper.slice(rest, 1); + router = router.parent; + if (isBlank(router)) { + throw new BaseException( + `Link "${ListWrapper.toJSON(linkParams)}" has too many "../" segments.`); + } + } + } else if (first != '.') { + // For a link with no leading `./`, `/`, or `../`, we look for a sibling and child. + // If both exist, we throw. Otherwise, we prefer whichever exists. + var childRouteExists = this.registry.hasRoute(first, this.hostComponent); + var parentRouteExists = + isPresent(this.parent) && this.registry.hasRoute(first, this.parent.hostComponent); + + if (parentRouteExists && childRouteExists) { + let msg = + `Link "${ListWrapper.toJSON(linkParams)}" is ambiguous, use "./" or "../" to disambiguate.`; + throw new BaseException(msg); + } + if (parentRouteExists) { + router = this.parent; + } + rest = linkParams; } - while (isPresent(router) && isPresent(router._currentInstruction)) { - nextInstruction = router._currentInstruction.replaceChild(nextInstruction); - router = router.parent; + if (rest[rest.length - 1] == '') { + rest.pop(); + } + + if (rest.length < 1) { + let msg = `Link "${ListWrapper.toJSON(linkParams)}" must include a route name.`; + throw new BaseException(msg); + } + + var nextInstruction = this.registry.generate(rest, router.hostComponent); + + var url = []; + var parent = router.parent; + while (isPresent(parent)) { + url.unshift(parent._currentInstruction); + parent = parent.parent; + } + + while (url.length > 0) { + nextInstruction = url.pop().replaceChild(nextInstruction); } return nextInstruction; @@ -449,8 +482,8 @@ export class RootRouter extends Router { } commit(instruction: Instruction, _skipLocationChange: boolean = false): Promise { - var emitPath = instruction.toUrlPath(); - var emitQuery = instruction.toUrlQuery(); + var emitPath = stringifyInstructionPath(instruction); + var emitQuery = stringifyInstructionQuery(instruction); if (emitPath.length > 0) { emitPath = '/' + emitPath; } @@ -488,6 +521,20 @@ class ChildRouter extends Router { } } +/* + * Given: ['/a/b', {c: 2}] + * Returns: ['', 'a', 'b', {c: 2}] + */ +function splitAndFlattenLinkParams(linkParams: any[]): any[] { + return linkParams.reduce((accumulation: any[], item) => { + if (isString(item)) { + let strItem: string = item; + return accumulation.concat(strItem.split('/')); + } + accumulation.push(item); + return accumulation; + }, []); +} function canActivateOne(nextInstruction: Instruction, prevInstruction: Instruction): Promise { diff --git a/modules/angular2/src/router/router_link.ts b/modules/angular2/src/router/router_link.ts index 0ca76bb4ec..cf96551b00 100644 --- a/modules/angular2/src/router/router_link.ts +++ b/modules/angular2/src/router/router_link.ts @@ -3,7 +3,7 @@ import {isString} from 'angular2/src/facade/lang'; import {Router} from './router'; import {Location} from './location'; -import {Instruction} from './instruction'; +import {Instruction, stringifyInstruction} from './instruction'; /** * The RouterLink directive lets you link to specific parts of your app. @@ -61,7 +61,7 @@ export class RouterLink { this._routeParams = changes; this._navigationInstruction = this._router.generate(this._routeParams); - var navigationHref = this._navigationInstruction.toLinkUrl(); + var navigationHref = stringifyInstruction(this._navigationInstruction); this.visibleHref = this._location.prepareExternalUrl(navigationHref); } diff --git a/modules/angular2/src/router/sync_route_handler.ts b/modules/angular2/src/router/sync_route_handler.ts index 1b951a098a..5ad7f0aa8d 100644 --- a/modules/angular2/src/router/sync_route_handler.ts +++ b/modules/angular2/src/router/sync_route_handler.ts @@ -1,19 +1,13 @@ -import {Promise, PromiseWrapper} from 'angular2/src/facade/async'; -import {isPresent, Type} from 'angular2/src/facade/lang'; - import {RouteHandler} from './route_handler'; -import {RouteData, BLANK_ROUTE_DATA} from './instruction'; - +import {Promise, PromiseWrapper} from 'angular2/src/facade/async'; +import {Type} from 'angular2/src/facade/lang'; export class SyncRouteHandler implements RouteHandler { - public data: RouteData; - /** @internal */ _resolvedComponent: Promise = null; - constructor(public componentType: Type, data?: {[key: string]: any}) { + constructor(public componentType: Type, public data?: {[key: string]: any}) { this._resolvedComponent = PromiseWrapper.resolve(componentType); - this.data = isPresent(data) ? new RouteData(data) : BLANK_ROUTE_DATA; } resolveComponentType(): Promise { return this._resolvedComponent; } diff --git a/modules/angular2/test/router/component_recognizer_spec.ts b/modules/angular2/test/router/component_recognizer_spec.ts deleted file mode 100644 index 5924bf5b73..0000000000 --- a/modules/angular2/test/router/component_recognizer_spec.ts +++ /dev/null @@ -1,216 +0,0 @@ -import { - AsyncTestCompleter, - describe, - it, - iit, - ddescribe, - expect, - inject, - beforeEach, - SpyObject -} from 'angular2/testing_internal'; - -import {Map, StringMapWrapper} from 'angular2/src/facade/collection'; - -import {RouteMatch, PathMatch, RedirectMatch} from 'angular2/src/router/route_recognizer'; -import {ComponentRecognizer} from 'angular2/src/router/component_recognizer'; - -import {Route, Redirect} from 'angular2/src/router/route_config_decorator'; -import {parser} from 'angular2/src/router/url_parser'; -import {Promise, PromiseWrapper} from 'angular2/src/facade/promise'; - - -export function main() { - describe('ComponentRecognizer', () => { - var recognizer: ComponentRecognizer; - - beforeEach(() => { recognizer = new ComponentRecognizer(); }); - - - it('should recognize a static segment', inject([AsyncTestCompleter], (async) => { - recognizer.config(new Route({path: '/test', component: DummyCmpA})); - recognize(recognizer, '/test') - .then((solutions: RouteMatch[]) => { - expect(solutions.length).toBe(1); - expect(getComponentType(solutions[0])).toEqual(DummyCmpA); - async.done(); - }); - })); - - - it('should recognize a single slash', inject([AsyncTestCompleter], (async) => { - recognizer.config(new Route({path: '/', component: DummyCmpA})); - recognize(recognizer, '/') - .then((solutions: RouteMatch[]) => { - expect(solutions.length).toBe(1); - expect(getComponentType(solutions[0])).toEqual(DummyCmpA); - async.done(); - }); - })); - - - it('should recognize a dynamic segment', inject([AsyncTestCompleter], (async) => { - recognizer.config(new Route({path: '/user/:name', component: DummyCmpA})); - recognize(recognizer, '/user/brian') - .then((solutions: RouteMatch[]) => { - expect(solutions.length).toBe(1); - expect(getComponentType(solutions[0])).toEqual(DummyCmpA); - expect(getParams(solutions[0])).toEqual({'name': 'brian'}); - async.done(); - }); - })); - - - it('should recognize a star segment', inject([AsyncTestCompleter], (async) => { - recognizer.config(new Route({path: '/first/*rest', component: DummyCmpA})); - recognize(recognizer, '/first/second/third') - .then((solutions: RouteMatch[]) => { - expect(solutions.length).toBe(1); - expect(getComponentType(solutions[0])).toEqual(DummyCmpA); - expect(getParams(solutions[0])).toEqual({'rest': 'second/third'}); - async.done(); - }); - })); - - - it('should throw when given two routes that start with the same static segment', () => { - recognizer.config(new Route({path: '/hello', component: DummyCmpA})); - expect(() => recognizer.config(new Route({path: '/hello', component: DummyCmpB}))) - .toThrowError('Configuration \'/hello\' conflicts with existing route \'/hello\''); - }); - - - it('should throw when given two routes that have dynamic segments in the same order', () => { - recognizer.config(new Route({path: '/hello/:person/how/:doyoudou', component: DummyCmpA})); - expect(() => recognizer.config( - new Route({path: '/hello/:friend/how/:areyou', component: DummyCmpA}))) - .toThrowError( - 'Configuration \'/hello/:friend/how/:areyou\' conflicts with existing route \'/hello/:person/how/:doyoudou\''); - - expect(() => recognizer.config( - new Redirect({path: '/hello/:pal/how/:goesit', redirectTo: ['/Foo']}))) - .toThrowError( - 'Configuration \'/hello/:pal/how/:goesit\' conflicts with existing route \'/hello/:person/how/:doyoudou\''); - }); - - - it('should recognize redirects', inject([AsyncTestCompleter], (async) => { - recognizer.config(new Route({path: '/b', component: DummyCmpA})); - recognizer.config(new Redirect({path: '/a', redirectTo: ['B']})); - recognize(recognizer, '/a') - .then((solutions: RouteMatch[]) => { - expect(solutions.length).toBe(1); - var solution = solutions[0]; - expect(solution).toBeAnInstanceOf(RedirectMatch); - if (solution instanceof RedirectMatch) { - expect(solution.redirectTo).toEqual(['B']); - } - async.done(); - }); - })); - - - it('should generate URLs with params', () => { - recognizer.config(new Route({path: '/app/user/:name', component: DummyCmpA, name: 'User'})); - var instruction = recognizer.generate('User', {'name': 'misko'}); - expect(instruction.urlPath).toEqual('app/user/misko'); - }); - - - it('should generate URLs with numeric params', () => { - recognizer.config(new Route({path: '/app/page/:number', component: DummyCmpA, name: 'Page'})); - expect(recognizer.generate('Page', {'number': 42}).urlPath).toEqual('app/page/42'); - }); - - - it('should throw in the absence of required params URLs', () => { - recognizer.config(new Route({path: 'app/user/:name', component: DummyCmpA, name: 'User'})); - expect(() => recognizer.generate('User', {})) - .toThrowError('Route generator for \'name\' was not included in parameters passed.'); - }); - - - it('should throw if the route alias is not TitleCase', () => { - expect(() => recognizer.config( - new Route({path: 'app/user/:name', component: DummyCmpA, name: 'user'}))) - .toThrowError( - `Route "app/user/:name" with name "user" does not begin with an uppercase letter. Route names should be CamelCase like "User".`); - }); - - - describe('params', () => { - it('should recognize parameters within the URL path', - inject([AsyncTestCompleter], (async) => { - recognizer.config( - new Route({path: 'profile/:name', component: DummyCmpA, name: 'User'})); - recognize(recognizer, '/profile/matsko?comments=all') - .then((solutions: RouteMatch[]) => { - expect(solutions.length).toBe(1); - expect(getParams(solutions[0])).toEqual({'name': 'matsko', 'comments': 'all'}); - async.done(); - }); - })); - - - it('should generate and populate the given static-based route with querystring params', - () => { - recognizer.config( - new Route({path: 'forum/featured', component: DummyCmpA, name: 'ForumPage'})); - - var params = {'start': 10, 'end': 100}; - - var result = recognizer.generate('ForumPage', params); - expect(result.urlPath).toEqual('forum/featured'); - expect(result.urlParams).toEqual(['start=10', 'end=100']); - }); - - - it('should prefer positional params over query params', - inject([AsyncTestCompleter], (async) => { - recognizer.config( - new Route({path: 'profile/:name', component: DummyCmpA, name: 'User'})); - recognize(recognizer, '/profile/yegor?name=igor') - .then((solutions: RouteMatch[]) => { - expect(solutions.length).toBe(1); - expect(getParams(solutions[0])).toEqual({'name': 'yegor'}); - async.done(); - }); - })); - - - it('should ignore matrix params for the top-level component', - inject([AsyncTestCompleter], (async) => { - recognizer.config( - new Route({path: '/home/:subject', component: DummyCmpA, name: 'User'})); - recognize(recognizer, '/home;sort=asc/zero;one=1?two=2') - .then((solutions: RouteMatch[]) => { - expect(solutions.length).toBe(1); - expect(getParams(solutions[0])).toEqual({'subject': 'zero', 'two': '2'}); - async.done(); - }); - })); - }); - }); -} - -function recognize(recognizer: ComponentRecognizer, url: string): Promise { - var parsedUrl = parser.parse(url); - return PromiseWrapper.all(recognizer.recognize(parsedUrl)); -} - -function getComponentType(routeMatch: RouteMatch): any { - if (routeMatch instanceof PathMatch) { - return routeMatch.instruction.componentType; - } - return null; -} - -function getParams(routeMatch: RouteMatch): any { - if (routeMatch instanceof PathMatch) { - return routeMatch.instruction.params; - } - return null; -} - -class DummyCmpA {} -class DummyCmpB {} diff --git a/modules/angular2/test/router/integration/README.md b/modules/angular2/test/router/integration/README.md deleted file mode 100644 index 157d7423d2..0000000000 --- a/modules/angular2/test/router/integration/README.md +++ /dev/null @@ -1,9 +0,0 @@ -# Router integration tests - -These tests only mock out `Location`, and otherwise use all the real parts of routing to ensure that -various routing scenarios work as expected. - -The Component Router in Angular 2 exposes only a handful of different options, but because they can -be combined and nested in so many ways, it's difficult to rigorously test all the cases. - -The address this problem, we introduce `describeRouter`, `describeWith`, and `describeWithout`. \ No newline at end of file diff --git a/modules/angular2/test/router/integration/async_route_spec.ts b/modules/angular2/test/router/integration/async_route_spec.ts deleted file mode 100644 index 40b182bfec..0000000000 --- a/modules/angular2/test/router/integration/async_route_spec.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { - describeRouter, - ddescribeRouter, - describeWith, - describeWithout, - describeWithAndWithout, - itShouldRoute -} from './util'; - -import {registerSpecs} from './impl/async_route_spec_impl'; - -export function main() { - registerSpecs(); - - ddescribeRouter('async routes', () => { - describeWithout('children', () => { - describeWith('route data', itShouldRoute); - describeWithAndWithout('params', itShouldRoute); - }); - - describeWith('sync children', - () => { describeWithAndWithout('default routes', itShouldRoute); }); - - describeWith('async children', () => { - describeWithAndWithout('params', () => { describeWithout('default routes', itShouldRoute); }); - }); - }); -} diff --git a/modules/angular2/test/router/integration/auxiliary_route_spec.ts b/modules/angular2/test/router/integration/auxiliary_route_spec.ts deleted file mode 100644 index 166d448ceb..0000000000 --- a/modules/angular2/test/router/integration/auxiliary_route_spec.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { - RootTestComponent, - AsyncTestCompleter, - TestComponentBuilder, - beforeEach, - ddescribe, - xdescribe, - describe, - el, - expect, - iit, - inject, - beforeEachProviders, - it, - xit -} from 'angular2/testing_internal'; - -import {provide, Component, Injector, Inject} from 'angular2/core'; - -import {Router, ROUTER_DIRECTIVES, RouteParams, RouteData, Location} from 'angular2/router'; -import {RouteConfig, Route, AuxRoute, Redirect} from 'angular2/src/router/route_config_decorator'; - -import {TEST_ROUTER_PROVIDERS, RootCmp, compile} from './util'; - -var cmpInstanceCount; -var childCmpInstanceCount; - -export function main() { - describe('auxiliary routes', () => { - - var tcb: TestComponentBuilder; - var rootTC: RootTestComponent; - var rtr; - - beforeEachProviders(() => TEST_ROUTER_PROVIDERS); - - beforeEach(inject([TestComponentBuilder, Router], (tcBuilder, router) => { - tcb = tcBuilder; - rtr = router; - childCmpInstanceCount = 0; - cmpInstanceCount = 0; - })); - - it('should recognize and navigate from the URL', inject([AsyncTestCompleter], (async) => { - compile(tcb, `main {} | aux {}`) - .then((rtc) => {rootTC = rtc}) - .then((_) => rtr.config([ - new Route({path: '/hello', component: HelloCmp, name: 'Hello'}), - new AuxRoute({path: '/modal', component: ModalCmp, name: 'Aux'}) - ])) - .then((_) => rtr.navigateByUrl('/hello(modal)')) - .then((_) => { - rootTC.detectChanges(); - expect(rootTC.debugElement.nativeElement).toHaveText('main {hello} | aux {modal}'); - async.done(); - }); - })); - - it('should navigate via the link DSL', inject([AsyncTestCompleter], (async) => { - compile(tcb, `main {} | aux {}`) - .then((rtc) => {rootTC = rtc}) - .then((_) => rtr.config([ - new Route({path: '/hello', component: HelloCmp, name: 'Hello'}), - new AuxRoute({path: '/modal', component: ModalCmp, name: 'Modal'}) - ])) - .then((_) => rtr.navigate(['/Hello', ['Modal']])) - .then((_) => { - rootTC.detectChanges(); - expect(rootTC.debugElement.nativeElement).toHaveText('main {hello} | aux {modal}'); - async.done(); - }); - })); - }); -} - - -@Component({selector: 'hello-cmp', template: `{{greeting}}`}) -class HelloCmp { - greeting: string; - constructor() { this.greeting = 'hello'; } -} - -@Component({selector: 'modal-cmp', template: `modal`}) -class ModalCmp { -} - -@Component({ - selector: 'aux-cmp', - template: 'main {} | ' + - 'aux {}', - directives: [ROUTER_DIRECTIVES], -}) -@RouteConfig([ - new Route({path: '/hello', component: HelloCmp, name: 'Hello'}), - new AuxRoute({path: '/modal', component: ModalCmp, name: 'Aux'}) -]) -class AuxCmp { -} diff --git a/modules/angular2/test/router/integration/impl/async_route_spec_impl.ts b/modules/angular2/test/router/integration/impl/async_route_spec_impl.ts deleted file mode 100644 index 583e4ecef8..0000000000 --- a/modules/angular2/test/router/integration/impl/async_route_spec_impl.ts +++ /dev/null @@ -1,655 +0,0 @@ -import { - AsyncTestCompleter, - beforeEach, - beforeEachProviders, - expect, - iit, - flushMicrotasks, - inject, - it, - TestComponentBuilder, - RootTestComponent, - xit, -} from 'angular2/testing_internal'; - -import {specs, compile, TEST_ROUTER_PROVIDERS, clickOnElement, getHref} from '../util'; - -import {Router, AsyncRoute, Route, Location} from 'angular2/router'; - -import { - HelloCmp, - helloCmpLoader, - UserCmp, - userCmpLoader, - TeamCmp, - asyncTeamLoader, - ParentCmp, - parentCmpLoader, - asyncParentCmpLoader, - asyncDefaultParentCmpLoader, - ParentWithDefaultCmp, - parentWithDefaultCmpLoader, - asyncRouteDataCmp -} from './fixture_components'; - -function getLinkElement(rtc: RootTestComponent) { - return rtc.debugElement.componentViewChildren[0].nativeElement; -} - -function asyncRoutesWithoutChildrenWithRouteData() { - var fixture; - var tcb; - var rtr; - - beforeEachProviders(() => TEST_ROUTER_PROVIDERS); - - beforeEach(inject([TestComponentBuilder, Router], (tcBuilder, router) => { - tcb = tcBuilder; - rtr = router; - })); - - it('should inject route data into the component', inject([AsyncTestCompleter], (async) => { - compile(tcb) - .then((rtc) => {fixture = rtc}) - .then((_) => rtr.config([ - new AsyncRoute( - {path: '/route-data', loader: asyncRouteDataCmp, data: {isAdmin: true}}) - ])) - .then((_) => rtr.navigateByUrl('/route-data')) - .then((_) => { - fixture.detectChanges(); - expect(fixture.debugElement.nativeElement).toHaveText('true'); - async.done(); - }); - })); - - it('should inject empty object if the route has no data property', - inject([AsyncTestCompleter], (async) => { - compile(tcb) - .then((rtc) => {fixture = rtc}) - .then((_) => rtr.config( - [new AsyncRoute({path: '/route-data-default', loader: asyncRouteDataCmp})])) - .then((_) => rtr.navigateByUrl('/route-data-default')) - .then((_) => { - fixture.detectChanges(); - expect(fixture.debugElement.nativeElement).toHaveText(''); - async.done(); - }); - })); -} - -function asyncRoutesWithoutChildrenWithoutParams() { - var fixture; - var tcb; - var rtr; - - beforeEachProviders(() => TEST_ROUTER_PROVIDERS); - - beforeEach(inject([TestComponentBuilder, Router], (tcBuilder, router) => { - tcb = tcBuilder; - rtr = router; - })); - - it('should navigate by URL', inject([AsyncTestCompleter], (async) => { - compile(tcb) - .then((rtc) => {fixture = rtc}) - .then((_) => rtr.config( - [new AsyncRoute({path: '/test', loader: helloCmpLoader, name: 'Hello'})])) - .then((_) => rtr.navigateByUrl('/test')) - .then((_) => { - fixture.detectChanges(); - expect(fixture.debugElement.nativeElement).toHaveText('hello'); - async.done(); - }); - })); - - it('should navigate by link DSL', inject([AsyncTestCompleter], (async) => { - compile(tcb) - .then((rtc) => {fixture = rtc}) - .then((_) => rtr.config( - [new AsyncRoute({path: '/test', loader: helloCmpLoader, name: 'Hello'})])) - .then((_) => rtr.navigate(['/Hello'])) - .then((_) => { - fixture.detectChanges(); - expect(fixture.debugElement.nativeElement).toHaveText('hello'); - async.done(); - }); - })); - - it('should generate a link URL', inject([AsyncTestCompleter], (async) => { - compile(tcb, `go to hello | `) - .then((rtc) => {fixture = rtc}) - .then((_) => rtr.config( - [new AsyncRoute({path: '/test', loader: helloCmpLoader, name: 'Hello'})])) - .then((_) => { - fixture.detectChanges(); - expect(getHref(getLinkElement(fixture))).toEqual('/test'); - async.done(); - }); - })); - - it('should navigate from a link click', - inject([AsyncTestCompleter, Location], (async, location) => { - compile(tcb, `go to hello | `) - .then((rtc) => {fixture = rtc}) - .then((_) => rtr.config( - [new AsyncRoute({path: '/test', loader: helloCmpLoader, name: 'Hello'})])) - .then((_) => { - fixture.detectChanges(); - expect(fixture.debugElement.nativeElement).toHaveText('go to hello | '); - - rtr.subscribe((_) => { - fixture.detectChanges(); - expect(fixture.debugElement.nativeElement).toHaveText('go to hello | hello'); - expect(location.urlChanges).toEqual(['/test']); - async.done(); - }); - - clickOnElement(getLinkElement(fixture)); - }); - })); -} - - -function asyncRoutesWithoutChildrenWithParams() { - var fixture; - var tcb; - var rtr; - - beforeEachProviders(() => TEST_ROUTER_PROVIDERS); - - beforeEach(inject([TestComponentBuilder, Router], (tcBuilder, router) => { - tcb = tcBuilder; - rtr = router; - })); - - it('should navigate by URL', inject([AsyncTestCompleter], (async) => { - compile(tcb) - .then((rtc) => {fixture = rtc}) - .then((_) => rtr.config( - [new AsyncRoute({path: '/user/:name', loader: userCmpLoader, name: 'User'})])) - .then((_) => rtr.navigateByUrl('/user/igor')) - .then((_) => { - fixture.detectChanges(); - expect(fixture.debugElement.nativeElement).toHaveText('hello igor'); - async.done(); - }); - })); - - it('should navigate by link DSL', inject([AsyncTestCompleter], (async) => { - compile(tcb) - .then((rtc) => {fixture = rtc}) - .then((_) => rtr.config( - [new Route({path: '/user/:name', component: UserCmp, name: 'User'})])) - .then((_) => rtr.navigate(['/User', {name: 'brian'}])) - .then((_) => { - fixture.detectChanges(); - expect(fixture.debugElement.nativeElement).toHaveText('hello brian'); - async.done(); - }); - })); - - it('should generate a link URL', inject([AsyncTestCompleter], (async) => { - compile(tcb, `greet naomi | `) - .then((rtc) => {fixture = rtc}) - .then((_) => rtr.config( - [new AsyncRoute({path: '/user/:name', loader: userCmpLoader, name: 'User'})])) - .then((_) => { - fixture.detectChanges(); - expect(getHref(getLinkElement(fixture))).toEqual('/user/naomi'); - async.done(); - }); - })); - - it('should navigate from a link click', - inject([AsyncTestCompleter, Location], (async, location) => { - compile(tcb, `greet naomi | `) - .then((rtc) => {fixture = rtc}) - .then((_) => rtr.config( - [new AsyncRoute({path: '/user/:name', loader: userCmpLoader, name: 'User'})])) - .then((_) => { - fixture.detectChanges(); - expect(fixture.debugElement.nativeElement).toHaveText('greet naomi | '); - - rtr.subscribe((_) => { - fixture.detectChanges(); - expect(fixture.debugElement.nativeElement).toHaveText('greet naomi | hello naomi'); - expect(location.urlChanges).toEqual(['/user/naomi']); - async.done(); - }); - - clickOnElement(getLinkElement(fixture)); - }); - })); - - it('should navigate between components with different parameters', - inject([AsyncTestCompleter], (async) => { - compile(tcb) - .then((rtc) => {fixture = rtc}) - .then((_) => rtr.config( - [new AsyncRoute({path: '/user/:name', loader: userCmpLoader, name: 'User'})])) - .then((_) => rtr.navigateByUrl('/user/brian')) - .then((_) => { - fixture.detectChanges(); - expect(fixture.debugElement.nativeElement).toHaveText('hello brian'); - }) - .then((_) => rtr.navigateByUrl('/user/igor')) - .then((_) => { - fixture.detectChanges(); - expect(fixture.debugElement.nativeElement).toHaveText('hello igor'); - async.done(); - }); - })); -} - - -function asyncRoutesWithSyncChildrenWithoutDefaultRoutes() { - var fixture; - var tcb; - var rtr; - - beforeEachProviders(() => TEST_ROUTER_PROVIDERS); - - beforeEach(inject([TestComponentBuilder, Router], (tcBuilder, router) => { - tcb = tcBuilder; - rtr = router; - })); - - it('should navigate by URL', inject([AsyncTestCompleter], (async) => { - compile(tcb, `outer { }`) - .then((rtc) => {fixture = rtc}) - .then((_) => rtr.config( - [new AsyncRoute({path: '/a/...', loader: parentCmpLoader, name: 'Parent'})])) - .then((_) => rtr.navigateByUrl('/a/b')) - .then((_) => { - fixture.detectChanges(); - expect(fixture.debugElement.nativeElement).toHaveText('outer { inner { hello } }'); - async.done(); - }); - })); - - it('should navigate by link DSL', inject([AsyncTestCompleter], (async) => { - compile(tcb, `outer { }`) - .then((rtc) => {fixture = rtc}) - .then((_) => rtr.config( - [new AsyncRoute({path: '/a/...', loader: parentCmpLoader, name: 'Parent'})])) - .then((_) => rtr.navigate(['/Parent', 'Child'])) - .then((_) => { - fixture.detectChanges(); - expect(fixture.debugElement.nativeElement).toHaveText('outer { inner { hello } }'); - async.done(); - }); - })); - - it('should generate a link URL', inject([AsyncTestCompleter], (async) => { - compile(tcb, `nav to child | outer { }`) - .then((rtc) => {fixture = rtc}) - .then((_) => rtr.config( - [new AsyncRoute({path: '/a/...', loader: parentCmpLoader, name: 'Parent'})])) - .then((_) => { - fixture.detectChanges(); - expect(getHref(getLinkElement(fixture))).toEqual('/a'); - async.done(); - }); - })); - - it('should navigate from a link click', - inject([AsyncTestCompleter, Location], (async, location) => { - compile(tcb, `nav to child | outer { }`) - .then((rtc) => {fixture = rtc}) - .then((_) => rtr.config( - [new AsyncRoute({path: '/a/...', loader: parentCmpLoader, name: 'Parent'})])) - .then((_) => { - fixture.detectChanges(); - expect(fixture.debugElement.nativeElement).toHaveText('nav to child | outer { }'); - - rtr.subscribe((_) => { - fixture.detectChanges(); - expect(fixture.debugElement.nativeElement) - .toHaveText('nav to child | outer { inner { hello } }'); - expect(location.urlChanges).toEqual(['/a/b']); - async.done(); - }); - - clickOnElement(getLinkElement(fixture)); - }); - })); -} - - -function asyncRoutesWithSyncChildrenWithDefaultRoutes() { - var fixture; - var tcb; - var rtr; - - beforeEachProviders(() => TEST_ROUTER_PROVIDERS); - - beforeEach(inject([TestComponentBuilder, Router], (tcBuilder, router) => { - tcb = tcBuilder; - rtr = router; - })); - - it('should navigate by URL', inject([AsyncTestCompleter], (async) => { - compile(tcb, `outer { }`) - .then((rtc) => {fixture = rtc}) - .then((_) => rtr.config([ - new AsyncRoute({path: '/a/...', loader: parentWithDefaultCmpLoader, name: 'Parent'}) - ])) - .then((_) => rtr.navigateByUrl('/a')) - .then((_) => { - fixture.detectChanges(); - expect(fixture.debugElement.nativeElement).toHaveText('outer { inner { hello } }'); - async.done(); - }); - })); - - it('should navigate by link DSL', inject([AsyncTestCompleter], (async) => { - compile(tcb, `outer { }`) - .then((rtc) => {fixture = rtc}) - .then((_) => rtr.config([ - new AsyncRoute({path: '/a/...', loader: parentWithDefaultCmpLoader, name: 'Parent'}) - ])) - .then((_) => rtr.navigate(['/Parent'])) - .then((_) => { - fixture.detectChanges(); - expect(fixture.debugElement.nativeElement).toHaveText('outer { inner { hello } }'); - async.done(); - }); - })); - - it('should generate a link URL', inject([AsyncTestCompleter], (async) => { - compile(tcb, `link to inner | outer { }`) - .then((rtc) => {fixture = rtc}) - .then((_) => rtr.config([ - new AsyncRoute({path: '/a/...', loader: parentWithDefaultCmpLoader, name: 'Parent'}) - ])) - .then((_) => { - fixture.detectChanges(); - expect(getHref(getLinkElement(fixture))).toEqual('/a'); - async.done(); - }); - })); - - it('should navigate from a link click', - inject([AsyncTestCompleter, Location], (async, location) => { - compile(tcb, `link to inner | outer { }`) - .then((rtc) => {fixture = rtc}) - .then((_) => rtr.config([ - new AsyncRoute({path: '/a/...', loader: parentWithDefaultCmpLoader, name: 'Parent'}) - ])) - .then((_) => { - fixture.detectChanges(); - expect(fixture.debugElement.nativeElement).toHaveText('link to inner | outer { }'); - - rtr.subscribe((_) => { - fixture.detectChanges(); - expect(fixture.debugElement.nativeElement) - .toHaveText('link to inner | outer { inner { hello } }'); - expect(location.urlChanges).toEqual(['/a/b']); - async.done(); - }); - - clickOnElement(getLinkElement(fixture)); - }); - })); -} - - -function asyncRoutesWithAsyncChildrenWithoutParamsWithoutDefaultRoutes() { - var rootTC; - var tcb; - var rtr; - - beforeEachProviders(() => TEST_ROUTER_PROVIDERS); - - beforeEach(inject([TestComponentBuilder, Router], (tcBuilder, router) => { - tcb = tcBuilder; - rtr = router; - })); - - it('should navigate by URL', inject([AsyncTestCompleter], (async) => { - compile(tcb, `outer { }`) - .then((rtc) => {rootTC = rtc}) - .then((_) => rtr.config([ - new AsyncRoute({path: '/a/...', loader: asyncParentCmpLoader, name: 'Parent'}) - ])) - .then((_) => rtr.navigateByUrl('/a/b')) - .then((_) => { - rootTC.detectChanges(); - expect(rootTC.debugElement.nativeElement).toHaveText('outer { inner { hello } }'); - async.done(); - }); - })); - - it('should navigate by link DSL', inject([AsyncTestCompleter], (async) => { - compile(tcb, `outer { }`) - .then((rtc) => {rootTC = rtc}) - .then((_) => rtr.config([ - new AsyncRoute({path: '/a/...', loader: asyncParentCmpLoader, name: 'Parent'}) - ])) - .then((_) => rtr.navigate(['/Parent', 'Child'])) - .then((_) => { - rootTC.detectChanges(); - expect(rootTC.debugElement.nativeElement).toHaveText('outer { inner { hello } }'); - async.done(); - }); - })); - - it('should generate a link URL', inject([AsyncTestCompleter], (async) => { - compile(tcb, `nav to child | outer { }`) - .then((rtc) => {rootTC = rtc}) - .then((_) => rtr.config([ - new AsyncRoute({path: '/a/...', loader: asyncParentCmpLoader, name: 'Parent'}) - ])) - .then((_) => { - rootTC.detectChanges(); - expect(getHref(getLinkElement(rootTC))).toEqual('/a'); - async.done(); - }); - })); - - it('should navigate from a link click', - inject([AsyncTestCompleter, Location], (async, location) => { - compile(tcb, `nav to child | outer { }`) - .then((rtc) => {rootTC = rtc}) - .then((_) => rtr.config([ - new AsyncRoute({path: '/a/...', loader: asyncParentCmpLoader, name: 'Parent'}) - ])) - .then((_) => { - rootTC.detectChanges(); - expect(rootTC.debugElement.nativeElement).toHaveText('nav to child | outer { }'); - - rtr.subscribe((_) => { - rootTC.detectChanges(); - expect(rootTC.debugElement.nativeElement) - .toHaveText('nav to child | outer { inner { hello } }'); - expect(location.urlChanges).toEqual(['/a/b']); - async.done(); - }); - - clickOnElement(getLinkElement(rootTC)); - }); - })); -} - - -function asyncRoutesWithAsyncChildrenWithoutParamsWithDefaultRoutes() { - var rootTC; - var tcb; - var rtr; - - beforeEachProviders(() => TEST_ROUTER_PROVIDERS); - - beforeEach(inject([TestComponentBuilder, Router], (tcBuilder, router) => { - tcb = tcBuilder; - rtr = router; - })); - - it('should navigate by URL', inject([AsyncTestCompleter], (async) => { - compile(tcb, `outer { }`) - .then((rtc) => {rootTC = rtc}) - .then((_) => rtr.config([ - new AsyncRoute( - {path: '/a/...', loader: asyncDefaultParentCmpLoader, name: 'Parent'}) - ])) - .then((_) => rtr.navigateByUrl('/a')) - .then((_) => { - rootTC.detectChanges(); - expect(rootTC.debugElement.nativeElement).toHaveText('outer { inner { hello } }'); - async.done(); - }); - })); - - it('should navigate by link DSL', inject([AsyncTestCompleter], (async) => { - compile(tcb, `outer { }`) - .then((rtc) => {rootTC = rtc}) - .then((_) => rtr.config([ - new AsyncRoute( - {path: '/a/...', loader: asyncDefaultParentCmpLoader, name: 'Parent'}) - ])) - .then((_) => rtr.navigate(['/Parent'])) - .then((_) => { - rootTC.detectChanges(); - expect(rootTC.debugElement.nativeElement).toHaveText('outer { inner { hello } }'); - async.done(); - }); - })); - - it('should generate a link URL', inject([AsyncTestCompleter], (async) => { - compile(tcb, `nav to child | outer { }`) - .then((rtc) => {rootTC = rtc}) - .then((_) => rtr.config([ - new AsyncRoute( - {path: '/a/...', loader: asyncDefaultParentCmpLoader, name: 'Parent'}) - ])) - .then((_) => { - rootTC.detectChanges(); - expect(getHref(getLinkElement(rootTC))).toEqual('/a'); - async.done(); - }); - })); - - it('should navigate from a link click', - inject([AsyncTestCompleter, Location], (async, location) => { - compile(tcb, `nav to child | outer { }`) - .then((rtc) => {rootTC = rtc}) - .then((_) => rtr.config([ - new AsyncRoute( - {path: '/a/...', loader: asyncDefaultParentCmpLoader, name: 'Parent'}) - ])) - .then((_) => { - rootTC.detectChanges(); - expect(rootTC.debugElement.nativeElement).toHaveText('nav to child | outer { }'); - - rtr.subscribe((_) => { - rootTC.detectChanges(); - expect(rootTC.debugElement.nativeElement) - .toHaveText('nav to child | outer { inner { hello } }'); - expect(location.urlChanges).toEqual(['/a/b']); - async.done(); - }); - - clickOnElement(getLinkElement(rootTC)); - }); - })); -} - - -function asyncRoutesWithAsyncChildrenWithParamsWithoutDefaultRoutes() { - var fixture; - var tcb; - var rtr; - - beforeEachProviders(() => TEST_ROUTER_PROVIDERS); - - beforeEach(inject([TestComponentBuilder, Router], (tcBuilder, router) => { - tcb = tcBuilder; - rtr = router; - })); - - it('should navigate by URL', inject([AsyncTestCompleter], (async) => { - compile(tcb, `{ }`) - .then((rtc) => {fixture = rtc}) - .then((_) => rtr.config([ - new AsyncRoute({path: '/team/:id/...', loader: asyncTeamLoader, name: 'Team'}) - ])) - .then((_) => rtr.navigateByUrl('/team/angular/user/matias')) - .then((_) => { - fixture.detectChanges(); - expect(fixture.debugElement.nativeElement) - .toHaveText('{ team angular | user { hello matias } }'); - async.done(); - }); - })); - - it('should navigate by link DSL', inject([AsyncTestCompleter], (async) => { - compile(tcb, `{ }`) - .then((rtc) => {fixture = rtc}) - .then((_) => rtr.config([ - new AsyncRoute({path: '/team/:id/...', loader: asyncTeamLoader, name: 'Team'}) - ])) - .then((_) => rtr.navigate(['/Team', {id: 'angular'}, 'User', {name: 'matias'}])) - .then((_) => { - fixture.detectChanges(); - expect(fixture.debugElement.nativeElement) - .toHaveText('{ team angular | user { hello matias } }'); - async.done(); - }); - })); - - it('should generate a link URL', inject([AsyncTestCompleter], (async) => { - compile( - tcb, - `nav to matias { }`) - .then((rtc) => {fixture = rtc}) - .then((_) => rtr.config([ - new AsyncRoute({path: '/team/:id/...', loader: asyncTeamLoader, name: 'Team'}) - ])) - .then((_) => { - fixture.detectChanges(); - expect(getHref(getLinkElement(fixture))).toEqual('/team/angular'); - async.done(); - }); - })); - - it('should navigate from a link click', - inject([AsyncTestCompleter, Location], (async, location) => { - compile( - tcb, - `nav to matias { }`) - .then((rtc) => {fixture = rtc}) - .then((_) => rtr.config([ - new AsyncRoute({path: '/team/:id/...', loader: asyncTeamLoader, name: 'Team'}) - ])) - .then((_) => { - fixture.detectChanges(); - expect(fixture.debugElement.nativeElement).toHaveText('nav to matias { }'); - - rtr.subscribe((_) => { - fixture.detectChanges(); - expect(fixture.debugElement.nativeElement) - .toHaveText('nav to matias { team angular | user { hello matias } }'); - expect(location.urlChanges).toEqual(['/team/angular/user/matias']); - async.done(); - }); - - clickOnElement(getLinkElement(fixture)); - }); - })); -} - -export function registerSpecs() { - specs['asyncRoutesWithoutChildrenWithRouteData'] = asyncRoutesWithoutChildrenWithRouteData; - specs['asyncRoutesWithoutChildrenWithoutParams'] = asyncRoutesWithoutChildrenWithoutParams; - specs['asyncRoutesWithoutChildrenWithParams'] = asyncRoutesWithoutChildrenWithParams; - specs['asyncRoutesWithSyncChildrenWithoutDefaultRoutes'] = - asyncRoutesWithSyncChildrenWithoutDefaultRoutes; - specs['asyncRoutesWithSyncChildrenWithDefaultRoutes'] = - asyncRoutesWithSyncChildrenWithDefaultRoutes; - specs['asyncRoutesWithAsyncChildrenWithoutParamsWithoutDefaultRoutes'] = - asyncRoutesWithAsyncChildrenWithoutParamsWithoutDefaultRoutes; - specs['asyncRoutesWithAsyncChildrenWithoutParamsWithDefaultRoutes'] = - asyncRoutesWithAsyncChildrenWithoutParamsWithDefaultRoutes; - specs['asyncRoutesWithAsyncChildrenWithParamsWithoutDefaultRoutes'] = - asyncRoutesWithAsyncChildrenWithParamsWithoutDefaultRoutes; -} diff --git a/modules/angular2/test/router/integration/impl/fixture_components.ts b/modules/angular2/test/router/integration/impl/fixture_components.ts deleted file mode 100644 index f11efd68a5..0000000000 --- a/modules/angular2/test/router/integration/impl/fixture_components.ts +++ /dev/null @@ -1,131 +0,0 @@ -import {Component} from 'angular2/angular2'; -import { - AsyncRoute, - Route, - Redirect, - RouteConfig, - RouteParams, - RouteData, - ROUTER_DIRECTIVES -} from 'angular2/router'; -import {PromiseWrapper} from 'angular2/src/facade/async'; - -@Component({selector: 'hello-cmp', template: `{{greeting}}`}) -export class HelloCmp { - greeting: string; - constructor() { this.greeting = 'hello'; } -} - -export function helloCmpLoader() { - return PromiseWrapper.resolve(HelloCmp); -} - - -@Component({selector: 'user-cmp', template: `hello {{user}}`}) -export class UserCmp { - user: string; - constructor(params: RouteParams) { this.user = params.get('name'); } -} - -export function userCmpLoader() { - return PromiseWrapper.resolve(UserCmp); -} - - -@Component({ - selector: 'parent-cmp', - template: `inner { }`, - directives: [ROUTER_DIRECTIVES], -}) -@RouteConfig([new Route({path: '/b', component: HelloCmp, name: 'Child'})]) -export class ParentCmp { -} - -export function parentCmpLoader() { - return PromiseWrapper.resolve(ParentCmp); -} - - -@Component({ - selector: 'parent-cmp', - template: `inner { }`, - directives: [ROUTER_DIRECTIVES], -}) -@RouteConfig([new AsyncRoute({path: '/b', loader: helloCmpLoader, name: 'Child'})]) -export class AsyncParentCmp { -} - -export function asyncParentCmpLoader() { - return PromiseWrapper.resolve(AsyncParentCmp); -} - -@Component({ - selector: 'parent-cmp', - template: `inner { }`, - directives: [ROUTER_DIRECTIVES], -}) -@RouteConfig( - [new AsyncRoute({path: '/b', loader: helloCmpLoader, name: 'Child', useAsDefault: true})]) -export class AsyncDefaultParentCmp { -} - -export function asyncDefaultParentCmpLoader() { - return PromiseWrapper.resolve(AsyncDefaultParentCmp); -} - - -@Component({ - selector: 'parent-cmp', - template: `inner { }`, - directives: [ROUTER_DIRECTIVES], -}) -@RouteConfig([new Route({path: '/b', component: HelloCmp, name: 'Child', useAsDefault: true})]) -export class ParentWithDefaultCmp { -} - -export function parentWithDefaultCmpLoader() { - return PromiseWrapper.resolve(ParentWithDefaultCmp); -} - - -@Component({ - selector: 'team-cmp', - template: `team {{id}} | user { }`, - directives: [ROUTER_DIRECTIVES], -}) -@RouteConfig([new Route({path: '/user/:name', component: UserCmp, name: 'User'})]) -export class TeamCmp { - id: string; - constructor(params: RouteParams) { this.id = params.get('id'); } -} - -@Component({ - selector: 'team-cmp', - template: `team {{id}} | user { }`, - directives: [ROUTER_DIRECTIVES], -}) -@RouteConfig([new AsyncRoute({path: '/user/:name', loader: userCmpLoader, name: 'User'})]) -export class AsyncTeamCmp { - id: string; - constructor(params: RouteParams) { this.id = params.get('id'); } -} - -export function asyncTeamLoader() { - return PromiseWrapper.resolve(AsyncTeamCmp); -} - - -@Component({selector: 'data-cmp', template: `{{myData}}`}) -export class RouteDataCmp { - myData: boolean; - constructor(data: RouteData) { this.myData = data.get('isAdmin'); } -} - -export function asyncRouteDataCmp() { - return PromiseWrapper.resolve(RouteDataCmp); -} - -@Component({selector: 'redirect-to-parent-cmp', template: 'redirect-to-parent'}) -@RouteConfig([new Redirect({path: '/child-redirect', redirectTo: ['../HelloSib']})]) -export class RedirectToParentCmp { -} diff --git a/modules/angular2/test/router/integration/impl/sync_route_spec_impl.ts b/modules/angular2/test/router/integration/impl/sync_route_spec_impl.ts deleted file mode 100644 index 15fbc3514c..0000000000 --- a/modules/angular2/test/router/integration/impl/sync_route_spec_impl.ts +++ /dev/null @@ -1,431 +0,0 @@ -import { - AsyncTestCompleter, - beforeEach, - beforeEachProviders, - expect, - iit, - flushMicrotasks, - inject, - it, - TestComponentBuilder, - RootTestComponent, - xit, -} from 'angular2/testing_internal'; - -import {specs, compile, TEST_ROUTER_PROVIDERS, clickOnElement, getHref} from '../util'; - -import {Router, Route, Location} from 'angular2/router'; - -import {HelloCmp, UserCmp, TeamCmp, ParentCmp, ParentWithDefaultCmp} from './fixture_components'; - - -function getLinkElement(rtc: RootTestComponent) { - return rtc.debugElement.componentViewChildren[0].nativeElement; -} - -function syncRoutesWithoutChildrenWithoutParams() { - var fixture; - var tcb; - var rtr; - - beforeEachProviders(() => TEST_ROUTER_PROVIDERS); - - beforeEach(inject([TestComponentBuilder, Router], (tcBuilder, router) => { - tcb = tcBuilder; - rtr = router; - })); - - it('should navigate by URL', inject([AsyncTestCompleter], (async) => { - compile(tcb) - .then((rtc) => {fixture = rtc}) - .then((_) => - rtr.config([new Route({path: '/test', component: HelloCmp, name: 'Hello'})])) - .then((_) => rtr.navigateByUrl('/test')) - .then((_) => { - fixture.detectChanges(); - expect(fixture.debugElement.nativeElement).toHaveText('hello'); - async.done(); - }); - })); - - it('should navigate by link DSL', inject([AsyncTestCompleter], (async) => { - compile(tcb) - .then((rtc) => {fixture = rtc}) - .then((_) => - rtr.config([new Route({path: '/test', component: HelloCmp, name: 'Hello'})])) - .then((_) => rtr.navigate(['/Hello'])) - .then((_) => { - fixture.detectChanges(); - expect(fixture.debugElement.nativeElement).toHaveText('hello'); - async.done(); - }); - })); - - it('should generate a link URL', inject([AsyncTestCompleter], (async) => { - compile(tcb, `go to hello | `) - .then((rtc) => {fixture = rtc}) - .then((_) => - rtr.config([new Route({path: '/test', component: HelloCmp, name: 'Hello'})])) - .then((_) => { - fixture.detectChanges(); - expect(getHref(getLinkElement(fixture))).toEqual('/test'); - async.done(); - }); - })); - - it('should navigate from a link click', - inject([AsyncTestCompleter, Location], (async, location) => { - compile(tcb, `go to hello | `) - .then((rtc) => {fixture = rtc}) - .then((_) => - rtr.config([new Route({path: '/test', component: HelloCmp, name: 'Hello'})])) - .then((_) => { - fixture.detectChanges(); - expect(fixture.debugElement.nativeElement).toHaveText('go to hello | '); - - rtr.subscribe((_) => { - fixture.detectChanges(); - expect(fixture.debugElement.nativeElement).toHaveText('go to hello | hello'); - expect(location.urlChanges).toEqual(['/test']); - async.done(); - }); - - clickOnElement(getLinkElement(fixture)); - }); - })); -} - - -function syncRoutesWithoutChildrenWithParams() { - var fixture; - var tcb; - var rtr; - - beforeEachProviders(() => TEST_ROUTER_PROVIDERS); - - beforeEach(inject([TestComponentBuilder, Router], (tcBuilder, router) => { - tcb = tcBuilder; - rtr = router; - })); - - it('should navigate by URL', inject([AsyncTestCompleter], (async) => { - compile(tcb) - .then((rtc) => {fixture = rtc}) - .then((_) => rtr.config( - [new Route({path: '/user/:name', component: UserCmp, name: 'User'})])) - .then((_) => rtr.navigateByUrl('/user/igor')) - .then((_) => { - fixture.detectChanges(); - expect(fixture.debugElement.nativeElement).toHaveText('hello igor'); - async.done(); - }); - })); - - it('should navigate by link DSL', inject([AsyncTestCompleter], (async) => { - compile(tcb) - .then((rtc) => {fixture = rtc}) - .then((_) => rtr.config( - [new Route({path: '/user/:name', component: UserCmp, name: 'User'})])) - .then((_) => rtr.navigate(['/User', {name: 'brian'}])) - .then((_) => { - fixture.detectChanges(); - expect(fixture.debugElement.nativeElement).toHaveText('hello brian'); - async.done(); - }); - })); - - it('should generate a link URL', inject([AsyncTestCompleter], (async) => { - compile(tcb, `greet naomi | `) - .then((rtc) => {fixture = rtc}) - .then((_) => rtr.config( - [new Route({path: '/user/:name', component: UserCmp, name: 'User'})])) - .then((_) => { - fixture.detectChanges(); - expect(getHref(getLinkElement(fixture))).toEqual('/user/naomi'); - async.done(); - }); - })); - - it('should navigate from a link click', - inject([AsyncTestCompleter, Location], (async, location) => { - compile(tcb, `greet naomi | `) - .then((rtc) => {fixture = rtc}) - .then((_) => rtr.config( - [new Route({path: '/user/:name', component: UserCmp, name: 'User'})])) - .then((_) => { - fixture.detectChanges(); - expect(fixture.debugElement.nativeElement).toHaveText('greet naomi | '); - - rtr.subscribe((_) => { - fixture.detectChanges(); - expect(fixture.debugElement.nativeElement).toHaveText('greet naomi | hello naomi'); - expect(location.urlChanges).toEqual(['/user/naomi']); - async.done(); - }); - - clickOnElement(getLinkElement(fixture)); - }); - })); - - it('should navigate between components with different parameters', - inject([AsyncTestCompleter], (async) => { - compile(tcb) - .then((rtc) => {fixture = rtc}) - .then((_) => rtr.config( - [new Route({path: '/user/:name', component: UserCmp, name: 'User'})])) - .then((_) => rtr.navigateByUrl('/user/brian')) - .then((_) => { - fixture.detectChanges(); - expect(fixture.debugElement.nativeElement).toHaveText('hello brian'); - }) - .then((_) => rtr.navigateByUrl('/user/igor')) - .then((_) => { - fixture.detectChanges(); - expect(fixture.debugElement.nativeElement).toHaveText('hello igor'); - async.done(); - }); - })); -} - - -function syncRoutesWithSyncChildrenWithoutDefaultRoutesWithoutParams() { - var fixture; - var tcb; - var rtr; - - beforeEachProviders(() => TEST_ROUTER_PROVIDERS); - - beforeEach(inject([TestComponentBuilder, Router], (tcBuilder, router) => { - tcb = tcBuilder; - rtr = router; - })); - - it('should navigate by URL', inject([AsyncTestCompleter], (async) => { - compile(tcb, `outer { }`) - .then((rtc) => {fixture = rtc}) - .then((_) => rtr.config( - [new Route({path: '/a/...', component: ParentCmp, name: 'Parent'})])) - .then((_) => rtr.navigateByUrl('/a/b')) - .then((_) => { - fixture.detectChanges(); - expect(fixture.debugElement.nativeElement).toHaveText('outer { inner { hello } }'); - async.done(); - }); - })); - - it('should navigate by link DSL', inject([AsyncTestCompleter], (async) => { - compile(tcb, `outer { }`) - .then((rtc) => {fixture = rtc}) - .then((_) => rtr.config( - [new Route({path: '/a/...', component: ParentCmp, name: 'Parent'})])) - .then((_) => rtr.navigate(['/Parent', 'Child'])) - .then((_) => { - fixture.detectChanges(); - expect(fixture.debugElement.nativeElement).toHaveText('outer { inner { hello } }'); - async.done(); - }); - })); - - it('should generate a link URL', inject([AsyncTestCompleter], (async) => { - compile(tcb, `nav to child | outer { }`) - .then((rtc) => {fixture = rtc}) - .then((_) => rtr.config( - [new Route({path: '/a/...', component: ParentCmp, name: 'Parent'})])) - .then((_) => { - fixture.detectChanges(); - expect(getHref(getLinkElement(fixture))).toEqual('/a/b'); - async.done(); - }); - })); - - it('should navigate from a link click', - inject([AsyncTestCompleter, Location], (async, location) => { - compile(tcb, `nav to child | outer { }`) - .then((rtc) => {fixture = rtc}) - .then((_) => rtr.config( - [new Route({path: '/a/...', component: ParentCmp, name: 'Parent'})])) - .then((_) => { - fixture.detectChanges(); - expect(fixture.debugElement.nativeElement).toHaveText('nav to child | outer { }'); - - rtr.subscribe((_) => { - fixture.detectChanges(); - expect(fixture.debugElement.nativeElement) - .toHaveText('nav to child | outer { inner { hello } }'); - expect(location.urlChanges).toEqual(['/a/b']); - async.done(); - }); - - clickOnElement(getLinkElement(fixture)); - }); - })); -} - - -function syncRoutesWithSyncChildrenWithoutDefaultRoutesWithParams() { - var fixture; - var tcb; - var rtr; - - beforeEachProviders(() => TEST_ROUTER_PROVIDERS); - - beforeEach(inject([TestComponentBuilder, Router], (tcBuilder, router) => { - tcb = tcBuilder; - rtr = router; - })); - - it('should navigate by URL', inject([AsyncTestCompleter], (async) => { - compile(tcb, `{ }`) - .then((rtc) => {fixture = rtc}) - .then((_) => rtr.config( - [new Route({path: '/team/:id/...', component: TeamCmp, name: 'Team'})])) - .then((_) => rtr.navigateByUrl('/team/angular/user/matias')) - .then((_) => { - fixture.detectChanges(); - expect(fixture.debugElement.nativeElement) - .toHaveText('{ team angular | user { hello matias } }'); - async.done(); - }); - })); - - it('should navigate by link DSL', inject([AsyncTestCompleter], (async) => { - compile(tcb, `{ }`) - .then((rtc) => {fixture = rtc}) - .then((_) => rtr.config( - [new Route({path: '/team/:id/...', component: TeamCmp, name: 'Team'})])) - .then((_) => rtr.navigate(['/Team', {id: 'angular'}, 'User', {name: 'matias'}])) - .then((_) => { - fixture.detectChanges(); - expect(fixture.debugElement.nativeElement) - .toHaveText('{ team angular | user { hello matias } }'); - async.done(); - }); - })); - - it('should generate a link URL', inject([AsyncTestCompleter], (async) => { - compile( - tcb, - `nav to matias { }`) - .then((rtc) => {fixture = rtc}) - .then((_) => rtr.config( - [new Route({path: '/team/:id/...', component: TeamCmp, name: 'Team'})])) - .then((_) => { - fixture.detectChanges(); - expect(getHref(getLinkElement(fixture))).toEqual('/team/angular/user/matias'); - async.done(); - }); - })); - - it('should navigate from a link click', - inject([AsyncTestCompleter, Location], (async, location) => { - compile( - tcb, - `nav to matias { }`) - .then((rtc) => {fixture = rtc}) - .then((_) => rtr.config( - [new Route({path: '/team/:id/...', component: TeamCmp, name: 'Team'})])) - .then((_) => { - fixture.detectChanges(); - expect(fixture.debugElement.nativeElement).toHaveText('nav to matias { }'); - - rtr.subscribe((_) => { - fixture.detectChanges(); - expect(fixture.debugElement.nativeElement) - .toHaveText('nav to matias { team angular | user { hello matias } }'); - expect(location.urlChanges).toEqual(['/team/angular/user/matias']); - async.done(); - }); - - clickOnElement(getLinkElement(fixture)); - }); - })); -} - - -function syncRoutesWithSyncChildrenWithDefaultRoutesWithoutParams() { - var fixture; - var tcb; - var rtr; - - beforeEachProviders(() => TEST_ROUTER_PROVIDERS); - - beforeEach(inject([TestComponentBuilder, Router], (tcBuilder, router) => { - tcb = tcBuilder; - rtr = router; - })); - - it('should navigate by URL', inject([AsyncTestCompleter], (async) => { - compile(tcb, `outer { }`) - .then((rtc) => {fixture = rtc}) - .then( - (_) => rtr.config( - [new Route({path: '/a/...', component: ParentWithDefaultCmp, name: 'Parent'})])) - .then((_) => rtr.navigateByUrl('/a')) - .then((_) => { - fixture.detectChanges(); - expect(fixture.debugElement.nativeElement).toHaveText('outer { inner { hello } }'); - async.done(); - }); - })); - - it('should navigate by link DSL', inject([AsyncTestCompleter], (async) => { - compile(tcb, `outer { }`) - .then((rtc) => {fixture = rtc}) - .then( - (_) => rtr.config( - [new Route({path: '/a/...', component: ParentWithDefaultCmp, name: 'Parent'})])) - .then((_) => rtr.navigate(['/Parent'])) - .then((_) => { - fixture.detectChanges(); - expect(fixture.debugElement.nativeElement).toHaveText('outer { inner { hello } }'); - async.done(); - }); - })); - - it('should generate a link URL', inject([AsyncTestCompleter], (async) => { - compile(tcb, `link to inner | outer { }`) - .then((rtc) => {fixture = rtc}) - .then( - (_) => rtr.config( - [new Route({path: '/a/...', component: ParentWithDefaultCmp, name: 'Parent'})])) - .then((_) => { - fixture.detectChanges(); - expect(getHref(getLinkElement(fixture))).toEqual('/a'); - async.done(); - }); - })); - - it('should navigate from a link click', - inject([AsyncTestCompleter, Location], (async, location) => { - compile(tcb, `link to inner | outer { }`) - .then((rtc) => {fixture = rtc}) - .then( - (_) => rtr.config( - [new Route({path: '/a/...', component: ParentWithDefaultCmp, name: 'Parent'})])) - .then((_) => { - fixture.detectChanges(); - expect(fixture.debugElement.nativeElement).toHaveText('link to inner | outer { }'); - - rtr.subscribe((_) => { - fixture.detectChanges(); - expect(fixture.debugElement.nativeElement) - .toHaveText('link to inner | outer { inner { hello } }'); - expect(location.urlChanges).toEqual(['/a/b']); - async.done(); - }); - - clickOnElement(getLinkElement(fixture)); - }); - })); -} - -export function registerSpecs() { - specs['syncRoutesWithoutChildrenWithoutParams'] = syncRoutesWithoutChildrenWithoutParams; - specs['syncRoutesWithoutChildrenWithParams'] = syncRoutesWithoutChildrenWithParams; - specs['syncRoutesWithSyncChildrenWithoutDefaultRoutesWithoutParams'] = - syncRoutesWithSyncChildrenWithoutDefaultRoutesWithoutParams; - specs['syncRoutesWithSyncChildrenWithoutDefaultRoutesWithParams'] = - syncRoutesWithSyncChildrenWithoutDefaultRoutesWithParams; - specs['syncRoutesWithSyncChildrenWithDefaultRoutesWithoutParams'] = - syncRoutesWithSyncChildrenWithDefaultRoutesWithoutParams; -} diff --git a/modules/angular2/test/router/integration/lifecycle_hook_spec.ts b/modules/angular2/test/router/integration/lifecycle_hook_spec.ts index 5130f2c090..57e5d4f533 100644 --- a/modules/angular2/test/router/integration/lifecycle_hook_spec.ts +++ b/modules/angular2/test/router/integration/lifecycle_hook_spec.ts @@ -10,7 +10,7 @@ import { expect, iit, inject, - beforeEachProviders, + beforeEachBindings, it, xit } from 'angular2/testing_internal'; @@ -25,6 +25,7 @@ import { ObservableWrapper } from 'angular2/src/facade/async'; +import {RootRouter} from 'angular2/src/router/router'; import {Router, RouterOutlet, RouterLink, RouteParams} from 'angular2/router'; import { RouteConfig, @@ -34,6 +35,9 @@ import { Redirect } from 'angular2/src/router/route_config_decorator'; +import {SpyLocation} from 'angular2/src/mock/location_mock'; +import {Location} from 'angular2/src/router/location'; +import {RouteRegistry} from 'angular2/src/router/route_registry'; import { OnActivate, OnDeactivate, @@ -43,9 +47,7 @@ import { } from 'angular2/src/router/interfaces'; import {CanActivate} from 'angular2/src/router/lifecycle_annotations'; import {ComponentInstruction} from 'angular2/src/router/instruction'; - - -import {TEST_ROUTER_PROVIDERS, RootCmp, compile} from './util'; +import {DirectiveResolver} from 'angular2/src/core/linker/directive_resolver'; var cmpInstanceCount; var log: string[]; @@ -59,7 +61,17 @@ export function main() { var fixture: ComponentFixture; var rtr; - beforeEachProviders(() => TEST_ROUTER_PROVIDERS); + beforeEachBindings(() => [ + RouteRegistry, + DirectiveResolver, + provide(Location, {useClass: SpyLocation}), + provide(Router, + { + useFactory: + (registry, location) => { return new RootRouter(registry, location, MyComp); }, + deps: [RouteRegistry, Location] + }) + ]); beforeEach(inject([TestComponentBuilder, Router], (tcBuilder, router) => { tcb = tcBuilder; @@ -69,9 +81,17 @@ export function main() { eventBus = new EventEmitter(); })); + function compile(template: string = "") { + return tcb.overrideView(MyComp, new View({ + template: ('
' + template + '
'), + directives: [RouterOutlet, RouterLink] + })) + .createAsync(MyComp) + .then((tc) => { fixture = tc; }); + } + it('should call the onActivate hook', inject([AsyncTestCompleter], (async) => { - compile(tcb) - .then((rtc) => {fixture = rtc}) + compile() .then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})])) .then((_) => rtr.navigateByUrl('/on-activate')) .then((_) => { @@ -84,8 +104,7 @@ export function main() { it('should wait for a parent component\'s onActivate hook to resolve before calling its child\'s', inject([AsyncTestCompleter], (async) => { - compile(tcb) - .then((rtc) => {fixture = rtc}) + compile() .then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})])) .then((_) => { ObservableWrapper.subscribe(eventBus, (ev) => { @@ -107,8 +126,7 @@ export function main() { })); it('should call the onDeactivate hook', inject([AsyncTestCompleter], (async) => { - compile(tcb) - .then((rtc) => {fixture = rtc}) + compile() .then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})])) .then((_) => rtr.navigateByUrl('/on-deactivate')) .then((_) => rtr.navigateByUrl('/a')) @@ -122,8 +140,7 @@ export function main() { it('should wait for a child component\'s onDeactivate hook to resolve before calling its parent\'s', inject([AsyncTestCompleter], (async) => { - compile(tcb) - .then((rtc) => {fixture = rtc}) + compile() .then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})])) .then((_) => rtr.navigateByUrl('/parent-deactivate/child-deactivate')) .then((_) => { @@ -148,8 +165,7 @@ export function main() { it('should reuse a component when the canReuse hook returns true', inject([AsyncTestCompleter], (async) => { - compile(tcb) - .then((rtc) => {fixture = rtc}) + compile() .then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})])) .then((_) => rtr.navigateByUrl('/on-reuse/1/a')) .then((_) => { @@ -171,8 +187,7 @@ export function main() { it('should not reuse a component when the canReuse hook returns false', inject([AsyncTestCompleter], (async) => { - compile(tcb) - .then((rtc) => {fixture = rtc}) + compile() .then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})])) .then((_) => rtr.navigateByUrl('/never-reuse/1/a')) .then((_) => { @@ -193,8 +208,7 @@ export function main() { it('should navigate when canActivate returns true', inject([AsyncTestCompleter], (async) => { - compile(tcb) - .then((rtc) => {fixture = rtc}) + compile() .then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})])) .then((_) => { ObservableWrapper.subscribe(eventBus, (ev) => { @@ -214,8 +228,7 @@ export function main() { it('should not navigate when canActivate returns false', inject([AsyncTestCompleter], (async) => { - compile(tcb) - .then((rtc) => {fixture = rtc}) + compile() .then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})])) .then((_) => { ObservableWrapper.subscribe(eventBus, (ev) => { @@ -235,8 +248,7 @@ export function main() { it('should navigate away when canDeactivate returns true', inject([AsyncTestCompleter], (async) => { - compile(tcb) - .then((rtc) => {fixture = rtc}) + compile() .then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})])) .then((_) => rtr.navigateByUrl('/can-deactivate/a')) .then((_) => { @@ -261,8 +273,7 @@ export function main() { it('should not navigate away when canDeactivate returns false', inject([AsyncTestCompleter], (async) => { - compile(tcb) - .then((rtc) => {fixture = rtc}) + compile() .then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})])) .then((_) => rtr.navigateByUrl('/can-deactivate/a')) .then((_) => { @@ -288,8 +299,7 @@ export function main() { it('should run activation and deactivation hooks in the correct order', inject([AsyncTestCompleter], (async) => { - compile(tcb) - .then((rtc) => {fixture = rtc}) + compile() .then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})])) .then((_) => rtr.navigateByUrl('/activation-hooks/child')) .then((_) => { @@ -315,8 +325,7 @@ export function main() { })); it('should only run reuse hooks when reusing', inject([AsyncTestCompleter], (async) => { - compile(tcb) - .then((rtc) => {fixture = rtc}) + compile() .then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})])) .then((_) => rtr.navigateByUrl('/reuse-hooks/1')) .then((_) => { @@ -343,7 +352,7 @@ export function main() { })); it('should not run reuse hooks when not reusing', inject([AsyncTestCompleter], (async) => { - compile(tcb) + compile() .then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})])) .then((_) => rtr.navigateByUrl('/reuse-hooks/1')) .then((_) => { @@ -374,16 +383,23 @@ export function main() { } -@Component({selector: 'a-cmp', template: "A"}) +@Component({selector: 'a-cmp'}) +@View({template: "A"}) class A { } -@Component({selector: 'b-cmp', template: "B"}) +@Component({selector: 'b-cmp'}) +@View({template: "B"}) class B { } +@Component({selector: 'my-comp'}) +class MyComp { + name; +} + function logHook(name: string, next: ComponentInstruction, prev: ComponentInstruction) { var message = name + ': ' + (isPresent(prev) ? ('/' + prev.urlPath) : 'null') + ' -> ' + (isPresent(next) ? ('/' + next.urlPath) : 'null'); @@ -391,18 +407,16 @@ function logHook(name: string, next: ComponentInstruction, prev: ComponentInstru ObservableWrapper.callEmit(eventBus, message); } -@Component({selector: 'activate-cmp', template: 'activate cmp'}) +@Component({selector: 'activate-cmp'}) +@View({template: 'activate cmp'}) class ActivateCmp implements OnActivate { onActivate(next: ComponentInstruction, prev: ComponentInstruction) { logHook('activate', next, prev); } } -@Component({ - selector: 'parent-activate-cmp', - template: `parent {}`, - directives: [RouterOutlet] -}) +@Component({selector: 'parent-activate-cmp'}) +@View({template: `parent {}`, directives: [RouterOutlet]}) @RouteConfig([new Route({path: '/child-activate', component: ActivateCmp})]) class ParentActivateCmp implements OnActivate { onActivate(next: ComponentInstruction, prev: ComponentInstruction): Promise { @@ -412,14 +426,16 @@ class ParentActivateCmp implements OnActivate { } } -@Component({selector: 'deactivate-cmp', template: 'deactivate cmp'}) +@Component({selector: 'deactivate-cmp'}) +@View({template: 'deactivate cmp'}) class DeactivateCmp implements OnDeactivate { onDeactivate(next: ComponentInstruction, prev: ComponentInstruction) { logHook('deactivate', next, prev); } } -@Component({selector: 'deactivate-cmp', template: 'deactivate cmp'}) +@Component({selector: 'deactivate-cmp'}) +@View({template: 'deactivate cmp'}) class WaitDeactivateCmp implements OnDeactivate { onDeactivate(next: ComponentInstruction, prev: ComponentInstruction): Promise { completer = PromiseWrapper.completer(); @@ -428,11 +444,8 @@ class WaitDeactivateCmp implements OnDeactivate { } } -@Component({ - selector: 'parent-deactivate-cmp', - template: `parent {}`, - directives: [RouterOutlet] -}) +@Component({selector: 'parent-deactivate-cmp'}) +@View({template: `parent {}`, directives: [RouterOutlet]}) @RouteConfig([new Route({path: '/child-deactivate', component: WaitDeactivateCmp})]) class ParentDeactivateCmp implements OnDeactivate { onDeactivate(next: ComponentInstruction, prev: ComponentInstruction) { @@ -440,37 +453,26 @@ class ParentDeactivateCmp implements OnDeactivate { } } -@Component({ - selector: 'reuse-cmp', - template: `reuse {}`, - directives: [RouterOutlet] -}) +@Component({selector: 'reuse-cmp'}) +@View({template: `reuse {}`, directives: [RouterOutlet]}) @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; } canReuse(next: ComponentInstruction, prev: ComponentInstruction) { return true; } onReuse(next: ComponentInstruction, prev: ComponentInstruction) { logHook('reuse', next, prev); } } -@Component({ - selector: 'never-reuse-cmp', - template: `reuse {}`, - directives: [RouterOutlet] -}) +@Component({selector: 'never-reuse-cmp'}) +@View({template: `reuse {}`, directives: [RouterOutlet]}) @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; } canReuse(next: ComponentInstruction, prev: ComponentInstruction) { return false; } onReuse(next: ComponentInstruction, prev: ComponentInstruction) { logHook('reuse', next, prev); } } -@Component({ - selector: 'can-activate-cmp', - template: `canActivate {}`, - directives: [RouterOutlet] -}) +@Component({selector: 'can-activate-cmp'}) +@View({template: `canActivate {}`, directives: [RouterOutlet]}) @RouteConfig([new Route({path: '/a', component: A}), new Route({path: '/b', component: B})]) @CanActivate(CanActivateCmp.canActivate) class CanActivateCmp { @@ -481,11 +483,8 @@ class CanActivateCmp { } } -@Component({ - selector: 'can-deactivate-cmp', - template: `canDeactivate {}`, - directives: [RouterOutlet] -}) +@Component({selector: 'can-deactivate-cmp'}) +@View({template: `canDeactivate {}`, directives: [RouterOutlet]}) @RouteConfig([new Route({path: '/a', component: A}), new Route({path: '/b', component: B})]) class CanDeactivateCmp implements CanDeactivate { canDeactivate(next: ComponentInstruction, prev: ComponentInstruction): Promise { @@ -495,7 +494,8 @@ class CanDeactivateCmp implements CanDeactivate { } } -@Component({selector: 'all-hooks-child-cmp', template: `child`}) +@Component({selector: 'all-hooks-child-cmp'}) +@View({template: `child`}) @CanActivate(AllHooksChildCmp.canActivate) class AllHooksChildCmp implements CanDeactivate, OnDeactivate, OnActivate { canDeactivate(next: ComponentInstruction, prev: ComponentInstruction) { @@ -517,15 +517,11 @@ class AllHooksChildCmp implements CanDeactivate, OnDeactivate, OnActivate { } } -@Component({ - selector: 'all-hooks-parent-cmp', - template: ``, - directives: [RouterOutlet] -}) +@Component({selector: 'all-hooks-parent-cmp'}) +@View({template: ``, directives: [RouterOutlet]}) @RouteConfig([new Route({path: '/child', component: AllHooksChildCmp})]) @CanActivate(AllHooksParentCmp.canActivate) -class AllHooksParentCmp implements CanDeactivate, - OnDeactivate, OnActivate { +class AllHooksParentCmp implements CanDeactivate, OnDeactivate, OnActivate { canDeactivate(next: ComponentInstruction, prev: ComponentInstruction) { logHook('canDeactivate parent', next, prev); return true; @@ -545,7 +541,8 @@ class AllHooksParentCmp implements CanDeactivate, } } -@Component({selector: 'reuse-hooks-cmp', template: 'reuse hooks cmp'}) +@Component({selector: 'reuse-hooks-cmp'}) +@View({template: 'reuse hooks cmp'}) @CanActivate(ReuseHooksCmp.canActivate) class ReuseHooksCmp implements OnActivate, OnReuse, OnDeactivate, CanReuse, CanDeactivate { canReuse(next: ComponentInstruction, prev: ComponentInstruction): Promise { @@ -577,11 +574,8 @@ class ReuseHooksCmp implements OnActivate, OnReuse, OnDeactivate, CanReuse, CanD } } -@Component({ - selector: 'lifecycle-cmp', - template: ``, - directives: [RouterOutlet] -}) +@Component({selector: 'lifecycle-cmp'}) +@View({template: ``, directives: [RouterOutlet]}) @RouteConfig([ new Route({path: '/a', component: A}), new Route({path: '/on-activate', component: ActivateCmp}), diff --git a/modules/angular2/test/router/integration/navigation_spec.ts b/modules/angular2/test/router/integration/navigation_spec.ts index 8e649ec9c2..cb420974e6 100644 --- a/modules/angular2/test/router/integration/navigation_spec.ts +++ b/modules/angular2/test/router/integration/navigation_spec.ts @@ -10,7 +10,7 @@ import { expect, iit, inject, - beforeEachProviders, + beforeEachBindings, it, xit } from 'angular2/testing_internal'; @@ -18,7 +18,8 @@ import { import {provide, Component, View, Injector, Inject} from 'angular2/core'; import {Promise, PromiseWrapper} from 'angular2/src/facade/async'; -import {Router, RouterOutlet, RouterLink, RouteParams, RouteData, Location} from 'angular2/router'; +import {RootRouter} from 'angular2/src/router/router'; +import {Router, RouterOutlet, RouterLink, RouteParams, RouteData} from 'angular2/router'; import { RouteConfig, Route, @@ -27,10 +28,14 @@ import { Redirect } from 'angular2/src/router/route_config_decorator'; -import {TEST_ROUTER_PROVIDERS, RootCmp, compile} from './util'; +import {SpyLocation} from 'angular2/src/mock/location_mock'; +import {Location} from 'angular2/src/router/location'; +import {RouteRegistry} from 'angular2/src/router/route_registry'; +import {DirectiveResolver} from 'angular2/src/core/linker/directive_resolver'; var cmpInstanceCount; var childCmpInstanceCount; +var log: string[]; export function main() { describe('navigation', () => { @@ -39,18 +44,37 @@ export function main() { var fixture: ComponentFixture; var rtr; - beforeEachProviders(() => TEST_ROUTER_PROVIDERS); + beforeEachBindings(() => [ + RouteRegistry, + DirectiveResolver, + provide(Location, {useClass: SpyLocation}), + provide(Router, + { + useFactory: + (registry, location) => { return new RootRouter(registry, location, MyComp); }, + deps: [RouteRegistry, Location] + }) + ]); beforeEach(inject([TestComponentBuilder, Router], (tcBuilder, router) => { tcb = tcBuilder; rtr = router; childCmpInstanceCount = 0; cmpInstanceCount = 0; + log = []; })); + function compile(template: string = "") { + return tcb.overrideView(MyComp, new View({ + template: ('
' + template + '
'), + directives: [RouterOutlet, RouterLink] + })) + .createAsync(MyComp) + .then((tc) => { fixture = tc; }); + } + it('should work in a simple case', inject([AsyncTestCompleter], (async) => { - compile(tcb) - .then((rtc) => {fixture = rtc}) + compile() .then((_) => rtr.config([new Route({path: '/test', component: HelloCmp})])) .then((_) => rtr.navigateByUrl('/test')) .then((_) => { @@ -63,8 +87,7 @@ export function main() { it('should navigate between components with different parameters', inject([AsyncTestCompleter], (async) => { - compile(tcb) - .then((rtc) => {fixture = rtc}) + compile() .then((_) => rtr.config([new Route({path: '/user/:name', component: UserCmp})])) .then((_) => rtr.navigateByUrl('/user/brian')) .then((_) => { @@ -79,9 +102,9 @@ export function main() { }); })); + it('should navigate to child routes', inject([AsyncTestCompleter], (async) => { - compile(tcb, 'outer { }') - .then((rtc) => {fixture = rtc}) + compile('outer { }') .then((_) => rtr.config([new Route({path: '/a/...', component: ParentCmp})])) .then((_) => rtr.navigateByUrl('/a/b')) .then((_) => { @@ -93,9 +116,7 @@ export function main() { it('should navigate to child routes that capture an empty path', inject([AsyncTestCompleter], (async) => { - - compile(tcb, 'outer { }') - .then((rtc) => {fixture = rtc}) + compile('outer { }') .then((_) => rtr.config([new Route({path: '/a/...', component: ParentCmp})])) .then((_) => rtr.navigateByUrl('/a')) .then((_) => { @@ -105,9 +126,9 @@ export function main() { }); })); + it('should navigate to child routes of async routes', inject([AsyncTestCompleter], (async) => { - compile(tcb, 'outer { }') - .then((rtc) => {fixture = rtc}) + compile('outer { }') .then((_) => rtr.config([new AsyncRoute({path: '/a/...', loader: parentLoader})])) .then((_) => rtr.navigateByUrl('/a/b')) .then((_) => { @@ -117,9 +138,26 @@ export function main() { }); })); + + it('should recognize and apply redirects', + inject([AsyncTestCompleter, Location], (async, location) => { + compile() + .then((_) => rtr.config([ + new Redirect({path: '/original', redirectTo: '/redirected'}), + new Route({path: '/redirected', component: HelloCmp}) + ])) + .then((_) => rtr.navigateByUrl('/original')) + .then((_) => { + fixture.detectChanges(); + expect(fixture.debugElement.nativeElement).toHaveText('hello'); + expect(location.urlChanges).toEqual(['/redirected']); + async.done(); + }); + })); + + it('should reuse common parent components', inject([AsyncTestCompleter], (async) => { - compile(tcb) - .then((rtc) => {fixture = rtc}) + compile() .then((_) => rtr.config([new Route({path: '/team/:id/...', component: TeamCmp})])) .then((_) => rtr.navigateByUrl('/team/angular/user/rado')) .then((_) => { @@ -139,8 +177,7 @@ export function main() { it('should not reuse children when parent components change', inject([AsyncTestCompleter], (async) => { - compile(tcb) - .then((rtc) => {fixture = rtc}) + compile() .then((_) => rtr.config([new Route({path: '/team/:id/...', component: TeamCmp})])) .then((_) => rtr.navigateByUrl('/team/angular/user/rado')) .then((_) => { @@ -160,8 +197,7 @@ export function main() { })); it('should inject route data into component', inject([AsyncTestCompleter], (async) => { - compile(tcb) - .then((rtc) => {fixture = rtc}) + compile() .then((_) => rtr.config([ new Route({path: '/route-data', component: RouteDataCmp, data: {isAdmin: true}}) ])) @@ -175,11 +211,10 @@ export function main() { it('should inject route data into component with AsyncRoute', inject([AsyncTestCompleter], (async) => { - compile(tcb) - .then((rtc) => {fixture = rtc}) + compile() .then((_) => rtr.config([ new AsyncRoute( - {path: '/route-data', loader: asyncRouteDataCmp, data: {isAdmin: true}}) + {path: '/route-data', loader: AsyncRouteDataCmp, data: {isAdmin: true}}) ])) .then((_) => rtr.navigateByUrl('/route-data')) .then((_) => { @@ -191,8 +226,7 @@ export function main() { it('should inject empty object if the route has no data property', inject([AsyncTestCompleter], (async) => { - compile(tcb) - .then((rtc) => {fixture = rtc}) + compile() .then((_) => rtr.config( [new Route({path: '/route-data-default', component: RouteDataCmp})])) .then((_) => rtr.navigateByUrl('/route-data-default')) @@ -202,28 +236,45 @@ export function main() { async.done(); }); })); + + describe('auxiliary routes', () => { + it('should recognize a simple case', inject([AsyncTestCompleter], (async) => { + compile() + .then((_) => rtr.config([new Route({path: '/...', component: AuxCmp})])) + .then((_) => rtr.navigateByUrl('/hello(modal)')) + .then((_) => { + fixture.detectChanges(); + expect(fixture.debugElement.nativeElement) + .toHaveText('main {hello} | aux {modal}'); + async.done(); + }); + })); + }); }); } -@Component({selector: 'hello-cmp', template: `{{greeting}}`}) +@Component({selector: 'hello-cmp'}) +@View({template: "{{greeting}}"}) class HelloCmp { greeting: string; - constructor() { this.greeting = 'hello'; } + constructor() { this.greeting = "hello"; } } -function asyncRouteDataCmp() { +function AsyncRouteDataCmp() { return PromiseWrapper.resolve(RouteDataCmp); } -@Component({selector: 'data-cmp', template: `{{myData}}`}) +@Component({selector: 'data-cmp'}) +@View({template: "{{myData}}"}) class RouteDataCmp { myData: boolean; constructor(data: RouteData) { this.myData = data.get('isAdmin'); } } -@Component({selector: 'user-cmp', template: `hello {{user}}`}) +@Component({selector: 'user-cmp'}) +@View({template: "hello {{user}}"}) class UserCmp { user: string; constructor(params: RouteParams) { @@ -237,9 +288,9 @@ function parentLoader() { return PromiseWrapper.resolve(ParentCmp); } -@Component({ - selector: 'parent-cmp', - template: `inner { }`, +@Component({selector: 'parent-cmp'}) +@View({ + template: "inner { }", directives: [RouterOutlet], }) @RouteConfig([ @@ -247,12 +298,13 @@ function parentLoader() { new Route({path: '/', component: HelloCmp}), ]) class ParentCmp { + constructor() {} } -@Component({ - selector: 'team-cmp', - template: `team {{id}} { }`, +@Component({selector: 'team-cmp'}) +@View({ + template: "team {{id}} { }", directives: [RouterOutlet], }) @RouteConfig([new Route({path: '/user/:name', component: UserCmp})]) @@ -263,3 +315,27 @@ class TeamCmp { cmpInstanceCount += 1; } } + + +@Component({selector: 'my-comp'}) +class MyComp { + name; +} + +@Component({selector: 'modal-cmp'}) +@View({template: "modal"}) +class ModalCmp { +} + +@Component({selector: 'aux-cmp'}) +@View({ + template: 'main {} | ' + + 'aux {}', + directives: [RouterOutlet], +}) +@RouteConfig([ + new Route({path: '/hello', component: HelloCmp}), + new AuxRoute({path: '/modal', component: ModalCmp}), +]) +class AuxCmp { +} diff --git a/modules/angular2/test/router/integration/redirect_route_spec.ts b/modules/angular2/test/router/integration/redirect_route_spec.ts deleted file mode 100644 index 7f8bb28a92..0000000000 --- a/modules/angular2/test/router/integration/redirect_route_spec.ts +++ /dev/null @@ -1,121 +0,0 @@ -import { - RootTestComponent, - AsyncTestCompleter, - TestComponentBuilder, - beforeEach, - ddescribe, - xdescribe, - describe, - el, - expect, - iit, - inject, - beforeEachProviders, - it, - xit -} from 'angular2/testing_internal'; - -import {Router, RouterOutlet, RouterLink, RouteParams, RouteData, Location} from 'angular2/router'; -import { - RouteConfig, - Route, - AuxRoute, - AsyncRoute, - Redirect -} from 'angular2/src/router/route_config_decorator'; - -import {TEST_ROUTER_PROVIDERS, RootCmp, compile} from './util'; -import {HelloCmp, RedirectToParentCmp} from './impl/fixture_components'; - -var cmpInstanceCount; -var childCmpInstanceCount; - -export function main() { - describe('redirects', () => { - - var tcb: TestComponentBuilder; - var rootTC: RootTestComponent; - var rtr; - - beforeEachProviders(() => TEST_ROUTER_PROVIDERS); - - beforeEach(inject([TestComponentBuilder, Router], (tcBuilder, router) => { - tcb = tcBuilder; - rtr = router; - childCmpInstanceCount = 0; - cmpInstanceCount = 0; - })); - - - it('should apply when navigating by URL', - inject([AsyncTestCompleter, Location], (async, location) => { - compile(tcb) - .then((rtc) => {rootTC = rtc}) - .then((_) => rtr.config([ - new Redirect({path: '/original', redirectTo: ['Hello']}), - new Route({path: '/redirected', component: HelloCmp, name: 'Hello'}) - ])) - .then((_) => rtr.navigateByUrl('/original')) - .then((_) => { - rootTC.detectChanges(); - expect(rootTC.debugElement.nativeElement).toHaveText('hello'); - expect(location.urlChanges).toEqual(['/redirected']); - async.done(); - }); - })); - - - it('should recognize and apply absolute redirects', - inject([AsyncTestCompleter, Location], (async, location) => { - compile(tcb) - .then((rtc) => {rootTC = rtc}) - .then((_) => rtr.config([ - new Redirect({path: '/original', redirectTo: ['/Hello']}), - new Route({path: '/redirected', component: HelloCmp, name: 'Hello'}) - ])) - .then((_) => rtr.navigateByUrl('/original')) - .then((_) => { - rootTC.detectChanges(); - expect(rootTC.debugElement.nativeElement).toHaveText('hello'); - expect(location.urlChanges).toEqual(['/redirected']); - async.done(); - }); - })); - - - it('should recognize and apply relative child redirects', - inject([AsyncTestCompleter, Location], (async, location) => { - compile(tcb) - .then((rtc) => {rootTC = rtc}) - .then((_) => rtr.config([ - new Redirect({path: '/original', redirectTo: ['./Hello']}), - new Route({path: '/redirected', component: HelloCmp, name: 'Hello'}) - ])) - .then((_) => rtr.navigateByUrl('/original')) - .then((_) => { - rootTC.detectChanges(); - expect(rootTC.debugElement.nativeElement).toHaveText('hello'); - expect(location.urlChanges).toEqual(['/redirected']); - async.done(); - }); - })); - - - it('should recognize and apply relative parent redirects', - inject([AsyncTestCompleter, Location], (async, location) => { - compile(tcb) - .then((rtc) => {rootTC = rtc}) - .then((_) => rtr.config([ - new Route({path: '/original/...', component: RedirectToParentCmp}), - new Route({path: '/redirected', component: HelloCmp, name: 'HelloSib'}) - ])) - .then((_) => rtr.navigateByUrl('/original/child-redirect')) - .then((_) => { - rootTC.detectChanges(); - expect(rootTC.debugElement.nativeElement).toHaveText('hello'); - expect(location.urlChanges).toEqual(['/redirected']); - async.done(); - }); - })); - }); -} diff --git a/modules/angular2/test/router/integration/bootstrap_spec.ts b/modules/angular2/test/router/integration/router_integration_spec.ts similarity index 79% rename from modules/angular2/test/router/integration/bootstrap_spec.ts rename to modules/angular2/test/router/integration/router_integration_spec.ts index ea4fe2a8b6..8d1fe44a59 100644 --- a/modules/angular2/test/router/integration/bootstrap_spec.ts +++ b/modules/angular2/test/router/integration/router_integration_spec.ts @@ -38,39 +38,44 @@ import {ApplicationRef} from 'angular2/src/core/application_ref'; import {MockApplicationRef} from 'angular2/src/mock/mock_application_ref'; export function main() { - describe('router bootstrap', () => { - beforeEachProviders(() => [ - ROUTER_PROVIDERS, - provide(LocationStrategy, {useClass: MockLocationStrategy}), - provide(ApplicationRef, {useClass: MockApplicationRef}) - ]); + describe('router injectables', () => { + beforeEachProviders(() => { + return [ + ROUTER_PROVIDERS, + provide(LocationStrategy, {useClass: MockLocationStrategy}), + provide(ApplicationRef, {useClass: MockApplicationRef}) + ]; + }); // do not refactor out the `bootstrap` functionality. We still want to // keep this test around so we can ensure that bootstrapping a router works - it('should bootstrap a simple app', inject([AsyncTestCompleter], (async) => { - var fakeDoc = DOM.createHtmlDocument(); - var el = DOM.createElement('app-cmp', fakeDoc); - DOM.appendChild(fakeDoc.body, el); + describe('bootstrap functionality', () => { + it('should bootstrap a simple app', inject([AsyncTestCompleter], (async) => { + var fakeDoc = DOM.createHtmlDocument(); + var el = DOM.createElement('app-cmp', fakeDoc); + DOM.appendChild(fakeDoc.body, el); - bootstrap(AppCmp, - [ - ROUTER_PROVIDERS, - provide(ROUTER_PRIMARY_COMPONENT, {useValue: AppCmp}), - provide(LocationStrategy, {useClass: MockLocationStrategy}), - provide(DOCUMENT, {useValue: fakeDoc}) - ]) - .then((applicationRef) => { - var router = applicationRef.hostComponent.router; - router.subscribe((_) => { - expect(el).toHaveText('outer { hello }'); - expect(applicationRef.hostComponent.location.path()).toEqual(''); - async.done(); + bootstrap(AppCmp, + [ + ROUTER_PROVIDERS, + provide(ROUTER_PRIMARY_COMPONENT, {useValue: AppCmp}), + provide(LocationStrategy, {useClass: MockLocationStrategy}), + provide(DOCUMENT, {useValue: fakeDoc}) + ]) + .then((applicationRef) => { + var router = applicationRef.hostComponent.router; + router.subscribe((_) => { + expect(el).toHaveText('outer { hello }'); + expect(applicationRef.hostComponent.location.path()).toEqual(''); + async.done(); + }); }); - }); - })); + })); + }); describe('broken app', () => { - beforeEachProviders(() => [provide(ROUTER_PRIMARY_COMPONENT, {useValue: BrokenAppCmp})]); + beforeEachProviders( + () => { return [provide(ROUTER_PRIMARY_COMPONENT, {useValue: BrokenAppCmp})]; }); it('should rethrow exceptions from component constructors', inject([AsyncTestCompleter, TestComponentBuilder], (async, tcb: TestComponentBuilder) => { @@ -86,7 +91,8 @@ export function main() { }); describe('back button app', () => { - beforeEachProviders(() => [provide(ROUTER_PRIMARY_COMPONENT, {useValue: HierarchyAppCmp})]); + beforeEachProviders( + () => { return [provide(ROUTER_PRIMARY_COMPONENT, {useValue: HierarchyAppCmp})]; }); it('should change the url without pushing a new history state for back navigations', inject([AsyncTestCompleter, TestComponentBuilder], (async, tcb: TestComponentBuilder) => { @@ -178,7 +184,7 @@ export function main() { })); }); }); - + // TODO: add a test in which the child component has bindings describe('querystring params app', () => { beforeEachProviders( @@ -237,21 +243,20 @@ export function main() { } -@Component({selector: 'hello-cmp', template: 'hello'}) +@Component({selector: 'hello-cmp'}) +@View({template: 'hello'}) class HelloCmp { public message: string; } -@Component({selector: 'hello2-cmp', template: 'hello2'}) +@Component({selector: 'hello2-cmp'}) +@View({template: 'hello2'}) class Hello2Cmp { public greeting: string; } -@Component({ - selector: 'app-cmp', - template: `outer { }`, - directives: ROUTER_DIRECTIVES -}) +@Component({selector: 'app-cmp'}) +@View({template: "outer { }", directives: ROUTER_DIRECTIVES}) @RouteConfig([new Route({path: '/', component: HelloCmp})]) class AppCmp { constructor(public router: Router, public location: LocationStrategy) {} @@ -278,29 +283,20 @@ class AppWithViewChildren implements AfterViewInit { afterViewInit() { this.helloCmp.message = 'Ahoy'; } } -@Component({ - selector: 'parent-cmp', - template: `parent { }`, - directives: ROUTER_DIRECTIVES -}) +@Component({selector: 'parent-cmp'}) +@View({template: `parent { }`, directives: ROUTER_DIRECTIVES}) @RouteConfig([new Route({path: '/child', component: HelloCmp})]) class ParentCmp { } -@Component({ - selector: 'super-parent-cmp', - template: `super-parent { }`, - directives: ROUTER_DIRECTIVES -}) +@Component({selector: 'super-parent-cmp'}) +@View({template: `super-parent { }`, directives: ROUTER_DIRECTIVES}) @RouteConfig([new Route({path: '/child', component: Hello2Cmp})]) class SuperParentCmp { } -@Component({ - selector: 'app-cmp', - template: `root { }`, - directives: ROUTER_DIRECTIVES -}) +@Component({selector: 'app-cmp'}) +@View({template: `root { }`, directives: ROUTER_DIRECTIVES}) @RouteConfig([ new Route({path: '/parent/...', component: ParentCmp}), new Route({path: '/super-parent/...', component: SuperParentCmp}) @@ -309,32 +305,28 @@ class HierarchyAppCmp { constructor(public router: Router, public location: LocationStrategy) {} } -@Component({selector: 'qs-cmp', template: `qParam = {{q}}`}) +@Component({selector: 'qs-cmp'}) +@View({template: "qParam = {{q}}"}) class QSCmp { q: string; constructor(params: RouteParams) { this.q = params.get('q'); } } -@Component({ - selector: 'app-cmp', - template: ``, - directives: ROUTER_DIRECTIVES -}) +@Component({selector: 'app-cmp'}) +@View({template: ``, directives: ROUTER_DIRECTIVES}) @RouteConfig([new Route({path: '/qs', component: QSCmp})]) class QueryStringAppCmp { constructor(public router: Router, public location: LocationStrategy) {} } -@Component({selector: 'oops-cmp', template: "oh no"}) +@Component({selector: 'oops-cmp'}) +@View({template: "oh no"}) class BrokenCmp { constructor() { throw new BaseException('oops!'); } } -@Component({ - selector: 'app-cmp', - template: `outer { }`, - directives: ROUTER_DIRECTIVES -}) +@Component({selector: 'app-cmp'}) +@View({template: `outer { }`, directives: ROUTER_DIRECTIVES}) @RouteConfig([new Route({path: '/cause-error', component: BrokenCmp})]) class BrokenAppCmp { constructor(public router: Router, public location: LocationStrategy) {} diff --git a/modules/angular2/test/router/integration/router_link_spec.ts b/modules/angular2/test/router/integration/router_link_spec.ts index 4cf862d6e2..5b76fd538b 100644 --- a/modules/angular2/test/router/integration/router_link_spec.ts +++ b/modules/angular2/test/router/integration/router_link_spec.ts @@ -9,7 +9,7 @@ import { expect, iit, inject, - beforeEachProviders, + beforeEachBindings, it, xit, TestComponentBuilder, @@ -21,7 +21,7 @@ import {NumberWrapper} from 'angular2/src/facade/lang'; import {PromiseWrapper} from 'angular2/src/facade/async'; import {ListWrapper} from 'angular2/src/facade/collection'; -import {provide, Component, View, DirectiveResolver} from 'angular2/core'; +import {provide, Component, DirectiveResolver} from 'angular2/core'; import {SpyLocation} from 'angular2/src/mock/location_mock'; import { @@ -47,7 +47,7 @@ export function main() { var fixture: ComponentFixture; var router, location; - beforeEachProviders(() => [ + beforeEachBindings(() => [ RouteRegistry, DirectiveResolver, provide(Location, {useClass: SpyLocation}), @@ -240,8 +240,8 @@ export function main() { .then((_) => router.config([new Route({path: '/...', component: AuxLinkCmp})])) .then((_) => router.navigateByUrl('/')) .then((_) => { - fixture.detectChanges(); - expect(DOM.getAttribute(fixture.debugElement.componentViewChildren[1] + rootTC.detectChanges(); + expect(DOM.getAttribute(rootTC.debugElement.componentViewChildren[1] .componentViewChildren[0] .nativeElement, 'href')) @@ -386,7 +386,10 @@ class MyComp { name; } -@Component({selector: 'user-cmp', template: "hello {{user}}"}) +@Component({ + selector: 'user-cmp', + template: "hello {{user}}" +}) class UserCmp { user: string; constructor(params: RouteParams) { this.user = params.get('name'); } @@ -422,11 +425,17 @@ class NoPrefixSiblingPageCmp { } } -@Component({selector: 'hello-cmp', template: 'hello'}) +@Component({ + selector: 'hello-cmp', + template: 'hello' +}) class HelloCmp { } -@Component({selector: 'hello2-cmp', template: 'hello2'}) +@Component({ + selector: 'hello2-cmp', + template: 'hello2' +}) class Hello2Cmp { } @@ -446,6 +455,7 @@ function parentCmpLoader() { new Route({path: '/better-grandchild', component: Hello2Cmp, name: 'BetterGrandchild'}) ]) class ParentCmp { + constructor(public router: Router) {} } @Component({ diff --git a/modules/angular2/test/router/integration/sync_route_spec.ts b/modules/angular2/test/router/integration/sync_route_spec.ts deleted file mode 100644 index 12a4d339fe..0000000000 --- a/modules/angular2/test/router/integration/sync_route_spec.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { - describeRouter, - ddescribeRouter, - describeWith, - describeWithout, - describeWithAndWithout, - itShouldRoute -} from './util'; - -import {registerSpecs} from './impl/sync_route_spec_impl'; - -export function main() { - registerSpecs(); - - describeRouter('sync routes', () => { - describeWithout('children', () => { describeWithAndWithout('params', itShouldRoute); }); - - describeWith('sync children', () => { - describeWithout('default routes', () => { describeWithAndWithout('params', itShouldRoute); }); - describeWith('default routes', () => { describeWithout('params', itShouldRoute); }); - - }); - }); -} diff --git a/modules/angular2/test/router/integration/util.ts b/modules/angular2/test/router/integration/util.ts deleted file mode 100644 index b63fc115f4..0000000000 --- a/modules/angular2/test/router/integration/util.ts +++ /dev/null @@ -1,138 +0,0 @@ -import {provide, Provider, Component, View} from 'angular2/core'; -export {Provider} from 'angular2/core'; -import {Type, isBlank} from 'angular2/src/facade/lang'; -import {BaseException} from 'angular2/src/facade/exceptions'; - -import { - RootTestComponent, - AsyncTestCompleter, - TestComponentBuilder, - beforeEach, - ddescribe, - xdescribe, - describe, - el, - inject, - beforeEachProviders, - it, - xit -} from 'angular2/testing_internal'; - -import {RootRouter} from 'angular2/src/router/router'; -import {Router, ROUTER_DIRECTIVES} from 'angular2/router'; - -import {SpyLocation} from 'angular2/src/mock/location_mock'; -import {Location} from 'angular2/src/router/location'; -import {RouteRegistry} from 'angular2/src/router/route_registry'; -import {DirectiveResolver} from 'angular2/src/core/linker/directive_resolver'; -import {DOM} from 'angular2/src/platform/dom/dom_adapter'; -export {ComponentFixture} from 'angular2/testing_internal'; - - -/** - * Router test helpers and fixtures - */ - -@Component({ - selector: 'root-comp', - template: ``, - directives: [ROUTER_DIRECTIVES] -}) -export class RootCmp { - name: string; -} - -export function compile(tcb: TestComponentBuilder, - template: string = "") { - return tcb.overrideTemplate(RootCmp, ('
' + template + '
')).createAsync(RootCmp); -} - -export var TEST_ROUTER_PROVIDERS = [ - RouteRegistry, - DirectiveResolver, - provide(Location, {useClass: SpyLocation}), - provide( - Router, - { - useFactory: (registry, location) => { return new RootRouter(registry, location, RootCmp);}, - deps: [RouteRegistry, Location] - }) -]; - -export function clickOnElement(anchorEl) { - var dispatchedEvent = DOM.createMouseEvent('click'); - DOM.dispatchEvent(anchorEl, dispatchedEvent); - return dispatchedEvent; -} - -export function getHref(elt) { - return DOM.getAttribute(elt, 'href'); -} - - -/** - * Router integration suite DSL - */ - -var specNameBuilder = []; - -// we add the specs themselves onto this map -export var specs = {}; - -export function describeRouter(description: string, fn: Function, exclusive = false): void { - var specName = descriptionToSpecName(description); - specNameBuilder.push(specName); - describe(description, fn); - specNameBuilder.pop(); -} - -export function ddescribeRouter(description: string, fn: Function, exclusive = false): void { - describeRouter(description, fn, true); -} - -export function describeWithAndWithout(description: string, fn: Function): void { - // the "without" case is usually simpler, so we opt to run this spec first - describeWithout(description, fn); - describeWith(description, fn); -} - -export function describeWith(description: string, fn: Function): void { - var specName = 'with ' + description; - specNameBuilder.push(specName); - describe(specName, fn); - specNameBuilder.pop(); -} - -export function describeWithout(description: string, fn: Function): void { - var specName = 'without ' + description; - specNameBuilder.push(specName); - describe(specName, fn); - specNameBuilder.pop(); -} - -function descriptionToSpecName(description: string): string { - return spaceCaseToCamelCase(description); -} - -// this helper looks up the suite registered from the "impl" folder in this directory -export function itShouldRoute() { - var specSuiteName = spaceCaseToCamelCase(specNameBuilder.join(' ')); - - var spec = specs[specSuiteName]; - if (isBlank(spec)) { - throw new BaseException(`Router integration spec suite "${specSuiteName}" was not found.`); - } else { - // todo: remove spec from map, throw if there are extra left over?? - spec(); - } -} - -function spaceCaseToCamelCase(str: string): string { - var words = str.split(' '); - var first = words.shift(); - return first + words.map(title).join(''); -} - -function title(str: string): string { - return str[0].toUpperCase() + str.substring(1); -} diff --git a/modules/angular2/test/router/path_recognizer_spec.ts b/modules/angular2/test/router/path_recognizer_spec.ts index 08fddc1044..979c21a7d0 100644 --- a/modules/angular2/test/router/path_recognizer_spec.ts +++ b/modules/angular2/test/router/path_recognizer_spec.ts @@ -12,82 +12,100 @@ import { import {PathRecognizer} from 'angular2/src/router/path_recognizer'; import {parser, Url, RootUrl} from 'angular2/src/router/url_parser'; +import {SyncRouteHandler} from 'angular2/src/router/sync_route_handler'; + +class DummyClass { + constructor() {} +} + +var mockRouteHandler = new SyncRouteHandler(DummyClass); export function main() { describe('PathRecognizer', () => { it('should throw when given an invalid path', () => { - expect(() => new PathRecognizer('/hi#')) + expect(() => new PathRecognizer('/hi#', mockRouteHandler)) .toThrowError(`Path "/hi#" should not include "#". Use "HashLocationStrategy" instead.`); - expect(() => new PathRecognizer('hi?')) + expect(() => new PathRecognizer('hi?', mockRouteHandler)) .toThrowError(`Path "hi?" contains "?" which is not allowed in a route config.`); - expect(() => new PathRecognizer('hi;')) + expect(() => new PathRecognizer('hi;', mockRouteHandler)) .toThrowError(`Path "hi;" contains ";" which is not allowed in a route config.`); - expect(() => new PathRecognizer('hi=')) + expect(() => new PathRecognizer('hi=', mockRouteHandler)) .toThrowError(`Path "hi=" contains "=" which is not allowed in a route config.`); - expect(() => new PathRecognizer('hi(')) + expect(() => new PathRecognizer('hi(', mockRouteHandler)) .toThrowError(`Path "hi(" contains "(" which is not allowed in a route config.`); - expect(() => new PathRecognizer('hi)')) + expect(() => new PathRecognizer('hi)', mockRouteHandler)) .toThrowError(`Path "hi)" contains ")" which is not allowed in a route config.`); - expect(() => new PathRecognizer('hi//there')) + expect(() => new PathRecognizer('hi//there', mockRouteHandler)) .toThrowError(`Path "hi//there" contains "//" which is not allowed in a route config.`); }); + it('should return the same instruction instance when recognizing the same path', () => { + var rec = new PathRecognizer('/one', mockRouteHandler); + + var one = new Url('one', null, null, {}); + + var firstMatch = rec.recognize(one); + var secondMatch = rec.recognize(one); + + expect(firstMatch.instruction).toBe(secondMatch.instruction); + }); + describe('querystring params', () => { it('should parse querystring params so long as the recognizer is a root', () => { - var rec = new PathRecognizer('/hello/there'); + var rec = new PathRecognizer('/hello/there', mockRouteHandler); var url = parser.parse('/hello/there?name=igor'); var match = rec.recognize(url); - expect(match['allParams']).toEqual({'name': 'igor'}); + expect(match.instruction.params).toEqual({'name': 'igor'}); }); it('should return a combined map of parameters with the param expected in the URL path', () => { - var rec = new PathRecognizer('/hello/:name'); + var rec = new PathRecognizer('/hello/:name', mockRouteHandler); var url = parser.parse('/hello/paul?topic=success'); var match = rec.recognize(url); - expect(match['allParams']).toEqual({'name': 'paul', 'topic': 'success'}); + expect(match.instruction.params).toEqual({'name': 'paul', 'topic': 'success'}); }); }); describe('matrix params', () => { it('should be parsed along with dynamic paths', () => { - var rec = new PathRecognizer('/hello/:id'); + var rec = new PathRecognizer('/hello/:id', mockRouteHandler); var url = new Url('hello', new Url('matias', null, null, {'key': 'value'})); var match = rec.recognize(url); - expect(match['allParams']).toEqual({'id': 'matias', 'key': 'value'}); + expect(match.instruction.params).toEqual({'id': 'matias', 'key': 'value'}); }); it('should be parsed on a static path', () => { - var rec = new PathRecognizer('/person'); + var rec = new PathRecognizer('/person', mockRouteHandler); var url = new Url('person', null, null, {'name': 'dave'}); var match = rec.recognize(url); - expect(match['allParams']).toEqual({'name': 'dave'}); + expect(match.instruction.params).toEqual({'name': 'dave'}); }); it('should be ignored on a wildcard segment', () => { - var rec = new PathRecognizer('/wild/*everything'); + var rec = new PathRecognizer('/wild/*everything', mockRouteHandler); var url = parser.parse('/wild/super;variable=value'); var match = rec.recognize(url); - expect(match['allParams']).toEqual({'everything': 'super;variable=value'}); + expect(match.instruction.params).toEqual({'everything': 'super;variable=value'}); }); it('should set matrix param values to true when no value is present', () => { - var rec = new PathRecognizer('/path'); + var rec = new PathRecognizer('/path', mockRouteHandler); var url = new Url('path', null, null, {'one': true, 'two': true, 'three': '3'}); var match = rec.recognize(url); - expect(match['allParams']).toEqual({'one': true, 'two': true, 'three': '3'}); + expect(match.instruction.params).toEqual({'one': true, 'two': true, 'three': '3'}); }); it('should be parsed on the final segment of the path', () => { - var rec = new PathRecognizer('/one/two/three'); + var rec = new PathRecognizer('/one/two/three', mockRouteHandler); var three = new Url('three', null, null, {'c': '3'}); var two = new Url('two', three, null, {'b': '2'}); var one = new Url('one', two, null, {'a': '1'}); var match = rec.recognize(one); - expect(match['allParams']).toEqual({'c': '3'}); + expect(match.instruction.params).toEqual({'c': '3'}); }); }); }); diff --git a/modules/angular2/test/router/route_config_spec.ts b/modules/angular2/test/router/route_config_spec.ts index 0a178f8207..bc1dc5400f 100644 --- a/modules/angular2/test/router/route_config_spec.ts +++ b/modules/angular2/test/router/route_config_spec.ts @@ -214,10 +214,7 @@ class HelloCmp { @Component({selector: 'app-cmp'}) @View({template: `root { }`, directives: ROUTER_DIRECTIVES}) -@RouteConfig([ - {path: '/before', redirectTo: ['Hello']}, - {path: '/after', component: HelloCmp, name: 'Hello'} -]) +@RouteConfig([{path: '/before', redirectTo: '/after'}, {path: '/after', component: HelloCmp}]) class RedirectAppCmp { constructor(public router: Router, public location: LocationStrategy) {} } diff --git a/modules/angular2/test/router/route_recognizer_spec.ts b/modules/angular2/test/router/route_recognizer_spec.ts new file mode 100644 index 0000000000..99426fd461 --- /dev/null +++ b/modules/angular2/test/router/route_recognizer_spec.ts @@ -0,0 +1,185 @@ +import { + AsyncTestCompleter, + describe, + it, + iit, + ddescribe, + expect, + inject, + beforeEach, + SpyObject +} from 'angular2/testing_internal'; + +import {Map, StringMapWrapper} from 'angular2/src/facade/collection'; + +import {RouteRecognizer} from 'angular2/src/router/route_recognizer'; +import {ComponentInstruction} from 'angular2/src/router/instruction'; + +import {Route, Redirect} from 'angular2/src/router/route_config_decorator'; +import {parser} from 'angular2/src/router/url_parser'; + +export function main() { + describe('RouteRecognizer', () => { + var recognizer; + + beforeEach(() => { recognizer = new RouteRecognizer(); }); + + + it('should recognize a static segment', () => { + recognizer.config(new Route({path: '/test', component: DummyCmpA})); + var solution = recognize(recognizer, '/test'); + expect(getComponentType(solution)).toEqual(DummyCmpA); + }); + + + it('should recognize a single slash', () => { + recognizer.config(new Route({path: '/', component: DummyCmpA})); + var solution = recognize(recognizer, '/'); + expect(getComponentType(solution)).toEqual(DummyCmpA); + }); + + + it('should recognize a dynamic segment', () => { + recognizer.config(new Route({path: '/user/:name', component: DummyCmpA})); + var solution = recognize(recognizer, '/user/brian'); + expect(getComponentType(solution)).toEqual(DummyCmpA); + expect(solution.params).toEqual({'name': 'brian'}); + }); + + + it('should recognize a star segment', () => { + recognizer.config(new Route({path: '/first/*rest', component: DummyCmpA})); + var solution = recognize(recognizer, '/first/second/third'); + expect(getComponentType(solution)).toEqual(DummyCmpA); + expect(solution.params).toEqual({'rest': 'second/third'}); + }); + + + it('should throw when given two routes that start with the same static segment', () => { + recognizer.config(new Route({path: '/hello', component: DummyCmpA})); + expect(() => recognizer.config(new Route({path: '/hello', component: DummyCmpB}))) + .toThrowError('Configuration \'/hello\' conflicts with existing route \'/hello\''); + }); + + + it('should throw when given two routes that have dynamic segments in the same order', () => { + recognizer.config(new Route({path: '/hello/:person/how/:doyoudou', component: DummyCmpA})); + expect(() => recognizer.config( + new Route({path: '/hello/:friend/how/:areyou', component: DummyCmpA}))) + .toThrowError( + 'Configuration \'/hello/:friend/how/:areyou\' conflicts with existing route \'/hello/:person/how/:doyoudou\''); + }); + + + it('should recognize redirects', () => { + recognizer.config(new Route({path: '/b', component: DummyCmpA})); + recognizer.config(new Redirect({path: '/a', redirectTo: 'b'})); + var solution = recognize(recognizer, '/a'); + expect(getComponentType(solution)).toEqual(DummyCmpA); + expect(solution.urlPath).toEqual('b'); + }); + + + it('should not perform root URL redirect on a non-root route', () => { + recognizer.config(new Redirect({path: '/', redirectTo: '/foo'})); + recognizer.config(new Route({path: '/bar', component: DummyCmpA})); + var solution = recognize(recognizer, '/bar'); + expect(solution.componentType).toEqual(DummyCmpA); + expect(solution.urlPath).toEqual('bar'); + }); + + + it('should perform a root URL redirect only for root routes', () => { + recognizer.config(new Redirect({path: '/', redirectTo: '/matias'})); + recognizer.config(new Route({path: '/matias', component: DummyCmpA})); + recognizer.config(new Route({path: '/fatias', component: DummyCmpA})); + + var solution; + + solution = recognize(recognizer, '/'); + expect(solution.urlPath).toEqual('matias'); + + solution = recognize(recognizer, '/fatias'); + expect(solution.urlPath).toEqual('fatias'); + + solution = recognize(recognizer, ''); + expect(solution.urlPath).toEqual('matias'); + }); + + + it('should generate URLs with params', () => { + recognizer.config(new Route({path: '/app/user/:name', component: DummyCmpA, name: 'User'})); + var instruction = recognizer.generate('User', {'name': 'misko'}); + expect(instruction.urlPath).toEqual('app/user/misko'); + }); + + + it('should generate URLs with numeric params', () => { + recognizer.config(new Route({path: '/app/page/:number', component: DummyCmpA, name: 'Page'})); + expect(recognizer.generate('Page', {'number': 42}).urlPath).toEqual('app/page/42'); + }); + + + it('should throw in the absence of required params URLs', () => { + recognizer.config(new Route({path: 'app/user/:name', component: DummyCmpA, name: 'User'})); + expect(() => recognizer.generate('User', {})) + .toThrowError('Route generator for \'name\' was not included in parameters passed.'); + }); + + + it('should throw if the route alias is not CamelCase', () => { + expect(() => recognizer.config( + new Route({path: 'app/user/:name', component: DummyCmpA, name: 'user'}))) + .toThrowError( + `Route "app/user/:name" with name "user" does not begin with an uppercase letter. Route names should be CamelCase like "User".`); + }); + + + describe('params', () => { + it('should recognize parameters within the URL path', () => { + recognizer.config(new Route({path: 'profile/:name', component: DummyCmpA, name: 'User'})); + var solution = recognize(recognizer, '/profile/matsko?comments=all'); + expect(solution.params).toEqual({'name': 'matsko', 'comments': 'all'}); + }); + + + it('should generate and populate the given static-based route with querystring params', + () => { + recognizer.config( + new Route({path: 'forum/featured', component: DummyCmpA, name: 'ForumPage'})); + + var params = {'start': 10, 'end': 100}; + + var result = recognizer.generate('ForumPage', params); + expect(result.urlPath).toEqual('forum/featured'); + expect(result.urlParams).toEqual(['start=10', 'end=100']); + }); + + + it('should prefer positional params over query params', () => { + recognizer.config(new Route({path: 'profile/:name', component: DummyCmpA, name: 'User'})); + + var solution = recognize(recognizer, '/profile/yegor?name=igor'); + expect(solution.params).toEqual({'name': 'yegor'}); + }); + + + it('should ignore matrix params for the top-level component', () => { + recognizer.config(new Route({path: '/home/:subject', component: DummyCmpA, name: 'User'})); + var solution = recognize(recognizer, '/home;sort=asc/zero;one=1?two=2'); + expect(solution.params).toEqual({'subject': 'zero', 'two': '2'}); + }); + }); + }); +} + +function recognize(recognizer: RouteRecognizer, url: string): ComponentInstruction { + return recognizer.recognize(parser.parse(url))[0].instruction; +} + +function getComponentType(routeMatch: ComponentInstruction): any { + return routeMatch.componentType; +} + +class DummyCmpA {} +class DummyCmpB {} diff --git a/modules/angular2/test/router/route_registry_spec.ts b/modules/angular2/test/router/route_registry_spec.ts index 869e4fb6ac..3b906bb749 100644 --- a/modules/angular2/test/router/route_registry_spec.ts +++ b/modules/angular2/test/router/route_registry_spec.ts @@ -21,9 +21,9 @@ import { AuxRoute, AsyncRoute } from 'angular2/src/router/route_config_decorator'; +import {stringifyInstruction} from 'angular2/src/router/instruction'; import {IS_DART} from 'angular2/src/facade/lang'; - export function main() { describe('RouteRegistry', () => { var registry; @@ -34,7 +34,7 @@ export function main() { registry.config(RootHostCmp, new Route({path: '/', component: DummyCmpA})); registry.config(RootHostCmp, new Route({path: '/test', component: DummyCmpB})); - registry.recognize('/test', [RootHostCmp]) + registry.recognize('/test', RootHostCmp) .then((instruction) => { expect(instruction.component.componentType).toBe(DummyCmpB); async.done(); @@ -45,32 +45,28 @@ export function main() { registry.config(RootHostCmp, new Route({path: '/first/...', component: DummyParentCmp, name: 'FirstCmp'})); - expect(stringifyInstruction(registry.generate(['FirstCmp', 'SecondCmp'], [RootHostCmp]))) + expect(stringifyInstruction(registry.generate(['FirstCmp', 'SecondCmp'], RootHostCmp))) .toEqual('first/second'); - expect(stringifyInstruction(registry.generate(['SecondCmp'], [DummyParentCmp]))) + expect(stringifyInstruction(registry.generate(['SecondCmp'], DummyParentCmp))) .toEqual('second'); }); - it('should generate URLs that account for default routes', () => { + xit('should generate URLs that account for redirects', () => { registry.config( RootHostCmp, - new Route({path: '/first/...', component: ParentWithDefaultRouteCmp, name: 'FirstCmp'})); + new Route({path: '/first/...', component: DummyParentRedirectCmp, name: 'FirstCmp'})); - var instruction = registry.generate(['FirstCmp'], [RootHostCmp]); - - expect(instruction.toLinkUrl()).toEqual('first'); - expect(instruction.toRootUrl()).toEqual('first/second'); + expect(stringifyInstruction(registry.generate(['FirstCmp'], RootHostCmp))) + .toEqual('first/second'); }); - it('should generate URLs in a hierarchy of default routes', () => { + xit('should generate URLs in a hierarchy of redirects', () => { registry.config( RootHostCmp, - new Route({path: '/first/...', component: MultipleDefaultCmp, name: 'FirstCmp'})); + new Route({path: '/first/...', component: DummyMultipleRedirectCmp, name: 'FirstCmp'})); - var instruction = registry.generate(['FirstCmp'], [RootHostCmp]); - - expect(instruction.toLinkUrl()).toEqual('first'); - expect(instruction.toRootUrl()).toEqual('first/second/third'); + expect(stringifyInstruction(registry.generate(['FirstCmp'], RootHostCmp))) + .toEqual('first/second/third'); }); it('should generate URLs with params', () => { @@ -79,13 +75,13 @@ export function main() { new Route({path: '/first/:param/...', component: DummyParentParamCmp, name: 'FirstCmp'})); var url = stringifyInstruction(registry.generate( - ['FirstCmp', {param: 'one'}, 'SecondCmp', {param: 'two'}], [RootHostCmp])); + ['FirstCmp', {param: 'one'}, 'SecondCmp', {param: 'two'}], RootHostCmp)); expect(url).toEqual('first/one/second/two'); }); it('should generate params as an empty StringMap when no params are given', () => { registry.config(RootHostCmp, new Route({path: '/test', component: DummyCmpA, name: 'Test'})); - var instruction = registry.generate(['Test'], [RootHostCmp]); + var instruction = registry.generate(['Test'], RootHostCmp); expect(instruction.component.params).toEqual({}); }); @@ -95,20 +91,20 @@ export function main() { RootHostCmp, new AsyncRoute({path: '/first/...', loader: asyncParentLoader, name: 'FirstCmp'})); - var instruction = registry.generate(['FirstCmp', 'SecondCmp'], [RootHostCmp]); + expect(() => registry.generate(['FirstCmp', 'SecondCmp'], RootHostCmp)) + .toThrowError('Could not find route named "SecondCmp".'); - expect(stringifyInstruction(instruction)).toEqual('first'); - - registry.recognize('/first/second', [RootHostCmp]) + registry.recognize('/first/second', RootHostCmp) .then((_) => { - var instruction = registry.generate(['FirstCmp', 'SecondCmp'], [RootHostCmp]); - expect(stringifyInstruction(instruction)).toEqual('first/second'); + expect( + stringifyInstruction(registry.generate(['FirstCmp', 'SecondCmp'], RootHostCmp))) + .toEqual('first/second'); async.done(); }); })); it('should throw when generating a url and a parent has no config', () => { - expect(() => registry.generate(['FirstCmp', 'SecondCmp'], [RootHostCmp])) + expect(() => registry.generate(['FirstCmp', 'SecondCmp'], RootHostCmp)) .toThrowError('Component "RootHostCmp" has no route config.'); }); @@ -117,7 +113,7 @@ export function main() { new Route({path: '/primary', component: DummyCmpA, name: 'Primary'})); registry.config(RootHostCmp, new AuxRoute({path: '/aux', component: DummyCmpB, name: 'Aux'})); - expect(stringifyInstruction(registry.generate(['Primary', ['Aux']], [RootHostCmp]))) + expect(stringifyInstruction(registry.generate(['Primary', ['Aux']], RootHostCmp))) .toEqual('primary(aux)'); }); @@ -125,7 +121,7 @@ export function main() { registry.config(RootHostCmp, new Route({path: '/:site', component: DummyCmpB})); registry.config(RootHostCmp, new Route({path: '/home', component: DummyCmpA})); - registry.recognize('/home', [RootHostCmp]) + registry.recognize('/home', RootHostCmp) .then((instruction) => { expect(instruction.component.componentType).toBe(DummyCmpA); async.done(); @@ -136,7 +132,7 @@ export function main() { registry.config(RootHostCmp, new Route({path: '/:site', component: DummyCmpA})); registry.config(RootHostCmp, new Route({path: '/*site', component: DummyCmpB})); - registry.recognize('/home', [RootHostCmp]) + registry.recognize('/home', RootHostCmp) .then((instruction) => { expect(instruction.component.componentType).toBe(DummyCmpA); async.done(); @@ -147,7 +143,7 @@ export function main() { registry.config(RootHostCmp, new Route({path: '/:first/*rest', component: DummyCmpA})); registry.config(RootHostCmp, new Route({path: '/*all', component: DummyCmpB})); - registry.recognize('/some/path', [RootHostCmp]) + registry.recognize('/some/path', RootHostCmp) .then((instruction) => { expect(instruction.component.componentType).toBe(DummyCmpA); async.done(); @@ -158,7 +154,7 @@ export function main() { registry.config(RootHostCmp, new Route({path: '/first/:second', component: DummyCmpA})); registry.config(RootHostCmp, new Route({path: '/:first/:second', component: DummyCmpB})); - registry.recognize('/first/second', [RootHostCmp]) + registry.recognize('/first/second', RootHostCmp) .then((instruction) => { expect(instruction.component.componentType).toBe(DummyCmpA); async.done(); @@ -172,7 +168,7 @@ export function main() { registry.config(RootHostCmp, new Route({path: '/first/:second/third', component: DummyCmpA})); - registry.recognize('/first/second/third', [RootHostCmp]) + registry.recognize('/first/second/third', RootHostCmp) .then((instruction) => { expect(instruction.component.componentType).toBe(DummyCmpB); async.done(); @@ -182,7 +178,7 @@ export function main() { it('should match the full URL using child components', inject([AsyncTestCompleter], (async) => { registry.config(RootHostCmp, new Route({path: '/first/...', component: DummyParentCmp})); - registry.recognize('/first/second', [RootHostCmp]) + registry.recognize('/first/second', RootHostCmp) .then((instruction) => { expect(instruction.component.componentType).toBe(DummyParentCmp); expect(instruction.child.component.componentType).toBe(DummyCmpB); @@ -194,14 +190,11 @@ export function main() { inject([AsyncTestCompleter], (async) => { registry.config(RootHostCmp, new Route({path: '/first/...', component: DummyAsyncCmp})); - registry.recognize('/first/second', [RootHostCmp]) + registry.recognize('/first/second', RootHostCmp) .then((instruction) => { expect(instruction.component.componentType).toBe(DummyAsyncCmp); - - instruction.child.resolveComponent().then((childComponentInstruction) => { - expect(childComponentInstruction.componentType).toBe(DummyCmpB); - async.done(); - }); + expect(instruction.child.component.componentType).toBe(DummyCmpB); + async.done(); }); })); @@ -210,14 +203,11 @@ export function main() { registry.config(RootHostCmp, new AsyncRoute({path: '/first/...', loader: asyncParentLoader})); - registry.recognize('/first/second', [RootHostCmp]) + registry.recognize('/first/second', RootHostCmp) .then((instruction) => { expect(instruction.component.componentType).toBe(DummyParentCmp); - - instruction.child.resolveComponent().then((childType) => { - expect(childType.componentType).toBe(DummyCmpB); - async.done(); - }); + expect(instruction.child.component.componentType).toBe(DummyCmpB); + async.done(); }); })); @@ -252,15 +242,15 @@ export function main() { it('should throw when linkParams are not terminal', () => { registry.config(RootHostCmp, new Route({path: '/first/...', component: DummyParentCmp, name: 'First'})); - expect(() => { registry.generate(['First'], [RootHostCmp]); }) - .toThrowError('Link "["First"]" does not resolve to a terminal instruction.'); + expect(() => { registry.generate(['First'], RootHostCmp); }) + .toThrowError('Link "["First"]" does not resolve to a terminal or async instruction.'); }); 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]) + registry.recognize('/first/second;filter=odd?comments=all', RootHostCmp) .then((instruction) => { expect(instruction.component.componentType).toBe(DummyParentCmp); expect(instruction.component.params).toEqual({'comments': 'all'}); @@ -286,18 +276,13 @@ export function main() { sort: 'asc', } ], - [RootHostCmp])); + RootHostCmp)); expect(url).toEqual('first/one/second/two;sort=asc?query=cats'); }); }); } -function stringifyInstruction(instruction): string { - return instruction.toRootUrl(); -} - - function asyncParentLoader() { return PromiseWrapper.resolve(DummyParentCmp); } @@ -315,22 +300,26 @@ class DummyAsyncCmp { class DummyCmpA {} class DummyCmpB {} -@RouteConfig( - [new Route({path: '/third', component: DummyCmpB, name: 'ThirdCmp', useAsDefault: true})]) -class DefaultRouteCmp { +@RouteConfig([ + new Redirect({path: '/', redirectTo: '/third'}), + new Route({path: '/third', component: DummyCmpB, name: 'ThirdCmp'}) +]) +class DummyRedirectCmp { } @RouteConfig([ - new Route( - {path: '/second/...', component: DefaultRouteCmp, name: 'SecondCmp', useAsDefault: true}) + new Redirect({path: '/', redirectTo: '/second'}), + new Route({path: '/second/...', component: DummyRedirectCmp, name: 'SecondCmp'}) ]) -class MultipleDefaultCmp { +class DummyMultipleRedirectCmp { } -@RouteConfig( - [new Route({path: '/second', component: DummyCmpB, name: 'SecondCmp', useAsDefault: true})]) -class ParentWithDefaultRouteCmp { +@RouteConfig([ + new Redirect({path: '/', redirectTo: '/second'}), + new Route({path: '/second', component: DummyCmpB, name: 'SecondCmp'}) +]) +class DummyParentRedirectCmp { } @RouteConfig([new Route({path: '/second', component: DummyCmpB, name: 'SecondCmp'})]) diff --git a/modules/angular2/test/router/router_link_spec.ts b/modules/angular2/test/router/router_link_spec.ts index 420fe9f0c6..b29d160ff6 100644 --- a/modules/angular2/test/router/router_link_spec.ts +++ b/modules/angular2/test/router/router_link_spec.ts @@ -8,7 +8,7 @@ import { expect, iit, inject, - beforeEachProviders, + beforeEachBindings, it, xit, TestComponentBuilder @@ -27,20 +27,24 @@ import { RouterOutlet, Route, RouteParams, + Instruction, ComponentInstruction } from 'angular2/router'; import {DOM} from 'angular2/src/platform/dom/dom_adapter'; -import {ResolvedInstruction} from 'angular2/src/router/instruction'; +import {ComponentInstruction_} from 'angular2/src/router/instruction'; +import {PathRecognizer} from 'angular2/src/router/path_recognizer'; +import {SyncRouteHandler} from 'angular2/src/router/sync_route_handler'; +let dummyPathRecognizer = new PathRecognizer('', new SyncRouteHandler(null)); let dummyInstruction = - new ResolvedInstruction(new ComponentInstruction('detail', [], null, null, true, 0), null, {}); + new Instruction(new ComponentInstruction_('detail', [], dummyPathRecognizer), null, {}); export function main() { describe('router-link directive', function() { var tcb: TestComponentBuilder; - beforeEachProviders(() => [ + beforeEachBindings(() => [ provide(Location, {useValue: makeDummyLocation()}), provide(Router, {useValue: makeDummyRouter()}) ]); @@ -102,6 +106,11 @@ export function main() { }); } +@Component({selector: 'my-comp'}) +class MyComp { + name; +} + @Component({selector: 'user-cmp'}) @View({template: "hello {{user}}"}) class UserCmp { diff --git a/modules/angular2/test/router/router_spec.ts b/modules/angular2/test/router/router_spec.ts index 0d611e3985..1fb2a3fb11 100644 --- a/modules/angular2/test/router/router_spec.ts +++ b/modules/angular2/test/router/router_spec.ts @@ -18,6 +18,7 @@ import {ListWrapper} from 'angular2/src/facade/collection'; import {Router, RootRouter} from 'angular2/src/router/router'; import {SpyLocation} from 'angular2/src/mock/location_mock'; import {Location} from 'angular2/src/router/location'; +import {stringifyInstruction} from 'angular2/src/router/instruction'; import {RouteRegistry} from 'angular2/src/router/route_registry'; import {RouteConfig, AsyncRoute, Route} from 'angular2/src/router/route_config_decorator'; @@ -224,11 +225,6 @@ export function main() { }); } - -function stringifyInstruction(instruction): string { - return instruction.toRootUrl(); -} - function loader(): Promise { return PromiseWrapper.resolve(DummyComponent); }