diff --git a/packages/spacebars-tests/template_tests.html b/packages/spacebars-tests/template_tests.html index 87473c56fd..24054a55ce 100644 --- a/packages/spacebars-tests/template_tests.html +++ b/packages/spacebars-tests/template_tests.html @@ -7,6 +7,10 @@ + + @@ -30,3 +34,15 @@ + + + + + + diff --git a/packages/spacebars-tests/template_tests.js b/packages/spacebars-tests/template_tests.js index 1fb6dccb1e..398bdce06b 100644 --- a/packages/spacebars-tests/template_tests.js +++ b/packages/spacebars-tests/template_tests.js @@ -104,3 +104,67 @@ Tinytest.add("spacebars - templates - triple", function (test) { test.equal(span.className, 'hi'); test.equal(span.innerHTML, 'blah'); }); + +Tinytest.add("spacebars - templates - inclusion args", function (test) { + var tmpl = Template.spacebars_template_test_inclusion_args; + + var R = ReactiveVar(Template.spacebars_template_test_aaa); + tmpl.foo = function () { return R.get(); }; + + var div = renderToDiv(tmpl); + // `{{> foo bar}}`, with `foo` resolving to Template.aaa, + // which consists of "aaa" + test.equal(div.innerHTML, 'aaa'); + R.set(Template.spacebars_template_test_bbb); + Deps.flush(); + test.equal(div.innerHTML, 'bbb'); + + ////// Ok, now `foo` *is* Template.aaa + tmpl.foo = Template.spacebars_template_test_aaa; + div = renderToDiv(tmpl); + test.equal(div.innerHTML, 'aaa'); + + ////// Ok, now `foo` is a template that takes an argument; bar is a string. + tmpl.foo = Template.spacebars_template_test_bracketed_this; + tmpl.bar = 'david'; + div = renderToDiv(tmpl); + test.equal(div.innerHTML, '[david]'); + + ////// Now `foo` is a template that takes an arg; bar is a function. + tmpl.foo = Template.spacebars_template_test_bracketed_this; + R = ReactiveVar('david'); + tmpl.bar = function () { return R.get(); }; + div = renderToDiv(tmpl); + test.equal(div.innerHTML, '[david]'); + R.set('avi'); + Deps.flush(); + test.equal(div.innerHTML, '[avi]'); +}); + +Tinytest.add("spacebars - templates - inclusion args 2", function (test) { + ///// `foo` is a function in `{{> foo bar baz}}`. + // `bar` and `baz` should be called and passed as an arg to it. + var tmpl = Template.spacebars_template_test_inclusion_args2; + tmpl.foo = function (x, y) { + return y === 999 ? Template.spacebars_template_test_aaa : + Template.spacebars_template_test_bracketed_this.withData(x + y); + }; + var R = ReactiveVar(3); + tmpl.bar = 4; + tmpl.baz = function () { return R.get(); }; + var div = renderToDiv(tmpl); + test.equal(div.innerHTML, '[7]'); + R.set(11); + Deps.flush(); + test.equal(div.innerHTML, '[15]'); + R.set(999); + Deps.flush(); + test.equal(div.innerHTML, 'aaa'); +}); + +Tinytest.add("spacebars - templates - inclusion args 2", function (test) { + // `{{> foo bar q=baz}}` + var tmpl = Template.spacebars_template_test_inclusion_args3; + + // XXX +}); diff --git a/packages/spacebars/spacebars.js b/packages/spacebars/spacebars.js index cae0408382..cc3cbd77f0 100644 --- a/packages/spacebars/spacebars.js +++ b/packages/spacebars/spacebars.js @@ -662,19 +662,8 @@ Spacebars.compile = function (inputString, options) { } } else { // positional argument - if (forComponent) { - // for Components, only take one positional - // argument, and call it `data` - if (i === 0) { - options = (options || {}); - options[toJSLiteral('data')] = argCode; - } else { - error("Only one positional argument is allowed"); - } - } else { - args = (args || []); - args.push(argCode); - } + args = (args || []); + args.push(argCode); } }); @@ -683,9 +672,8 @@ Spacebars.compile = function (inputString, options) { options = (options || {}); options[toJSLiteral(k)] = v; }); - - // components get one argument, the options dictionary - args = [options ? makeObjectLiteral(options) : '{}']; + // put options as dictionary at beginning of args for component + args.unshift(options ? makeObjectLiteral(options) : 'null'); } else { // put options as dictionary at end of args if (options) { @@ -698,26 +686,31 @@ Spacebars.compile = function (inputString, options) { }; var codeGenComponent = function (path, args, funcInfo, - compOptions) { + compOptions, isBlock) { var nameCode = codeGenPath(path, funcInfo); var argCode = (args.length || compOptions) ? - codeGenArgs(args, funcInfo, compOptions || {})[0] : null; + codeGenArgs(args, funcInfo, compOptions || {}) : null; // XXX provide a better error message if // `foo` in `{{> foo}}` is not found? - // Instead of `null`, we could evaluate to the path - // as a string, and then the renderer could choke on - // that in a way where it ends up in the error message. - var compFunc = 'function () { return (Spacebars.call(' + nameCode + - ') || null); }'; + var comp = nameCode; if (path.length === 1) - compFunc = 'Template[' + toJSLiteral(path[0]) + '] || ' + compFunc; + comp = '(Template[' + toJSLiteral(path[0]) + '] || ' + comp + ')'; - return '{kind: ' + compFunc + (argCode ? ', props: ' + argCode : '') + - '}'; + // XXX For now, handle the calling convention for `{{> foo}}` and `{{#foo}` + // using a wrapper component, which processes the arguments based + // on the type of tag and the type of `foo` (component or function). + // If `foo` changes reactively, the wrapper component is invalidated. + // + // This should be cleaned up to make the generated code cleaner and + // to not have all the extra components and DomRanges hurting + // peformance and showing up during debugging. + return '{kind: UI.DynamicComponent, props: {' + + (isBlock? 'isBlock: true, ' : '') + 'compKind: ' + comp + + (argCode ? ', compArgs: [' + argCode.join(', ') + ']': '') + '}}'; }; var codeGenBasicStache = function (tag, funcInfo) { @@ -829,7 +822,7 @@ Spacebars.compile = function (inputString, options) { renderables.push(codeGenComponent( block.openTag.path, block.openTag.args, - funcInfo, extraArgs)); + funcInfo, extraArgs, true)); } else { switch (tag.type) { case 'INCLUSION': diff --git a/packages/ui/components.js b/packages/ui/components.js index f2fdb1f238..b9ff4479cb 100644 --- a/packages/ui/components.js +++ b/packages/ui/components.js @@ -80,4 +80,51 @@ UI.With = Component.extend({ render: function (buf) { buf.write(this.content); } +}); + +var callIfFunction = function (value) { + return (typeof value === 'function') ? value() : value; +}; + +UI.DynamicComponent = Component.extend({ + kind: 'DynamicComponent', + render: function (buf) { + var kind = this.compKind; + var args = this.compArgs; + + var kwArgs = args && args.length && args[0]; + var posArgs = args && args.length > 1 && args.slice(1); + + var props = _.extend({}, kwArgs); + if (typeof kind === 'function') { + // Calling a helper function as a template. Evaluate the + // arguments and pass them to the function to get back + // a component. Completely different use of args than + // when calling a bare component like `Template.foo` or + // a "helper" that is a constant component (in which case + // the args are used to extend the component). + // + // `kind` should be already bound with a `this`, so it + // doesn't matter what we pass in for the first argument + // to `apply`. Same with arguments. + if (posArgs) { + for (var i = 0; i < posArgs.length; i++) + posArgs[i] = callIfFunction(posArgs[i]); + } + kind = kind.apply(null, posArgs || []); + } else { + if (posArgs && posArgs.length) { + if (posArgs.length > 1) + throw new Error("Can't have more than one argument to a template"); + + if (posArgs.length) { + props.data = posArgs[0]; + } + } + } + + if (kind) { + buf.write({kind: kind, props: props}); + } + } }); \ No newline at end of file diff --git a/packages/ui/render.js b/packages/ui/render.js index 58b00bb949..f1484f0bcf 100644 --- a/packages/ui/render.js +++ b/packages/ui/render.js @@ -12,8 +12,8 @@ var DomRange = UI.DomRange; // - String (any raw HTML substring with no limitations) // - Component (a kind to instantiate and insert) // - function returning Component (reactive kind) -// - { kind: functionOrComponent, ... } (reactive kind, instantiated -// with props (the `...`) +// - { kind: functionOrComponent, props: dict } (reactive kind, instantiated +// with props // - { attrs: functionOrDictionary } - reactive dictionary of attributes, // only allowed inside an HTML tag. For example, // `buf.write("
text
")`.