diff --git a/modules/angular2/router.ts b/modules/angular2/router.ts index cbe6b9092a..47ccdcc44f 100644 --- a/modules/angular2/router.ts +++ b/modules/angular2/router.ts @@ -12,7 +12,7 @@ export {RouterLink} from './src/router/router_link'; export {RouteParams} from './src/router/instruction'; export {RouteRegistry} from './src/router/route_registry'; export {BrowserLocation} from './src/router/browser_location'; -export {Location} from './src/router/location'; +export {Location, appBaseHrefToken} from './src/router/location'; export {Pipeline} from './src/router/pipeline'; export * from './src/router/route_config_decorator'; diff --git a/modules/angular2/src/mock/browser_location_mock.ts b/modules/angular2/src/mock/browser_location_mock.ts new file mode 100644 index 0000000000..d339d2b10e --- /dev/null +++ b/modules/angular2/src/mock/browser_location_mock.ts @@ -0,0 +1,43 @@ +import {proxy, SpyObject} from 'angular2/test_lib'; +import {IMPLEMENTS, BaseException} from 'angular2/src/facade/lang'; +import {EventEmitter, ObservableWrapper} from 'angular2/src/facade/async'; +import {List, ListWrapper} from 'angular2/src/facade/collection'; +import {BrowserLocation} from 'angular2/src/router/browser_location'; + +@proxy +@IMPLEMENTS(BrowserLocation) +export class DummyBrowserLocation extends SpyObject { + internalBaseHref: string = '/'; + internalPath: string = '/'; + internalTitle: string = ''; + urlChanges: List = ListWrapper.create(); + _subject: EventEmitter = new EventEmitter(); + constructor() { super(); } + + simulatePopState(url): void { + this.internalPath = url; + ObservableWrapper.callNext(this._subject, null); + } + + path(): string { return this.internalPath; } + + simulateUrlPop(pathname: string): void { + ObservableWrapper.callNext(this._subject, {'url': pathname}); + } + + pushState(ctx: any, title: string, url: string): void { + this.internalTitle = title; + this.internalPath = url; + ListWrapper.push(this.urlChanges, url); + } + + forward(): void { throw new BaseException('Not implemented yet!'); } + + back(): void { throw new BaseException('Not implemented yet!'); } + + onPopState(fn): void { ObservableWrapper.subscribe(this._subject, fn); } + + getBaseHref(): string { return this.internalBaseHref; } + + noSuchMethod(m) { return super.noSuchMethod(m); } +} diff --git a/modules/angular2/src/router/location.ts b/modules/angular2/src/router/location.ts index 38d1cf12b7..b17a510990 100644 --- a/modules/angular2/src/router/location.ts +++ b/modules/angular2/src/router/location.ts @@ -1,16 +1,19 @@ import {BrowserLocation} from './browser_location'; -import {StringWrapper} from 'angular2/src/facade/lang'; +import {StringWrapper, isPresent, CONST_EXPR} from 'angular2/src/facade/lang'; import {EventEmitter, ObservableWrapper} from 'angular2/src/facade/async'; -import {Injectable} from 'angular2/di'; +import {OpaqueToken, Injectable, Optional, Inject} from 'angular2/di'; + +export const appBaseHrefToken: OpaqueToken = CONST_EXPR(new OpaqueToken('locationHrefToken')); @Injectable() export class Location { private _subject: EventEmitter; private _baseHref: string; - constructor(public _browserLocation: BrowserLocation) { + constructor(public _browserLocation: BrowserLocation, + @Optional() @Inject(appBaseHrefToken) href?: string) { this._subject = new EventEmitter(); - this._baseHref = stripIndexHtml(this._browserLocation.getBaseHref()); + this._baseHref = stripIndexHtml(isPresent(href) ? href : this._browserLocation.getBaseHref()); this._browserLocation.onPopState((_) => this._onPopState(_)); } diff --git a/modules/angular2/test/router/location_spec.ts b/modules/angular2/test/router/location_spec.ts index 043ac2027d..0d21d54e51 100644 --- a/modules/angular2/test/router/location_spec.ts +++ b/modules/angular2/test/router/location_spec.ts @@ -11,53 +11,50 @@ import { beforeEachBindings, SpyObject } from 'angular2/test_lib'; -import {IMPLEMENTS} from 'angular2/src/facade/lang'; -import {EventEmitter, ObservableWrapper} from 'angular2/src/facade/async'; +import {Injector, bind} from 'angular2/di'; +import {CONST_EXPR} from 'angular2/src/facade/lang'; +import {Location, appBaseHrefToken} from 'angular2/src/router/location'; import {BrowserLocation} from 'angular2/src/router/browser_location'; -import {Location} from 'angular2/src/router/location'; +import {DummyBrowserLocation} from 'angular2/src/mock/browser_location_mock'; export function main() { describe('Location', () => { var browserLocation, location; - beforeEach(() => { + function makeLocation(baseHref: string = '/my/app', binding: any = CONST_EXPR([])): Location { browserLocation = new DummyBrowserLocation(); - browserLocation.spy('pushState'); - browserLocation.baseHref = '/my/app'; - location = new Location(browserLocation); - }); + browserLocation.internalBaseHref = baseHref; + let injector = Injector.resolveAndCreate( + [Location, bind(BrowserLocation).toValue(browserLocation), binding]); + return location = injector.get(Location); + } + + beforeEach(makeLocation); it('should normalize relative urls on navigate', () => { location.go('user/btford'); - expect(browserLocation.spy('pushState')) - .toHaveBeenCalledWith(null, '', '/my/app/user/btford'); + expect(browserLocation.path()).toEqual('/my/app/user/btford'); }); it('should not prepend urls with starting slash when an empty URL is provided', - () => { expect(location.normalizeAbsolutely('')).toEqual(browserLocation.baseHref); }); + () => { expect(location.normalizeAbsolutely('')).toEqual(browserLocation.getBaseHref()); }); it('should not prepend path with an extra slash when a baseHref has a trailing slash', () => { - browserLocation = new DummyBrowserLocation(); - browserLocation.spy('pushState'); - browserLocation.baseHref = '/my/slashed/app/'; - location = new Location(browserLocation); + let location = makeLocation('/my/slashed/app/'); expect(location.normalizeAbsolutely('/page')).toEqual('/my/slashed/app/page'); }); it('should not append urls with leading slash on navigate', () => { location.go('/my/app/user/btford'); - expect(browserLocation.spy('pushState')) - .toHaveBeenCalledWith(null, '', '/my/app/user/btford'); + expect(browserLocation.path()).toEqual('/my/app/user/btford'); }); it('should remove index.html from base href', () => { - browserLocation.baseHref = '/my/app/index.html'; - location = new Location(browserLocation); + let location = makeLocation('/my/app/index.html'); location.go('user/btford'); - expect(browserLocation.spy('pushState')) - .toHaveBeenCalledWith(null, '', '/my/app/user/btford'); + expect(browserLocation.path()).toEqual('/my/app/user/btford'); }); it('should normalize urls on popstate', inject([AsyncTestCompleter], (async) => { @@ -72,31 +69,11 @@ export function main() { browserLocation.internalPath = '/my/app/user/btford'; expect(location.path()).toEqual('/user/btford'); }); + + it('should use optional base href param', () => { + let location = makeLocation('/', bind(appBaseHrefToken).toValue('/my/custom/href')); + location.go('user/btford'); + expect(browserLocation.path()).toEqual('/my/custom/href/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); } -} diff --git a/modules/angular2/test/router/router_integration_spec.ts b/modules/angular2/test/router/router_integration_spec.ts index caa2842498..addb2addd3 100644 --- a/modules/angular2/test/router/router_integration_spec.ts +++ b/modules/angular2/test/router/router_integration_spec.ts @@ -17,12 +17,11 @@ import {DOM} from 'angular2/src/dom/dom_adapter'; import {bind} from 'angular2/di'; import {DOCUMENT_TOKEN} from 'angular2/src/render/dom/dom_renderer'; import {RouteConfig} from 'angular2/src/router/route_config_decorator'; -import {routerInjectables, Router} from 'angular2/router'; -import {RouterOutlet} from 'angular2/src/router/router_outlet'; -import {SpyLocation} from 'angular2/src/mock/location_mock'; -import {Location} from 'angular2/src/router/location'; import {PromiseWrapper} from 'angular2/src/facade/async'; import {BaseException} from 'angular2/src/facade/lang'; +import {routerInjectables, Router, appBaseHrefToken, routerDirectives} from 'angular2/router'; +import {BrowserLocation} from 'angular2/src/router/browser_location'; +import {DummyBrowserLocation} from 'angular2/src/mock/browser_location_mock'; export function main() { describe('router injectables', () => { @@ -33,17 +32,23 @@ export function main() { DOM.appendChild(fakeDoc.body, el); testBindings = [ routerInjectables, - bind(Location).toClass(SpyLocation), + bind(BrowserLocation) + .toFactory(() => { + var browserLocation = new DummyBrowserLocation(); + browserLocation.spy('pushState'); + return browserLocation; + }), bind(DOCUMENT_TOKEN).toValue(fakeDoc) ]; }); - it('should support bootstrap a simple app', inject([AsyncTestCompleter], (async) => { + it('should bootstrap a simple app', inject([AsyncTestCompleter], (async) => { bootstrap(AppCmp, testBindings) .then((applicationRef) => { var router = applicationRef.hostComponent.router; router.subscribe((_) => { expect(el).toHaveText('outer { hello }'); + expect(applicationRef.hostComponent.location.path()).toEqual('/'); async.done(); }); }); @@ -62,22 +67,64 @@ export function main() { }); })); + it('should bootstrap an app with a hierarchy', inject([AsyncTestCompleter], (async) => { + bootstrap(HierarchyAppCmp, testBindings) + .then((applicationRef) => { + var router = applicationRef.hostComponent.router; + router.subscribe((_) => { + expect(el).toHaveText('root { parent { hello } }'); + expect(applicationRef.hostComponent.location.path()).toEqual('/parent/child'); + async.done(); + }); + router.navigate('/parent/child'); + }); + })); + + it('should bootstrap an app with a custom app base href', + inject([AsyncTestCompleter], (async) => { + bootstrap(HierarchyAppCmp, [testBindings, bind(appBaseHrefToken).toValue('/my/app')]) + .then((applicationRef) => { + var router = applicationRef.hostComponent.router; + router.subscribe((_) => { + expect(el).toHaveText('root { parent { hello } }'); + expect(applicationRef.hostComponent.location.path()) + .toEqual('/my/app/parent/child'); + async.done(); + }); + router.navigate('/parent/child'); + }); + })); + // TODO: add a test in which the child component has bindings }); } @Component({selector: 'hello-cmp'}) -@View({template: "hello"}) +@View({template: 'hello'}) class HelloCmp { } @Component({selector: 'app-cmp'}) -@View({template: "outer { }", directives: [RouterOutlet]}) +@View({template: "outer { }", directives: routerDirectives}) @RouteConfig([{path: '/', component: HelloCmp}]) class AppCmp { - router: Router; - constructor(router: Router) { this.router = router; } + constructor(public router: Router, public location: BrowserLocation) {} +} + + +@Component({selector: 'parent-cmp'}) +@View({template: `parent { }`, directives: routerDirectives}) +@RouteConfig([{path: '/child', component: HelloCmp}]) +class ParentCmp { +} + + +@Component({selector: 'app-cmp'}) +@View({template: `root { }`, directives: routerDirectives}) +@RouteConfig([{path: '/parent', component: ParentCmp}]) +class HierarchyAppCmp { + constructor(public router: Router, public location: BrowserLocation) {} } @Component({selector: 'oops-cmp'}) @@ -87,9 +134,8 @@ class BrokenCmp { } @Component({selector: 'app-cmp'}) -@View({template: "outer { }", directives: [RouterOutlet]}) +@View({template: "outer { }", directives: routerDirectives}) @RouteConfig([{path: '/cause-error', component: BrokenCmp}]) class BrokenAppCmp { - router: Router; - constructor(router: Router) { this.router = router; } + constructor(public router: Router, public location: BrowserLocation) {} }