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 @@ - + + src="../angular-debug.js" ng:autobind ng:css="css/angular.css"> - Hello {{'World'}}! + + +
+ +
+ - + \ 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 @@ + + + + + + + +
$location={{$location}}
+ Hash Search: + +
+ href:
+ hash:
+ hashPath:
+ hashSearch:
+ + 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 @@ Buttons - ng-change
ng:click + ng:change
ng:click
-
-
-
+
+
+
action
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("
" + html + "
"); 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('
'); - scope.$eval(); - expect(scope.$get('clicked')).toBeFalsy(); + describe('ng:click', function(){ + it('should fire event', function(){ + var scope = compile('
'); + 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('
'); 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(){