Skip to content

Commit 0649009

Browse files
committed
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
1 parent eefb920 commit 0649009

11 files changed

+206
-259
lines changed

Rakefile

+1-1
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ task :lint do
121121
print out
122122
end
123123

124-
desc 'push_angularajs'
124+
desc 'push_angularjs'
125125
task :push_angularjs do
126126
Rake::Task['compile'].execute 0
127127
sh %(cat angularjs.ftp | ftp -N angularjs.netrc angularjs.org)

scenario/browser.html

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
2+
<html xmlns:ng="http://angularjs.org">
3+
<head>
4+
<script type="text/javascript" src="../src/angular-bootstrap.js" ng:autobind></script>
5+
</head>
6+
<body ng:init="$window.$scope = this">
7+
8+
<h1>Should mark input field red and create hover</h1>
9+
<input type="text" name="name" ng:required/>
10+
11+
<h1>Should reflect changes in URL</h1>
12+
<pre>$location={{$location}}</pre>
13+
hash: <input type="text" name="$location.hash"/> <br/>
14+
hashPath: <input type="text" name="$location.hashPath"/> <br/>
15+
hashSearch: <input type="text" name="$location.hashSearch" ng:format="json"/> <br/>
16+
17+
<h1>Should reflect changes in Cookie</h1>
18+
<pre>$cookies={{$cookies}}</pre>
19+
$cookies: <input type="text" name="$cookies" ng:format="json"/> <br/>
20+
21+
</body>
22+
</html>

scenario/widgets.html

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
2-
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
1+
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
2+
<html xmlns:ng="http://angularjs.org">
33
<head>
44
<link rel="stylesheet" type="text/css" href="style.css"/>
5-
<script type="text/javascript" src="../src/angular-bootstrap.js#autobind"></script>
5+
<script type="text/javascript" src="../src/angular-bootstrap.js" ng:autobind></script>
66
</head>
77
<body ng:init="$window.$scope = this">
88
<table>

src/Angular.js

+1
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ var _undefined = undefined,
3535
msie = !!/(msie) ([\w.]+)/.exec(lowercase(navigator.userAgent)),
3636
jqLite = jQuery || jqLiteWrap,
3737
slice = Array.prototype.slice,
38+
push = Array.prototype.push,
3839
error = window[$console] ? bind(window[$console], window[$console]['error'] || noop) : noop,
3940
angular = window[$angular] || (window[$angular] = {}),
4041
angularTextMarkup = extensionMap(angular, 'markup'),

src/AngularPublic.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ angularService('$browser', function browserFactory(){
44
browserSingleton = new Browser(
55
window.location,
66
jqLite(window.document),
7-
jqLite(window.document.getElementsByTagName('head')[0]));
8-
browserSingleton.startUrlWatcher();
9-
browserSingleton.startCookieWatcher();
7+
jqLite(window.document.getElementsByTagName('head')[0]),
8+
XHR);
9+
browserSingleton.startPoller(50, function(delay, fn){setTimeout(delay,fn);});
1010
browserSingleton.bind();
1111
}
1212
return browserSingleton;

src/Browser.js

+114-147
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,118 @@
11
//////////////////////////////
22
// Browser
33
//////////////////////////////
4+
var XHR = window.XMLHttpRequest || function () {
5+
try { return new ActiveXObject("Msxml2.XMLHTTP.6.0"); } catch (e1) {}
6+
try { return new ActiveXObject("Msxml2.XMLHTTP.3.0"); } catch (e2) {}
7+
try { return new ActiveXObject("Msxml2.XMLHTTP"); } catch (e3) {}
8+
throw new Error("This browser does not support XMLHttpRequest.");
9+
};
410

5-
function Browser(location, document, head) {
6-
this.delay = 50;
7-
this.expectedUrl = location.href;
8-
this.urlListeners = [];
9-
this.hoverListener = noop;
10-
this.isMock = false;
11-
this.outstandingRequests = { count: 0, callbacks:[]};
11+
function Browser(location, document, head, XHR) {
12+
var self = this;
13+
self.isMock = false;
1214

13-
this.XHR = window.XMLHttpRequest || function () {
14-
try { return new ActiveXObject("Msxml2.XMLHTTP.6.0"); } catch (e1) {}
15-
try { return new ActiveXObject("Msxml2.XMLHTTP.3.0"); } catch (e2) {}
16-
try { return new ActiveXObject("Msxml2.XMLHTTP"); } catch (e3) {}
17-
throw new Error("This browser does not support XMLHttpRequest.");
18-
};
19-
this.setTimeout = function(fn, delay) {
20-
window.setTimeout(fn, delay);
15+
//////////////////////////////////////////////////////////////
16+
// XHR API
17+
//////////////////////////////////////////////////////////////
18+
var idCounter = 0;
19+
var outstandingRequestCount = 0;
20+
var outstandingRequestCallbacks = [];
21+
22+
self.xhr = function(method, url, post, callback){
23+
if (isFunction(post)) {
24+
callback = post;
25+
post = _null;
26+
}
27+
if (lowercase(method) == 'json') {
28+
var callbackId = "angular_" + Math.random() + '_' + (idCounter++);
29+
callbackId = callbackId.replace(/\d\./, '');
30+
var script = document[0].createElement('script');
31+
script.type = 'text/javascript';
32+
script.src = url.replace('JSON_CALLBACK', callbackId);
33+
head.append(script);
34+
window[callbackId] = function(data){
35+
window[callbackId] = _undefined;
36+
callback(200, data);
37+
};
38+
} else {
39+
var xhr = new XHR();
40+
xhr.open(method, url, true);
41+
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
42+
xhr.setRequestHeader("Accept", "application/json, text/plain, */*");
43+
xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
44+
outstandingRequestCount ++;
45+
xhr.onreadystatechange = function() {
46+
if (xhr.readyState == 4) {
47+
try {
48+
callback(xhr.status || 200, xhr.responseText);
49+
} finally {
50+
outstandingRequestCount--;
51+
if (outstandingRequestCount === 0) {
52+
while(outstandingRequestCallbacks.length) {
53+
try {
54+
outstandingRequestCallbacks.pop()();
55+
} catch (e) {
56+
}
57+
}
58+
}
59+
}
60+
}
61+
};
62+
xhr.send(post || '');
63+
}
2164
};
2265

23-
this.location = location;
24-
this.document = document;
25-
var rawDocument = document[0];
26-
this.head = head;
27-
this.idCounter = 0;
66+
self.notifyWhenNoOutstandingRequests = function(callback){
67+
if (outstandingRequestCount === 0) {
68+
callback();
69+
} else {
70+
outstandingRequestCallbacks.push(callback);
71+
}
72+
};
2873

29-
this.cookies = cookies;
30-
this.watchCookies = function(fn){ cookieListeners.push(fn); };
74+
//////////////////////////////////////////////////////////////
75+
// Poll Watcher API
76+
//////////////////////////////////////////////////////////////
77+
var pollFns = [];
78+
function poll(){
79+
foreach(pollFns, function(pollFn){ pollFn(); });
80+
}
81+
self.poll = poll;
82+
self.addPollFn = bind(pollFns, push);
83+
self.startPoller = function(interval, setTimeout){
84+
(function check(){
85+
poll();
86+
setTimeout(check, interval);
87+
})();
88+
};
3189

32-
// functions
90+
//////////////////////////////////////////////////////////////
91+
// URL API
92+
//////////////////////////////////////////////////////////////
93+
self.setUrl = function(url) {
94+
var existingURL = location.href;
95+
if (!existingURL.match(/#/)) existingURL += '#';
96+
if (!url.match(/#/)) url += '#';
97+
location.href = url;
98+
};
99+
self.getUrl = function() {
100+
return location.href;
101+
};
102+
103+
//////////////////////////////////////////////////////////////
104+
// Cookies API
105+
//////////////////////////////////////////////////////////////
106+
var rawDocument = document[0];
33107
var lastCookies = {};
34108
var lastCookieString = '';
35-
var cookieListeners = [];
36109
/**
37110
* cookies() -> hash of all cookies
38111
* cookies(name, value) -> set name to value
39112
* if value is undefined delete it
40113
* cookies(name) -> should get value, but deletes (no one calls it right now that way)
41114
*/
42-
function cookies(name, value){
115+
self.cookies = function (name, value){
43116
if (name) {
44117
if (value === _undefined) {
45118
delete lastCookies[name];
@@ -59,139 +132,33 @@ function Browser(location, document, head) {
59132
lastCookies[unescape(keyValue[0])] = unescape(keyValue[1]);
60133
}
61134
}
62-
foreach(cookieListeners, function(fn){
63-
fn(lastCookies);
64-
});
65135
}
66136
return lastCookies;
67137
}
68-
}
69-
}
70-
71-
Browser.prototype = {
138+
};
72139

73-
bind: function() {
74-
var self = this;
75-
self.document.bind("mouseover", function(event){
76-
self.hoverListener(jqLite(msie ? event.srcElement : event.target), true);
140+
//////////////////////////////////////////////////////////////
141+
// Misc API
142+
//////////////////////////////////////////////////////////////
143+
var hoverListener = noop;
144+
self.hover = function(listener) { hoverListener = listener; };
145+
self.bind = function() {
146+
document.bind("mouseover", function(event){
147+
hoverListener(jqLite(msie ? event.srcElement : event.target), true);
77148
return true;
78149
});
79-
self.document.bind("mouseleave mouseout click dblclick keypress keyup", function(event){
80-
self.hoverListener(jqLite(event.target), false);
150+
document.bind("mouseleave mouseout click dblclick keypress keyup", function(event){
151+
hoverListener(jqLite(event.target), false);
81152
return true;
82153
});
83-
},
154+
};
84155

85-
hover: function(hoverListener) {
86-
this.hoverListener = hoverListener;
87-
},
88156

89-
addCss: function(url) {
90-
var doc = this.document[0],
91-
head = jqLite(doc.getElementsByTagName('head')[0]),
92-
link = jqLite(doc.createElement('link'));
157+
self.addCss = function(url) {
158+
var link = jqLite(rawDocument.createElement('link'));
93159
link.attr('rel', 'stylesheet');
94160
link.attr('type', 'text/css');
95161
link.attr('href', url);
96162
head.append(link);
97-
},
98-
99-
xhr: function(method, url, post, callback){
100-
if (isFunction(post)) {
101-
callback = post;
102-
post = _null;
103-
}
104-
if (lowercase(method) == 'json') {
105-
var callbackId = "angular_" + Math.random() + '_' + (this.idCounter++);
106-
callbackId = callbackId.replace(/\d\./, '');
107-
var script = this.document[0].createElement('script');
108-
script.type = 'text/javascript';
109-
script.src = url.replace('JSON_CALLBACK', callbackId);
110-
this.head.append(script);
111-
window[callbackId] = function(data){
112-
window[callbackId] = _undefined;
113-
callback(200, data);
114-
};
115-
} else {
116-
var xhr = new this.XHR(),
117-
self = this;
118-
xhr.open(method, url, true);
119-
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
120-
xhr.setRequestHeader("Accept", "application/json, text/plain, */*");
121-
xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
122-
this.outstandingRequests.count ++;
123-
xhr.onreadystatechange = function() {
124-
if (xhr.readyState == 4) {
125-
try {
126-
callback(xhr.status || 200, xhr.responseText);
127-
} finally {
128-
self.outstandingRequests.count--;
129-
self.processRequestCallbacks();
130-
}
131-
}
132-
};
133-
xhr.send(post || '');
134-
}
135-
},
136-
137-
processRequestCallbacks: function(){
138-
if (this.outstandingRequests.count === 0) {
139-
while(this.outstandingRequests.callbacks.length) {
140-
try {
141-
this.outstandingRequests.callbacks.pop()();
142-
} catch (e) {
143-
}
144-
}
145-
}
146-
},
147-
148-
notifyWhenNoOutstandingRequests: function(callback){
149-
if (this.outstandingRequests.count === 0) {
150-
callback();
151-
} else {
152-
this.outstandingRequests.callbacks.push(callback);
153-
}
154-
},
155-
156-
watchUrl: function(fn){
157-
this.urlListeners.push(fn);
158-
},
159-
160-
startUrlWatcher: function() {
161-
var self = this;
162-
(function pull () {
163-
if (self.expectedUrl !== self.location.href) {
164-
foreach(self.urlListeners, function(listener){
165-
try {
166-
listener(self.location.href);
167-
} catch (e) {
168-
error(e);
169-
}
170-
});
171-
self.expectedUrl = self.location.href;
172-
}
173-
self.setTimeout(pull, self.delay);
174-
})();
175-
},
176-
177-
startCookieWatcher: function() {
178-
var self = this;
179-
(function poll() {
180-
self.cookies();
181-
self.setTimeout(poll, self.delay);
182-
})();
183-
},
184-
185-
setUrl: function(url) {
186-
var existingURL = this.location.href;
187-
if (!existingURL.match(/#/)) existingURL += '#';
188-
if (!url.match(/#/)) url += '#';
189-
if (existingURL != url) {
190-
this.location.href = this.expectedUrl = url;
191-
}
192-
},
193-
194-
getUrl: function() {
195-
return this.location.href;
196-
}
197-
};
163+
};
164+
}

0 commit comments

Comments
 (0)