parent
602641dffd
commit
30de2db349
|
@ -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';
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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(); }
|
||||
}
|
|
@ -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(); }
|
||||
}
|
|
@ -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<any> = 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;
|
||||
}
|
|
@ -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'));
|
|
@ -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(); }
|
||||
}
|
|
@ -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; }
|
|
@ -10,17 +10,18 @@ import {reflector} from 'angular2/src/core/reflection/reflection';
|
|||
|
||||
export function recognize(componentResolver: ComponentResolver, type: Type,
|
||||
url: Tree<UrlSegment>): Promise<Tree<RouteSegment>> {
|
||||
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<RouteSegment>(new TreeNode<RouteSegment>(segment, children)));
|
||||
});
|
||||
let matched = new _MatchResult(type, [url.root], null, rootNode(url).children, []);
|
||||
return _constructSegment(componentResolver, matched)
|
||||
.then(roots => new Tree<RouteSegment>(roots[0]));
|
||||
}
|
||||
|
||||
function _recognize(componentResolver: ComponentResolver, parentType: Type,
|
||||
url: TreeNode<UrlSegment>): Promise<TreeNode<RouteSegment>[]> {
|
||||
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<TreeNode<RouteSegment>[]> {
|
||||
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<RouteSegment>(segment, children)]);
|
||||
} else {
|
||||
return [new TreeNode<RouteSegment>(segment, [])];
|
||||
return _recognizeLeftOvers(componentResolver, matched.component)
|
||||
.then(children => [new TreeNode<RouteSegment>(segment, children)]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function _recognizeLeftOvers(componentResolver: ComponentResolver,
|
||||
parentType: Type): Promise<TreeNode<RouteSegment>[]> {
|
||||
return componentResolver.resolveComponent(parentType)
|
||||
.then(factory => {
|
||||
let metadata = _readMetadata(parentType);
|
||||
if (isBlank(metadata)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
let r = (<any[]>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<RouteSegment>(segment, children)];
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -66,11 +94,14 @@ function _match(metadata: RoutesMetadata, url: TreeNode<UrlSegment>): _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<UrlSegment>): _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<UrlSegment>): _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<RouteSegment>[]): TreeNode<R
|
|||
}
|
||||
|
||||
class _MatchResult {
|
||||
constructor(public route: RouteMetadata, public consumedUrlSegments: UrlSegment[],
|
||||
constructor(public component: Type, public consumedUrlSegments: UrlSegment[],
|
||||
public parameters: {[key: string]: string},
|
||||
public leftOverUrl: TreeNode<UrlSegment>[], public aux: TreeNode<UrlSegment>[]) {}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
|
@ -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<RouteSegment>;
|
||||
private _urlTree: Tree<UrlSegment>;
|
||||
|
||||
private _location: Location;
|
||||
|
||||
private _changes: EventEmitter<void> = new EventEmitter<void>();
|
||||
|
||||
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<UrlSegment> { 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<RouteSegment>, private prevTree: Tree<RouteSegment>) {}
|
||||
|
||||
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<RouteSegment>, prevNode: TreeNode<RouteSegment>,
|
||||
parentOutletMap: RouterOutletMap): void {
|
||||
let curr = currNode.value;
|
||||
|
|
|
@ -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})),
|
||||
]);
|
|
@ -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);
|
||||
}
|
|
@ -64,7 +64,7 @@ class _UrlParser {
|
|||
parse(url: string): TreeNode<UrlSegment> {
|
||||
this._remaining = url;
|
||||
if (url == '' || url == '/') {
|
||||
return new TreeNode<UrlSegment>(new UrlSegment('', {}, null), []);
|
||||
return new TreeNode<UrlSegment>(new UrlSegment('', null, null), []);
|
||||
} else {
|
||||
return this.parseRoot();
|
||||
}
|
||||
|
@ -72,7 +72,7 @@ class _UrlParser {
|
|||
|
||||
parseRoot(): TreeNode<UrlSegment> {
|
||||
let segments = this.parseSegments();
|
||||
let queryParams = this.peekStartsWith('?') ? this.parseQueryParams() : {};
|
||||
let queryParams = this.peekStartsWith('?') ? this.parseQueryParams() : null;
|
||||
return new TreeNode<UrlSegment>(new UrlSegment('', queryParams, null), segments);
|
||||
}
|
||||
|
||||
|
|
|
@ -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; }
|
||||
|
||||
|
|
|
@ -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)");
|
||||
})));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
}
|
|
@ -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));
|
||||
// });
|
||||
});
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue