diff options
| author | Misko Hevery | 2010-03-23 14:57:11 -0700 |
|---|---|---|
| committer | Misko Hevery | 2010-03-23 14:57:11 -0700 |
| commit | bb98ae14f2aef74efbd8345e93f62ac67f460f7f (patch) | |
| tree | b50ac4417c7b1bda996dd569069d7eb834eef2ff | |
| parent | 6ff550cfa9524bbb124d10caf1fc13c911ab3b4b (diff) | |
| download | angular.js-bb98ae14f2aef74efbd8345e93f62ac67f460f7f.tar.bz2 | |
markup now wroks, some refactorings
| -rw-r--r-- | src/Angular.js | 77 | ||||
| -rw-r--r-- | src/Compiler.js | 259 | ||||
| -rw-r--r-- | src/Filters.js | 52 | ||||
| -rw-r--r-- | src/Scope.js | 2 | ||||
| -rw-r--r-- | src/directives.js | 65 | ||||
| -rw-r--r-- | src/jqLite.js | 185 | ||||
| -rw-r--r-- | src/markup.js | 64 | ||||
| -rw-r--r-- | src/widgets2.js | 18 | ||||
| -rw-r--r-- | test/CompilerSpec.js | 9 | ||||
| -rw-r--r-- | test/directivesSpec.js | 12 | ||||
| -rw-r--r-- | test/markupSpec.js | 49 |
11 files changed, 503 insertions, 289 deletions
diff --git a/src/Angular.js b/src/Angular.js index ce1038cc..95f7325a 100644 --- a/src/Angular.js +++ b/src/Angular.js @@ -1,6 +1,7 @@ if (typeof document.getAttribute == 'undefined') document.getAttribute = function() {}; if (typeof Node == 'undefined') { + //TODO: can we get rid of this? Node = { ELEMENT_NODE : 1, ATTRIBUTE_NODE : 2, @@ -21,7 +22,7 @@ function noop() {} function identity($) {return $;} if (!window['console']) window['console']={'log':noop, 'error':noop}; -function extension(angular, name) { +function extensionMap(angular, name) { var extPoint; return angular[name] || (extPoint = angular[name] = function (name, fn, prop){ if (isDefined(fn)) { @@ -31,20 +32,30 @@ function extension(angular, name) { }); } +function extensionList(angular, name) { + var extPoint, length = 0; + return angular[name] || (extPoint = angular[name] = function (fn, prop){ + if (isDefined(fn)) { + extPoint[length] = extend(fn, prop || {}); + length++; + } + return extPoint; + }); +} + var consoleNode, msie, - jQuery = window['jQuery'] || window['$'], // weirdness to make IE happy - foreach = _.each, - extend = _.extend, - slice = Array.prototype.slice, - angular = window['angular'] || (window['angular'] = {}), - angularDirective = extension(angular, 'directive'), - angularMarkup = extension(angular, 'markup'), - angularWidget = extension(angular, 'widget'), - angularValidator = extension(angular, 'validator'), - angularFilter = extension(angular, 'filter'), - angularFormatter = extension(angular, 'formatter'), - angularCallbacks = extension(angular, 'callbacks'), - angularAlert = angular['alert'] || (angular['alert'] = function(){ + jQuery = window['jQuery'] || window['$'], // weirdness to make IE happy + slice = Array.prototype.slice, + angular = window['angular'] || (window['angular'] = {}), + angularTextMarkup = extensionList(angular, 'textMarkup'), + angularAttrMarkup = extensionList(angular, 'attrMarkup'), + angularDirective = extensionMap(angular, 'directive'), + angularWidget = extensionMap(angular, 'widget'), + angularValidator = extensionMap(angular, 'validator'), + angularFilter = extensionMap(angular, 'filter'), + angularFormatter = extensionMap(angular, 'formatter'), + angularCallbacks = extensionMap(angular, 'callbacks'), + angularAlert = angular['alert'] || (angular['alert'] = function(){ log(arguments); window.alert.apply(window, arguments); }); angular['copy'] = copy; @@ -53,6 +64,29 @@ var isVisible = isVisible || function (element) { return jQuery(element).is(":visible"); }; +function foreach(obj, iterator, context) { + var key; + if (obj) { + if (obj.forEach) { + obj.forEach(iterator, context); + } else if (obj instanceof Array) { + for (key = 0; key < obj.length; key++) + iterator.call(context, obj[key], key); + } else { + for (key in obj) + iterator.call(context, obj[key], key); + } + } + return obj; +} + +function extend(dst, obj) { + foreach(obj, function(value, key){ + dst[key] = value; + }); + return dst; +} + function isDefined(value){ return typeof value != 'undefined'; } function isObject(value){ return typeof value == 'object';} function isString(value){ return typeof value == 'string';} @@ -112,14 +146,15 @@ function isNode(inp) { } function isLeafNode (node) { - switch (node.nodeName) { - case "OPTION": - case "PRE": - case "TITLE": - return true; - default: - return false; + if (node) { + switch (node.nodeName) { + case "OPTION": + case "PRE": + case "TITLE": + return true; + } } + return false; } function copy(source, destination){ diff --git a/src/Compiler.js b/src/Compiler.js index ca94c893..ba598a43 100644 --- a/src/Compiler.js +++ b/src/Compiler.js @@ -48,224 +48,41 @@ Template.prototype = { }; /////////////////////////////////// -//JQLite +//Compiler ////////////////////////////////// - -var jqCache = {}; -var jqName = 'ng-' + new Date().getTime(); -var jqId = 1; -function jqNextId() { return jqId++; } - -var addEventListener = window.document.attachEvent ? - function(element, type, fn) { - element.attachEvent('on' + type, fn); - } : function(element, type, fn) { - element.addEventListener(type, fn, false); - }; - -var removeEventListener = window.document.detachEvent ? - function(element, type, fn) { - element.detachEvent('on' + type, fn); - } : function(element, type, fn) { - element.removeEventListener(type, fn, false); - }; - -function jqClearData(element) { - var cacheId = element[jqName], - cache = jqCache[cacheId]; - if (cache) { - foreach(cache.bind || {}, function(fn, type){ - removeEventListener(element, type, fn); - }); - delete jqCache[cacheId]; - delete element[jqName]; - } -}; - -function JQLite(element) { - this[0] = element; +function isTextNode(node) { + return node.nodeType == Node.TEXT_NODE; } -function jqLite(element) { - if (typeof element == 'string') { - var div = document.createElement('div'); - div.innerHTML = element; - element = div.childNodes[0]; +function eachTextNode(element, fn){ + var i, chldNodes = element[0].childNodes || [], size = chldNodes.length, chld; + for (i = 0; i < size; i++) { + if(isTextNode(chld = chldNodes[i])) { + fn(jqLite(chld), i); + } } - return element instanceof JQLite ? element : new JQLite(element); } -JQLite.prototype = { - data: function(key, value) { - var element = this[0], - cacheId = element[jqName], - cache = jqCache[cacheId || -1]; - if (isDefined(value)) { - if (!cache) { - element[jqName] = cacheId = jqNextId(); - cache = jqCache[cacheId] = {}; - } - cache[key] = value; - } else { - return cache ? cache[key] : null; +function eachNode(element, fn){ + var i, chldNodes = element[0].childNodes || [], size = chldNodes.length, chld; + for (i = 0; i < size; i++) { + if(!isTextNode(chld = chldNodes[i])) { + fn(jqLite(chld), i); } - }, - - removeData: function(){ - jqClearData(this[0]); - }, - - dealoc: function(){ - (function dealoc(element){ - jqClearData(element); - for ( var i = 0, children = element.childNodes; i < children.length; i++) { - dealoc(children[0]); - } - })(this[0]); - }, - - bind: function(type, fn){ - var element = this[0], - bind = this.data('bind'), - eventHandler; - if (!bind) this.data('bind', bind = {}); - eventHandler = bind[type]; - if (!eventHandler) { - bind[type] = eventHandler = function() { - var self = this; - foreach(eventHandler.fns, function(fn){ - fn.apply(self, arguments); - }); - }; - eventHandler.fns = []; - addEventListener(element, type, eventHandler); - } - eventHandler.fns.push(fn); - }, - - trigger: function(type) { - var cache = this.data('bind'); - if (cache) { - (cache[type] || noop)(); - } - }, - - click: function(fn) { - if (fn) - this.bind('click', fn); - else - this.trigger('click'); - }, - - eachTextNode: function(fn){ - var i, chldNodes = this[0].childNodes || [], size = chldNodes.length, chld; - for (i = 0; i < size; i++) { - if((chld = new JQLite(chldNodes[i])).isText()) { - fn(chld, i); - } - } - }, - - - eachNode: function(fn){ - var i, chldNodes = this[0].childNodes || [], size = chldNodes.length, chld; - for (i = 0; i < size; i++) { - if(!(chld = new JQLite(chldNodes[i])).isText()) { - fn(chld, i); - } - } - }, - - eachAttribute: function(fn){ - var i, attrs = this[0].attributes || [], size = attrs.length, chld, attr; - for (i = 0; i < size; i++) { - var attr = attrs[i]; - fn(attr.name, attr.value); - } - }, - - replaceWith: function(replaceNode) { - this[0].parentNode.replaceChild(jqLite(replaceNode)[0], this[0]); - }, - - remove: function() { - this.dealoc(); - this[0].parentNode.removeChild(this[0]); - }, - - removeAttr: function(name) { - this[0].removeAttribute(name); - }, - - after: function(element) { - this[0].parentNode.insertBefore(jqLite(element)[0], this[0].nextSibling); - }, - - hasClass: function(selector) { - var className = " " + selector + " "; - if ( (" " + this[0].className + " ").replace(/[\n\t]/g, " ").indexOf( className ) > -1 ) { - return true; - } - return false; - }, - - addClass: function( selector ) { - if (!this.hasClass(selector)) { - this[0].className += ' ' + selector; - } - }, - - css: function(name, value) { - var style = this[0].style; - if (isString(name)) { - if (isDefined(value)) { - style[name] = value; - } else { - return style[name]; - } - } else { - extend(style, name); - } - }, - - attr: function(name, value){ - var e = this[0]; - if (isObject(name)) { - foreach(name, function(value, name){ - e.setAttribute(name, value); - }); - } else if (isDefined(value)) { - e.setAttribute(name, value); - } else { - return e.getAttribute(name); - } - }, - - text: function(value) { - if (isDefined(value)) { - this[0].textContent = value; - } - return this[0].textContent; - }, - - html: function(value) { - if (isDefined(value)) { - this[0].innerHTML = value; - } - return this[0].innerHTML; - }, - - parent: function() { return jqLite(this[0].parentNode);}, - isText: function() { return this[0].nodeType == Node.TEXT_NODE; }, - clone: function() { return jqLite(this[0].cloneNode(true)); } -}; + } +} -/////////////////////////////////// -//Compiler -////////////////////////////////// +function eachAttribute(element, fn){ + var i, attrs = element[0].attributes || [], size = attrs.length, chld, attr; + for (i = 0; i < size; i++) { + var attr = attrs[i]; + fn(attr.name, attr.value); + } +} -function Compiler(markup, directives, widgets){ - this.markup = markup; +function Compiler(textMarkup, attrMarkup, directives, widgets){ + this.textMarkup = textMarkup; + this.attrMarkup = attrMarkup; this.directives = directives; this.widgets = widgets; } @@ -291,8 +108,6 @@ Compiler.prototype = { elementName = element[0].nodeName, widgets = self.widgets, widget = widgets[elementName], - markup = self.markup, - markupSize = markup.length, directives = self.directives, descend = true, exclusive = false, @@ -300,7 +115,9 @@ Compiler.prototype = { template = new Template(), selfApi = { compile: bind(self, self.compile), - reference:function(name) {return jqLite(document.createComment(name));}, + comment:function(text) {return jqLite(document.createComment(text));}, + element:function(type) {return jqLite(document.createElement(type));}, + text:function(text) {return jqLite(document.createTextNode(text));}, descend: function(value){ if(isDefined(value)) descend = value; return descend;} }; @@ -308,14 +125,20 @@ Compiler.prototype = { template.addInit(widget.call(selfApi, element)); } else { // process markup for text nodes only - element.eachTextNode(function(textNode){ - for (var i = 0, text = textNode.text(); i < markupSize; i++) { - markup[i].call(selfApi, text, textNode, element); - } + eachTextNode(element, function(textNode){ + var text = textNode.text(); + foreach(self.textMarkup, function(markup){ + markup.call(selfApi, text, textNode, element); + }); }); // Process attributes/directives - element.eachAttribute(function(name, value){ + eachAttribute(element, function(name, value){ + foreach(self.attrMarkup, function(markup){ + markup.call(selfApi, value, name, element); + }); + }); + eachAttribute(element, function(name, value){ var directive = directives[name]; if (!exclusive && directive) { if (directive.exclusive) { @@ -333,7 +156,7 @@ Compiler.prototype = { // Process non text child nodes if (descend) { - element.eachNode(function(child, i){ + eachNode(element, function(child, i){ template.addChild(i, self.templatize(child)); }); } diff --git a/src/Filters.js b/src/Filters.js index 60d53fb9..dac8d31d 100644 --- a/src/Filters.js +++ b/src/Filters.js @@ -27,7 +27,7 @@ foreach({ jQuery(this.$element).toggleClass('ng-format-negative', amount < 0); return '$' + angularFilter['number'].apply(this, [amount, 2]); }, - + 'number': function(amount, fractionSize){ if (isNaN(amount) || !isFinite(amount)) { return ''; @@ -55,15 +55,15 @@ foreach({ } return text; }, - + 'date': function(amount) { }, - + 'json': function(object) { jQuery(this.$element).addClass("ng-monospace"); return toJson(object, true); }, - + 'trackPackage': (function(){ var MATCHERS = [ { name: "UPS", @@ -89,7 +89,7 @@ foreach({ var returnValue; foreach(MATCHERS, function(carrier){ foreach(carrier.regexp, function(regexp){ - if (regexp.test(tNo)) { + if (!returnValue && regexp.test(tNo)) { var text = carrier.name + ": " + trackingNo; var url = carrier.url + trackingNo; returnValue = new angularFilter.Meta({ @@ -97,19 +97,17 @@ foreach({ url:url, html: '<a href="' + escapeAttr(url) + '">' + text + '</a>', trackingNo:trackingNo}); - _.breakLoop(); } }); - if (returnValue) _.breakLoop(); }); - if (returnValue) + if (returnValue) return returnValue; else if (trackingNo) return noMatch || new angularFilter.Meta({text:trackingNo + " is not recognized"}); else return null; };})(), - + 'link': function(obj, title) { var text = title || angularFilter.Meta.get(obj); var url = angularFilter.Meta.get(obj, "url") || angularFilter.Meta.get(obj); @@ -122,13 +120,13 @@ foreach({ } return obj; }, - - + + 'bytes': (function(){ var SUFFIX = ['bytes', 'KB', 'MB', 'GB', 'TB', 'PB']; return function(size) { if(size === null) return ""; - + var suffix = 0; while (size > 1000) { size = size / 1024; @@ -142,7 +140,7 @@ foreach({ return txt + " " + SUFFIX[suffix]; }; })(), - + 'image': function(obj, width, height) { if (obj && obj.url) { var style = ""; @@ -155,36 +153,36 @@ foreach({ } return null; }, - + 'lowercase': function (obj) { var text = angularFilter.Meta.get(obj); return text ? ("" + text).toLowerCase() : text; }, - + 'uppercase': function (obj) { var text = angularFilter.Meta.get(obj); return text ? ("" + text).toUpperCase() : text; }, - + 'linecount': function (obj) { var text = angularFilter.Meta.get(obj); if (text==='' || !text) return 1; return text.split(/\n|\f/).length; }, - + 'if': function (result, expression) { return expression ? result : undefined; }, - + 'unless': function (result, expression) { return expression ? undefined : result; }, - + 'googleChartApi': extend( function(type, data, width, height) { data = data || {}; var chart = { - 'cht':type, + 'cht':type, 'chco':angularFilterGoogleChartApi['collect'](data, 'color'), 'chtt':angularFilterGoogleChartApi['title'](data), 'chdl':angularFilterGoogleChartApi['collect'](data, 'label'), @@ -210,7 +208,7 @@ foreach({ var values = seriesValues.join('|'); return values === "" ? null : "t:" + values; }, - + 'title': function(data){ var titles = []; var title = data['title'] || []; @@ -219,7 +217,7 @@ foreach({ }); return titles.join('|'); }, - + 'collect': function(data, key){ var outterValues = []; var count = 0; @@ -234,7 +232,7 @@ foreach({ }); return count?outterValues.join(','):null; }, - + 'encode': function(params, width, height) { width = width || 200; height = height || width; @@ -253,8 +251,8 @@ foreach({ } } ), - - + + 'qrcode': function(value, width, height) { return angularFilterGoogleChartApi['encode']({ 'cht':'qr', 'chl':encodeURIComponent(value)}, width, height); @@ -291,11 +289,11 @@ foreach({ return angularFilterGoogleChartApi('s', data, width, height); } }, - + 'html': function(html){ return new angularFilter.Meta({html:html}); }, - + 'linky': function(text){ if (!text) return text; function regExpEscape(text) { diff --git a/src/Scope.js b/src/Scope.js index a3e128b6..daafabb0 100644 --- a/src/Scope.js +++ b/src/Scope.js @@ -116,7 +116,7 @@ Scope.prototype = { }, compile: function(exp) { - if (isFunction(exp)) return exp; + if (isFunction(exp)) return bind(this.state, exp); var expFn = Scope.expressionCache[exp], self = this; if (!expFn) { var parser = new Parser(exp); diff --git a/src/directives.js b/src/directives.js index bbf68669..747da3f5 100644 --- a/src/directives.js +++ b/src/directives.js @@ -18,9 +18,50 @@ angularDirective("ng-bind", function(expression){ }; }); +var bindTemplateCache = {}; +function compileBindTemplate(template){ + var fn = bindTemplateCache[template]; + if (!fn) { + var bindings = []; + foreach(parseBindings(template), function(text){ + var exp = binding(text); + bindings.push(exp ? function(){ + return this.$eval(exp); + } : function(){ + return text; + }); + }); + bindTemplateCache[template] = fn = function(){ + var parts = [], self = this; + foreach(bindings, function(fn){ + parts.push(fn.call(self)); + }); + return parts.join(''); + }; + } + return fn; +}; +angularDirective("ng-bind-template", function(expression){ + var templateFn = compileBindTemplate(expression); + return function(element) { + var lastValue; + this.$addEval(function() { + var value = templateFn.call(this); + if (value != lastValue) { + element.text(value); + lastValue = value; + } + }); + }; +}); + angularDirective("ng-bind-attr", function(expression){ return function(element){ - this.$watch(expression, bind(element, element.attr)); + this.$addEval(function(){ + foreach(this.$eval(expression), function(value, key){ + element.attr(key, compileBindTemplate(value).call(this)); + }, this); + }); }; }); @@ -29,7 +70,7 @@ angularDirective("ng-non-bindable", function(){ }); angularDirective("ng-repeat", function(expression, element){ - var reference = this.reference("ng-repeat: " + expression), + var reference = this.comment("ng-repeat: " + expression), r = element.removeAttr('ng-repeat'), template = this.compile(element), match = expression.match(/^\s*(.+)\s+in\s+(.*)\s*$/), @@ -135,18 +176,12 @@ angularDirective("ng-hide", function(expression, element){ }); }; }); -///////////////////////////////////////// -///////////////////////////////////////// -///////////////////////////////////////// -///////////////////////////////////////// -///////////////////////////////////////// - - - - -//widget related -//ng-validate, ng-required, ng-formatter -//ng-error +angularDirective("ng-style", function(expression, element){ + return function(element){ + this.$addEval(expression, function(value){ + element.css(value); + }); + }; +}); -//ng-scope ng-controller???? diff --git a/src/jqLite.js b/src/jqLite.js new file mode 100644 index 00000000..035a7a1b --- /dev/null +++ b/src/jqLite.js @@ -0,0 +1,185 @@ + +/////////////////////////////////// +//JQLite +////////////////////////////////// + +var jqCache = {}; +var jqName = 'ng-' + new Date().getTime(); +var jqId = 1; +function jqNextId() { return jqId++; } + +var addEventListener = window.document.attachEvent ? + function(element, type, fn) { + element.attachEvent('on' + type, fn); + } : function(element, type, fn) { + element.addEventListener(type, fn, false); + }; + +var removeEventListener = window.document.detachEvent ? + function(element, type, fn) { + element.detachEvent('on' + type, fn); + } : function(element, type, fn) { + element.removeEventListener(type, fn, false); + }; + +function jqClearData(element) { + var cacheId = element[jqName], + cache = jqCache[cacheId]; + if (cache) { + foreach(cache.bind || {}, function(fn, type){ + removeEventListener(element, type, fn); + }); + delete jqCache[cacheId]; + delete element[jqName]; + } +}; + +function JQLite(element) { + this[0] = element; +} + +function jqLite(element) { + if (typeof element == 'string') { + var div = document.createElement('div'); + div.innerHTML = element; + element = div.childNodes[0]; + } + return element instanceof JQLite ? element : new JQLite(element); +} + +JQLite.prototype = { + data: function(key, value) { + var element = this[0], + cacheId = element[jqName], + cache = jqCache[cacheId || -1]; + if (isDefined(value)) { + if (!cache) { + element[jqName] = cacheId = jqNextId(); + cache = jqCache[cacheId] = {}; + } + cache[key] = value; + } else { + return cache ? cache[key] : null; + } + }, + + removeData: function(){ + jqClearData(this[0]); + }, + + dealoc: function(){ + (function dealoc(element){ + jqClearData(element); + for ( var i = 0, children = element.childNodes; i < children.length; i++) { + dealoc(children[0]); + } + })(this[0]); + }, + + bind: function(type, fn){ + var element = this[0], + bind = this.data('bind'), + eventHandler; + if (!bind) this.data('bind', bind = {}); + eventHandler = bind[type]; + if (!eventHandler) { + bind[type] = eventHandler = function() { + var self = this; + foreach(eventHandler.fns, function(fn){ + fn.apply(self, arguments); + }); + }; + eventHandler.fns = []; + addEventListener(element, type, eventHandler); + } + eventHandler.fns.push(fn); + }, + + trigger: function(type) { + var cache = this.data('bind'); + if (cache) { + (cache[type] || noop)(); + } + }, + + click: function(fn) { + if (fn) + this.bind('click', fn); + else + this.trigger('click'); + }, + + replaceWith: function(replaceNode) { + this[0].parentNode.replaceChild(jqLite(replaceNode)[0], this[0]); + }, + + remove: function() { + this.dealoc(); + this[0].parentNode.removeChild(this[0]); + }, + + removeAttr: function(name) { + this[0].removeAttribute(name); + }, + + after: function(element) { + this[0].parentNode.insertBefore(jqLite(element)[0], this[0].nextSibling); + }, + + hasClass: function(selector) { + var className = " " + selector + " "; + if ( (" " + this[0].className + " ").replace(/[\n\t]/g, " ").indexOf( className ) > -1 ) { + return true; + } + return false; + }, + + addClass: function( selector ) { + if (!this.hasClass(selector)) { + this[0].className += ' ' + selector; + } + }, + + css: function(name, value) { + var style = this[0].style; + if (isString(name)) { + if (isDefined(value)) { + style[name] = value; + } else { + return style[name]; + } + } else { + extend(style, name); + } + }, + + attr: function(name, value){ + var e = this[0]; + if (isObject(name)) { + foreach(name, function(value, name){ + e.setAttribute(name, value); + }); + } else if (isDefined(value)) { + e.setAttribute(name, value); + } else { + return e.getAttribute(name); + } + }, + + text: function(value) { + if (isDefined(value)) { + this[0].textContent = value; + } + return this[0].textContent; + }, + + html: function(value) { + if (isDefined(value)) { + this[0].innerHTML = value; + } + return this[0].innerHTML; + }, + + parent: function() { return jqLite(this[0].parentNode);}, + clone: function() { return jqLite(this[0].cloneNode(true)); } +}; diff --git a/src/markup.js b/src/markup.js new file mode 100644 index 00000000..add7ce03 --- /dev/null +++ b/src/markup.js @@ -0,0 +1,64 @@ +function parseBindings(string) { + var results = []; + var lastIndex = 0; + var index; + while((index = string.indexOf('{{', lastIndex)) > -1) { + if (lastIndex < index) + results.push(string.substr(lastIndex, index - lastIndex)); + lastIndex = index; + + index = string.indexOf('}}', index); + index = index < 0 ? string.length : index + 2; + + results.push(string.substr(lastIndex, index - lastIndex)); + lastIndex = index; + } + if (lastIndex != string.length) + results.push(string.substr(lastIndex, string.length - lastIndex)); + return results.length === 0 ? [ string ] : results; +}; + +function binding(string) { + var binding = string.replace(/\n/gm, ' ').match(/^\{\{(.*)\}\}$/); + return binding ? binding[1] : null; +}; + +function hasBindings(bindings) { + return bindings.length > 1 || Binder.binding(bindings[0]) !== null; +}; + +angularTextMarkup(function(text, textNode, parentElement) { + var bindings = parseBindings(text), + self = this; + if (isLeafNode(parentElement[0])) { + parentElement.attr('ng-bind-template', text); + } else { + var cursor = textNode, newElement; + foreach(parseBindings(text), function(text){ + var exp = binding(text); + if (exp) { + newElement = self.element('span'); + newElement.attr('ng-bind', exp); + } else { + newElement = self.text(text); + } + cursor.after(newElement); + cursor = newElement; + }); + } + textNode.remove(); +}); + +var NG_BIND_ATTR = 'ng-bind-attr'; +angularAttrMarkup(function(value, name, element){ + if (name.substr(0, 3) != 'ng-') { + var bindings = parseBindings(value), + bindAttr; + if (hasBindings(bindings)) { + element.removeAttr(name); + bindAttr = fromJson(element.attr(NG_BIND_ATTR) || "{}"); + bindAttr[name] = value; + element.attr(NG_BIND_ATTR, toJson(bindAttr)); + } + } +}); diff --git a/src/widgets2.js b/src/widgets2.js index 0d7bbd49..b0f467d4 100644 --- a/src/widgets2.js +++ b/src/widgets2.js @@ -1,3 +1,19 @@ +///////////////////////////////////////// +///////////////////////////////////////// +///////////////////////////////////////// +///////////////////////////////////////// +///////////////////////////////////////// + + + + + +//widget related +//ng-validate, ng-required, ng-formatter +//ng-error + +//ng-scope ng-controller???? + // <input type="text" name="bla" ng-action=""> -> <ng:textinput name="" ng-action=""/> angular.widget("inputtext", function(element) { var expression = element.attr('name'); @@ -49,7 +65,7 @@ angular.widget("colorpicker", function(element) { this.$watch(expression, function(cmyk){ element.setColor(formatter.format(cmyk)); }); - } + }; }); angular.widget("template", function(element) { diff --git a/test/CompilerSpec.js b/test/CompilerSpec.js index 57b597c4..a49a2551 100644 --- a/test/CompilerSpec.js +++ b/test/CompilerSpec.js @@ -3,7 +3,7 @@ describe('compiler', function(){ return jQuery(html)[0]; } - var compiler, markup, directives, widgets, compile, log; + var compiler, textMarkup, directives, widgets, compile, log; beforeEach(function(){ log = ""; @@ -24,9 +24,10 @@ describe('compiler', function(){ } }; - markup = []; + textMarkup = []; + attrMarkup = []; widgets = {}; - compiler = new Compiler(markup, directives, widgets); + compiler = new Compiler(textMarkup, attrMarkup, directives, widgets); compile = function(html){ var e = element("<div>" + html + "</div>"); var view = compiler.compile(e)(e); @@ -108,7 +109,7 @@ describe('compiler', function(){ }); it('should process markup before directives', function(){ - markup.push(function(text, textNode, parentNode) { + textMarkup.push(function(text, textNode, parentNode) { if (text == 'middle') { expect(textNode.text()).toEqual(text); parentNode.attr('hello', text); diff --git a/test/directivesSpec.js b/test/directivesSpec.js index d125d326..18bedb64 100644 --- a/test/directivesSpec.js +++ b/test/directivesSpec.js @@ -3,7 +3,7 @@ describe("directives", function(){ var compile, element; beforeEach(function() { - var compiler = new Compiler(angularMarkup, angularDirective, angularWidget); + var compiler = new Compiler(angularTextMarkup, angularAttrMarkup, angularDirective, angularWidget); compile = function(html) { element = jqLite(html); var view = compiler.compile(element)(element); @@ -39,6 +39,14 @@ describe("directives", function(){ expect(element.text()).toEqual('misko'); }); + it('should ng-bind-template', function() { + var scope = compile('<div ng-bind-template="Hello {{name}}!"></div>'); + expect(element.text()).toEqual(''); + scope.set('name', 'Misko'); + scope.updateView(); + expect(element.text()).toEqual('Hello Misko!'); + }); + it('should ng-bind-attr', function(){ var scope = compile('<img ng-bind-attr="{src:\'mysrc\', alt:\'myalt\'}"/>'); expect(element.attr('src')).toEqual(null); @@ -81,7 +89,7 @@ describe("directives", function(){ it('should error on wrong parsing of ng-repeat', function(){ var scope = compile('<ul><li ng-repeat="i dont parse"></li></ul>'); var log = ""; - element.eachNode(function(li){ + eachNode(element, function(li){ log += li.attr('ng-error') + ';'; log += li.hasClass('ng-exception') + ';'; }); diff --git a/test/markupSpec.js b/test/markupSpec.js new file mode 100644 index 00000000..9e89af7b --- /dev/null +++ b/test/markupSpec.js @@ -0,0 +1,49 @@ +describe("markups", function(){ + + var compile, element, scope; + + beforeEach(function() { + scope = null; + element = null; + var compiler = new Compiler(angularTextMarkup, angularAttrMarkup, angularDirective, angularWidget); + compile = function(html) { + element = jqLite(html); + var view = compiler.compile(element)(element); + view.init(); + scope = view.scope; + }; + }); + + afterEach(function(){ + if (element) { + element.remove(); + } + expect(_(jqCache).size()).toEqual(0); + }); + + it('should translate {{}} in text', function(){ + compile('<div>hello {{name}}!</div>'); + expect(element.html()).toEqual('hello <span ng-bind="name"></span>!'); + scope.set('name', 'Misko'); + scope.updateView(); + expect(element.html()).toEqual('hello <span ng-bind="name">Misko</span>!'); + }); + + it('should translate {{}} in terminal nodes', function(){ + compile('<select><option>Greet {{name}}!</option></select>'); + expect(element.html()).toEqual('<option ng-bind-template="Greet {{name}}!"></option>'); + scope.set('name', 'Misko'); + scope.updateView(); + expect(element.html()).toEqual('<option ng-bind-template="Greet {{name}}!">Greet Misko!</option>'); + }); + + it('should translate {{}} in attributes', function(){ + compile('<img src="http://server/{{path}}.png"/>'); + expect(element.attr('src')).toEqual(); + expect(element.attr('ng-bind-attr')).toEqual('{"src":"http://server/{{path}}.png"}'); + scope.set('path', 'a/b'); + scope.updateView(); + expect(element.attr('src')).toEqual("http://server/a/b.png"); + }); + +}); |
