diff options
| -rw-r--r-- | example/temp.html | 34 | ||||
| -rw-r--r-- | jsTestDriver-jquery.conf | 1 | ||||
| -rw-r--r-- | jsTestDriver.conf | 1 | ||||
| -rw-r--r-- | scenario/location.html | 19 | ||||
| -rw-r--r-- | scenario/widgets.html | 8 | ||||
| -rw-r--r-- | src/Angular.js | 52 | ||||
| -rw-r--r-- | src/Browser.js | 3 | ||||
| -rw-r--r-- | src/Compiler.js | 6 | ||||
| -rw-r--r-- | src/Parser.js | 13 | ||||
| -rw-r--r-- | src/angular-bootstrap.js | 6 | ||||
| -rw-r--r-- | src/angular.suffix | 2 | ||||
| -rw-r--r-- | src/directives.js | 4 | ||||
| -rw-r--r-- | src/formatters.js | 1 | ||||
| -rw-r--r-- | src/jqLite.js | 17 | ||||
| -rw-r--r-- | src/services.js | 110 | ||||
| -rw-r--r-- | src/widgets.js | 5 | ||||
| -rw-r--r-- | test/CompilerSpec.js | 10 | ||||
| -rw-r--r-- | test/directivesSpec.js | 14 | ||||
| -rw-r--r-- | test/servicesSpec.js | 21 |
19 files changed, 207 insertions, 120 deletions
diff --git a/example/temp.html b/example/temp.html index 337f7fba..8a1246ed 100644 --- a/example/temp.html +++ b/example/temp.html @@ -1,10 +1,36 @@ <!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="../src/angular-bootstrap.js#autobind"></script> + src="../angular-debug.js" ng:autobind ng:css="css/angular.css"></script> </head> <body ng:init="$window.$root = this"> - Hello {{'World'}}! + +<script> +angular.widget('my:greeter', function(compileElement){ + var compiler = this; + compileElement.css('style', 'block'); + var salutaitonExp = compileElement.attr('salutation'); + var nameExp = compileElement.attr('name'); + return function(linkElement){ + var salutaitonSpan = angular.element('<span class="salutation"></span'); + var nameSpan = angular.element('<span class="name"></span>'); + linkElement.append(salutaitonSpan); + linkElement.append(compiler.text(' ')); + linkElement.append(nameSpan); + linkElement.append(compiler.text('!')); + this.$watch(salutaitonExp, function(value){ + salutaitonSpan.text(value); + }); + this.$watch(nameExp, function(value){ + nameSpan.text(value); + }); + }; +}); +</script> +<div ng:init="salutation='Hello'; name='World'"> + <my:greeter salutation="salutation" name="name"/> +</div> + </body> -</html> +</html>
\ No newline at end of file diff --git a/jsTestDriver-jquery.conf b/jsTestDriver-jquery.conf index 34538bce..e5dac727 100644 --- a/jsTestDriver-jquery.conf +++ b/jsTestDriver-jquery.conf @@ -6,6 +6,7 @@ load: - lib/jquery/jquery-1.4.2.js - test/jquery_alias.js - src/Angular.js + - src/JSON.js - src/*.js - src/scenario/Runner.js - src/scenario/*.js diff --git a/jsTestDriver.conf b/jsTestDriver.conf index 16bcf1db..bcd01694 100644 --- a/jsTestDriver.conf +++ b/jsTestDriver.conf @@ -6,6 +6,7 @@ load: - lib/jquery/jquery-1.4.2.js - test/jquery_remove.js - src/Angular.js + - src/JSON.js - src/*.js - src/scenario/Runner.js - src/scenario/*.js diff --git a/scenario/location.html b/scenario/location.html new file mode 100644 index 00000000..75041615 --- /dev/null +++ b/scenario/location.html @@ -0,0 +1,19 @@ + <!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> + Hash Search: + <ul> + <li ng:repeat="(key, value) in $location.hashSearch"><tt>{{key}}={{value}}</tt></li> + </ul> + <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> diff --git a/scenario/widgets.html b/scenario/widgets.html index 2626843d..d5285ea6 100644 --- a/scenario/widgets.html +++ b/scenario/widgets.html @@ -72,12 +72,12 @@ </tr> <tr><th colspan="3">Buttons</th></tr> <tr> - <td>ng-change<br/>ng:click</td> + <td>ng:change<br/>ng:click</td> <td ng:init="button.count = 0"> <form> - <input type="button" value="button" ng-change="button.count = button.count + 1"/> <br/> - <input type="submit" value="submit" ng-change="button.count = button.count + 1"/><br/> - <input type="image" src="" ng-change="button.count = button.count + 1"/><br/> + <input type="button" value="button" ng:change="button.count = button.count + 1"/> <br/> + <input type="submit" value="submit" ng:change="button.count = button.count + 1"/><br/> + <input type="image" src="" ng:change="button.count = button.count + 1"/><br/> <a href="" ng:click="button.count = button.count + 1">action</a> </form> </td> diff --git a/src/Angular.js b/src/Angular.js index 850fe34c..42e2ce89 100644 --- a/src/Angular.js +++ b/src/Angular.js @@ -18,7 +18,7 @@ var consoleNode, slice = Array.prototype.slice, error = window['console'] ? bind(window['console'], window['console']['error'] || noop) : noop, angular = window['angular'] || (window['angular'] = {}), - angularTextMarkup = extensionMap(angular, 'textMarkup'), + angularTextMarkup = extensionMap(angular, 'markup'), angularAttrMarkup = extensionMap(angular, 'attrMarkup'), angularDirective = extensionMap(angular, 'directive'), angularWidget = extensionMap(angular, 'widget', lowercase), @@ -293,13 +293,18 @@ function escapeAttr(html) { function bind(_this, _function) { var curryArgs = slice.call(arguments, 2, arguments.length); - return curryArgs.length == 0 ? - function() { - return _function.apply(_this, arguments); - } : - function() { - return _function.apply(_this, curryArgs.concat(slice.call(arguments, 0, arguments.length))); - }; + if (typeof _function == 'function') { + return curryArgs.length == 0 ? + function() { + return _function.apply(_this, arguments); + } : + function() { + return _function.apply(_this, curryArgs.concat(slice.call(arguments, 0, arguments.length))); + }; + } else { + // in IE, native methods ore not functions and so they can not be bound (but they don't need to be) + return _function; + } } function outerHTML(node) { @@ -347,8 +352,8 @@ function parseKeyValue(keyValue) { foreach((keyValue || "").split('&'), function(keyValue){ if (keyValue) { key_value = keyValue.split('='); - key = decodeURIComponent(key_value[0]); - obj[key] = key_value[1] ? decodeURIComponent(key_value[1]) : true; + key = unescape(key_value[0]); + obj[key] = key_value[1] ? unescape(key_value[1]) : true; } }); return obj; @@ -357,29 +362,42 @@ function parseKeyValue(keyValue) { function toKeyValue(obj) { var parts = []; foreach(obj, function(value, key){ - parts.push(encodeURIComponent(key) + '=' + encodeURIComponent(value)); + parts.push(escape(key) + '=' + escape(value)); }); return parts.length ? parts.join('&') : ''; } function angularInit(config){ if (config.autobind) { - var scope = compile(window.document, null, {'$config':config}); // TODO default to the source of angular.js - scope.$browser.addCss('css/angular.css'); + var scope = compile(window.document, null, {'$config':config}); + if (config.css) + scope.$browser.addCss(config.base_url + config.css); scope.$init(); } } -function angularJsConfig(document) { - var filename = /(.*)\/angular(-(.*))?.js(#(.*))?/, +function angularJsConfig(document, config) { + var filename = /^(.*)\/angular(-([^\/]*))?.js(#(.*))?$/, scripts = document.getElementsByTagName("script"), match; + config = extend({ + base_url: '', + css: '../css/angular.css' + }, config); for(var j = 0; j < scripts.length; j++) { match = (scripts[j].src || "").match(filename); if (match) { - return match[5]; + config.base_url = match[1] + '/'; + extend(config, parseKeyValue(match[5])); + eachAttribute(jqLite(scripts[j]), function(value, name){ + if (/^ng:/.exec(name)) { + name = name.substring(3).replace(/-/g, '_'); + if (name == 'autobind') value = true; + config[name] = value; + } + }); } } - return ""; + return config; } diff --git a/src/Browser.js b/src/Browser.js index 3287cf0e..b4314e2c 100644 --- a/src/Browser.js +++ b/src/Browser.js @@ -74,6 +74,9 @@ Browser.prototype = { 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) { diff --git a/src/Compiler.js b/src/Compiler.js index bcf1f61a..e09f1876 100644 --- a/src/Compiler.js +++ b/src/Compiler.js @@ -69,8 +69,8 @@ Template.prototype = { /////////////////////////////////// //Compiler ////////////////////////////////// -function Compiler(textMarkup, attrMarkup, directives, widgets){ - this.textMarkup = textMarkup; +function Compiler(markup, attrMarkup, directives, widgets){ + this.markup = markup; this.attrMarkup = attrMarkup; this.directives = directives; this.widgets = widgets; @@ -158,7 +158,7 @@ Compiler.prototype = { // process markup for text nodes only eachTextNode(element, function(textNode){ var text = textNode.text(); - foreach(self.textMarkup, function(markup){ + foreach(self.markup, function(markup){ markup.call(selfApi, text, textNode, element); }); }); diff --git a/src/Parser.js b/src/Parser.js index 5c2307e4..5eb75713 100644 --- a/src/Parser.js +++ b/src/Parser.js @@ -599,14 +599,11 @@ Parser.prototype = { for ( var i = 0; i < argsFn.length; i++) { args.push(argsFn[i](self)); } - var fnPtr = fn(self); - if (typeof fnPtr === 'function') { - return fnPtr.apply(self, args); - } else if (fnPtr === undefined) { - return fnPtr; - } else { - throw "Expression '" + fn.isAssignable + "' is not a function."; - } + var fnPtr = fn(self) || noop; + // IE stupidity! + return fnPtr.apply ? + fnPtr.apply(self, args) : + fnPtr(args[0], args[1], args[2], args[3], args[4]); }; }, diff --git a/src/angular-bootstrap.js b/src/angular-bootstrap.js index 90e1104e..1f03b8a3 100644 --- a/src/angular-bootstrap.js +++ b/src/angular-bootstrap.js @@ -22,16 +22,14 @@ * THE SOFTWARE. */ (function(previousOnLoad){ - var filename = /(.*)\/angular-(.*).js(#(.*))?/, + var filename = /^(.*)\/angular-bootstrap.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(angularJsConfig(document)); }; })(window.onload); diff --git a/src/angular.suffix b/src/angular.suffix index 36d73df2..7e86c5d5 100644 --- a/src/angular.suffix +++ b/src/angular.suffix @@ -3,7 +3,7 @@ try { if (previousOnLoad) previousOnLoad(); } catch(e) {} - angularInit(parseKeyValue(angularJsConfig(document))); + angularInit(angularJsConfig(document)); }; })(window, document, window.onload); diff --git a/src/directives.js b/src/directives.js index ffe37890..9aadbd11 100644 --- a/src/directives.js +++ b/src/directives.js @@ -199,10 +199,10 @@ angularWidget("@ng:repeat", function(expression, element){ angularDirective("ng:click", function(expression, element){ return function(element){ var self = this; - element.bind('click', function(){ + element.bind('click', function(event){ self.$tryEval(expression, element); self.$root.$eval(); - return false; + event.preventDefault(); }); }; }); diff --git a/src/formatters.js b/src/formatters.js index 40462cf3..ca1ce83e 100644 --- a/src/formatters.js +++ b/src/formatters.js @@ -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){ diff --git a/src/jqLite.js b/src/jqLite.js index cff9ae00..22b3c070 100644 --- a/src/jqLite.js +++ b/src/jqLite.js @@ -104,19 +104,14 @@ JQLite.prototype = { eventHandler = bind[type]; if (!eventHandler) { bind[type] = eventHandler = function(event) { - var bubbleEvent = false; - foreach(eventHandler.fns, function(fn){ - bubbleEvent = bubbleEvent || fn.call(self, event); - }); - if (!bubbleEvent) { - if (msie) { + if (!event.preventDefault) { + event.preventDefault = function(){ event.returnValue = false; - event.cancelBubble = true; - } else { - event.preventDefault(); - event.stopPropagation(); - } + }; } + foreach(eventHandler.fns, function(fn){ + fn.call(self, event); + }); }; eventHandler.fns = []; addEventListener(element, type, eventHandler); diff --git a/src/services.js b/src/services.js index 106f8954..8df23564 100644 --- a/src/services.js +++ b/src/services.js @@ -7,61 +7,87 @@ 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') || checkProtocol(); + var hash = check('hash'); + if (isUndefined(hash)) hash = checkHashPathSearch(); + if (isDefined(hash)) { + href = (href || location.href).split('#')[0]; + href+= '#' + hash; + } + if (isDefined(href)) { + parseUrl(href); + browser.setUrl(href); + } + } + } + + function check(param) { + return lastLocation[param] == location[param] ? undefined : location[param]; + } + + 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 : ''); + } + + 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 : ''); + } + 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); + location.hash = match[10] || ''; + match = HASH_MATCH.exec(location.hash); + location.hashPath = unescape(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){ diff --git a/src/widgets.js b/src/widgets.js index 5f0fcf7c..87a302fa 100644 --- a/src/widgets.js +++ b/src/widgets.js @@ -198,14 +198,15 @@ function inputWidget(events, modelAccessor, viewAccessor, initFn) { this.$eval(element.attr('ng:init')||''); // Don't register a handler if we are a button (noopAccessor) and there is no action if (action || modelAccessor !== noopAccessor) { - element.bind(events, function(){ + element.bind(events, function(event){ model.set(view.get()); lastValue = model.get(); scope.$tryEval(action, element); scope.$root.$eval(); // if we have noop initFn than we are just a button, // therefore we want to prevent default action - return initFn != noop; + if(initFn == noop) + event.preventDefault(); }); } view.set(lastValue = model.get()); diff --git a/test/CompilerSpec.js b/test/CompilerSpec.js index 3736ff4e..1091337b 100644 --- a/test/CompilerSpec.js +++ b/test/CompilerSpec.js @@ -1,5 +1,5 @@ describe('compiler', function(){ - var compiler, textMarkup, directives, widgets, compile, log; + var compiler, markup, directives, widgets, compile, log; beforeEach(function(){ log = ""; @@ -20,10 +20,10 @@ describe('compiler', function(){ } }; - textMarkup = []; + markup = []; attrMarkup = []; widgets = extensionMap({}, 'widget'); - compiler = new Compiler(textMarkup, attrMarkup, directives, widgets); + compiler = new Compiler(markup, attrMarkup, directives, widgets); compile = function(html){ var e = jqLite("<div>" + html + "</div>"); var scope = compiler.compile(e)(e); @@ -94,7 +94,7 @@ describe('compiler', function(){ }); it('should process markup before directives', function(){ - textMarkup.push(function(text, textNode, parentNode) { + markup.push(function(text, textNode, parentNode) { if (text == 'middle') { expect(textNode.text()).toEqual(text); parentNode.attr('hello', text); @@ -126,7 +126,7 @@ describe('compiler', function(){ this.directives(true); return noop; }; - textMarkup.push(function(text, textNode, parent){ + markup.push(function(text, textNode, parent){ if (text == '{{1+2}}') parent.text('3'); }); diff --git a/test/directivesSpec.js b/test/directivesSpec.js index 8a7da41d..278f9c4c 100644 --- a/test/directivesSpec.js +++ b/test/directivesSpec.js @@ -160,13 +160,15 @@ describe("directives", function(){ expect(scope.$get('count')).toEqual(1); }); - it('should ng:click', function(){ - var scope = compile('<div ng:click="clicked = true"></div>'); - scope.$eval(); - expect(scope.$get('clicked')).toBeFalsy(); + describe('ng:click', function(){ + it('should fire event', function(){ + var scope = compile('<div ng:click="clicked = true"></div>'); + scope.$eval(); + expect(scope.$get('clicked')).toBeFalsy(); - element.trigger('click'); - expect(scope.$get('clicked')).toEqual(true); + element.trigger('click'); + expect(scope.$get('clicked')).toEqual(true); + }); }); it('should ng:class', function(){ diff --git a/test/servicesSpec.js b/test/servicesSpec.js index 91538703..ffd01267 100644 --- a/test/servicesSpec.js +++ b/test/servicesSpec.js @@ -91,7 +91,7 @@ describe("service", function(){ 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=http://path?k=a%3Db'); + expect(scope.$location.toString()).toEqual('http://host:123/p/a/t/h.html?query=value#page%3Dhttp%3A//path?k=a%3Db'); }); it('should parse file://', function(){ @@ -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(){ |
