From 853d1de6ecf2acb69e86ba4f39ad95a6a97c0ea7 Mon Sep 17 00:00:00 2001 From: Brian Ford Date: Wed, 6 May 2015 18:28:24 -0700 Subject: [PATCH] fix(router): strip base href from URLs when navigating --- modules/angular2/router.js | 2 + .../angular2/src/router/browser_location.js | 36 ++++++++ modules/angular2/src/router/location.js | 47 +++++++--- modules/angular2/test/router/location_spec.js | 85 +++++++++++++++++++ 4 files changed, 158 insertions(+), 12 deletions(-) create mode 100644 modules/angular2/src/router/browser_location.js create mode 100644 modules/angular2/test/router/location_spec.js diff --git a/modules/angular2/router.js b/modules/angular2/router.js index a311805b7c..48b92a2271 100644 --- a/modules/angular2/router.js +++ b/modules/angular2/router.js @@ -13,6 +13,7 @@ export {RouteParams} from './src/router/instruction'; export * from './src/router/route_config_annotation'; export * from './src/router/route_config_decorator'; +import {BrowserLocation} from './src/router/browser_location'; import {Router, RootRouter} from './src/router/router'; import {RouteRegistry} from './src/router/route_registry'; import {Pipeline} from './src/router/pipeline'; @@ -23,6 +24,7 @@ import {bind} from './di'; export var routerInjectables:List = [ RouteRegistry, Pipeline, + BrowserLocation, Location, bind(Router).toFactory((registry, pipeline, location, meta) => { return new RootRouter(registry, pipeline, location, meta.type); diff --git a/modules/angular2/src/router/browser_location.js b/modules/angular2/src/router/browser_location.js new file mode 100644 index 0000000000..b9b37dcf20 --- /dev/null +++ b/modules/angular2/src/router/browser_location.js @@ -0,0 +1,36 @@ +import {DOM} from 'angular2/src/dom/dom_adapter'; + +export class BrowserLocation { + _location; + _history; + _baseHref:string; + constructor() { + this._location = DOM.getLocation(); + this._history = DOM.getHistory(); + this._baseHref = DOM.getBaseHref(); + } + + onPopState(fn) { + DOM.getGlobalEventTarget('window').addEventListener('popstate', fn, false); + } + + getBaseHref() { + return this._baseHref; + } + + path() { + return this._location.pathname; + } + + pushState(state:any, title:string, url:string) { + this._history.pushState(state, title, url); + } + + forward() { + this._history.forward(); + } + + back() { + this._history.back(); + } +} diff --git a/modules/angular2/src/router/location.js b/modules/angular2/src/router/location.js index 1d903619d1..6f9328e5cb 100644 --- a/modules/angular2/src/router/location.js +++ b/modules/angular2/src/router/location.js @@ -1,40 +1,63 @@ -import {DOM} from 'angular2/src/dom/dom_adapter'; +import {BrowserLocation} from './browser_location'; +import {StringWrapper} from 'angular2/src/facade/lang'; import {EventEmitter, ObservableWrapper} from 'angular2/src/facade/async'; export class Location { - _location; _subject:EventEmitter; - _history; - constructor() { + _browserLocation:BrowserLocation; + _baseHref:string; + constructor(browserLocation:BrowserLocation) { this._subject = new EventEmitter(); - this._location = DOM.getLocation(); - this._history = DOM.getHistory(); - DOM.getGlobalEventTarget('window').addEventListener('popstate', (_) => this._onPopState(_), false); + this._browserLocation = browserLocation; + this._baseHref = stripIndexHtml(this._browserLocation.getBaseHref()); + this._browserLocation.onPopState((_) => this._onPopState(_)); } _onPopState(_) { ObservableWrapper.callNext(this._subject, { - 'url': this._location.pathname + 'url': this.path() }); } path() { - return this._location.pathname; + return this.normalize(this._browserLocation.path()); + } + + normalize(url) { + return this._stripBaseHref(stripIndexHtml(url)); + } + + _stripBaseHref(url) { + if (this._baseHref.length > 0 && StringWrapper.startsWith(url, this._baseHref)) { + return StringWrapper.substring(url, this._baseHref.length); + } + return url; } go(url:string) { - this._history.pushState(null, null, url); + url = this._stripBaseHref(url); + this._browserLocation.pushState(null, null, url); } forward() { - this._history.forward(); + this._browserLocation.forward(); } back() { - this._history.back() + this._browserLocation.back(); } subscribe(onNext, onThrow = null, onReturn = null) { ObservableWrapper.subscribe(this._subject, onNext, onThrow, onReturn); } } + + + +function stripIndexHtml(url) { + // '/index.html'.length == 11 + if (url.length > 10 && StringWrapper.substring(url, url.length - 11) == '/index.html') { + return StringWrapper.substring(url, 0, url.length - 11); + } + return url; +} diff --git a/modules/angular2/test/router/location_spec.js b/modules/angular2/test/router/location_spec.js new file mode 100644 index 0000000000..e0cd179245 --- /dev/null +++ b/modules/angular2/test/router/location_spec.js @@ -0,0 +1,85 @@ +import { + AsyncTestCompleter, + describe, + proxy, + it, iit, + ddescribe, expect, + inject, beforeEach, beforeEachBindings, + SpyObject} from 'angular2/test_lib'; +import {IMPLEMENTS} from 'angular2/src/facade/lang'; +import {EventEmitter, ObservableWrapper} from 'angular2/src/facade/async'; + +import {BrowserLocation} from 'angular2/src/router/browser_location'; +import {Location} from 'angular2/src/router/location'; + +export function main() { + + describe('Location', () => { + + var browserLocation, location; + + beforeEach(() => { + browserLocation = new DummyBrowserLocation(); + browserLocation.spy('pushState'); + browserLocation.baseHref = '/my/app'; + location = new Location(browserLocation); + }); + + it('should normalize urls on navigate', () => { + location.go('/my/app/user/btford'); + expect(browserLocation.spy('pushState')).toHaveBeenCalledWith(null, null, '/user/btford'); + }); + + it('should remove index.html from base href', () => { + browserLocation.baseHref = '/my/app/index.html'; + location = new Location(browserLocation); + location.go('/my/app/user/btford'); + expect(browserLocation.spy('pushState')).toHaveBeenCalledWith(null, null, '/user/btford'); + }); + + it('should normalize urls on popstate', inject([AsyncTestCompleter], (async) => { + browserLocation.simulatePopState('/my/app/user/btford'); + location.subscribe((ev) => { + expect(ev['url']).toEqual('/user/btford'); + async.done(); + }) + })); + + it('should normalize location path', () => { + browserLocation.internalPath = '/my/app/user/btford'; + expect(location.path()).toEqual('/user/btford'); + }); + }); +} + +@proxy +@IMPLEMENTS(BrowserLocation) +class DummyBrowserLocation extends SpyObject { + baseHref; + internalPath; + _subject:EventEmitter; + constructor() { + super(); + this.internalPath = '/'; + this._subject = new EventEmitter(); + } + + simulatePopState(url) { + this.internalPath = url; + ObservableWrapper.callNext(this._subject, null); + } + + path() { + return this.internalPath; + } + + onPopState(fn) { + ObservableWrapper.subscribe(this._subject, fn); + } + + getBaseHref() { + return this.baseHref; + } + + noSuchMethod(m){return super.noSuchMethod(m);} +}