diff options
| author | Vojta Jina | 2010-10-16 15:22:36 +0100 |
|---|---|---|
| committer | Misko Hevery | 2010-10-18 09:58:37 -0700 |
| commit | 341b2b3a9b1257e376a7d319f131008162f584ae (patch) | |
| tree | 8d7b7e42cc7333dab14a9fe8d00b838893207f37 | |
| parent | 9e9bdbdc405b6afecd2e536e375c9d8fe40f110b (diff) | |
| download | angular.js-341b2b3a9b1257e376a7d319f131008162f584ae.tar.bz2 | |
Update $location API Close #62
update(objOrString)
updateHash(objOrString [, objOrString])
toString()
cancel()
Examples:
$location.update('http://www.angularjs.org/path#path?a=b');
$location.update({port: 443, protocol: 'https'});
$location.updateHash('hashPath');
$location.updateHash({a: 'b'});
$location.updateHash('hashPath', {a: 'b'});
This commit was produced by squash of more commits, here are the old messages:
- Change tests to use update() instead of parse().
- First implementation of update() method
- Test for update() with object parameter
- Add new tests for location, refactor location code
- Add tests for updateHash()
- Implement updateHash()
- Take one or two arguments, could be string - update hashPath, or hash object - update hashSearch...
- Fixed other service tests, to use new $location.update()
Added $location.cancel() method (with test)
Added $location.parse() for back compatability
Remove parse() method
| -rw-r--r-- | src/services.js | 253 | ||||
| -rw-r--r-- | test/servicesSpec.js | 134 |
2 files changed, 298 insertions, 89 deletions
diff --git a/src/services.js b/src/services.js index 6857d693..79bc75c4 100644 --- a/src/services.js +++ b/src/services.js @@ -13,90 +13,213 @@ angularServiceInject("$document", function(window){ return jqLite(window.document); }, ['$window'], EAGER_PUBLISHED); -angularServiceInject("$location", function(browser){ +angularServiceInject("$location", function(browser) { var scope = this, - location = {parse:parseUrl, toString:toString, update:update}, - lastLocation = {}; - var lastBrowserUrl = browser.getUrl(); + location = {toString:toString, update:update, updateHash: updateHash, cancel: cancel}, + lastLocationHref = browser.getUrl(), + lastLocationHash; browser.addPollFn(function(){ - if (lastBrowserUrl !== browser.getUrl()) { - update(lastBrowserUrl = browser.getUrl()); + if (lastLocationHref !== browser.getUrl()) { + update(lastLocationHref = browser.getUrl()); scope.$eval(); } }); - this.$onEval(PRIORITY_FIRST, update); - this.$onEval(PRIORITY_LAST, update); - update(lastBrowserUrl); + + this.$onEval(PRIORITY_FIRST, updateBrowser); + this.$onEval(PRIORITY_LAST, updateBrowser); + + update(lastLocationHref); + lastLocationHash = location.hash; + return location; - - function update(href){ - if (href) { - parseUrl(href); - } else { - href = check('href') || checkProtocol(); - var hash = check('hash'); - if (isUndefined(hash)) hash = checkHashPathSearch(); - if (isDefined(hash)) { - href = (href || location.href).split('#')[0]; - href+= '#' + hash; + + // PUBLIC METHODS + + /** + * Update location object + * Does not immediately update the browser + * Browser is updated at the end of $eval() + * + * @example + * scope.$location.update('http://www.angularjs.org/path#hash?search=x'); + * scope.$location.update({host: 'www.google.com', protocol: 'https'}); + * scope.$location.update({hashPath: '/path', hashSearch: {a: 'b', x: true}}); + * + * @param {String | Object} Full href as a string or hash object with properties + */ + function update(href) { + if (isString(href)) { + extend(location, parseHref(href)); + } + else { + if (isDefined(href.hash)) { + extend(href, parseHash(href.hash)); } - if (isDefined(href)) { - parseUrl(href); - browser.setUrl(href); + + extend(location, href); + + if (isDefined(href.hashPath || href.hashSearch)) { + location.hash = composeHash(location); } + + location.href = composeHref(location); } } - - function check(param) { - return lastLocation[param] == location[param] ? _undefined : location[param]; + + /** + * Update location hash + * @see update() + * + * @example + * scope.$location.updateHash('/hp') + * ==> update({hashPath: '/hp'}) + * + * scope.$location.updateHash({a: true, b: 'val'}) + * ==> update({hashSearch: {a: true, b: 'val'}}) + * + * scope.$location.updateHash('/hp', {a: true}) + * ==> update({hashPath: '/hp', hashSearch: {a: true}}) + * + * @param {String | Object} hashPath as String or hashSearch as Object + * @param {String | Object} hashPath as String or hashSearch as Object [optional] + */ + function updateHash() { + var hash = {}; + for (var i = 0; i < Math.min(arguments.length, 2); i++) { + hash[isString(arguments[i]) ? 'hashPath' : 'hashSearch'] = arguments[i]; + } + update(hash); } - - function checkProtocol(){ - if (lastLocation.protocol === location.protocol && - lastLocation.host === location.host && - lastLocation.port === location.port && - lastLocation.path === location.path && - equals(lastLocation.search, location.search)) - return _undefined; - var url = toKeyValue(location.search); - var port = (location.port == DEFAULT_PORTS[location.protocol] ? _null : location.port); - return location.protocol + '://' + location.host + - (port ? ':' + port : '') + location.path + - (url ? '?' + url : ''); + + /** + * Returns string representation - href + * + * @return {String} Location's href property + */ + function toString() { + updateLocation(); + return location.href; } - - function checkHashPathSearch(){ - if (lastLocation.hashPath === location.hashPath && - equals(lastLocation.hashSearch, location.hashSearch) ) - return _undefined; - var url = toKeyValue(location.hashSearch); - return escape(location.hashPath) + (url ? '?' + url : ''); + + /** + * Cancel change of the location + * + * Calling update(), updateHash() or setting a property does not immediately + * change the browser's url. Url is changed at the end of $eval() + * + * By calling this method, you can cancel the change (before end of $eval()) + * + */ + function cancel() { + update(lastLocationHref); } + + // INNER METHODS - function parseUrl(url){ - if (isDefined(url)) { - var match = URL_MATCH.exec(url); - if (match) { - location.href = url.replace('#$', ''); - location.protocol = match[1]; - location.host = match[3] || ''; - location.port = match[5] || DEFAULT_PORTS[location.protocol] || _null; - location.path = match[6]; - location.search = parseKeyValue(match[8]); - location.hash = match[10] || ''; - match = HASH_MATCH.exec(location.hash); - location.hashPath = unescape(match[1] || ''); - location.hashSearch = parseKeyValue(match[3]); - - copy(location, lastLocation); + /** + * Update location object + * + * User is allowed to change properties, so after property change, + * location object is not in consistent state. + * + * @example + * scope.$location.href = 'http://www.angularjs.org/path#a/b' + * immediately after this call, other properties are still the old ones... + * + * This method checks the changes and update location to the consistent state + */ + function updateLocation() { + if (location.href == lastLocationHref) { + if (location.hash == lastLocationHash) { + location.hash = composeHash(location); } + location.href = composeHref(location); + } + update(location.href); + } + + /** + * If location has changed, update the browser + * This method is called at the end of $eval() phase + */ + function updateBrowser() { + updateLocation(); + + if (location.href != lastLocationHref) { + browser.setUrl(lastLocationHref = location.href); + lastLocationHash = location.hash; } } - function toString() { - update(); - return location.href; + /** + * Compose href string from a location object + * + * @param {Object} Location object with all properties + * @return {String} Composed href + */ + function composeHref(loc) { + var url = toKeyValue(loc.search); + var port = (loc.port == DEFAULT_PORTS[loc.protocol] ? _null : loc.port); + + return loc.protocol + '://' + loc.host + + (port ? ':' + port : '') + loc.path + + (url ? '?' + url : '') + (loc.hash ? '#' + loc.hash : ''); + } + + /** + * Compose hash string from location object + * + * @param {Object} Object with hashPath and hashSearch properties + * @return {String} Hash string + */ + function composeHash(loc) { + var hashSearch = toKeyValue(loc.hashSearch); + return escape(loc.hashPath) + (hashSearch ? '?' + hashSearch : ''); + } + + /** + * Parse href string into location object + * + * @param {String} Href + * @return {Object} Location + */ + function parseHref(href) { + var loc = {}; + var match = URL_MATCH.exec(href); + + if (match) { + loc.href = href.replace('#$', ''); + loc.protocol = match[1]; + loc.host = match[3] || ''; + loc.port = match[5] || DEFAULT_PORTS[loc.protocol] || _null; + loc.path = match[6]; + loc.search = parseKeyValue(match[8]); + loc.hash = match[10] || ''; + + extend(loc, parseHash(loc.hash)); + } + + return loc; + } + + /** + * Parse hash string into object + * + * @param {String} Hash + * @param {Object} Object with hashPath and hashSearch properties + */ + function parseHash(hash) { + var h = {}; + var match = HASH_MATCH.exec(hash); + + if (match) { + h.hash = hash; + h.hashPath = unescape(match[1] || ''); + h.hashSearch = parseKeyValue(match[3]); + } + + return h; } }, ['$browser'], EAGER_PUBLISHED); diff --git a/test/servicesSpec.js b/test/servicesSpec.js index 6c586d0a..77ad59df 100644 --- a/test/servicesSpec.js +++ b/test/servicesSpec.js @@ -82,10 +82,14 @@ describe("service", function(){ }); describe("$location", function(){ - it("should inject $location", function(){ - scope.$location.parse('http://host:123/p/a/t/h.html?query=value#path?key=value&flag&key2='); - expect(scope.$location.href). - toEqual("http://host:123/p/a/t/h.html?query=value#path?key=value&flag&key2="); + it("should inject $location", function() { + expect(scope.$location).toBeDefined(); + }); + + it("update should update location object immediately", function() { + var href = 'http://host:123/p/a/t/h.html?query=value#path?key=value&flag&key2='; + scope.$location.update(href); + expect(scope.$location.href).toEqual(href); expect(scope.$location.protocol).toEqual("http"); expect(scope.$location.host).toEqual("host"); expect(scope.$location.port).toEqual("123"); @@ -94,16 +98,38 @@ describe("service", function(){ expect(scope.$location.hash).toEqual('path?key=value&flag&key2='); expect(scope.$location.hashPath).toEqual('path'); expect(scope.$location.hashSearch).toEqual({key: 'value', flag: true, key2: ''}); - - scope.$location.hashPath = 'page=http://path'; - scope.$location.hashSearch = {k:'a=b'}; - - expect(scope.$location.toString()). - toEqual('http://host:123/p/a/t/h.html?query=value#page%3Dhttp%3A//path?k=a%3Db'); + }); + + it('toString() should return actual representation', function() { + var href = 'http://host:123/p/a/t/h.html?query=value#path?key=value&flag&key2='; + scope.$location.update(href); + expect(scope.$location.toString()).toEqual(href); + scope.$eval(); + + scope.$location.host = 'new'; + scope.$location.path = ''; + expect(scope.$location.toString()).toEqual('http://new:123?query=value#path?key=value&flag&key2='); + }); + + it('toString() should not update browser', function() { + var url = $browser.getUrl(); + scope.$location.update('http://www.angularjs.org'); + expect(scope.$location.toString()).toEqual('http://www.angularjs.org'); + expect($browser.getUrl()).toEqual(url); + }); + + it('should update browser at the end of $eval', function() { + var url = $browser.getUrl(); + scope.$location.update('http://www.angularjs.org/'); + scope.$location.update({path: '/a/b'}); + expect(scope.$location.toString()).toEqual('http://www.angularjs.org/a/b'); + expect($browser.getUrl()).toEqual(url); + scope.$eval(); + expect($browser.getUrl()).toEqual('http://www.angularjs.org/a/b'); }); it('should parse file://', function(){ - scope.$location.parse('file:///Users/Shared/misko/work/angular.js/scenario/widgets.html'); + scope.$location.update('file:///Users/Shared/misko/work/angular.js/scenario/widgets.html'); expect(scope.$location.href).toEqual("file:///Users/Shared/misko/work/angular.js/scenario/widgets.html"); expect(scope.$location.protocol).toEqual("file"); expect(scope.$location.host).toEqual(""); @@ -113,26 +139,47 @@ describe("service", function(){ expect(scope.$location.hash).toEqual(''); expect(scope.$location.hashPath).toEqual(''); expect(scope.$location.hashSearch).toEqual({}); + }); - expect(scope.$location.toString()).toEqual('file:///Users/Shared/misko/work/angular.js/scenario/widgets.html'); + it('should update hashPath and hashSearch on hash update', function(){ + scope.$location.update('http://server/#path?a=b'); + scope.$eval(); + scope.$location.update({hash: ''}); + + expect(scope.$location.hashPath).toEqual(''); + expect(scope.$location.hashSearch).toEqual({}); + }); + + it('should update hash on hashPath or hashSearch update', function() { + scope.$location.update('http://server/#path?a=b'); + scope.$eval(); + scope.$location.update({hashPath: '', hashSearch: {}}); + + expect(scope.$location.hash).toEqual(''); }); - it('should update url on hash change', function(){ - scope.$location.parse('http://server/#path?a=b'); + it('should update hashPath and hashSearch on hash property change', function(){ + scope.$location.update('http://server/#path?a=b'); + scope.$eval(); scope.$location.hash = ''; - expect(scope.$location.toString()).toEqual('http://server/#'); + + expect(scope.$location.toString()).toEqual('http://server/'); expect(scope.$location.hashPath).toEqual(''); + expect(scope.$location.hashSearch).toEqual({}); }); - - it('should update url on hashPath change', function(){ - scope.$location.parse('http://server/#path?a=b'); + + it('should update hash on hashPath or hashSearch property change', function() { + scope.$location.update('http://server/#path?a=b'); + scope.$eval(); scope.$location.hashPath = ''; - expect(scope.$location.toString()).toEqual('http://server/#?a=b'); - expect(scope.$location.hash).toEqual('?a=b'); + scope.$location.hashSearch = {}; + + expect(scope.$location.toString()).toEqual('http://server/'); + expect(scope.$location.hash).toEqual(''); }); it("should parse url which contains - in host", function(){ - scope.$location.parse('http://a-b1.c-d.09/path'); + scope.$location.update('http://a-b1.c-d.09/path'); expect(scope.$location.href).toEqual('http://a-b1.c-d.09/path'); expect(scope.$location.protocol).toEqual('http'); expect(scope.$location.host).toEqual('a-b1.c-d.09'); @@ -152,6 +199,45 @@ describe("service", function(){ scope.$eval(); expect(log).toEqual('/abc;'); }); + + it('udpate() should accept hash object and update only given properties', function() { + scope.$location.update("http://host:123/p/a/t/h.html?query=value#path?key=value&flag&key2="); + scope.$location.update({host: 'new', port: 24}); + + expect(scope.$location.host).toEqual('new'); + expect(scope.$location.port).toEqual(24); + expect(scope.$location.protocol).toEqual('http'); + expect(scope.$location.href).toEqual("http://new:24/p/a/t/h.html?query=value#path?key=value&flag&key2="); + }); + + it('updateHash() should accept one string argument to update path', function() { + scope.$location.updateHash('path'); + expect(scope.$location.hash).toEqual('path'); + expect(scope.$location.hashPath).toEqual('path'); + }); + + it('updateHash() should accept one hash argument to update search', function() { + scope.$location.updateHash({a: 'b'}); + expect(scope.$location.hash).toEqual('?a=b'); + expect(scope.$location.hashSearch).toEqual({a: 'b'}); + }); + + it('updateHash() should accept path and search both', function() { + scope.$location.updateHash('path', {a: 'b'}); + expect(scope.$location.hash).toEqual('path?a=b'); + expect(scope.$location.hashSearch).toEqual({a: 'b'}); + expect(scope.$location.hashPath).toEqual('path'); + }); + + it('should not update browser if you call cancel()', function() { + spyOn($browser, 'setUrl'); + + scope.$location.update('http://www.angularjs.org/a/b#a/b'); + scope.$location.cancel(); + scope.$eval(); + + expect($browser.setUrl).not.toHaveBeenCalled(); + }); }); describe("$invalidWidgets", function(){ @@ -195,7 +281,7 @@ describe("service", function(){ $route.onChange(function(){ log += 'onChange();'; }); - scope.$location.parse('http://server#/Book/Moby/Chapter/Intro?p=123'); + scope.$location.update('http://server#/Book/Moby/Chapter/Intro?p=123'); scope.$eval(); expect(log).toEqual('onChange();'); expect($route.current.params).toEqual({book:'Moby', chapter:'Intro', p:'123'}); @@ -203,14 +289,14 @@ describe("service", function(){ var lastId = $route.current.scope.$id; log = ''; - scope.$location.parse('http://server#/Blank?ignore'); + scope.$location.update('http://server#/Blank?ignore'); scope.$eval(); expect(log).toEqual('onChange();'); expect($route.current.params).toEqual({ignore:true}); expect($route.current.scope.$id).not.toEqual(lastId); log = ''; - scope.$location.parse('http://server#/NONE'); + scope.$location.update('http://server#/NONE'); scope.$eval(); expect(log).toEqual('onChange();'); expect($route.current).toEqual(null); |
