diff options
| author | Misko Hevery | 2010-09-22 13:24:40 +0200 |
|---|---|---|
| committer | Misko Hevery | 2010-09-22 16:17:44 +0200 |
| commit | 0649009624e8e7bd6fb39537f62c6f00facbfb16 (patch) | |
| tree | e85077e148220ce75926bffce2d1e7daf8069945 | |
| parent | eefb920d0e0345485a8eb120aeecc3b1aa9f6719 (diff) | |
| download | angular.js-0649009624e8e7bd6fb39537f62c6f00facbfb16.tar.bz2 | |
Refactored the Browser:
- change from using prototype to inner functions to help with better compression
- removed watchers (url/cookie) and introduced a poller concept
- moved the checking of URL and cookie into services which register with poolers
Benefits:
- Smaller minified file
- can call $browser.poll() from tests to simulate polling
- single place where setTimeout needs to be tested
- More testable $browser
| -rw-r--r-- | Rakefile | 2 | ||||
| -rw-r--r-- | scenario/browser.html | 22 | ||||
| -rw-r--r-- | scenario/widgets.html | 6 | ||||
| -rw-r--r-- | src/Angular.js | 1 | ||||
| -rw-r--r-- | src/AngularPublic.js | 6 | ||||
| -rw-r--r-- | src/Browser.js | 261 | ||||
| -rw-r--r-- | src/services.js | 23 | ||||
| -rw-r--r-- | test/BrowserSpecs.js | 110 | ||||
| -rw-r--r-- | test/ScenarioSpec.js | 4 | ||||
| -rw-r--r-- | test/angular-mocks.js | 24 | ||||
| -rw-r--r-- | test/servicesSpec.js | 6 |
11 files changed, 206 insertions, 259 deletions
@@ -121,7 +121,7 @@ task :lint do print out end -desc 'push_angularajs' +desc 'push_angularjs' task :push_angularjs do Rake::Task['compile'].execute 0 sh %(cat angularjs.ftp | ftp -N angularjs.netrc angularjs.org) diff --git a/scenario/browser.html b/scenario/browser.html new file mode 100644 index 00000000..eac43692 --- /dev/null +++ b/scenario/browser.html @@ -0,0 +1,22 @@ +<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> +<html xmlns:ng="http://angularjs.org"> + <head> + <script type="text/javascript" src="../src/angular-bootstrap.js" ng:autobind></script> + </head> + <body ng:init="$window.$scope = this"> + + <h1>Should mark input field red and create hover</h1> + <input type="text" name="name" ng:required/> + + <h1>Should reflect changes in URL</h1> + <pre>$location={{$location}}</pre> + hash: <input type="text" name="$location.hash"/> <br/> + hashPath: <input type="text" name="$location.hashPath"/> <br/> + hashSearch: <input type="text" name="$location.hashSearch" ng:format="json"/> <br/> + + <h1>Should reflect changes in Cookie</h1> + <pre>$cookies={{$cookies}}</pre> + $cookies: <input type="text" name="$cookies" ng:format="json"/> <br/> + + </body> + </html> diff --git a/scenario/widgets.html b/scenario/widgets.html index d5285ea6..08443d2a 100644 --- a/scenario/widgets.html +++ b/scenario/widgets.html @@ -1,8 +1,8 @@ - <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> -<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> +<html xmlns:ng="http://angularjs.org"> <head> <link rel="stylesheet" type="text/css" href="style.css"/> - <script type="text/javascript" src="../src/angular-bootstrap.js#autobind"></script> + <script type="text/javascript" src="../src/angular-bootstrap.js" ng:autobind></script> </head> <body ng:init="$window.$scope = this"> <table> diff --git a/src/Angular.js b/src/Angular.js index ef1187f2..e3d33c73 100644 --- a/src/Angular.js +++ b/src/Angular.js @@ -35,6 +35,7 @@ var _undefined = undefined, msie = !!/(msie) ([\w.]+)/.exec(lowercase(navigator.userAgent)), jqLite = jQuery || jqLiteWrap, slice = Array.prototype.slice, + push = Array.prototype.push, error = window[$console] ? bind(window[$console], window[$console]['error'] || noop) : noop, angular = window[$angular] || (window[$angular] = {}), angularTextMarkup = extensionMap(angular, 'markup'), diff --git a/src/AngularPublic.js b/src/AngularPublic.js index e9f20b59..40425b8d 100644 --- a/src/AngularPublic.js +++ b/src/AngularPublic.js @@ -4,9 +4,9 @@ angularService('$browser', function browserFactory(){ browserSingleton = new Browser( window.location, jqLite(window.document), - jqLite(window.document.getElementsByTagName('head')[0])); - browserSingleton.startUrlWatcher(); - browserSingleton.startCookieWatcher(); + jqLite(window.document.getElementsByTagName('head')[0]), + XHR); + browserSingleton.startPoller(50, function(delay, fn){setTimeout(delay,fn);}); browserSingleton.bind(); } return browserSingleton; diff --git a/src/Browser.js b/src/Browser.js index 0dacf3c4..e21e419b 100644 --- a/src/Browser.js +++ b/src/Browser.js @@ -1,45 +1,118 @@ ////////////////////////////// // Browser ////////////////////////////// +var XHR = window.XMLHttpRequest || function () { + try { return new ActiveXObject("Msxml2.XMLHTTP.6.0"); } catch (e1) {} + try { return new ActiveXObject("Msxml2.XMLHTTP.3.0"); } catch (e2) {} + try { return new ActiveXObject("Msxml2.XMLHTTP"); } catch (e3) {} + throw new Error("This browser does not support XMLHttpRequest."); +}; -function Browser(location, document, head) { - this.delay = 50; - this.expectedUrl = location.href; - this.urlListeners = []; - this.hoverListener = noop; - this.isMock = false; - this.outstandingRequests = { count: 0, callbacks:[]}; +function Browser(location, document, head, XHR) { + var self = this; + self.isMock = false; - this.XHR = window.XMLHttpRequest || function () { - try { return new ActiveXObject("Msxml2.XMLHTTP.6.0"); } catch (e1) {} - try { return new ActiveXObject("Msxml2.XMLHTTP.3.0"); } catch (e2) {} - try { return new ActiveXObject("Msxml2.XMLHTTP"); } catch (e3) {} - throw new Error("This browser does not support XMLHttpRequest."); - }; - this.setTimeout = function(fn, delay) { - window.setTimeout(fn, delay); + ////////////////////////////////////////////////////////////// + // XHR API + ////////////////////////////////////////////////////////////// + var idCounter = 0; + var outstandingRequestCount = 0; + var outstandingRequestCallbacks = []; + + self.xhr = function(method, url, post, callback){ + if (isFunction(post)) { + callback = post; + post = _null; + } + if (lowercase(method) == 'json') { + var callbackId = "angular_" + Math.random() + '_' + (idCounter++); + callbackId = callbackId.replace(/\d\./, ''); + var script = document[0].createElement('script'); + script.type = 'text/javascript'; + script.src = url.replace('JSON_CALLBACK', callbackId); + head.append(script); + window[callbackId] = function(data){ + window[callbackId] = _undefined; + callback(200, data); + }; + } else { + var xhr = new XHR(); + xhr.open(method, url, true); + xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); + xhr.setRequestHeader("Accept", "application/json, text/plain, */*"); + xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest"); + outstandingRequestCount ++; + xhr.onreadystatechange = function() { + if (xhr.readyState == 4) { + try { + callback(xhr.status || 200, xhr.responseText); + } finally { + outstandingRequestCount--; + if (outstandingRequestCount === 0) { + while(outstandingRequestCallbacks.length) { + try { + outstandingRequestCallbacks.pop()(); + } catch (e) { + } + } + } + } + } + }; + xhr.send(post || ''); + } }; - this.location = location; - this.document = document; - var rawDocument = document[0]; - this.head = head; - this.idCounter = 0; + self.notifyWhenNoOutstandingRequests = function(callback){ + if (outstandingRequestCount === 0) { + callback(); + } else { + outstandingRequestCallbacks.push(callback); + } + }; - this.cookies = cookies; - this.watchCookies = function(fn){ cookieListeners.push(fn); }; + ////////////////////////////////////////////////////////////// + // Poll Watcher API + ////////////////////////////////////////////////////////////// + var pollFns = []; + function poll(){ + foreach(pollFns, function(pollFn){ pollFn(); }); + } + self.poll = poll; + self.addPollFn = bind(pollFns, push); + self.startPoller = function(interval, setTimeout){ + (function check(){ + poll(); + setTimeout(check, interval); + })(); + }; - // functions + ////////////////////////////////////////////////////////////// + // URL API + ////////////////////////////////////////////////////////////// + self.setUrl = function(url) { + var existingURL = location.href; + if (!existingURL.match(/#/)) existingURL += '#'; + if (!url.match(/#/)) url += '#'; + location.href = url; + }; + self.getUrl = function() { + return location.href; + }; + + ////////////////////////////////////////////////////////////// + // Cookies API + ////////////////////////////////////////////////////////////// + var rawDocument = document[0]; var lastCookies = {}; var lastCookieString = ''; - var cookieListeners = []; /** * cookies() -> hash of all cookies * cookies(name, value) -> set name to value * if value is undefined delete it * cookies(name) -> should get value, but deletes (no one calls it right now that way) */ - function cookies(name, value){ + self.cookies = function (name, value){ if (name) { if (value === _undefined) { delete lastCookies[name]; @@ -59,139 +132,33 @@ function Browser(location, document, head) { lastCookies[unescape(keyValue[0])] = unescape(keyValue[1]); } } - foreach(cookieListeners, function(fn){ - fn(lastCookies); - }); } return lastCookies; } - } -} - -Browser.prototype = { + }; - bind: function() { - var self = this; - self.document.bind("mouseover", function(event){ - self.hoverListener(jqLite(msie ? event.srcElement : event.target), true); + ////////////////////////////////////////////////////////////// + // Misc API + ////////////////////////////////////////////////////////////// + var hoverListener = noop; + self.hover = function(listener) { hoverListener = listener; }; + self.bind = function() { + document.bind("mouseover", function(event){ + hoverListener(jqLite(msie ? event.srcElement : event.target), true); return true; }); - self.document.bind("mouseleave mouseout click dblclick keypress keyup", function(event){ - self.hoverListener(jqLite(event.target), false); + document.bind("mouseleave mouseout click dblclick keypress keyup", function(event){ + hoverListener(jqLite(event.target), false); return true; }); - }, + }; - hover: function(hoverListener) { - this.hoverListener = hoverListener; - }, - addCss: function(url) { - var doc = this.document[0], - head = jqLite(doc.getElementsByTagName('head')[0]), - link = jqLite(doc.createElement('link')); + self.addCss = function(url) { + var link = jqLite(rawDocument.createElement('link')); link.attr('rel', 'stylesheet'); link.attr('type', 'text/css'); link.attr('href', url); head.append(link); - }, - - xhr: function(method, url, post, callback){ - if (isFunction(post)) { - callback = post; - post = _null; - } - if (lowercase(method) == 'json') { - var callbackId = "angular_" + Math.random() + '_' + (this.idCounter++); - callbackId = callbackId.replace(/\d\./, ''); - var script = this.document[0].createElement('script'); - script.type = 'text/javascript'; - script.src = url.replace('JSON_CALLBACK', callbackId); - this.head.append(script); - window[callbackId] = function(data){ - window[callbackId] = _undefined; - callback(200, data); - }; - } else { - var xhr = new this.XHR(), - self = this; - xhr.open(method, url, true); - xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); - xhr.setRequestHeader("Accept", "application/json, text/plain, */*"); - xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest"); - this.outstandingRequests.count ++; - xhr.onreadystatechange = function() { - if (xhr.readyState == 4) { - try { - callback(xhr.status || 200, xhr.responseText); - } finally { - self.outstandingRequests.count--; - self.processRequestCallbacks(); - } - } - }; - xhr.send(post || ''); - } - }, - - processRequestCallbacks: function(){ - if (this.outstandingRequests.count === 0) { - while(this.outstandingRequests.callbacks.length) { - try { - this.outstandingRequests.callbacks.pop()(); - } catch (e) { - } - } - } - }, - - notifyWhenNoOutstandingRequests: function(callback){ - if (this.outstandingRequests.count === 0) { - callback(); - } else { - this.outstandingRequests.callbacks.push(callback); - } - }, - - watchUrl: function(fn){ - this.urlListeners.push(fn); - }, - - startUrlWatcher: function() { - var self = this; - (function pull () { - if (self.expectedUrl !== self.location.href) { - foreach(self.urlListeners, function(listener){ - try { - listener(self.location.href); - } catch (e) { - error(e); - } - }); - self.expectedUrl = self.location.href; - } - self.setTimeout(pull, self.delay); - })(); - }, - - startCookieWatcher: function() { - var self = this; - (function poll() { - self.cookies(); - self.setTimeout(poll, self.delay); - })(); - }, - - setUrl: function(url) { - var existingURL = this.location.href; - if (!existingURL.match(/#/)) existingURL += '#'; - if (!url.match(/#/)) url += '#'; - if (existingURL != url) { - this.location.href = this.expectedUrl = url; - } - }, - - getUrl: function() { - return this.location.href; - } -}; + }; +} diff --git a/src/services.js b/src/services.js index a84a55db..a0317f20 100644 --- a/src/services.js +++ b/src/services.js @@ -11,14 +11,17 @@ angularService("$location", function(browser){ var scope = this, location = {parse:parseUrl, toString:toString, update:update}, lastLocation = {}; + var lastBrowserUrl = browser.getUrl(); - browser.watchUrl(function(url){ - update(url); - scope.$root.$eval(); + browser.addPollFn(function(){ + if (lastBrowserUrl !== browser.getUrl()) { + update(lastBrowserUrl = browser.getUrl()); + scope.$eval(); + } }); this.$onEval(PRIORITY_FIRST, update); this.$onEval(PRIORITY_LAST, update); - update(browser.getUrl()); + update(lastBrowserUrl); return location; function update(href){ @@ -395,10 +398,14 @@ angularService('$resource', function($xhr){ angularService('$cookies', function($browser) { - var cookies = {}, rootScope = this; - $browser.watchCookies(function(newCookies){ - copy(newCookies, cookies); - rootScope.$eval(); + var cookies = {}, rootScope = this, lastCookies; + $browser.addPollFn(function(){ + var currentCookies = $browser.cookies(); + if (lastCookies != currentCookies) { + lastCookies = currentCookies; + copy(currentCookies, cookies); + rootScope.$eval(); + } }); this.$onEval(PRIORITY_FIRST, update); this.$onEval(PRIORITY_LAST, update); diff --git a/test/BrowserSpecs.js b/test/BrowserSpecs.js index 4138a9d9..abd761eb 100644 --- a/test/BrowserSpecs.js +++ b/test/BrowserSpecs.js @@ -1,6 +1,6 @@ describe('browser', function(){ - var browser, location, head; + var browser, location, head, xhr; beforeEach(function(){ location = {href:"http://server", hash:""}; @@ -8,44 +8,19 @@ describe('browser', function(){ scripts: [], append: function(node){head.scripts.push(node);} }; - browser = new Browser(location, jqLite(window.document), head); - browser.setTimeout = noop; - }); - - it('should watch url', function(){ - browser.delay = 1; - expectAsserts(2); - browser.watchUrl(function(url){ - assertEquals('http://getangular.test', url); + xhr = null; + browser = new Browser(location, jqLite(window.document), head, function(){ + xhr = this; + this.open = noop; + this.setRequestHeader = noop; + this.send = noop; }); - browser.setTimeout = function(fn, delay){ - assertEquals(1, delay); - location.href = "http://getangular.test"; - browser.setTimeout = function(fn, delay) {}; - fn(); - }; - browser.startUrlWatcher(); }); it('should contain cookie cruncher', function() { expect(browser.cookies).toBeDefined(); }); - it('should be able to start cookie watcher', function() { - browser.delay = 1; - expectAsserts(2); - browser.watchCookies(function(cookies){ - assertEquals({'foo':'bar'}, cookies); - }); - browser.setTimeout = function(fn, delay){ - assertEquals(1, delay); - document.cookie = 'foo=bar'; - browser.setTimeout = function(fn, delay) {}; - fn(); - }; - browser.startCookieWatcher(); - }); - describe('outstading requests', function(){ it('should process callbacks immedietly with no outstanding requests', function(){ var callback = jasmine.createSpy('callback'); @@ -55,15 +30,12 @@ describe('browser', function(){ it('should queue callbacks with outstanding requests', function(){ var callback = jasmine.createSpy('callback'); - browser.outstandingRequests.count = 1; + browser.xhr('GET', '/url', noop); browser.notifyWhenNoOutstandingRequests(callback); expect(callback).not.wasCalled(); - browser.processRequestCallbacks(); - expect(callback).not.wasCalled(); - - browser.outstandingRequests.count = 0; - browser.processRequestCallbacks(); + xhr.readyState = 4; + xhr.onreadystatechange(); expect(callback).wasCalled(); }); }); @@ -220,44 +192,6 @@ describe('browser', function(){ }); - describe('watch', function() { - - it('should allow listeners to be registered', function() { - expectAsserts(1); - - browser.watchCookies(function(cookies) { - assertEquals({'aaa':'bbb'}, cookies); - }); - - browser.cookies('aaa','bbb'); - browser.cookies(); - }); - - - it('should fire listeners when cookie changes are discovered', function() { - expectAsserts(1); - - browser.watchCookies(function(cookies) { - assertEquals({'foo':'bar'}, cookies); - }); - - document.cookie = 'foo=bar'; - browser.cookies(); - }); - - - it('should not fire listeners when no cookies were changed', function() { - expectAsserts(0); - - browser.cookies(function(cookies) { - assertEquals({'shouldnt':'fire'}, cookies); - }); - - browser.cookies(true); - }); - }); - - it('should pick up external changes made to browser cookies', function() { browser.cookies('oatmealCookie', 'drool'); expect(browser.cookies()).toEqual({'oatmealCookie':'drool'}); @@ -274,5 +208,29 @@ describe('browser', function(){ }); }); + + describe('poll', function(){ + it('should call all fns on poll', function(){ + var log = ''; + browser.addPollFn(function(){log+='a';}); + browser.addPollFn(function(){log+='b';}); + expect(log).toEqual(''); + browser.poll(); + expect(log).toEqual('ab'); + browser.poll(); + expect(log).toEqual('abab'); + }); + + it('should startPoller', function(){ + var log = ''; + var setTimeoutSpy = jasmine.createSpy('setTimeout'); + browser.addPollFn(function(){log+='.';}); + browser.startPoller(50, setTimeoutSpy); + expect(log).toEqual('.'); + expect(setTimeoutSpy.mostRecentCall.args[1]).toEqual(50); + setTimeoutSpy.mostRecentCall.args[0](); + expect(log).toEqual('..'); + }); + }); }); diff --git a/test/ScenarioSpec.js b/test/ScenarioSpec.js index 7ea3192d..ede49a49 100644 --- a/test/ScenarioSpec.js +++ b/test/ScenarioSpec.js @@ -42,10 +42,10 @@ describe("ScenarioSpec: configuration", function(){ it("should take location object", function(){ var url = "http://server/#?book=moby"; var scope = compile("<div>{{$location}}</div>"); - var $location = scope.$get('$location'); + var $location = scope.$location; expect($location.hashSearch.book).toBeUndefined(); scope.$browser.setUrl(url); - scope.$browser.fireUrlWatchers(); + scope.$browser.poll(); expect($location.hashSearch.book).toEqual('moby'); }); }); diff --git a/test/angular-mocks.js b/test/angular-mocks.js index 1e547f77..a0d25042 100644 --- a/test/angular-mocks.js +++ b/test/angular-mocks.js @@ -29,7 +29,7 @@ function MockBrowser() { this.isMock = true; self.url = "http://server"; - self.watches = []; + self.pollFns = []; self.xhr = function(method, url, data, callback) { if (angular.isFunction(data)) { @@ -78,6 +78,14 @@ function MockBrowser() { } MockBrowser.prototype = { + poll: function poll(){ + foreach(this.pollFns, function(pollFn){ pollFn(); }); + }, + + addPollFn: function(pollFn) { + this.pollFns.push(pollFn); + }, + hover: function(onHover) { }, @@ -89,20 +97,6 @@ MockBrowser.prototype = { this.url = url; }, - watchUrl: function(fn) { - this.watches.push(fn); - }, - - watchCookies: function(fn) { - this.watches.push(fn); - }, - - fireUrlWatchers: function() { - for(var i=0; i<this.watches.length; i++) { - this.watches[i](this.url); - } - }, - cookies: function(name, value) { if (name) { if (value == undefined) { diff --git a/test/servicesSpec.js b/test/servicesSpec.js index b39e401c..6fa2c5f5 100644 --- a/test/servicesSpec.js +++ b/test/servicesSpec.js @@ -377,8 +377,7 @@ describe("service", function(){ expect(scope.$cookies).toEqual({}); scope.$browser.cookies('brandNew', 'cookie'); - //TODO: This is a hacky way of calling the watch function, once pooling is refactored, this will go away. - scope.$browser.watches[1](scope.$browser.cookies()); + scope.$browser.poll(); expect(scope.$cookies).toEqual({'brandNew':'cookie'}); }); @@ -448,8 +447,7 @@ describe("service", function(){ it('should deserialize json to object', function() { scope.$browser.cookies('objectCookie', '{"id":123,"name":"blah"}'); - //TODO: This is a hacky way of calling the watch function, once pooling is refactored, this will go away. - scope.$browser.watches[1](scope.$browser.cookies()); + scope.$browser.poll(); expect(scope.$sessionStore.get('objectCookie')).toEqual({id: 123, name: 'blah'}); }); |
