aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorVojta Jina2010-10-16 15:22:36 +0100
committerMisko Hevery2010-10-18 09:58:37 -0700
commit341b2b3a9b1257e376a7d319f131008162f584ae (patch)
tree8d7b7e42cc7333dab14a9fe8d00b838893207f37
parent9e9bdbdc405b6afecd2e536e375c9d8fe40f110b (diff)
downloadangular.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.js253
-rw-r--r--test/servicesSpec.js134
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);