aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMisko Hevery2010-09-22 13:24:40 +0200
committerMisko Hevery2010-09-22 16:17:44 +0200
commit0649009624e8e7bd6fb39537f62c6f00facbfb16 (patch)
treee85077e148220ce75926bffce2d1e7daf8069945
parenteefb920d0e0345485a8eb120aeecc3b1aa9f6719 (diff)
downloadangular.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--Rakefile2
-rw-r--r--scenario/browser.html22
-rw-r--r--scenario/widgets.html6
-rw-r--r--src/Angular.js1
-rw-r--r--src/AngularPublic.js6
-rw-r--r--src/Browser.js261
-rw-r--r--src/services.js23
-rw-r--r--test/BrowserSpecs.js110
-rw-r--r--test/ScenarioSpec.js4
-rw-r--r--test/angular-mocks.js24
-rw-r--r--test/servicesSpec.js6
11 files changed, 206 insertions, 259 deletions
diff --git a/Rakefile b/Rakefile
index c4e53987..1af9e303 100644
--- a/Rakefile
+++ b/Rakefile
@@ -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'});
});