From 30de2db3496cc51407fde1c55dee6a2ba8ce610d Mon Sep 17 00:00:00 2001 From: vsavkin Date: Thu, 28 Apr 2016 18:33:48 -0700 Subject: [PATCH] cleanup(router): make analyzer happy Closes #8220 --- modules/angular2/alt_router.ts | 10 +- .../src/alt_router/directives/router_link.ts | 4 +- .../location/browser_platform_location.ts | 57 ------ .../location/hash_location_strategy.ts | 101 ---------- .../src/alt_router/location/location.ts | 179 ------------------ .../alt_router/location/location_strategy.ts | 62 ------ .../location/path_location_strategy.ts | 105 ---------- .../alt_router/location/platform_location.ts | 48 ----- modules/angular2/src/alt_router/recognize.ts | 65 +++++-- modules/angular2/src/alt_router/router.ts | 22 +-- .../src/alt_router/router_providers.ts | 12 ++ .../src/alt_router/router_providers_common.ts | 35 ++++ .../src/alt_router/router_url_serializer.ts | 4 +- modules/angular2/src/alt_router/segments.ts | 4 +- .../test/alt_router/integration_spec.ts | 60 +++--- .../test/alt_router/recognize_spec.ts | 32 +++- .../alt_router/router_url_serializer_spec.ts | 20 -- 17 files changed, 174 insertions(+), 646 deletions(-) delete mode 100644 modules/angular2/src/alt_router/location/browser_platform_location.ts delete mode 100644 modules/angular2/src/alt_router/location/hash_location_strategy.ts delete mode 100644 modules/angular2/src/alt_router/location/location.ts delete mode 100644 modules/angular2/src/alt_router/location/location_strategy.ts delete mode 100644 modules/angular2/src/alt_router/location/path_location_strategy.ts delete mode 100644 modules/angular2/src/alt_router/location/platform_location.ts create mode 100644 modules/angular2/src/alt_router/router_providers.ts create mode 100644 modules/angular2/src/alt_router/router_providers_common.ts diff --git a/modules/angular2/alt_router.ts b/modules/angular2/alt_router.ts index f3ccb9c703..f4565b22a4 100644 --- a/modules/angular2/alt_router.ts +++ b/modules/angular2/alt_router.ts @@ -5,7 +5,7 @@ */ export {Router, RouterOutletMap} from './src/alt_router/router'; -export {RouteSegment} from './src/alt_router/segments'; +export {RouteSegment, UrlSegment, Tree} from './src/alt_router/segments'; export {Routes} from './src/alt_router/metadata/decorators'; export {Route} from './src/alt_router/metadata/metadata'; export { @@ -13,13 +13,7 @@ export { DefaultRouterUrlSerializer } from './src/alt_router/router_url_serializer'; export {OnActivate} from './src/alt_router/interfaces'; - -export {Location} from './src/alt_router/location/location'; -export {LocationStrategy} from './src/alt_router/location/location_strategy'; -export {PathLocationStrategy} from './src/alt_router/location/path_location_strategy'; -export {HashLocationStrategy} from './src/alt_router/location/hash_location_strategy'; -export {PlatformLocation} from './src/alt_router/location/platform_location'; -export {BrowserPlatformLocation} from './src/alt_router/location/browser_platform_location'; +export {ROUTER_PROVIDERS} from './src/alt_router/router_providers'; import {RouterOutlet} from './src/alt_router/directives/router_outlet'; import {RouterLink} from './src/alt_router/directives/router_link'; diff --git a/modules/angular2/src/alt_router/directives/router_link.ts b/modules/angular2/src/alt_router/directives/router_link.ts index 09735da6d6..7a440e4059 100644 --- a/modules/angular2/src/alt_router/directives/router_link.ts +++ b/modules/angular2/src/alt_router/directives/router_link.ts @@ -28,7 +28,7 @@ export class RouterLink implements OnDestroy { @HostBinding() private href: string; - constructor(private _router: Router, private _segment: RouteSegment) { + constructor(private _router: Router) { this._subscription = ObservableWrapper.subscribe(_router.changes, (_) => { this._targetUrl = _router.urlTree; this._updateTargetUrlAndHref(); @@ -53,7 +53,7 @@ export class RouterLink implements OnDestroy { } private _updateTargetUrlAndHref(): void { - this._targetUrl = link(this._segment, this._router.urlTree, this._changes); + this._targetUrl = link(null, this._router.urlTree, this._changes); this.href = this._router.serializeUrl(this._targetUrl); } } \ No newline at end of file diff --git a/modules/angular2/src/alt_router/location/browser_platform_location.ts b/modules/angular2/src/alt_router/location/browser_platform_location.ts deleted file mode 100644 index 3572080582..0000000000 --- a/modules/angular2/src/alt_router/location/browser_platform_location.ts +++ /dev/null @@ -1,57 +0,0 @@ -import {Injectable} from 'angular2/src/core/di/decorators'; -import {UrlChangeListener, PlatformLocation} from './platform_location'; -import {History, Location} from 'angular2/src/facade/browser'; -import {DOM} from 'angular2/src/platform/dom/dom_adapter'; - -/** - * `PlatformLocation` encapsulates all of the direct calls to platform APIs. - * This class should not be used directly by an application developer. Instead, use - * {@link Location}. - */ -@Injectable() -export class BrowserPlatformLocation extends PlatformLocation { - private _location: Location; - private _history: History; - - constructor() { - super(); - this._init(); - } - - // This is moved to its own method so that `MockPlatformLocationStrategy` can overwrite it - /** @internal */ - _init() { - this._location = DOM.getLocation(); - this._history = DOM.getHistory(); - } - - /** @internal */ - get location(): Location { return this._location; } - - getBaseHrefFromDOM(): string { return DOM.getBaseHref(); } - - onPopState(fn: UrlChangeListener): void { - DOM.getGlobalEventTarget('window').addEventListener('popstate', fn, false); - } - - onHashChange(fn: UrlChangeListener): void { - DOM.getGlobalEventTarget('window').addEventListener('hashchange', fn, false); - } - - get pathname(): string { return this._location.pathname; } - get search(): string { return this._location.search; } - get hash(): string { return this._location.hash; } - set pathname(newPath: string) { this._location.pathname = newPath; } - - pushState(state: any, title: string, url: string): void { - this._history.pushState(state, title, url); - } - - replaceState(state: any, title: string, url: string): void { - this._history.replaceState(state, title, url); - } - - forward(): void { this._history.forward(); } - - back(): void { this._history.back(); } -} diff --git a/modules/angular2/src/alt_router/location/hash_location_strategy.ts b/modules/angular2/src/alt_router/location/hash_location_strategy.ts deleted file mode 100644 index 0f1fccd994..0000000000 --- a/modules/angular2/src/alt_router/location/hash_location_strategy.ts +++ /dev/null @@ -1,101 +0,0 @@ -import {Injectable, Inject, Optional} from 'angular2/core'; -import {LocationStrategy, APP_BASE_HREF} from './location_strategy'; -import {Location} from './location'; -import {UrlChangeListener, PlatformLocation} from './platform_location'; -import {isPresent} from 'angular2/src/facade/lang'; - -/** - * `HashLocationStrategy` is a {@link LocationStrategy} used to configure the - * {@link Location} service to represent its state in the - * [hash fragment](https://en.wikipedia.org/wiki/Uniform_Resource_Locator#Syntax) - * of the browser's URL. - * - * For instance, if you call `location.go('/foo')`, the browser's URL will become - * `example.com#/foo`. - * - * ### Example - * - * ``` - * import {Component, provide} from 'angular2/core'; - * import { - * Location, - * LocationStrategy, - * HashLocationStrategy - * } from 'angular2/platform/common'; - * import { - * ROUTER_DIRECTIVES, - * ROUTER_PROVIDERS, - * RouteConfig - * } from 'angular2/router'; - * - * @Component({directives: [ROUTER_DIRECTIVES]}) - * @RouteConfig([ - * {...}, - * ]) - * class AppCmp { - * constructor(location: Location) { - * location.go('/foo'); - * } - * } - * - * bootstrap(AppCmp, [ - * ROUTER_PROVIDERS, - * provide(LocationStrategy, {useClass: HashLocationStrategy}) - * ]); - * ``` - */ -@Injectable() -export class HashLocationStrategy extends LocationStrategy { - private _baseHref: string = ''; - constructor(private _platformLocation: PlatformLocation, - @Optional() @Inject(APP_BASE_HREF) _baseHref?: string) { - super(); - if (isPresent(_baseHref)) { - this._baseHref = _baseHref; - } - } - - onPopState(fn: UrlChangeListener): void { - this._platformLocation.onPopState(fn); - this._platformLocation.onHashChange(fn); - } - - getBaseHref(): string { return this._baseHref; } - - path(): string { - // the hash value is always prefixed with a `#` - // and if it is empty then it will stay empty - var path = this._platformLocation.hash; - if (!isPresent(path)) path = '#'; - - // Dart will complain if a call to substring is - // executed with a position value that extends the - // length of string. - return (path.length > 0 ? path.substring(1) : path); - } - - prepareExternalUrl(internal: string): string { - var url = Location.joinWithSlash(this._baseHref, internal); - return url.length > 0 ? ('#' + url) : url; - } - - pushState(state: any, title: string, path: string, queryParams: string) { - var url = this.prepareExternalUrl(path + Location.normalizeQueryParams(queryParams)); - if (url.length == 0) { - url = this._platformLocation.pathname; - } - this._platformLocation.pushState(state, title, url); - } - - replaceState(state: any, title: string, path: string, queryParams: string) { - var url = this.prepareExternalUrl(path + Location.normalizeQueryParams(queryParams)); - if (url.length == 0) { - url = this._platformLocation.pathname; - } - this._platformLocation.replaceState(state, title, url); - } - - forward(): void { this._platformLocation.forward(); } - - back(): void { this._platformLocation.back(); } -} diff --git a/modules/angular2/src/alt_router/location/location.ts b/modules/angular2/src/alt_router/location/location.ts deleted file mode 100644 index ef4254be27..0000000000 --- a/modules/angular2/src/alt_router/location/location.ts +++ /dev/null @@ -1,179 +0,0 @@ -import {EventEmitter, ObservableWrapper} from 'angular2/src/facade/async'; -import {Injectable, Inject} from 'angular2/core'; -import {LocationStrategy} from './location_strategy'; - -/** - * `Location` is a service that applications can use to interact with a browser's URL. - * Depending on which {@link LocationStrategy} is used, `Location` will either persist - * to the URL's path or the URL's hash segment. - * - * Note: it's better to use {@link Router#navigate} service to trigger route changes. Use - * `Location` only if you need to interact with or create normalized URLs outside of - * routing. - * - * `Location` is responsible for normalizing the URL against the application's base href. - * A normalized URL is absolute from the URL host, includes the application's base href, and has no - * trailing slash: - * - `/my/app/user/123` is normalized - * - `my/app/user/123` **is not** normalized - * - `/my/app/user/123/` **is not** normalized - * - * ### Example - * - * ``` - * import {Component} from 'angular2/core'; - * import {Location} from 'angular2/platform/common'; - * import { - * ROUTER_DIRECTIVES, - * ROUTER_PROVIDERS, - * RouteConfig - * } from 'angular2/router'; - * - * @Component({directives: [ROUTER_DIRECTIVES]}) - * @RouteConfig([ - * {...}, - * ]) - * class AppCmp { - * constructor(location: Location) { - * location.go('/foo'); - * } - * } - * - * bootstrap(AppCmp, [ROUTER_PROVIDERS]); - * ``` - */ -@Injectable() -export class Location { - /** @internal */ - _subject: EventEmitter = new EventEmitter(); - /** @internal */ - _baseHref: string; - - constructor(public platformStrategy: LocationStrategy) { - var browserBaseHref = this.platformStrategy.getBaseHref(); - this._baseHref = Location.stripTrailingSlash(_stripIndexHtml(browserBaseHref)); - this.platformStrategy.onPopState((ev) => { - ObservableWrapper.callEmit(this._subject, {'url': this.path(), 'pop': true, 'type': ev.type}); - }); - } - - /** - * Returns the normalized URL path. - */ - path(): string { return this.normalize(this.platformStrategy.path()); } - - /** - * Given a string representing a URL, returns the normalized URL path without leading or - * trailing slashes - */ - normalize(url: string): string { - return Location.stripTrailingSlash(_stripBaseHref(this._baseHref, _stripIndexHtml(url))); - } - - /** - * Given a string representing a URL, returns the platform-specific external URL path. - * If the given URL doesn't begin with a leading slash (`'/'`), this method adds one - * before normalizing. This method will also add a hash if `HashLocationStrategy` is - * used, or the `APP_BASE_HREF` if the `PathLocationStrategy` is in use. - */ - prepareExternalUrl(url: string): string { - if (url.length > 0 && !url.startsWith('/')) { - url = '/' + url; - } - return this.platformStrategy.prepareExternalUrl(url); - } - - // TODO: rename this method to pushState - /** - * Changes the browsers URL to the normalized version of the given URL, and pushes a - * new item onto the platform's history. - */ - go(path: string, query: string = ''): void { - this.platformStrategy.pushState(null, '', path, query); - } - - /** - * Changes the browsers URL to the normalized version of the given URL, and replaces - * the top item on the platform's history stack. - */ - replaceState(path: string, query: string = ''): void { - this.platformStrategy.replaceState(null, '', path, query); - } - - /** - * Navigates forward in the platform's history. - */ - forward(): void { this.platformStrategy.forward(); } - - /** - * Navigates back in the platform's history. - */ - back(): void { this.platformStrategy.back(); } - - /** - * Subscribe to the platform's `popState` events. - */ - subscribe(onNext: (value: any) => void, onThrow: (exception: any) => void = null, - onReturn: () => void = null): Object { - return ObservableWrapper.subscribe(this._subject, onNext, onThrow, onReturn); - } - - /** - * Given a string of url parameters, prepend with '?' if needed, otherwise return parameters as - * is. - */ - public static normalizeQueryParams(params: string): string { - return (params.length > 0 && params.substring(0, 1) != '?') ? ('?' + params) : params; - } - - /** - * Given 2 parts of a url, join them with a slash if needed. - */ - public static joinWithSlash(start: string, end: string): string { - if (start.length == 0) { - return end; - } - if (end.length == 0) { - return start; - } - var slashes = 0; - if (start.endsWith('/')) { - slashes++; - } - if (end.startsWith('/')) { - slashes++; - } - if (slashes == 2) { - return start + end.substring(1); - } - if (slashes == 1) { - return start + end; - } - return start + '/' + end; - } - - /** - * If url has a trailing slash, remove it, otherwise return url as is. - */ - public static stripTrailingSlash(url: string): string { - if (/\/$/g.test(url)) { - url = url.substring(0, url.length - 1); - } - return url; - } -} - -function _stripBaseHref(baseHref: string, url: string): string { - if (baseHref.length > 0 && url.startsWith(baseHref)) { - return url.substring(baseHref.length); - } - return url; -} - -function _stripIndexHtml(url: string): string { - if (/\/index.html$/g.test(url)) { - // '/index.html'.length == 11 - return url.substring(0, url.length - 11); - } - return url; -} diff --git a/modules/angular2/src/alt_router/location/location_strategy.ts b/modules/angular2/src/alt_router/location/location_strategy.ts deleted file mode 100644 index c7e93db163..0000000000 --- a/modules/angular2/src/alt_router/location/location_strategy.ts +++ /dev/null @@ -1,62 +0,0 @@ -import {CONST_EXPR} from 'angular2/src/facade/lang'; -import {OpaqueToken} from 'angular2/core'; -import {UrlChangeListener} from './platform_location'; - -/** - * `LocationStrategy` is responsible for representing and reading route state - * from the browser's URL. Angular provides two strategies: - * {@link HashLocationStrategy} and {@link PathLocationStrategy} (default). - * - * This is used under the hood of the {@link Location} service. - * - * Applications should use the {@link Router} or {@link Location} services to - * interact with application route state. - * - * For instance, {@link HashLocationStrategy} produces URLs like - * `http://example.com#/foo`, and {@link PathLocationStrategy} produces - * `http://example.com/foo` as an equivalent URL. - * - * See these two classes for more. - */ -export abstract class LocationStrategy { - abstract path(): string; - abstract prepareExternalUrl(internal: string): string; - abstract pushState(state: any, title: string, url: string, queryParams: string): void; - abstract replaceState(state: any, title: string, url: string, queryParams: string): void; - abstract forward(): void; - abstract back(): void; - abstract onPopState(fn: UrlChangeListener): void; - abstract getBaseHref(): string; -} - - -/** - * The `APP_BASE_HREF` token represents the base href to be used with the - * {@link PathLocationStrategy}. - * - * If you're using {@link PathLocationStrategy}, you must provide a provider to a string - * representing the URL prefix that should be preserved when generating and recognizing - * URLs. - * - * ### Example - * - * ``` - * import {Component} from 'angular2/core'; - * import {ROUTER_DIRECTIVES, ROUTER_PROVIDERS, RouteConfig} from 'angular2/router'; - * import {APP_BASE_HREF} from 'angular2/platform/common'; - * - * @Component({directives: [ROUTER_DIRECTIVES]}) - * @RouteConfig([ - * {...}, - * ]) - * class AppCmp { - * // ... - * } - * - * bootstrap(AppCmp, [ - * ROUTER_PROVIDERS, - * provide(APP_BASE_HREF, {useValue: '/my/app'}) - * ]); - * ``` - */ -export const APP_BASE_HREF: OpaqueToken = CONST_EXPR(new OpaqueToken('appBaseHref')); diff --git a/modules/angular2/src/alt_router/location/path_location_strategy.ts b/modules/angular2/src/alt_router/location/path_location_strategy.ts deleted file mode 100644 index e5d953a045..0000000000 --- a/modules/angular2/src/alt_router/location/path_location_strategy.ts +++ /dev/null @@ -1,105 +0,0 @@ -import {Injectable, Inject, Optional} from 'angular2/core'; -import {isBlank} from 'angular2/src/facade/lang'; -import {BaseException} from 'angular2/src/facade/exceptions'; -import {PlatformLocation, UrlChangeListener} from './platform_location'; -import {LocationStrategy, APP_BASE_HREF} from './location_strategy'; -import {Location} from './location'; - -/** - * `PathLocationStrategy` is a {@link LocationStrategy} used to configure the - * {@link Location} service to represent its state in the - * [path](https://en.wikipedia.org/wiki/Uniform_Resource_Locator#Syntax) of the - * browser's URL. - * - * `PathLocationStrategy` is the default binding for {@link LocationStrategy} - * provided in {@link ROUTER_PROVIDERS}. - * - * If you're using `PathLocationStrategy`, you must provide a provider for - * {@link APP_BASE_HREF} to a string representing the URL prefix that should - * be preserved when generating and recognizing URLs. - * - * For instance, if you provide an `APP_BASE_HREF` of `'/my/app'` and call - * `location.go('/foo')`, the browser's URL will become - * `example.com/my/app/foo`. - * - * ### Example - * - * ``` - * import {Component, provide} from 'angular2/core'; - * import {bootstrap} from 'angular2/platform/browser'; - * import { - * Location, - * APP_BASE_HREF - * } from 'angular2/platform/common'; - * import { - * ROUTER_DIRECTIVES, - * ROUTER_PROVIDERS, - * RouteConfig - * } from 'angular2/router'; - * - * @Component({directives: [ROUTER_DIRECTIVES]}) - * @RouteConfig([ - * {...}, - * ]) - * class AppCmp { - * constructor(location: Location) { - * location.go('/foo'); - * } - * } - * - * bootstrap(AppCmp, [ - * ROUTER_PROVIDERS, // includes binding to PathLocationStrategy - * provide(APP_BASE_HREF, {useValue: '/my/app'}) - * ]); - * ``` - */ -@Injectable() -export class PathLocationStrategy extends LocationStrategy { - private _baseHref: string; - - constructor(private _platformLocation: PlatformLocation, - @Optional() @Inject(APP_BASE_HREF) href?: string) { - super(); - - if (isBlank(href)) { - href = this._platformLocation.getBaseHrefFromDOM(); - } - - if (isBlank(href)) { - throw new BaseException( - `No base href set. Please provide a value for the APP_BASE_HREF token or add a base element to the document.`); - } - - this._baseHref = href; - } - - onPopState(fn: UrlChangeListener): void { - this._platformLocation.onPopState(fn); - this._platformLocation.onHashChange(fn); - } - - getBaseHref(): string { return this._baseHref; } - - prepareExternalUrl(internal: string): string { - return Location.joinWithSlash(this._baseHref, internal); - } - - path(): string { - return this._platformLocation.pathname + - Location.normalizeQueryParams(this._platformLocation.search); - } - - pushState(state: any, title: string, url: string, queryParams: string) { - var externalUrl = this.prepareExternalUrl(url + Location.normalizeQueryParams(queryParams)); - this._platformLocation.pushState(state, title, externalUrl); - } - - replaceState(state: any, title: string, url: string, queryParams: string) { - var externalUrl = this.prepareExternalUrl(url + Location.normalizeQueryParams(queryParams)); - this._platformLocation.replaceState(state, title, externalUrl); - } - - forward(): void { this._platformLocation.forward(); } - - back(): void { this._platformLocation.back(); } -} diff --git a/modules/angular2/src/alt_router/location/platform_location.ts b/modules/angular2/src/alt_router/location/platform_location.ts deleted file mode 100644 index 7c41d2e692..0000000000 --- a/modules/angular2/src/alt_router/location/platform_location.ts +++ /dev/null @@ -1,48 +0,0 @@ -/** - * This class should not be used directly by an application developer. Instead, use - * {@link Location}. - * - * `PlatformLocation` encapsulates all calls to DOM apis, which allows the Router to be platform - * agnostic. - * This means that we can have different implementation of `PlatformLocation` for the different - * platforms - * that angular supports. For example, the default `PlatformLocation` is {@link - * BrowserPlatformLocation}, - * however when you run your app in a WebWorker you use {@link WebWorkerPlatformLocation}. - * - * The `PlatformLocation` class is used directly by all implementations of {@link LocationStrategy} - * when - * they need to interact with the DOM apis like pushState, popState, etc... - * - * {@link LocationStrategy} in turn is used by the {@link Location} service which is used directly - * by - * the {@link Router} in order to navigate between routes. Since all interactions between {@link - * Router} / - * {@link Location} / {@link LocationStrategy} and DOM apis flow through the `PlatformLocation` - * class - * they are all platform independent. - */ -export abstract class PlatformLocation { - abstract getBaseHrefFromDOM(): string; - abstract onPopState(fn: UrlChangeListener): void; - abstract onHashChange(fn: UrlChangeListener): void; - - /* abstract */ get pathname(): string { return null; } - /* abstract */ get search(): string { return null; } - /* abstract */ get hash(): string { return null; } - - abstract replaceState(state: any, title: string, url: string): void; - - abstract pushState(state: any, title: string, url: string): void; - - abstract forward(): void; - - abstract back(): void; -} - -/** - * A serializable version of the event from onPopState or onHashChange - */ -export interface UrlChangeEvent { type: string; } - -export interface UrlChangeListener { (e: UrlChangeEvent): any; } diff --git a/modules/angular2/src/alt_router/recognize.ts b/modules/angular2/src/alt_router/recognize.ts index 5adb71fd15..7fb631566c 100644 --- a/modules/angular2/src/alt_router/recognize.ts +++ b/modules/angular2/src/alt_router/recognize.ts @@ -10,17 +10,18 @@ import {reflector} from 'angular2/src/core/reflection/reflection'; export function recognize(componentResolver: ComponentResolver, type: Type, url: Tree): Promise> { - return componentResolver.resolveComponent(type).then(factory => { - let segment = - new RouteSegment([url.root], url.root.parameters, DEFAULT_OUTLET_NAME, type, factory); - return _recognizeMany(componentResolver, type, rootNode(url).children) - .then(children => new Tree(new TreeNode(segment, children))); - }); + let matched = new _MatchResult(type, [url.root], null, rootNode(url).children, []); + return _constructSegment(componentResolver, matched) + .then(roots => new Tree(roots[0])); } function _recognize(componentResolver: ComponentResolver, parentType: Type, url: TreeNode): Promise[]> { let metadata = _readMetadata(parentType); // should read from the factory instead + if (isBlank(metadata)) { + throw new BaseException( + `Component '${stringify(parentType)}' does not have route configuration`); + } let match; try { @@ -43,18 +44,45 @@ function _recognizeMany(componentResolver: ComponentResolver, parentType: Type, function _constructSegment(componentResolver: ComponentResolver, matched: _MatchResult): Promise[]> { - return componentResolver.resolveComponent(matched.route.component) + return componentResolver.resolveComponent(matched.component) .then(factory => { let urlOutlet = matched.consumedUrlSegments[0].outlet; let segment = new RouteSegment(matched.consumedUrlSegments, matched.parameters, isBlank(urlOutlet) ? DEFAULT_OUTLET_NAME : urlOutlet, - matched.route.component, factory); + matched.component, factory); if (matched.leftOverUrl.length > 0) { - return _recognizeMany(componentResolver, matched.route.component, matched.leftOverUrl) + return _recognizeMany(componentResolver, matched.component, matched.leftOverUrl) .then(children => [new TreeNode(segment, children)]); } else { - return [new TreeNode(segment, [])]; + return _recognizeLeftOvers(componentResolver, matched.component) + .then(children => [new TreeNode(segment, children)]); + } + }); +} + +function _recognizeLeftOvers(componentResolver: ComponentResolver, + parentType: Type): Promise[]> { + return componentResolver.resolveComponent(parentType) + .then(factory => { + let metadata = _readMetadata(parentType); + if (isBlank(metadata)) { + return []; + } + + let r = (metadata.routes).filter(r => r.path == "" || r.path == "/"); + if (r.length === 0) { + return PromiseWrapper.resolve([]); + } else { + return _recognizeLeftOvers(componentResolver, r[0].component) + .then(children => { + return componentResolver.resolveComponent(r[0].component) + .then(factory => { + let segment = + new RouteSegment([], null, DEFAULT_OUTLET_NAME, r[0].component, factory); + return [new TreeNode(segment, children)]; + }); + }); } }); } @@ -66,11 +94,14 @@ function _match(metadata: RoutesMetadata, url: TreeNode): _MatchResu return matchingResult; } } - throw new BaseException("Cannot match any routes"); + let availableRoutes = metadata.routes.map(r => `'${r.path}'`).join(", "); + throw new BaseException( + `Cannot match any routes. Current segment: '${url.value}'. Available routes: [${availableRoutes}].`); } function _matchWithParts(route: RouteMetadata, url: TreeNode): _MatchResult { - let parts = route.path.split("/"); + let path = route.path.startsWith("/") ? route.path.substring(1) : route.path; + let parts = path.split("/"); let positionalParams = {}; let consumedUrlSegments = []; @@ -113,7 +144,7 @@ function _matchWithParts(route: RouteMetadata, url: TreeNode): _Matc <{[key: string]: string}>StringMapWrapper.merge(isBlank(p) ? {} : p, positionalParams); let axuUrlSubtrees = isPresent(lastParent) ? lastParent.children.slice(1) : []; - return new _MatchResult(route, consumedUrlSegments, parameters, lastSegment.children, + return new _MatchResult(route.component, consumedUrlSegments, parameters, lastSegment.children, axuUrlSubtrees); } @@ -132,16 +163,12 @@ function _checkOutletNameUniqueness(nodes: TreeNode[]): TreeNode[], public aux: TreeNode[]) {} } function _readMetadata(componentType: Type) { let metadata = reflector.annotations(componentType).filter(f => f instanceof RoutesMetadata); - if (metadata.length === 0) { - throw new BaseException( - `Component '${stringify(componentType)}' does not have route configuration`); - } - return metadata[0]; + return ListWrapper.first(metadata); } \ No newline at end of file diff --git a/modules/angular2/src/alt_router/router.ts b/modules/angular2/src/alt_router/router.ts index 95e60feabf..8c3a20730d 100644 --- a/modules/angular2/src/alt_router/router.ts +++ b/modules/angular2/src/alt_router/router.ts @@ -1,13 +1,12 @@ -import {provide, ReflectiveInjector, ComponentResolver} from 'angular2/core'; +import {OnInit, provide, ReflectiveInjector, ComponentResolver} from 'angular2/core'; import {RouterOutlet} from './directives/router_outlet'; import {Type, isBlank, isPresent} from 'angular2/src/facade/lang'; import {EventEmitter, Observable} from 'angular2/src/facade/async'; import {StringMapWrapper} from 'angular2/src/facade/collection'; import {BaseException} from 'angular2/src/facade/exceptions'; -import {BaseException} from 'angular2/src/facade/exceptions'; import {RouterUrlSerializer} from './router_url_serializer'; import {recognize} from './recognize'; -import {Location} from './location/location'; +import {Location} from 'angular2/platform/common'; import { equalSegments, routeSegmentComponentFactory, @@ -31,15 +30,12 @@ export class Router { private _prevTree: Tree; private _urlTree: Tree; - private _location: Location; - private _changes: EventEmitter = new EventEmitter(); constructor(private _componentType: Type, private _componentResolver: ComponentResolver, private _urlSerializer: RouterUrlSerializer, - private _routerOutletMap: RouterOutletMap, location: Location) { - this._location = location; - this.navigateByUrl(location.path()); + private _routerOutletMap: RouterOutletMap, private _location: Location) { + this.navigateByUrl(this._location.path()); } get urlTree(): Tree { return this._urlTree; } @@ -48,9 +44,7 @@ export class Router { this._urlTree = url; return recognize(this._componentResolver, this._componentType, url) .then(currTree => { - let prevRoot = isPresent(this._prevTree) ? rootNode(this._prevTree) : null; - new _LoadSegments(currTree, this._prevTree) - .loadSegments(rootNode(currTree), prevRoot, this._routerOutletMap); + new _LoadSegments(currTree, this._prevTree).load(this._routerOutletMap); this._prevTree = currTree; this._location.go(this._urlSerializer.serialize(this._urlTree)); this._changes.emit(null); @@ -69,6 +63,12 @@ export class Router { class _LoadSegments { constructor(private currTree: Tree, private prevTree: Tree) {} + load(parentOutletMap: RouterOutletMap): void { + let prevRoot = isPresent(this.prevTree) ? rootNode(this.prevTree) : null; + let currRoot = rootNode(this.currTree); + this.loadChildSegments(currRoot, prevRoot, parentOutletMap); + } + loadSegments(currNode: TreeNode, prevNode: TreeNode, parentOutletMap: RouterOutletMap): void { let curr = currNode.value; diff --git a/modules/angular2/src/alt_router/router_providers.ts b/modules/angular2/src/alt_router/router_providers.ts new file mode 100644 index 0000000000..c54d3d6769 --- /dev/null +++ b/modules/angular2/src/alt_router/router_providers.ts @@ -0,0 +1,12 @@ +import {ROUTER_PROVIDERS_COMMON} from './router_providers_common'; +import {Provider} from 'angular2/core'; +import { + BrowserPlatformLocation +} from 'angular2/src/platform/browser/location/browser_platform_location'; +import {PlatformLocation} from 'angular2/platform/common'; +import {CONST_EXPR} from 'angular2/src/facade/lang'; + +export const ROUTER_PROVIDERS: any[] = CONST_EXPR([ + ROUTER_PROVIDERS_COMMON, + CONST_EXPR(new Provider(PlatformLocation, {useClass: BrowserPlatformLocation})), +]); \ No newline at end of file diff --git a/modules/angular2/src/alt_router/router_providers_common.ts b/modules/angular2/src/alt_router/router_providers_common.ts new file mode 100644 index 0000000000..1f2aa87e33 --- /dev/null +++ b/modules/angular2/src/alt_router/router_providers_common.ts @@ -0,0 +1,35 @@ +import {OpaqueToken, ComponentResolver} from 'angular2/core'; +import {LocationStrategy, PathLocationStrategy, Location} from 'angular2/platform/common'; +import {Router, RouterOutletMap} from './router'; +import {RouterUrlSerializer, DefaultRouterUrlSerializer} from './router_url_serializer'; +import {CONST_EXPR} from 'angular2/src/facade/lang'; +import {ApplicationRef, Provider} from 'angular2/core'; +import {BaseException} from 'angular2/src/facade/exceptions'; + +export const ROUTER_PROVIDERS_COMMON: any[] = CONST_EXPR([ + RouterOutletMap, + CONST_EXPR(new Provider(RouterUrlSerializer, {useClass: DefaultRouterUrlSerializer})), + CONST_EXPR(new Provider(LocationStrategy, {useClass: PathLocationStrategy})), + Location, + CONST_EXPR(new Provider(Router, + { + useFactory: routerFactory, + deps: CONST_EXPR([ + ApplicationRef, + ComponentResolver, + RouterUrlSerializer, + RouterOutletMap, + Location + ]) + })) +]); + +function routerFactory(app: ApplicationRef, componentResolver: ComponentResolver, + urlSerializer: RouterUrlSerializer, routerOutletMap: RouterOutletMap, + location: Location): Router { + if (app.componentTypes.length == 0) { + throw new BaseException("Bootstrap at least one component before injecting Router."); + } + return new Router(app.componentTypes[0], componentResolver, urlSerializer, routerOutletMap, + location); +} \ No newline at end of file diff --git a/modules/angular2/src/alt_router/router_url_serializer.ts b/modules/angular2/src/alt_router/router_url_serializer.ts index ccb3adb798..7757e7f1e5 100644 --- a/modules/angular2/src/alt_router/router_url_serializer.ts +++ b/modules/angular2/src/alt_router/router_url_serializer.ts @@ -64,7 +64,7 @@ class _UrlParser { parse(url: string): TreeNode { this._remaining = url; if (url == '' || url == '/') { - return new TreeNode(new UrlSegment('', {}, null), []); + return new TreeNode(new UrlSegment('', null, null), []); } else { return this.parseRoot(); } @@ -72,7 +72,7 @@ class _UrlParser { parseRoot(): TreeNode { let segments = this.parseSegments(); - let queryParams = this.peekStartsWith('?') ? this.parseQueryParams() : {}; + let queryParams = this.peekStartsWith('?') ? this.parseQueryParams() : null; return new TreeNode(new UrlSegment('', queryParams, null), segments); } diff --git a/modules/angular2/src/alt_router/segments.ts b/modules/angular2/src/alt_router/segments.ts index 0559e58dd1..1564fc7f4c 100644 --- a/modules/angular2/src/alt_router/segments.ts +++ b/modules/angular2/src/alt_router/segments.ts @@ -89,7 +89,9 @@ export class RouteSegment { this._componentFactory = componentFactory; } - getParam(param: string): string { return this.parameters[param]; } + getParam(param: string): string { + return isPresent(this.parameters) ? this.parameters[param] : null; + } get type(): Type { return this._type; } diff --git a/modules/angular2/test/alt_router/integration_spec.ts b/modules/angular2/test/alt_router/integration_spec.ts index a78c78b5ac..558baf557f 100644 --- a/modules/angular2/test/alt_router/integration_spec.ts +++ b/modules/angular2/test/alt_router/integration_spec.ts @@ -28,10 +28,10 @@ import { Routes, RouterUrlSerializer, DefaultRouterUrlSerializer, - OnActivate, - Location + OnActivate } from 'angular2/alt_router'; import {SpyLocation} from 'angular2/src/mock/location_mock'; +import {Location} from 'angular2/platform/common'; import {DOM} from 'angular2/src/platform/dom/dom_adapter'; export function main() { @@ -58,6 +58,7 @@ export function main() { router.navigateByUrl('/team/33/simple'); advance(fixture); + expect(location.path()).toEqual('/team/33/simple'); }))); @@ -123,40 +124,43 @@ export function main() { expect(fixture.debugElement.nativeElement).toHaveText('team 22 { hello fedor, aux: }'); }))); - it("should support router links", - fakeAsync(inject([Router, TestComponentBuilder], (router, tcb) => { - let fixture = tcb.createFakeAsync(RootCmp); - advance(fixture); + if (DOM.supportsDOMEvents()) { // this is required to use fakeAsync - router.navigateByUrl('/team/22/link'); - advance(fixture); - expect(fixture.debugElement.nativeElement).toHaveText('team 22 { link, aux: }'); + it("should support router links", + fakeAsync(inject([Router, TestComponentBuilder], (router, tcb) => { + let fixture = tcb.createFakeAsync(RootCmp); + advance(fixture); - let native = DOM.querySelector(fixture.debugElement.nativeElement, "a"); - expect(DOM.getAttribute(native, "href")).toEqual("/team/33/simple"); - DOM.dispatchEvent(native, DOM.createMouseEvent('click')); - advance(fixture); + router.navigateByUrl('/team/22/link'); + advance(fixture); + expect(fixture.debugElement.nativeElement).toHaveText('team 22 { link, aux: }'); - expect(fixture.debugElement.nativeElement).toHaveText('team 33 { simple, aux: }'); - }))); + let native = DOM.querySelector(fixture.debugElement.nativeElement, "a"); + expect(DOM.getAttribute(native, "href")).toEqual("/team/33/simple"); + DOM.dispatchEvent(native, DOM.createMouseEvent('click')); + advance(fixture); - it("should update router links when router changes", - fakeAsync(inject([Router, TestComponentBuilder], (router, tcb) => { - let fixture = tcb.createFakeAsync(RootCmp); - advance(fixture); + expect(fixture.debugElement.nativeElement).toHaveText('team 33 { simple, aux: }'); + }))); - router.navigateByUrl('/team/22/link(simple)'); - advance(fixture); - expect(fixture.debugElement.nativeElement).toHaveText('team 22 { link, aux: simple }'); + it("should update router links when router changes", + fakeAsync(inject([Router, TestComponentBuilder], (router, tcb) => { + let fixture = tcb.createFakeAsync(RootCmp); + advance(fixture); - let native = DOM.querySelector(fixture.debugElement.nativeElement, "a"); - expect(DOM.getAttribute(native, "href")).toEqual("/team/33/simple(aux:simple)"); + router.navigateByUrl('/team/22/link(simple)'); + advance(fixture); + expect(fixture.debugElement.nativeElement).toHaveText('team 22 { link, aux: simple }'); - router.navigateByUrl('/team/22/link(simple2)'); - advance(fixture); + let native = DOM.querySelector(fixture.debugElement.nativeElement, "a"); + expect(DOM.getAttribute(native, "href")).toEqual("/team/33/simple(aux:simple)"); - expect(DOM.getAttribute(native, "href")).toEqual("/team/33/simple(aux:simple2)"); - }))); + router.navigateByUrl('/team/22/link(simple2)'); + advance(fixture); + + expect(DOM.getAttribute(native, "href")).toEqual("/team/33/simple(aux:simple2)"); + }))); + } }); } diff --git a/modules/angular2/test/alt_router/recognize_spec.ts b/modules/angular2/test/alt_router/recognize_spec.ts index 454d8958d2..888966045a 100644 --- a/modules/angular2/test/alt_router/recognize_spec.ts +++ b/modules/angular2/test/alt_router/recognize_spec.ts @@ -48,6 +48,26 @@ export function main() { }); })); + it('should support empty routes', + inject([AsyncTestCompleter, ComponentResolver], (async, resolver) => { + recognize(resolver, ComponentA, tree("f")) + .then(r => { + let a = r.root; + expect(stringifyUrl(a.urlSegments)).toEqual([""]); + expect(a.type).toBe(ComponentA); + + let f = r.firstChild(r.root); + expect(stringifyUrl(f.urlSegments)).toEqual(["f"]); + expect(f.type).toBe(ComponentF); + + let d = r.firstChild(r.firstChild(r.root)); + expect(stringifyUrl(d.urlSegments)).toEqual([]); + expect(d.type).toBe(ComponentD); + + async.done(); + }); + })); + it('should handle aux routes', inject([AsyncTestCompleter, ComponentResolver], (async, resolver) => { recognize(resolver, ComponentA, tree("b/paramB(/d//right:d)")) @@ -133,7 +153,7 @@ export function main() { inject([AsyncTestCompleter, ComponentResolver], (async, resolver) => { recognize(resolver, ComponentA, tree("invalid")) .catch(e => { - expect(e.message).toEqual("Cannot match any routes"); + expect(e.message).toContain("Cannot match any routes"); async.done(); }); })); @@ -142,7 +162,7 @@ export function main() { inject([AsyncTestCompleter, ComponentResolver], (async, resolver) => { recognize(resolver, ComponentA, tree("b")) .catch(e => { - expect(e.message).toEqual("Cannot match any routes"); + expect(e.message).toContain("Cannot match any routes"); async.done(); }); })); @@ -175,6 +195,11 @@ class ComponentD { class ComponentE { } +@Component({selector: 'f', template: 't'}) +@Routes([new Route({path: "/", component: ComponentD})]) +class ComponentF { +} + @Component({selector: 'c', template: 't'}) @Routes([new Route({path: "d", component: ComponentD})]) class ComponentC { @@ -193,7 +218,8 @@ class ComponentB { @Routes([ new Route({path: "b/:b", component: ComponentB}), new Route({path: "d", component: ComponentD}), - new Route({path: "e", component: ComponentE}) + new Route({path: "e", component: ComponentE}), + new Route({path: "f", component: ComponentF}) ]) class ComponentA { } \ No newline at end of file diff --git a/modules/angular2/test/alt_router/router_url_serializer_spec.ts b/modules/angular2/test/alt_router/router_url_serializer_spec.ts index b4ab239a8f..148712bf38 100644 --- a/modules/angular2/test/alt_router/router_url_serializer_spec.ts +++ b/modules/angular2/test/alt_router/router_url_serializer_spec.ts @@ -106,26 +106,6 @@ export function main() { expect(url.serialize(tree)).toEqual("/one;a=true"); }); - - // it("should parse key-value query params", () => { - // let tree = url.parse("/one?a=1&b=2"); - // expect(tree.root).toEqual(new UrlSegment("", {'a': '1', 'b': '2'}, DEFAULT_OUTLET_NAME)); - // }); - // - // it("should parse key only query params", () => { - // let tree = url.parse("/one?a"); - // expect(tree.root).toEqual(new UrlSegment("", {'a': "true"}, DEFAULT_OUTLET_NAME)); - // }); - // - // it("should parse a url with only query params", () => { - // let tree = url.parse("?a"); - // expect(tree.root).toEqual(new UrlSegment("", {'a': "true"}, DEFAULT_OUTLET_NAME)); - // }); - // - // it("should allow slashes within query params", () => { - // let tree = url.parse("?a=http://boo"); - // expect(tree.root).toEqual(new UrlSegment("", {'a': "http://boo"}, DEFAULT_OUTLET_NAME)); - // }); }); }