diff options
| -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);  | 
