diff --git a/packages/spacebars-tests/package.js b/packages/spacebars-tests/package.js index ed386d1c0d..4d9ffa2689 100644 --- a/packages/spacebars-tests/package.js +++ b/packages/spacebars-tests/package.js @@ -8,6 +8,7 @@ Package.on_test(function (api) { api.use('underscore'); api.use('spacebars'); api.use('tinytest'); + api.use('jquery'); api.use('test-helpers'); api.use('templating', 'client'); diff --git a/packages/spacebars-tests/template_tests.html b/packages/spacebars-tests/template_tests.html index a0f49b5288..a371f310c6 100644 --- a/packages/spacebars-tests/template_tests.html +++ b/packages/spacebars-tests/template_tests.html @@ -18,3 +18,11 @@ + + + + diff --git a/packages/spacebars-tests/template_tests.js b/packages/spacebars-tests/template_tests.js index ee696bc986..68c839a688 100644 --- a/packages/spacebars-tests/template_tests.js +++ b/packages/spacebars-tests/template_tests.js @@ -1,3 +1,9 @@ +var renderToDiv = function (comp) { + var div = document.createElement("DIV"); + UI.insert(UI.render(comp), div); + return div; +}; + Tinytest.add("spacebars - templates - simple helper", function (test) { var tmpl = Template.spacebars_template_test_simple_helper; tmpl.foo = function (x) { @@ -6,8 +12,7 @@ Tinytest.add("spacebars - templates - simple helper", function (test) { tmpl.bar = function () { return 123; }; - var div = document.createElement("DIV"); - UI.insert(UI.render(tmpl), div); + var div = renderToDiv(tmpl); test.equal(div.innerHTML, "124"); }); @@ -20,9 +25,7 @@ Tinytest.add("spacebars - templates - dynamic template", function (test) { tmpl.foo = function () { return R.get() === 'aaa' ? aaa : bbb; }; - var div = document.createElement("DIV"); - UI.insert(UI.render(tmpl), div); - + var div = renderToDiv(tmpl); test.equal(div.innerHTML, "aaa"); R.set('bbb'); @@ -30,3 +33,44 @@ Tinytest.add("spacebars - templates - dynamic template", function (test) { test.equal(div.innerHTML, "bbb"); }); + +Tinytest.add("spacebars - templates - interpolate attribute", function (test) { + var tmpl = Template.spacebars_template_test_interpolate_attribute; + tmpl.foo = function (x) { + return x+1; + }; + tmpl.bar = function () { + return 123; + }; + var div = renderToDiv(tmpl); + + test.equal($(div).find('div')[0].className, "aaa124zzz"); +}); + +Tinytest.add("spacebars - templates - dynamic attrs", function (test) { + var tmpl = Template.spacebars_template_test_dynamic_attrs; + + var R1 = ReactiveVar(''); + var R2 = ReactiveVar('n=1'); + var R3 = ReactiveVar('selected'); + tmpl.attrs1 = function () { return R1.get(); }; + tmpl.attrs2 = function () { return R2.get(); }; + tmpl.k = 'x'; + tmpl.v = 'y'; + tmpl.x = function () { return R3.get(); }; + var div = renderToDiv(tmpl); + var span = $(div).find('span')[0]; + test.equal(span.innerHTML, 'hi'); + test.equal(span.getAttribute('n'), "1"); + test.equal(span.getAttribute('x'), 'y'); + test.isTrue(span.hasAttribute('selected')); + + R1.set('zanzibar="where the heart is"'); + R2.set(''); + R3.set(''); + Deps.flush(); + test.equal(span.innerHTML, 'hi'); + test.isFalse(span.hasAttribute('n')); + test.isFalse(span.hasAttribute('selected')); + test.equal(span.getAttribute('zanzibar'), 'where the heart is'); +}); diff --git a/packages/spacebars/spacebars.js b/packages/spacebars/spacebars.js index 922d4fb49a..cae0408382 100644 --- a/packages/spacebars/spacebars.js +++ b/packages/spacebars/spacebars.js @@ -577,6 +577,12 @@ var makeObjectLiteral = function (obj) { return buf.join(''); }; +// Generates a render function (i.e. JS source code) from a template +// string or a pre-parsed template string. Consumes the AST from the +// parser, which consists of HTML tokens with embedded stache tags. A +// "block" (i.e. `{{#foo}}...{{/foo}}`) is represented as a single tag +// (always as part of an HTML "Characters" token), which has content +// that contains more HTML. Spacebars.compile = function (inputString, options) { var tree; if (typeof inputString === 'object') { @@ -641,15 +647,14 @@ Spacebars.compile = function (inputString, options) { argCode = toJSLiteral(argValue); break; case 'PATH': - argCode = 'function () { return Spacebars.call(' + - codeGenPath(argValue, funcInfo) + '); }'; + argCode = codeGenPath(argValue, funcInfo); break; default: error("Unexpected arg type: " + argType); } if (arg.length > 2) { - // keyword argument + // keyword argument (represented as [type, value, name]) options = (options || {}); if (! (forComponentWithOpts && (arg[2] in forComponentWithOpts))) { @@ -719,7 +724,7 @@ Spacebars.compile = function (inputString, options) { var nameCode = codeGenPath(tag.path, funcInfo); var argCode = codeGenArgs(tag.args, funcInfo); - return 'Spacebars.call(' + nameCode + + return 'Spacebars.mustache(' + nameCode + (argCode ? ', ' + argCode.join(', ') : '') + ')'; }; @@ -861,6 +866,7 @@ Spacebars.compile = function (inputString, options) { var name = kv.nodeName; var value = kv.nodeValue; if ((typeof name) === 'string') { + // attribute name has no tags attrs = (attrs || {}); attrs[toJSLiteral(name)] = interpolate(value, funcInfo, @@ -870,6 +876,8 @@ Spacebars.compile = function (inputString, options) { } else if (value === '' && name.length === 1 && name[0].type === 'TRIPLE') { + // attribute name is a triple-stache, no value, as in: + // `
`. renderables.push( '{attrs: function () { return Spacebars.parseAttrs(' + codeGenBasicStache(name[0], funcInfo) + '); }}'); @@ -877,7 +885,7 @@ Spacebars.compile = function (inputString, options) { pairsWithReactiveNames.push( interpolate(name, funcInfo, INTERPOLATE_ATTR_VALUE), - interpolate(name, funcInfo, + interpolate(value, funcInfo, INTERPOLATE_ATTR_VALUE)); isReactive = true; } @@ -977,6 +985,21 @@ Spacebars.call = function (value/*, args*/) { return value.apply(null, args); }; +// Executes `{{foo bar baz}}` when called on `(foo, bar, baz)`. +// If `bar` and `baz` are functions, they are called. `foo` +// may be a non-function, in which case the arguments are +// discarded (though they may still be evaluated, i.e. called). +Spacebars.mustache = function (value/*, args*/) { + // call any arg that is a function (checked in Spacebars.call) + for (var i = 1; i < arguments.length; i++) + arguments[i] = Spacebars.call(arguments[i]); + + var result = Spacebars.call.apply(null, arguments); + // map `null` and `undefined` to "", stringify anything else + // (e.g. strings, booleans, numbers including 0). + return String(result == null ? '' : result); +}; + Spacebars.extend = function (obj/*, k1, v1, k2, v2, ...*/) { for (var i = 1; i < arguments.length; i += 2) obj[arguments[i]] = arguments[i+1]; @@ -996,8 +1019,8 @@ Spacebars.parseAttrs = function (attrs) { if (tokens.length && tokens[0].type === 'StartTag') { _.each(tokens[0].data, function (kv) { - if (UI.isValidAttributeName(kv[0])) - dict[kv[0]] = kv[1]; + if (UI.isValidAttributeName(kv.nodeName)) + dict[kv.nodeName] = kv.nodeValue; }); } return dict; diff --git a/packages/ui/attrs.js b/packages/ui/attrs.js index eeb47c526d..c9de1ee6d9 100644 --- a/packages/ui/attrs.js +++ b/packages/ui/attrs.js @@ -34,10 +34,12 @@ AttributeManager = function (dictOrFunc) { var handlers = self.handlers; for (var attrName in dict) { + if (! attrName) + continue; // ignore empty attribute names // perform a sanity check, since we'll be inserting // attrName directly into the HTML stream if (! isValidAttributeName(attrName)) - throw new Error("Illegal HTML attribute name: '" + attrName + "'"); + throw new Error("Expected single HTML attribute name, found: '" + attrName + "'"); handlers[attrName] = makeAttributeHandler( attrName, dict[attrName]); @@ -88,12 +90,14 @@ _extend(AttributeManager.prototype, { h.update(element, oldValue, h.value); } for (var k in newDict) { + if (! k) + continue; // ignore empty attributes if (! handlers.hasOwnProperty(k)) { // need a new handler var attrName = k; if (! isValidAttributeName(attrName)) - throw new Error("Illegal HTML attribute name: " + attrName); + throw new Error("Expected single HTML attribute name, found: " + attrName); var h = makeAttributeHandler( attrName, newDict[attrName]);