handlebars-lang / handlebars.js

Minimal templating on steroids.
http://handlebarsjs.com
MIT License
17.9k stars 2.04k forks source link

Implement named helper variable references #907

Closed kpdecker closed 9 years ago

kpdecker commented 9 years ago

Implement the remainder of the feature started in #906.

We want to allow for helpers to provide named values that may be accessed by any child program run. The goal is to allow explicit naming of helper data fields so they can easily be accessed in nested scope, vs. arbitrary @../index references.

For example:

{{#each foos as |foo indexFoo|}}
  {{#each bars as |bar indexBar|}}
    {{indexFoo}}.{{indexBar}}. {{foo}} {{bar}}
  {{/each}}
{{/each}}
kpdecker commented 9 years ago

@wycats @mmun any thoughts on how these should interact with partials? Compat mode?

wycats commented 9 years ago

What is the exact current status of partial state inheritance?

mmun commented 9 years ago

A couple notes...

1) I didn't implement block params on inverses, but it seems like something worth exploring. For example

{{my-each items as |item|}}
  {{item.name}}
{{else as |error|}}
  {{error.message}}
{{/my-each}}

This might be annoying to implement because {{each}} is just a token and because of some ambiguities with inverse blocks {{else foo}}{{/foo}}. Maybe you already worked this stuff out with your inverse chaining work.

2) For backwards compatibility / to ease transitioning, you may want to tag child templates with options.fn.hasBlockParams = true;, options.inverse.hasBlockParams = true;, etc. This will give the helper an opportunity to use an old API. For example in ember we will support the new syntax {{#each items in |item|}} but we might also want to accept the old pseudosyntax that Ember invented {{#each item in items}} if no block params were passed. Though it could probably just be inferred from arguments.length.

3) I think partial should reset the environment/stack frame/data/whatever :). I guess it mostly depends on how you are doing it now and what your users expect. This actually raises another issue: there's no syntax for defining block params on the root program. It may not be necessary in practice, but I'm not sure. I think @wycats has some insights on this topic.

kpdecker commented 9 years ago

@wycats When in compat mode, Partials will resolve through the parent scope in the same manner as other lookups. If in the default mode, only parameters explicitly passed via the partial hash construct.

For the non-compat mode I think its a no brainer that these should not apply. When compat mode is set I think there are some ways that we can have the similar behavior with minimal additional overhead but I'm not sure if it matter that much.

@mmun 1) I had not thought of that case but it certainly adds a lot of complexity. What happens with chained helpers? Who is the provider of the final set of parameters?

mmun commented 9 years ago

I'm not too sure how chained helpers work. Are there other use cases besides if? How does the fall through to the next else work one?

kpdecker commented 9 years ago

Chained helpers are basically syntactic sugar for this fall through construct:

{{#if foo}}
{{else}}
  {{#anyHelper}}
  {{else}}
  {{/anyHelper}}
{{/if}}

but are written as

{{#if foo}}
{{else anyHelper}}
{{else}}
{{/if}}

I think we are better off leaving this feature off of simple else cases as they are ambiguous and I'm not sure what the use case would be. The chained else should still support it in the positive case (we should check the parser for that).

mmun commented 9 years ago

Ah that makes sense. Leaving it off else is fine with me.

kpdecker commented 9 years ago

@mmun what API is being exposed to helpers for this in HTMLbars? Right now something like:

fn(context[key], { data: data, blockParams: [context[key], key] })

Is the winner for me personally. I don't want to play games with argv and having options at the end of the list like helpers implement now and the only other thing that comes to mind is some sort of setter API.

fn.blockParams([context[key], key]);
fn(context[key], { data: data })

Which I'm not certain that I'm keen on.

Also, getting into it, the question of if this should be conditional or not occurs. I.e. if someone does {{#each foo as |bar|}} should the context change or should bar be the only way to access the iteration object? More generically, should helpers be made aware that block params were passed or not?

mmun commented 9 years ago

First, HTMLBars has diverged in the template structure (and will probably diverge even more). The signature currently is

fn(context, options/environment, contextualDOMElement, arrayOfBlockParams)

We are planning on making templates first class objects soon so that we can store cached computations and reduce closures. Your second suggestion is interesting.

Secondly, In HTMLBars we are passing the number of block params to the helper (as options.blockParams) so that the helper can choose to yield only the values that the template has defined names for. It can also be used for backwards compatibility (by checking if options.blockParams is 0 or undefined or something).

We are not passing the names of the block params because that will inevitably lead to people yielding different values based on the name. Unfortunately, knowing the number of block params still is a bit sketchy because a helper-writer may choose to permute the arguments based on the number of block params, e.g. {{#each items as |item|}} and {{#each items as |index item|}}. But ultimately performance won over "correctness".

kpdecker commented 9 years ago

Signature: Ok, sounds like things are distinct enough (there are numerous helper differences anyway, that was one of the sticking points of the merge) Number: Agreed that it should be available Names: Also agreed that it should not be available. That fully breaks the whole premise of being able to provide distinct names for nested args.

String params mode will be an interesting one to handle. Does Ember plan on supporting classical Handlebars in the first release that has block params or is everyone expected to be running HTMLbars on upgrade?

kpdecker commented 9 years ago

Released in 3.0.0

ErisDS commented 9 years ago

Could I please trouble someone for a little help understanding how this works? I've been trawling through the code changes, tests, and trying it out, and although I've got it working, I'm not sure on a few things. I'll explain what I understand so far, and hopefully this'll end up being a sort of documentation :grin:

Taking the first block param test as an example:

it('should take presedence over context values', function() {
    var hash = {value: 'foo'};
    var helpers = {
      goodbyes: function(options) {
        equals(options.fn.blockParams, 1);
        return options.fn({value: 'bar'}, {blockParams: [1, 2]});
      }
    };
    shouldCompileTo('{{#goodbyes as |value|}}{{value}}{{/goodbyes}}{{value}}', [hash, helpers], '1foo');
});

Inside a block helper, if a block param was specified when the helper was called, we now get access to options.fn.blockParams which tells us how many block params the user asked for. In this case it was just 1, which was declared as value.

When we call fn, we pass the context as the first argument, and can now pass a blockParams option as a property on the second (options?) argument. blockParams should be an array.

The first item of the array is the data we want to resolve for our blockParam. In the the test, the value block param is going to resolve to 1. The test shows that 1 gets output instead of bar which is the value of value in the context, asserting that the block param takes precedence.

What I don't understand, is what the second item of the array 2 is for? Is this purely for testing purposes to show that passing extra block params doesn't do anything, or does it mean something else?

The #each implementation passes an array with 2 properties, essentially the value and the index as you'd expect. It does this regardless of whether 1 or 2 block params are declared when calling the helper. I guess it's simpler to always pass both rather than checking to see what was declared.

The #each helper also adds a path property to the blockParams array, which so far for me always evaluates to [ NaN, null ]. I'm not at all sure what this path property is for, or when it comes into play - it seems to be related to contextPath but I'm not clear on what that is either!

Basically, I'm trying to figure out exactly what's needed and why in order to determine what might be necessary in a custom helper. Thanks in advance!

kpdecker commented 9 years ago

@ErisDS we need to do a better job of documenting this for implementors of helpers.

Your analysis is pretty much spot on, and it might be a good thing for us to pull this into the formal docs.

Yes, 2 is strictly for testing. Wanted to make sure that it didn't blow up if additional values are passed and didn't want the extra time cost of another test for that case explicitly.

Context path is a little used feature similar to the strings mode used by Ember. This is a little used feature that allows code to attempt to lookup a data field from the root context value based on handlebar's iteration. I believe the only consumer of this is Thorax's SSJS implementation and you can likely ignore if you aren't touching that.

ErisDS commented 9 years ago

Context path is a little used feature similar to the strings mode used by Ember. This is a little used feature that allows code to attempt to lookup a data field from the root context value based on handlebar's iteration. I believe the only consumer of this is Thorax's SSJS implementation and you can likely ignore if you aren't touching that.

Do you have a reference link to any sort of example of this in action that I can take a peek at? If I manage to understand it I'll have a go at writing up an in depth explanation.

Just an FYI - I wrote some documentation on Handlebars for Ghost theme developers: http://themes.ghost.org/docs/handlebars not sure if it's helpful but please steal from it if it is.

kpdecker commented 9 years ago

@ErisDS the built in helpers implement context path tracking and there is test coverage. https://github.com/wycats/handlebars.js/blob/master/spec/track-ids.js#L140. The only existing documentation is https://github.com/wycats/handlebars-site/blob/master/src/pages/reference.haml#L338.

kpdecker commented 9 years ago

@ErisDS wycats/handlebars-site@33feeb9 has an attempt at this. I always feel like my documentation skills are lacking so any input is appreciated. I actively chose to ignore the context path behaviors in the docs as I think it's a very niche use case that will only confuse further in the basic docs. If we do provide better docs, then it's probably better to document it in a section specific to that.

Bertrand commented 8 years ago

@kpdecker I'm currently integrating this in handlebars-objc, and from what I understand from JS implementation, context provided by the helper can be different from the first block parameter. I find this very confusing and I cannot find any useful use case.

Did you have anything specific in mind?

Bertrand commented 8 years ago

@kpdecker Oh,maybe you wanted block helpers to be able to set named block params without changing current context. Is that it?

kpdecker commented 8 years ago

Context and block helpers are completely separate. That they match in each is just a function of that specific helper.

Bertrand commented 8 years ago

@kpdecker yep, that is what I figured out. I was just trying to find an example of a helper where this was both useful and not totally confusing for users.