refactored $location service so that it correctly updates under all conditions

This commit is contained in:
Misko Hevery 2010-07-29 12:50:14 -07:00
parent 6bd8006edc
commit 1b768b8443
8 changed files with 165 additions and 65 deletions

View file

@ -1,15 +1,85 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<html xmlns:ng="http://angularjs.org">
<head>
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.js"></script>
<script type="text/javascript"
src="../src/angular-bootstrap.js#autobind"></script>
</head>
<body ng:init="$window.$root = this">
<div ng:click="$window.alert('outter')">
outter
<div ng:click="$window.alert('inner')">inner</div>
<a href="#ERROR" ng:click="$window.alert('link')">link</a>
</div>
<script>
function TicTacToeCntl(){
this.cellStyle= {
'height': '20px',
'width': '20px',
'border': '1px solid black',
'text-align': 'center',
'vertical-align': 'middle',
'cursor': 'pointer'
};
this.reset();
this.$watch('$location.hashPath', this.setMemento);
this.$onEval(function(){
this.$location.hashPath = this.getMemento();
});
}
TicTacToeCntl.prototype = {
dropPiece: function(row, col) {
if (!this.winner && !this.board[row][col]) {
this.board[row][col] = this.nextMove;
this.nextMove = this.nextMove == 'X' ? 'O' : 'X';
this.grade();
}
},
reset: function(){
this.board = [
['', '', ''],
['', '', ''],
['', '', '']
];
this.nextMove = 'X';
this.winner = '';
},
grade: function(){
var b = this.board;
this.winner =
row(0) || row(1) || row(2) ||
col(0) || col(1) || col(2) ||
diagonal(-1) || diagonal(1);
function row(r) { return same(b[r][0], b[r][1], b[r][2]);}
function col(c) { return same(b[0][c], b[1][c], b[2][c]);}
function diagonal(i) { return same(b[0][1-i], b[1][1], b[2][1+i]);}
function same(a, b, c) { return (a==b && b==c) ? a : '';};
},
getMemento: function(){
var rows = [];
angular.foreach(this.board, function(row){
rows.push(row.join(','));
});
return rows.join(';') + '/' + this.nextMove;
},
setMemento: function(value) {
if (value) {
value = value.split('/');
this.nextMove = value[1];
angular.foreach(value[0].split(';'), function(row, i){
this.board[i] = row.split(',');
}, this);
} else {
this.reset();
}
}
};
</script>
<h3>Tic-Tac-Toe</h3>
Next Player: {{nextMove}}
<div ng:show="winner">Player {{winner}} has won!</div>
<table ng:controller="TicTacToeCntl">
<tr ng:repeat="row in board" style="height:15px;">
<td ng:repeat="cell in row" ng:style="cellStyle"
ng:click="dropPiece($parent.$index, $index)">{{cell}}</td>
</tr>
</table>
<button ng:click="reset()">reset board</button>
</body>
</html>
</html>

15
scenario/location.html Normal file
View file

@ -0,0 +1,15 @@
<!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">
<head>
<link rel="stylesheet" type="text/css" href="style.css"/>
<script type="text/javascript" src="../src/angular-bootstrap.js#autobind"></script>
</head>
<body ng:init="$window.$scope = this">
<pre>$location={{$location}}</pre>
<hr/>
href: <input type="text" name="$location.href" size="120"/> <br/>
hash: <input type="text" name="$location.hash" size="120"/> <br/>
hashPath: <input type="text" name="$location.hashPath" size="120"/> <br/>
hashSearch: <input type="text" name="$location.hashSearch" size="120" ng:format="json"/> <br/>
</body>
</html>

View file

@ -300,10 +300,10 @@ function bind(_this, _function) {
} :
function() {
return _function.apply(_this, curryArgs.concat(slice.call(arguments, 0, arguments.length)));
}
};
} else {
// in IE, native methonds ore not functions and so they can not be bound (but they don't need to be)
return function(a, b, c, d, e){ return _function(a, b, c, d, e); };
// in IE, native methods ore not functions and so they can not be bound (but they don't need to be)
return _function;
}
}

View file

@ -22,16 +22,14 @@
* THE SOFTWARE.
*/
(function(previousOnLoad){
var filename = /(.*)\/angular-(.*).js(#(.*))?/,
var filename = /(.*)\/angular-(.*).js(#.*)?/,
scripts = document.getElementsByTagName("SCRIPT"),
serverPath,
config,
match;
for(var j = 0; j < scripts.length; j++) {
match = (scripts[j].src || "").match(filename);
if (match) {
serverPath = match[1];
config = match[4];
}
}
@ -63,7 +61,7 @@
try {
if (previousOnLoad) previousOnLoad();
} catch(e) {}
angularInit(parseKeyValue(config));
angularInit(parseKeyValue(angularJsConfig(document)));
};
})(window.onload);

View file

@ -5,6 +5,7 @@ var NUMBER = /^\s*[-+]?\d*(\.\d*)?\s*$/;
extend(angularFormatter, {
'noop':formatter(identity, identity),
'json':formatter(toJson, fromJson),
'boolean':formatter(toString, toBoolean),
'number':formatter(toString,
function(obj){

View file

@ -107,7 +107,7 @@ JQLite.prototype = {
if (!event.preventDefault) {
event.preventDefault = function(){
event.returnValue = false;
}
};
}
foreach(eventHandler.fns, function(fn){
fn.call(self, event);

View file

@ -7,61 +7,78 @@ var URL_MATCH = /^(file|ftp|http|https):\/\/(\w+:{0,1}\w*@)?([\w\.-]*)(:([0-9]+)
var HASH_MATCH = /^([^\?]*)?(\?([^\?]*))?$/;
var DEFAULT_PORTS = {'http': 80, 'https': 443, 'ftp':21};
angularService("$location", function(browser){
var scope = this, location = {parse:parseUrl, toString:toString};
var lastHash, lastUrl;
var scope = this,
location = {parse:parseUrl, toString:toString, update:update},
lastLocation = {};
browser.watchUrl(function(url){
update(url);
scope.$root.$eval();
});
this.$onEval(PRIORITY_FIRST, update);
this.$onEval(PRIORITY_LAST, update);
update(browser.getUrl());
return location;
function update(href){
if (href) {
parseUrl(href);
} else {
href = check('href') || check('protocol', '://', 'host', ':', 'port', '', 'path', '?', 'search');
var hash = check('hash');
if (isUndefined(hash)) hash = check('hashPath', '?', 'hashSearch');
if (isDefined(hash)) {
href = (href || location.href).split('#')[0];
href+= '#' + hash;
}
if (isDefined(href)) {
parseUrl(href);
browser.setUrl(href);
}
}
}
function check() {
var i = -1,
length=arguments.length,
name, seperator, parts = [],
value, same = true;
for(; i<length; i = i+2) {
parts.push(seperator = (arguments[i] || ''));
name = arguments[i + 1];
value=location[name];
parts.push(typeof value == 'object' ? toKeyValue(value) : value);
same = same && equals(lastLocation[name], value);
}
return same ? undefined : parts.join('');
}
function parseUrl(url){
if (isDefined(url)) {
var match = URL_MATCH.exec(url);
if (match) {
location.href = url;
location.href = url.replace('#$', '');
location.protocol = match[1];
location.host = match[3] || '';
location.port = match[5] || DEFAULT_PORTS[location.href] || null;
location.port = match[5] || DEFAULT_PORTS[location.protocol] || null;
location.path = match[6];
location.search = parseKeyValue(match[8]);
location.hash = match[9] || '';
if (location.hash)
location.hash = location.hash.substr(1);
parseHash(location.hash);
match = HASH_MATCH.exec(location.hash);
location.hashPath = match[1] || '';
location.hashSearch = parseKeyValue(match[3]);
copy(location, lastLocation);
}
}
}
function parseHash(hash) {
var match = HASH_MATCH.exec(hash);
location.hashPath = match[1] || '';
location.hashSearch = parseKeyValue(match[3]);
lastHash = hash;
}
function toString() {
if (lastHash === location.hash) {
var hashKeyValue = toKeyValue(location.hashSearch),
hash = (location.hashPath ? location.hashPath : '') + (hashKeyValue ? '?' + hashKeyValue : ''),
url = location.href.split('#')[0] + '#' + (hash ? hash : '');
if (url !== location.href) parseUrl(url);
return url;
} else {
parseUrl(location.href.split('#')[0] + '#' + location.hash);
return toString();
}
update();
return location.href;
}
browser.watchUrl(function(url){
parseUrl(url);
scope.$root.$eval();
});
parseUrl(browser.getUrl());
this.$onEval(PRIORITY_FIRST, function(){
if (location.hash != lastHash) {
parseHash(location.hash);
}
});
this.$onEval(PRIORITY_LAST, function(){
var url = toString();
if (lastUrl != url) {
browser.setUrl(url);
lastUrl = url;
}
});
return location;
}, {inject: ['$browser']});
angularService("$log", function($window){

View file

@ -106,7 +106,7 @@ describe("service", function(){
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#');
expect(scope.$location.toString()).toEqual('file:///Users/Shared/misko/work/angular.js/scenario/widgets.html');
});
it('should update url on hash change', function(){
@ -123,6 +123,14 @@ describe("service", function(){
expect(scope.$location.hash).toEqual('?a=b');
});
it("should parse url which contains - in host", function(){
scope.$location.parse('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');
expect(scope.$location.path).toEqual('/path');
});
it('should update hash before any processing', function(){
var scope = compile('<div>');
var log = '';
@ -136,15 +144,6 @@ describe("service", function(){
scope.$eval();
expect(log).toEqual('/abc;');
});
it("should parse url which contains - in host", function(){
scope.$location.parse('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');
expect(scope.$location.path).toEqual('/path');
});
});
describe("$invalidWidgets", function(){