feat(router): change location when navigating
This commit is contained in:
parent
de56dd5f30
commit
560cc14d97
|
@ -14,6 +14,13 @@ export {
|
||||||
} from './src/alt_router/router_url_serializer';
|
} from './src/alt_router/router_url_serializer';
|
||||||
export {OnActivate} from './src/alt_router/interfaces';
|
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';
|
||||||
|
|
||||||
import {RouterOutlet} from './src/alt_router/directives/router_outlet';
|
import {RouterOutlet} from './src/alt_router/directives/router_outlet';
|
||||||
import {RouterLink} from './src/alt_router/directives/router_link';
|
import {RouterLink} from './src/alt_router/directives/router_link';
|
||||||
import {CONST_EXPR} from './src/facade/lang';
|
import {CONST_EXPR} from './src/facade/lang';
|
||||||
|
|
|
@ -0,0 +1,57 @@
|
||||||
|
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(); }
|
||||||
|
}
|
|
@ -0,0 +1,101 @@
|
||||||
|
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(); }
|
||||||
|
}
|
|
@ -0,0 +1,179 @@
|
||||||
|
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;
|
||||||
|
}
|
|
@ -0,0 +1,62 @@
|
||||||
|
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'));
|
|
@ -0,0 +1,105 @@
|
||||||
|
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(); }
|
||||||
|
}
|
|
@ -0,0 +1,48 @@
|
||||||
|
/**
|
||||||
|
* 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; }
|
|
@ -4,8 +4,10 @@ import {Type, isBlank, isPresent} from 'angular2/src/facade/lang';
|
||||||
import {EventEmitter, Observable} from 'angular2/src/facade/async';
|
import {EventEmitter, Observable} from 'angular2/src/facade/async';
|
||||||
import {StringMapWrapper} from 'angular2/src/facade/collection';
|
import {StringMapWrapper} from 'angular2/src/facade/collection';
|
||||||
import {BaseException} from 'angular2/src/facade/exceptions';
|
import {BaseException} from 'angular2/src/facade/exceptions';
|
||||||
|
import {BaseException} from 'angular2/src/facade/exceptions';
|
||||||
import {RouterUrlSerializer} from './router_url_serializer';
|
import {RouterUrlSerializer} from './router_url_serializer';
|
||||||
import {recognize} from './recognize';
|
import {recognize} from './recognize';
|
||||||
|
import {Location} from './location/location';
|
||||||
import {
|
import {
|
||||||
equalSegments,
|
equalSegments,
|
||||||
routeSegmentComponentFactory,
|
routeSegmentComponentFactory,
|
||||||
|
@ -29,11 +31,16 @@ export class Router {
|
||||||
private _prevTree: Tree<RouteSegment>;
|
private _prevTree: Tree<RouteSegment>;
|
||||||
private _urlTree: Tree<UrlSegment>;
|
private _urlTree: Tree<UrlSegment>;
|
||||||
|
|
||||||
|
private _location: Location;
|
||||||
|
|
||||||
private _changes: EventEmitter<void> = new EventEmitter<void>();
|
private _changes: EventEmitter<void> = new EventEmitter<void>();
|
||||||
|
|
||||||
constructor(private _componentType: Type, private _componentResolver: ComponentResolver,
|
constructor(private _componentType: Type, private _componentResolver: ComponentResolver,
|
||||||
private _urlSerializer: RouterUrlSerializer,
|
private _urlSerializer: RouterUrlSerializer,
|
||||||
private _routerOutletMap: RouterOutletMap) {}
|
private _routerOutletMap: RouterOutletMap, location: Location) {
|
||||||
|
this._location = location;
|
||||||
|
this.navigateByUrl(location.path());
|
||||||
|
}
|
||||||
|
|
||||||
get urlTree(): Tree<UrlSegment> { return this._urlTree; }
|
get urlTree(): Tree<UrlSegment> { return this._urlTree; }
|
||||||
|
|
||||||
|
@ -45,6 +52,7 @@ export class Router {
|
||||||
new _LoadSegments(currTree, this._prevTree)
|
new _LoadSegments(currTree, this._prevTree)
|
||||||
.loadSegments(rootNode(currTree), prevRoot, this._routerOutletMap);
|
.loadSegments(rootNode(currTree), prevRoot, this._routerOutletMap);
|
||||||
this._prevTree = currTree;
|
this._prevTree = currTree;
|
||||||
|
this._location.go(this._urlSerializer.serialize(this._urlTree));
|
||||||
this._changes.emit(null);
|
this._changes.emit(null);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,9 +10,6 @@ export abstract class RouterUrlSerializer {
|
||||||
|
|
||||||
export class DefaultRouterUrlSerializer extends RouterUrlSerializer {
|
export class DefaultRouterUrlSerializer extends RouterUrlSerializer {
|
||||||
parse(url: string): Tree<UrlSegment> {
|
parse(url: string): Tree<UrlSegment> {
|
||||||
if (url.length === 0) {
|
|
||||||
throw new BaseException(`Invalid url '${url}'`);
|
|
||||||
}
|
|
||||||
let root = new _UrlParser().parse(url);
|
let root = new _UrlParser().parse(url);
|
||||||
return new Tree<UrlSegment>(root);
|
return new Tree<UrlSegment>(root);
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,8 +28,10 @@ import {
|
||||||
Routes,
|
Routes,
|
||||||
RouterUrlSerializer,
|
RouterUrlSerializer,
|
||||||
DefaultRouterUrlSerializer,
|
DefaultRouterUrlSerializer,
|
||||||
OnActivate
|
OnActivate,
|
||||||
|
Location
|
||||||
} from 'angular2/alt_router';
|
} from 'angular2/alt_router';
|
||||||
|
import {SpyLocation} from 'angular2/src/mock/location_mock';
|
||||||
import {DOM} from 'angular2/src/platform/dom/dom_adapter';
|
import {DOM} from 'angular2/src/platform/dom/dom_adapter';
|
||||||
|
|
||||||
export function main() {
|
export function main() {
|
||||||
|
@ -37,14 +39,28 @@ export function main() {
|
||||||
beforeEachProviders(() => [
|
beforeEachProviders(() => [
|
||||||
provide(RouterUrlSerializer, {useClass: DefaultRouterUrlSerializer}),
|
provide(RouterUrlSerializer, {useClass: DefaultRouterUrlSerializer}),
|
||||||
RouterOutletMap,
|
RouterOutletMap,
|
||||||
|
provide(Location, {useClass: SpyLocation}),
|
||||||
provide(Router,
|
provide(Router,
|
||||||
{
|
{
|
||||||
useFactory: (resolver, urlParser, outletMap) =>
|
useFactory: (resolver, urlParser, outletMap, location) =>
|
||||||
new Router(RootCmp, resolver, urlParser, outletMap),
|
new Router(RootCmp, resolver, urlParser, outletMap, location),
|
||||||
deps: [ComponentResolver, RouterUrlSerializer, RouterOutletMap]
|
deps: [ComponentResolver, RouterUrlSerializer, RouterOutletMap, Location]
|
||||||
})
|
})
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
it('should update location when navigating',
|
||||||
|
fakeAsync(inject([Router, TestComponentBuilder, Location], (router, tcb, location) => {
|
||||||
|
let fixture = tcb.createFakeAsync(RootCmp);
|
||||||
|
|
||||||
|
router.navigateByUrl('/team/22/user/victor');
|
||||||
|
advance(fixture);
|
||||||
|
expect(location.path()).toEqual('/team/22/user/victor');
|
||||||
|
|
||||||
|
router.navigateByUrl('/team/33/simple');
|
||||||
|
advance(fixture);
|
||||||
|
expect(location.path()).toEqual('/team/33/simple');
|
||||||
|
})));
|
||||||
|
|
||||||
it('should support nested routes',
|
it('should support nested routes',
|
||||||
fakeAsync(inject([Router, TestComponentBuilder], (router, tcb) => {
|
fakeAsync(inject([Router, TestComponentBuilder], (router, tcb) => {
|
||||||
let fixture = tcb.createFakeAsync(RootCmp);
|
let fixture = tcb.createFakeAsync(RootCmp);
|
||||||
|
|
|
@ -19,11 +19,9 @@ import {DefaultRouterUrlSerializer} from 'angular2/src/alt_router/router_url_ser
|
||||||
import {UrlSegment} from 'angular2/src/alt_router/segments';
|
import {UrlSegment} from 'angular2/src/alt_router/segments';
|
||||||
|
|
||||||
export function main() {
|
export function main() {
|
||||||
describe('url parsing', () => {
|
describe('url serializer', () => {
|
||||||
let url = new DefaultRouterUrlSerializer();
|
let url = new DefaultRouterUrlSerializer();
|
||||||
|
|
||||||
it('should throw on an empty urls', () => { expect(() => url.parse("")).toThrow(); });
|
|
||||||
|
|
||||||
it('should parse the root url', () => {
|
it('should parse the root url', () => {
|
||||||
let tree = url.parse("/");
|
let tree = url.parse("/");
|
||||||
expectSegment(tree.root, "");
|
expectSegment(tree.root, "");
|
||||||
|
|
Loading…
Reference in New Issue