Handlebars and helpers and meteor

873 views
Skip to first unread message

Tom Coleman

unread,
Jun 29, 2012, 1:04:43 AM6/29/12
to meteo...@googlegroups.com
Hey,

Just wanted to flag some behaviour which I think is quite weird in meteor's extension to allows nested handlebars calls.

Suppose I am rendering with the data context: 
{
foo: function(){ return 'foo'; }, 
bar: 'bar';
}

then we have the following behaviours:

{{ helper foo }} === {{ helper(data.foo()) }} === {{ helper('foo') }}. [1]

{{ helper foo bar }} === {{ helper(data.foo(data.bar)) }} == {{ helper('foo') }}.  [2]

{{ helper bar foo }} === {{ helper(data.bar, data.foo) }} == {{ helper('bar', function…) }} [3]

Now, 
[1] is perfect

[2] _does_ make sense when foo is an helper function defined either by
a) Handlebars.registerHelper OR
b) Template.X.foo = 
but, imho doesn't make much sense in this case.

[3] doesn't really make sense (to me) at all. At the very least why is helper not passed in the result of foo() ?

What this means practically is that if I want to pass in the result of functions to a helper, I need to

A) do a lot of boilerplate in my helpers like:
if (typeof arg == 'function') 
arg = arg.call(this) 

B) ensure that I only pass one function argument to a helper and that it's not the first argument. This is obviously a bit of an issue.

I don't really have a good suggestion on how it _should_ work, maybe some extra syntax is needed to differentiate what you are trying to do when nesting calls. Has anyone else run into this? Any suggestions?

Tom

David Greenspan

unread,
Jun 29, 2012, 3:57:22 AM6/29/12
to meteo...@googlegroups.com
This is admittedly a bit of a mess.

The original extension was to support nested helpers, as in the example {{#if league_is "junior"}} in the docs.  The rule is, if the first argument is a function, it's called on the remaining arguments, otherwise it's considered an argument.

We don't actually call methods in argument positions right now, but maybe we should.  {{helper foo bar baz}} is starting to seem like it should call helper(foo, bar, baz) whether any of the arguments are functions or not, but this conflicts with the existing rule.  I'm not sure what the right answer is.

-- David



Tom

--
You received this message because you are subscribed to the Google Groups "meteor-core" group.
To post to this group, send email to meteo...@googlegroups.com.
To unsubscribe from this group, send email to meteor-core...@googlegroups.com.
For more options, visit this group at http://groups.google.com/group/meteor-core?hl=en.

Iain Shigeoka

unread,
Jun 29, 2012, 1:26:32 PM6/29/12
to meteo...@googlegroups.com
I would vote for the most consistent and easiest to understand (Meteor principal #7) option (which IMO is your proposed call helper(foo,bar,baz) no matter what foo, bar and baz are). 

It should be easy for users to create helpers that recreate other variations if users want/need some sort of fancier parameters-as-functions-to-call handling. 

Earlier seems to be better than later to break the existing rule. :)

-iain

David Greenspan

unread,
Jun 29, 2012, 7:01:49 PM6/29/12
to meteo...@googlegroups.com
Ok, here is my proposal to unify the two features.

*Block* helper expressions are now only allowed to receive one argument.  You can pass multiple arguments, and they will be interpreted as a (non-block) helper invocation.

So {{#helper foo bar baz}} means calculate {{foo bar baz}} and then pass it to #helper, and {{foo bar baz}} always means foo(bar, baz).  Block helpers and non-block helpers are already invoked slightly differently, despite sharing the same namespace, so this just redefines what a block helper invocation is.

The only downside is you can't define a block helper that takes more than one positional argument.  I haven't seen such a helper in use, however.    Actually, that's a lie -- I just checked and I used one on our docs site.  But there is a workaround: make all arguments to the block helper be keyword arguments:

{{#helper x=foo y=bar}}

This is probably clearer anyway.  Defining a new block helper is relatively advanced and we can be sure to document the limitation that you get one positional argument or any number of keyword arguments.

Thoughts?

-- David

Iain Shigeoka

unread,
Jul 1, 2012, 12:39:14 AM7/1/12
to meteo...@googlegroups.com
Looks like a reasonable approach to me. I don't have any helpers that use multiple arguments - I agree with you that it should be a relatively rare case. I'd be interested to see what Tom says since I'm assuming he ran into the issue while using multiple arguments in a real-world scenario.

-iain

Tom Coleman

unread,
Jul 1, 2012, 12:43:46 AM7/1/12
to meteo...@googlegroups.com
Hey David,

This seems like a sensible and consistent scheme.

A remaining question is when you plan to evaluate arguments (if they are functions?). 

For instance suppose foo and bar are functions and  I do:
{{helper foo bar}} 

does that "mean" {{helper(foo, bar)}} or {{helper(foo(), bar())}} ?

likewise, is it possible to do {{#helper foo=bar}} and have it "mean" {{#helper foo=bar()}} ?

The reason I'm interested in this point is because there are many use cases[1] where the data context object has function that simulate properties.

Template.whatever.data = {
  foo: 'foo',  // a property
  bar: function() { return 'bar'; } // a function
}

I feel like it makes sense for them to behave 'the same'[2], but I can appreciate that it adds complexity.

Cheers,
Tom

[1] My main use case is models, but this exact thing came up the other day when we were talking about reactivity and re-rendering right?

[2] In the same way that:

Template.foo.bar = 'x';
Template.foo.bar = function() { return 'x'; }

behave 'the same' (obviously there's a big difference in terms of reactivity).

Tom Coleman

unread,
Jul 1, 2012, 12:46:03 AM7/1/12
to meteo...@googlegroups.com
Oh, just replied. 

I do have a block helper that take multiple arguments

   {{if_equal "foo" bar}}
    ...
   {{/if_equal}}

but I can certainly live without it in order to figure this out.

Cheers,
Tom
--

David Greenspan

unread,
Jul 1, 2012, 3:21:54 PM7/1/12
to meteo...@googlegroups.com
Hi Tom,

Arguments will absolutely be evaluated if they are functions!  That makes perfect sense.  The current draft of Auth provides a global helper currentUser() as well that has to work in all these positions.

That's an interesting example of a two-argument block helper (if_equal).  Since the original motivation for the nested helper feature was also an #if testing for equality, I gave some thought to what the best pattern might be.

How about:

- {{#if equal "foo" bar}}

...assuming a global two-argument helper called "equal".

-- David

David Greenspan

unread,
Jul 11, 2012, 9:58:48 PM7/11/12
to David Greenspan, meteo...@googlegroups.com
Just a heads up, the Handlebars improvements have landed on "devel"!  You can pass functions as arguments to helpers, and block helpers now take one positional argument (interpreted as a nested helper invocation), or any number of keyword arguments.  It turns out keyword arguments were totally broken before; oops. :)

A write-up of the behavior is on the wiki: https://github.com/meteor/meteor/wiki/Handlebars

-- David

Tom Coleman

unread,
Jul 11, 2012, 11:02:25 PM7/11/12
to meteo...@googlegroups.com, David Greenspan
Thanks David! I've simplified and rationalized my helpers a fair bit thanks to this.

One (very minor) point: I have a block helper {{#if_equals}} that I was expecting to have to change to something like {{#if equals}}. However, it is still working as long as I call it like so (ie. with a string as first argument):

{{#if_equals "home" current_page}}

Note that I don't have a problem with this! But it isn't consistent with what you say on the wiki page:
The exact rule is that a block helper is always invoked with either no arguments; one positional argument (and no keyword arguments); or only keyword arguments

Just thought I'd flag this. Thanks again,

Tom

David Greenspan

unread,
Jul 12, 2012, 12:01:30 AM7/12/12
to Tom Coleman, meteo...@googlegroups.com
Great catch, Tom!  That case slipped through the cracks in my testing.  It's now fixed on devel.

The rationale is that this rule will lead to fewer surprises as it constrains how helpers are defined rather than how they are called.

Thanks!

-- David

Tom Coleman

unread,
Jul 12, 2012, 12:10:38 AM7/12/12
to David Greenspan, meteo...@googlegroups.com
Yep. Plus I think {{#if equals a b}} reads better anyway :)

It works.

Thanks David,
Tom

Rick Richardson

unread,
Aug 3, 2012, 7:06:15 AM8/3/12
to meteo...@googlegroups.com
when I see a string of symbols that need to be evaluated, such as

if equals foo bar 

I immediately think of Haskell.  I'm not proposing we use Haskell's default function evaluation (every function takes only 1 parameter and returns a function that takes the remaining parameters, although that could be done, js doesn't lend itself well to that)

We could, however, get a consistent evaluation of any string by making the evaluation of the entire string of symbols right associative and turn it into a tree, sort of like function composition (.) in Haskell.

We start from right to left, check each symbol to see if it is a function, then check its arity and ensure that it can be evaluated with the remaining symbols in the string:

so if we had something like this pathological example (where foo is function(a) { return (a == -1);} and bar is function() {return 42;} ):

if and equals foo bar equals 1 0 loggedIn

loggedIn is a function of arity 0 so we eval it to true
//[if and equals foo bar equals 1 0 true]
0 stays put
1 stays put
equals evals to false and replaces 1 0
//[if and equals foo bar false true]
bar is a function of arity 0 so we run it and it evals to 42
//[if and equals foo 42 false true]
foo is a function of arity 1  so we pass it 42 and it evals to false
//[if and equals false false true]
equals is arity 2 so we pass it  false false which evals to true
//[if and true true]
and is arity 2 so we pass it true true which evals to true
//[if true]

Ok. So we have just created our own mini VM within javascript, but it actually wouldn't be that difficult to implement.  The upside is that we now have a consistent evaluation scheme that isn't going to surprise anyone, and we won't have to add any custom logic or exceptions later.   I can take a whack at creating this eval function if you like.

Or just tell me I'm crazy, that's fine too.


David Greenspan

unread,
Aug 3, 2012, 3:12:18 PM8/3/12
to meteo...@googlegroups.com
I think this might be a little too complicated.  I've always found the splashes of free-form "foo bar baz qux" syntax in, for example, Scala and CoffeeScript to be confusing.  Also, this particular scheme depends heavily on function arity -- that it be definite and programmatically inspectable, which isn't something we've tended to assume in our JavaScript so far.

I sold the Meteor team on {{#if equals foo bar}} by arguing that a block expression consists of a block name ("#if") and a normal helper expression ({{equals foo bar}}), and that's all that's going on.  You get a chance to use a helper to evaluate the condition, and in exchange, future block helpers have to be one-argument (or take only keyword arguments).  It's a compromise between flexibility and complexity.

That said, I don't want to discount the underlying motivation -- that Handlebars expressions sometimes feel very constraining!  Many other template languages avoid this problem by allowing arbitrary JS expressions in such places.  The main argument for using a very small, logically-impoverished template language is usually that it keeps code out of the template, either to enforce good separation between view and business logic or because then your designers don't have to know programming.  The counterargument goes that template languages tend to expand to become full programming languages, just really bad ones, and you might as well have JS be the language you drop into when you need it.

Meteor will get better support for template languages besides Handlebars, and hopefully Handlebars will improve a little too.  We spent a little time on small extensions that we felt were needed to move forwards, so we now have a sort of "Meteor-flavored Handlebars," but I'm not sure how far we'll go with that.  Ideally we'd support Handlebars, Jade, and others, and the authors of those would move them forward.

-- David


--
You received this message because you are subscribed to the Google Groups "meteor-core" group.
To view this discussion on the web visit https://groups.google.com/d/msg/meteor-core/-/hNoVNERpiWYJ.

Rick Richardson

unread,
Aug 3, 2012, 4:15:59 PM8/3/12
to meteo...@googlegroups.com
When you say relying on being definite, you're saying that people might be relying on the Arguments list or perhaps functions with arity N but receiving receiving N-1 params?  Yea, this is an annoyance of mine.  

I do agree that such a scheme might be easily abused in a templating system, instead one should put the complicated boolean logic in a function to be called by the template.   I may have had too much coffee this morning :) 

Either way I implemented most of the VM.  If you want it at some point, let me know :P 

To unsubscribe from this group, send email to meteor-core+unsubscribe@googlegroups.com.
Reply all
Reply to author
Forward
0 new messages