From ea546f5069efcb2b34e445190b4bad4dc6a77910 Mon Sep 17 00:00:00 2001 From: Brian Ford Date: Tue, 21 Apr 2015 11:23:23 -0700 Subject: [PATCH] feat(router): add location service --- modules/angular2/src/mock/location_mock.js | 58 +++++++++++++++++++++ modules/angular2/src/router/location.js | 40 ++++++++++++++ modules/angular2/src/router/router.js | 20 ++++--- modules/angular2/test/router/outlet_spec.js | 4 +- modules/angular2/test/router/router_spec.js | 37 +++++++++++-- 5 files changed, 149 insertions(+), 10 deletions(-) create mode 100644 modules/angular2/src/mock/location_mock.js create mode 100644 modules/angular2/src/router/location.js diff --git a/modules/angular2/src/mock/location_mock.js b/modules/angular2/src/mock/location_mock.js new file mode 100644 index 0000000000..d615aa5a98 --- /dev/null +++ b/modules/angular2/src/mock/location_mock.js @@ -0,0 +1,58 @@ +import {SpyObject, proxy} from 'angular2/test_lib'; + +import {isBlank, isPresent, IMPLEMENTS} from 'angular2/src/facade/lang'; +import {EventEmitter, ObservableWrapper} from 'angular2/src/facade/async'; +import {List, ListWrapper} from 'angular2/src/facade/collection'; +import {Location} from 'angular2/src/router/location'; + + +@proxy +@IMPLEMENTS(Location) +export class DummyLocation extends SpyObject { + urlChanges:List; + _path:string; + _subject:EventEmitter; + + constructor() { + super(); + this._path = '/'; + this.urlChanges = ListWrapper.create(); + this._subject = new EventEmitter(); + } + + setInitialPath(url:string) { + this._path = url; + } + + path():string { + return this._path; + } + + simulateUrlPop(pathname:string) { + ObservableWrapper.callNext(this._subject, { + 'url': pathname + }); + } + + go(url:string) { + if (this._path === url) { + return; + } + this._path = url; + ListWrapper.push(this.urlChanges, url); + } + + forward() { + // TODO + } + + back() { + // TODO + } + + subscribe(onNext, onThrow = null, onReturn = null) { + ObservableWrapper.subscribe(this._subject, onNext, onThrow, onReturn); + } + + noSuchMethod(m){return super.noSuchMethod(m);} +} diff --git a/modules/angular2/src/router/location.js b/modules/angular2/src/router/location.js new file mode 100644 index 0000000000..fe32064493 --- /dev/null +++ b/modules/angular2/src/router/location.js @@ -0,0 +1,40 @@ +import {global} from 'angular2/src/facade/lang'; +import {EventEmitter, ObservableWrapper} from 'angular2/src/facade/async'; + +export class Location { + _location; + _subject:EventEmitter; + _history; + constructor() { + this._subject = new EventEmitter(); + this._location = global.location; + this._history = global.history; + global.addEventListener('popstate', (_) => this._onPopState(_), false); + } + + _onPopState(_) { + ObservableWrapper.callNext(this._subject, { + 'url': this._location.pathname + }); + } + + path() { + return this._location.pathname; + } + + go(url:string) { + this._history.pushState(null, null, url); + } + + forward() { + this._history.forward(); + } + + back() { + this._history.back() + } + + subscribe(onNext, onThrow = null, onReturn = null) { + ObservableWrapper.subscribe(this._subject, onNext, onThrow, onReturn); + } +} diff --git a/modules/angular2/src/router/router.js b/modules/angular2/src/router/router.js index 6dfd4ac57b..006493e03a 100644 --- a/modules/angular2/src/router/router.js +++ b/modules/angular2/src/router/router.js @@ -6,6 +6,7 @@ import {RouteRegistry} from './route_registry'; import {Pipeline} from './pipeline'; import {Instruction} from './instruction'; import {RouterOutlet} from './router_outlet'; +import {Location} from './location'; /** * # Router @@ -28,17 +29,21 @@ export class Router { _outlets:Map; _children:Map; _subject:EventEmitter; - - constructor(registry:RouteRegistry, pipeline:Pipeline, parent:Router = null, name = '/') { + _location:Location; + + constructor(registry:RouteRegistry, pipeline:Pipeline, location:Location, parent:Router = null, name = '/') { this.name = name; this.navigating = false; this.parent = parent; this.previousUrl = null; this._outlets = MapWrapper.create(); this._children = MapWrapper.create(); + this._location = location; this._registry = registry; this._pipeline = pipeline; this._subject = new EventEmitter(); + this._location.subscribe((url) => this.navigate(url)); + this.navigate(location.path()); } @@ -97,6 +102,9 @@ export class Router { this._startNavigating(); var result = this._pipeline.process(instruction) + .then((_) => { + this._location.go(instruction.matchedUrl); + }) .then((_) => { ObservableWrapper.callNext(this._subject, instruction.matchedUrl); }) @@ -170,19 +178,19 @@ export class Router { } static getRoot():Router { - return new RootRouter(new Pipeline()); + return new RootRouter(new Pipeline(), new Location()); } } export class RootRouter extends Router { - constructor(pipeline:Pipeline) { - super(new RouteRegistry(), pipeline, null, '/'); + constructor(pipeline:Pipeline, location:Location) { + super(new RouteRegistry(), pipeline, location, null, '/'); } } class ChildRouter extends Router { constructor(parent, name) { - super(parent._registry, parent._pipeline, parent, name); + super(parent._registry, parent._pipeline, parent._location, parent, name); this.parent = parent; } } diff --git a/modules/angular2/test/router/outlet_spec.js b/modules/angular2/test/router/outlet_spec.js index a7c0483d88..da16d0d64d 100644 --- a/modules/angular2/test/router/outlet_spec.js +++ b/modules/angular2/test/router/outlet_spec.js @@ -25,6 +25,8 @@ import {Router, RouterOutlet, RouterLink, RouteConfig, RouteParams} from 'angula import {DOM} from 'angular2/src/dom/dom_adapter'; +import {DummyLocation} from 'angular2/src/mock/location_mock'; + export function main() { describe('Outlet Directive', () => { @@ -36,7 +38,7 @@ export function main() { })); beforeEachBindings(() => { - router = new RootRouter(new Pipeline()); + router = new RootRouter(new Pipeline(), new DummyLocation()); return [ bind(Router).toValue(router) ]; diff --git a/modules/angular2/test/router/router_spec.js b/modules/angular2/test/router/router_spec.js index 0540bb23c7..387d397c14 100644 --- a/modules/angular2/test/router/router_spec.js +++ b/modules/angular2/test/router/router_spec.js @@ -12,16 +12,47 @@ import {Promise, PromiseWrapper} from 'angular2/src/facade/async'; import {RootRouter} from 'angular2/src/router/router'; import {Pipeline} from 'angular2/src/router/pipeline'; import {RouterOutlet} from 'angular2/src/router/router_outlet'; - +import {DummyLocation} from 'angular2/src/mock/location_mock' export function main() { describe('Router', () => { - var router; + var router, + location; beforeEach(() => { - router = new RootRouter(new Pipeline()); + location = new DummyLocation(); + router = new RootRouter(new Pipeline(), location); }); + + it('should navigate based on the initial URL state', inject([AsyncTestCompleter], (async) => { + var outlet = makeDummyRef(); + + router.config('/', {'component': 'Index' }) + .then((_) => router.registerOutlet(outlet)) + .then((_) => { + expect(outlet.spy('activate')).toHaveBeenCalled(); + expect(location.urlChanges).toEqual(['/']); + async.done(); + }); + })); + + + it('should activate viewports and update URL on navigate', inject([AsyncTestCompleter], (async) => { + var outlet = makeDummyRef(); + + router.registerOutlet(outlet) + .then((_) => { + return router.config('/a', {'component': 'A' }); + }) + .then((_) => router.navigate('/a')) + .then((_) => { + expect(outlet.spy('activate')).toHaveBeenCalled(); + expect(location.urlChanges).toEqual(['/a']); + async.done(); + }); + })); + it('should navigate after being configured', inject([AsyncTestCompleter], (async) => { var outlet = makeDummyRef();