Skip to content

Commit e5de1f7

Browse files
committed
refactor(router): refactor BrowserLocation into LocationStrategy
This makes it easy to mock browser location and paves the way to implementing hash routing.
1 parent b48f000 commit e5de1f7

File tree

8 files changed

+85
-61
lines changed

8 files changed

+85
-61
lines changed

modules/angular2/router.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,14 @@ export {RouterOutlet} from './src/router/router_outlet';
1111
export {RouterLink} from './src/router/router_link';
1212
export {RouteParams} from './src/router/instruction';
1313
export {RouteRegistry} from './src/router/route_registry';
14-
export {BrowserLocation} from './src/router/browser_location';
14+
export {LocationStrategy} from './src/router/location_strategy';
15+
export {HTML5LocationStrategy} from './src/router/html5_location_strategy';
1516
export {Location, appBaseHrefToken} from './src/router/location';
1617
export {Pipeline} from './src/router/pipeline';
1718
export * from './src/router/route_config_decorator';
1819

19-
import {BrowserLocation} from './src/router/browser_location';
20+
import {LocationStrategy} from './src/router/location_strategy';
21+
import {HTML5LocationStrategy} from './src/router/html5_location_strategy';
2022
import {Router, RootRouter} from './src/router/router';
2123
import {RouterOutlet} from './src/router/router_outlet';
2224
import {RouterLink} from './src/router/router_link';
@@ -33,7 +35,7 @@ export const routerDirectives: List<any> = CONST_EXPR([RouterOutlet, RouterLink]
3335
export var routerInjectables: List<any> = [
3436
RouteRegistry,
3537
Pipeline,
36-
BrowserLocation,
38+
bind(LocationStrategy).toClass(HTML5LocationStrategy),
3739
Location,
3840
bind(Router)
3941
.toFactory((registry, pipeline, location,

modules/angular2/src/mock/browser_location_mock.ts renamed to modules/angular2/src/mock/mock_location_strategy.ts

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,9 @@
1-
import {proxy, SpyObject} from 'angular2/test_lib';
2-
import {IMPLEMENTS, BaseException} from 'angular2/src/facade/lang';
31
import {EventEmitter, ObservableWrapper} from 'angular2/src/facade/async';
4-
import {List, ListWrapper} from 'angular2/src/facade/collection';
5-
import {BrowserLocation} from 'angular2/src/router/browser_location';
2+
import {List} from 'angular2/src/facade/collection';
3+
import {LocationStrategy} from 'angular2/src/router/location_strategy';
64

7-
@proxy
8-
@IMPLEMENTS(BrowserLocation)
9-
export class DummyBrowserLocation extends SpyObject {
5+
6+
export class MockLocationStrategy extends LocationStrategy {
107
internalBaseHref: string = '/';
118
internalPath: string = '/';
129
internalTitle: string = '';
@@ -31,13 +28,7 @@ export class DummyBrowserLocation extends SpyObject {
3128
this.urlChanges.push(url);
3229
}
3330

34-
forward(): void { throw new BaseException('Not implemented yet!'); }
35-
36-
back(): void { throw new BaseException('Not implemented yet!'); }
37-
3831
onPopState(fn): void { ObservableWrapper.subscribe(this._subject, fn); }
3932

4033
getBaseHref(): string { return this.internalBaseHref; }
41-
42-
noSuchMethod(m) { return super.noSuchMethod(m); }
4334
}

modules/angular2/src/router/browser_location.ts renamed to modules/angular2/src/router/html5_location_strategy.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
import {DOM} from 'angular2/src/dom/dom_adapter';
22
import {Injectable} from 'angular2/di';
33
import {EventListener, History, Location} from 'angular2/src/facade/browser';
4+
import {LocationStrategy} from './location_strategy';
45

56
@Injectable()
6-
export class BrowserLocation {
7+
export class HTML5LocationStrategy extends LocationStrategy {
78
private _location: Location;
89
private _history: History;
910
private _baseHref: string;
1011

1112
constructor() {
13+
super();
1214
this._location = DOM.getLocation();
1315
this._history = DOM.getHistory();
1416
this._baseHref = DOM.getBaseHref();
Lines changed: 36 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,70 @@
1-
import {BrowserLocation} from './browser_location';
1+
import {LocationStrategy} from './location_strategy';
22
import {StringWrapper, isPresent, CONST_EXPR} from 'angular2/src/facade/lang';
33
import {EventEmitter, ObservableWrapper} from 'angular2/src/facade/async';
44
import {OpaqueToken, Injectable, Optional, Inject} from 'angular2/di';
55

66
export const appBaseHrefToken: OpaqueToken = CONST_EXPR(new OpaqueToken('locationHrefToken'));
77

8+
/**
9+
* This is the service that an application developer will directly interact with.
10+
*
11+
* Responsible for normalizing the URL against the application's base href.
12+
* A normalized URL is absolute from the URL host, includes the application's base href, and has no
13+
* trailing slash:
14+
* - `/my/app/user/123` is normalized
15+
* - `my/app/user/123` **is not** normalized
16+
* - `/my/app/user/123/` **is not** normalized
17+
*/
818
@Injectable()
919
export class Location {
1020
private _subject: EventEmitter;
1121
private _baseHref: string;
1222

13-
constructor(public _browserLocation: BrowserLocation,
23+
constructor(public _platformStrategy: LocationStrategy,
1424
@Optional() @Inject(appBaseHrefToken) href?: string) {
1525
this._subject = new EventEmitter();
16-
this._baseHref = stripIndexHtml(isPresent(href) ? href : this._browserLocation.getBaseHref());
17-
this._browserLocation.onPopState((_) => this._onPopState(_));
26+
this._baseHref = stripTrailingSlash(
27+
stripIndexHtml(isPresent(href) ? href : this._platformStrategy.getBaseHref()));
28+
this._platformStrategy.onPopState((_) => this._onPopState(_));
1829
}
1930

2031
_onPopState(_): void { ObservableWrapper.callNext(this._subject, {'url': this.path()}); }
2132

22-
path(): string { return this.normalize(this._browserLocation.path()); }
33+
path(): string { return this.normalize(this._platformStrategy.path()); }
2334

24-
normalize(url: string): string { return this._stripBaseHref(stripIndexHtml(url)); }
35+
normalize(url: string): string {
36+
return stripTrailingSlash(this._stripBaseHref(stripIndexHtml(url)));
37+
}
2538

2639
normalizeAbsolutely(url: string): string {
27-
if (url.length > 0 && url[0] != '/') {
40+
if (!url.startsWith('/')) {
2841
url = '/' + url;
2942
}
30-
return this._addBaseHref(url);
43+
return stripTrailingSlash(this._addBaseHref(url));
3144
}
3245

3346
_stripBaseHref(url: string): string {
34-
if (this._baseHref.length > 0 && StringWrapper.startsWith(url, this._baseHref)) {
35-
return StringWrapper.substring(url, this._baseHref.length);
47+
if (this._baseHref.length > 0 && url.startsWith(this._baseHref)) {
48+
return url.substring(this._baseHref.length);
3649
}
3750
return url;
3851
}
3952

4053
_addBaseHref(url: string): string {
41-
if (!StringWrapper.startsWith(url, this._baseHref)) {
54+
if (!url.startsWith(this._baseHref)) {
4255
return this._baseHref + url;
4356
}
4457
return url;
4558
}
4659

4760
go(url: string): void {
4861
var finalUrl = this.normalizeAbsolutely(url);
49-
this._browserLocation.pushState(null, '', finalUrl);
62+
this._platformStrategy.pushState(null, '', finalUrl);
5063
}
5164

52-
forward(): void { this._browserLocation.forward(); }
65+
forward(): void { this._platformStrategy.forward(); }
5366

54-
back(): void { this._browserLocation.back(); }
67+
back(): void { this._platformStrategy.back(); }
5568

5669
subscribe(onNext, onThrow = null, onReturn = null): void {
5770
ObservableWrapper.subscribe(this._subject, onNext, onThrow, onReturn);
@@ -61,12 +74,16 @@ export class Location {
6174

6275

6376
function stripIndexHtml(url: string): string {
64-
// '/index.html'.length == 11
65-
if (url.length > 10 && StringWrapper.substring(url, url.length - 11) == '/index.html') {
66-
return StringWrapper.substring(url, 0, url.length - 11);
77+
if (/\/index.html$/g.test(url)) {
78+
// '/index.html'.length == 11
79+
return url.substring(0, url.length - 11);
6780
}
68-
if (url.length > 1 && url[url.length - 1] == '/') {
69-
url = StringWrapper.substring(url, 0, url.length - 1);
81+
return url;
82+
}
83+
84+
function stripTrailingSlash(url: string): string {
85+
if (/\/$/g.test(url)) {
86+
url = url.substring(0, url.length - 1);
7087
}
7188
return url;
7289
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import {BaseException} from 'angular2/src/facade/lang';
2+
3+
function _abstract() {
4+
return new BaseException('This method is abstract');
5+
}
6+
7+
export class LocationStrategy {
8+
path(): string { throw _abstract(); }
9+
pushState(ctx: any, title: string, url: string): void { throw _abstract(); }
10+
forward(): void { throw _abstract(); }
11+
back(): void { throw _abstract(); }
12+
onPopState(fn): void { throw _abstract(); }
13+
getBaseHref(): string { throw _abstract(); }
14+
}

modules/angular2/src/test_lib/test_injector.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ import {EventManager, DomEventsPlugin} from 'angular2/src/render/dom/events/even
3232

3333
import {MockTemplateResolver} from 'angular2/src/mock/template_resolver_mock';
3434
import {MockXHR} from 'angular2/src/render/xhr_mock';
35+
import {MockLocationStrategy} from 'angular2/src/mock/mock_location_strategy';
36+
import {LocationStrategy} from 'angular2/src/router/location_strategy';
3537
import {MockNgZone} from 'angular2/src/mock/ng_zone_mock';
3638

3739
import {TestBed} from './test_bed';
@@ -109,6 +111,7 @@ function _getAppBindings() {
109111
Parser,
110112
Lexer,
111113
ExceptionHandler,
114+
bind(LocationStrategy).toClass(MockLocationStrategy),
112115
bind(XHR).toClass(MockXHR),
113116
ComponentUrlMapper,
114117
UrlResolver,

modules/angular2/test/router/location_spec.ts

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,31 +15,31 @@ import {
1515
import {Injector, bind} from 'angular2/di';
1616
import {CONST_EXPR} from 'angular2/src/facade/lang';
1717
import {Location, appBaseHrefToken} from 'angular2/src/router/location';
18-
import {BrowserLocation} from 'angular2/src/router/browser_location';
19-
import {DummyBrowserLocation} from 'angular2/src/mock/browser_location_mock';
18+
import {LocationStrategy} from 'angular2/src/router/location_strategy';
19+
import {MockLocationStrategy} from 'angular2/src/mock/mock_location_strategy';
2020

2121
export function main() {
2222
describe('Location', () => {
2323

24-
var browserLocation, location;
24+
var locationStrategy, location;
2525

2626
function makeLocation(baseHref: string = '/my/app', binding: any = CONST_EXPR([])): Location {
27-
browserLocation = new DummyBrowserLocation();
28-
browserLocation.internalBaseHref = baseHref;
27+
locationStrategy = new MockLocationStrategy();
28+
locationStrategy.internalBaseHref = baseHref;
2929
let injector = Injector.resolveAndCreate(
30-
[Location, bind(BrowserLocation).toValue(browserLocation), binding]);
30+
[Location, bind(LocationStrategy).toValue(locationStrategy), binding]);
3131
return location = injector.get(Location);
3232
}
3333

3434
beforeEach(makeLocation);
3535

3636
it('should normalize relative urls on navigate', () => {
3737
location.go('user/btford');
38-
expect(browserLocation.path()).toEqual('/my/app/user/btford');
38+
expect(locationStrategy.path()).toEqual('/my/app/user/btford');
3939
});
4040

4141
it('should not prepend urls with starting slash when an empty URL is provided',
42-
() => { expect(location.normalizeAbsolutely('')).toEqual(browserLocation.getBaseHref()); });
42+
() => { expect(location.normalizeAbsolutely('')).toEqual(locationStrategy.getBaseHref()); });
4343

4444
it('should not prepend path with an extra slash when a baseHref has a trailing slash', () => {
4545
let location = makeLocation('/my/slashed/app/');
@@ -48,32 +48,32 @@ export function main() {
4848

4949
it('should not append urls with leading slash on navigate', () => {
5050
location.go('/my/app/user/btford');
51-
expect(browserLocation.path()).toEqual('/my/app/user/btford');
51+
expect(locationStrategy.path()).toEqual('/my/app/user/btford');
5252
});
5353

5454
it('should remove index.html from base href', () => {
5555
let location = makeLocation('/my/app/index.html');
5656
location.go('user/btford');
57-
expect(browserLocation.path()).toEqual('/my/app/user/btford');
57+
expect(locationStrategy.path()).toEqual('/my/app/user/btford');
5858
});
5959

6060
it('should normalize urls on popstate', inject([AsyncTestCompleter], (async) => {
61-
browserLocation.simulatePopState('/my/app/user/btford');
61+
locationStrategy.simulatePopState('/my/app/user/btford');
6262
location.subscribe((ev) => {
6363
expect(ev['url']).toEqual('/user/btford');
6464
async.done();
6565
})
6666
}));
6767

6868
it('should normalize location path', () => {
69-
browserLocation.internalPath = '/my/app/user/btford';
69+
locationStrategy.internalPath = '/my/app/user/btford';
7070
expect(location.path()).toEqual('/user/btford');
7171
});
7272

7373
it('should use optional base href param', () => {
7474
let location = makeLocation('/', bind(appBaseHrefToken).toValue('/my/custom/href'));
7575
location.go('user/btford');
76-
expect(browserLocation.path()).toEqual('/my/custom/href/user/btford');
76+
expect(locationStrategy.path()).toEqual('/my/custom/href/user/btford');
7777
});
7878
});
7979
}

modules/angular2/test/router/router_integration_spec.ts

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ import {RouteConfig} from 'angular2/src/router/route_config_decorator';
2020
import {PromiseWrapper} from 'angular2/src/facade/async';
2121
import {BaseException} from 'angular2/src/facade/lang';
2222
import {routerInjectables, Router, appBaseHrefToken, routerDirectives} from 'angular2/router';
23-
import {BrowserLocation} from 'angular2/src/router/browser_location';
24-
import {DummyBrowserLocation} from 'angular2/src/mock/browser_location_mock';
23+
import {LocationStrategy} from 'angular2/src/router/location_strategy';
24+
import {MockLocationStrategy} from 'angular2/src/mock/mock_location_strategy';
2525

2626
export function main() {
2727
describe('router injectables', () => {
@@ -32,12 +32,7 @@ export function main() {
3232
DOM.appendChild(fakeDoc.body, el);
3333
testBindings = [
3434
routerInjectables,
35-
bind(BrowserLocation)
36-
.toFactory(() => {
37-
var browserLocation = new DummyBrowserLocation();
38-
browserLocation.spy('pushState');
39-
return browserLocation;
40-
}),
35+
bind(LocationStrategy).toClass(MockLocationStrategy),
4136
bind(DOCUMENT_TOKEN).toValue(fakeDoc)
4237
];
4338
});
@@ -48,7 +43,7 @@ export function main() {
4843
var router = applicationRef.hostComponent.router;
4944
router.subscribe((_) => {
5045
expect(el).toHaveText('outer { hello }');
51-
expect(applicationRef.hostComponent.location.path()).toEqual('/');
46+
expect(applicationRef.hostComponent.location.path()).toEqual('');
5247
async.done();
5348
});
5449
});
@@ -109,7 +104,7 @@ class HelloCmp {
109104
@View({template: "outer { <router-outlet></router-outlet> }", directives: routerDirectives})
110105
@RouteConfig([{path: '/', component: HelloCmp}])
111106
class AppCmp {
112-
constructor(public router: Router, public location: BrowserLocation) {}
107+
constructor(public router: Router, public location: LocationStrategy) {}
113108
}
114109

115110

@@ -124,7 +119,7 @@ class ParentCmp {
124119
@View({template: `root { <router-outlet></router-outlet> }`, directives: routerDirectives})
125120
@RouteConfig([{path: '/parent/...', component: ParentCmp}])
126121
class HierarchyAppCmp {
127-
constructor(public router: Router, public location: BrowserLocation) {}
122+
constructor(public router: Router, public location: LocationStrategy) {}
128123
}
129124

130125
@Component({selector: 'oops-cmp'})
@@ -137,5 +132,5 @@ class BrokenCmp {
137132
@View({template: "outer { <router-outlet></router-outlet> }", directives: routerDirectives})
138133
@RouteConfig([{path: '/cause-error', component: BrokenCmp}])
139134
class BrokenAppCmp {
140-
constructor(public router: Router, public location: BrowserLocation) {}
135+
constructor(public router: Router, public location: LocationStrategy) {}
141136
}

0 commit comments

Comments
 (0)