fix component invocations, make dynamic (wip)

TODO next:
- fix keyword args when foo's a function
  - pass them to function not template
  - evaluate them
- back-compat block helper calling convention
This commit is contained in:
David Greenspan
2013-10-04 15:28:22 -07:00
parent 17129ee45a
commit 25764255ff
5 changed files with 149 additions and 29 deletions

View File

@@ -7,6 +7,10 @@
<template name="spacebars_template_test_bbb">
bbb
</template>
<template name="spacebars_template_test_bracketed_this">
[{{this}}]
</template>
<!-- -->
@@ -30,3 +34,15 @@
<template name="spacebars_template_test_triple">
{{{html}}}
</template>
<template name="spacebars_template_test_inclusion_args">
{{> foo bar}}
</template>
<template name="spacebars_template_test_inclusion_args2">
{{> foo bar baz}}
</template>
<template name="spacebars_template_test_inclusion_args3">
{{> foo bar q=baz}}
</template>

View File

@@ -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
});

View File

@@ -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':

View File

@@ -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});
}
}
});

View File

@@ -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("<div ", { attrs: { 'class': 'foo' } }, ">text</div>")`.