Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RFC: Block params #3

Merged
merged 1 commit into from
Nov 20, 2014
Merged

RFC: Block params #3

merged 1 commit into from
Nov 20, 2014

Conversation

mmun
Copy link
Member

@mmun mmun commented Aug 18, 2014

Introduce block parameters to the Handlebars language to standardize context-preserving helpers, for example:

{{#each people as |person|}}
  {{person.name}}
{{/each}}

Rendered View

@knownasilya
Copy link
Contributor

Interesting use of Rubyism here, I kind of like it.

@wycats
Copy link
Member

wycats commented Aug 19, 2014

@mmun can you describe how this would work in HTML syntax?

@mmun
Copy link
Member Author

mmun commented Aug 30, 2014

@wycats I thinks the HTML syntax merits its own RFC first. A few of my own questions about the HTML syntax:

  • Are we sticking with hyphenated tags ↔ components?
  • How do we support namespaced components i.e. blog/disqus-comment?
  • How do we distinguish between attrs and props? Values and strings?

@amiller-gh
Copy link

Not a big fan of this syntax... especially if HTMLBars is being designed to be used internally in multiple projects.

For one, the @ expressions will confuse long time Handlebars who are used.

Second, it doesn't fix the problem that "Custom syntaxes are not in the spirit of the Handlebars language and require the consumer to know the special keywords per helper." It just moves the special knowledge requirement to a less semantic syntax.

Correct me if I'm wrong, but this syntax:

{{#for-each obj |key, val|}}
  {{@key}}: {{@val}}
{{/for-each}}

Still requires the developer to know that the for-each helper takes two params, the first being the key and second being the value and is no less complicated than {{for-each obj as key and value}}, just harder to read.

Just like with choosing any API to work with, the onus should (and with this syntax would still be) on the developer to know and understand the syntax presented to them by the helpers they load on the page. How these helpers' syntaxes are handled should be dictated by the author of the helper and all their use cases can be covered by by Handlebar's existing @data syntax.

If a helper injects values into the rendered block's context, like the default {{@index}}} or {{@first)) data variables in Mustache, or provides a custom syntax to use in the mustache itself, like Ember, the responsibility is on the developer to know how to use the helpers he/she chose. If a helper messes with your context too much, or provides a confusing custom syntax, then just like how we choose not to use bad APIs, these helpers will be ignored for those with better defined syntaxes.

Also, it means we don't have to add a large amount of functionality to Handlebars, a framework used in many other projects besides this.

By allowing block helpers to inject @DaTa into the context of the blocks they are rendering, and letting template authors access @DaTa expressions in their templates: tildeio/htmlbars#98 (comment) we achieve this flexibility with a minimally invasive change.

@knownasilya
Copy link
Contributor

@EpicMiller I think this is a proposal for Ember as an extension to Handlebars, so only those using Ember with Handlebars will get this.

Second, it doesn't fix the problem that "Custom syntaxes are not in the spirit of the Handlebars language and require the consumer to know the special keywords per helper." It just moves the special knowledge requirement to a less semantic syntax.

Ember already adds a good amount of custom handlebars syntax, so this isn't really an issue.

@mmun
Copy link
Member Author

mmun commented Oct 1, 2014

For one, the @ expressions will confuse long time Handlebars who are used.

Block params have the exact same semantics as the existing data vars:

Handlebars:

{{#each items}}
  {{@index}}: {{this}}
{{/for-each}}

Proposal:

{{#each items |item, index|}}
  {{@index}}: {{@item}}
{{/for-each}}

It just gives you a standardized way to name a yielded variable.

Second, it doesn't fix the problem that "Custom syntaxes are not in the spirit of the Handlebars language and require the consumer to know the special keywords per helper." It just moves the special knowledge requirement to a less semantic syntax.

It absolutely does. You will need only to learn only one standardize API to access data yielded by the helper. It also makes writing a helper that yields data simpler as well: you don't have to worry about keywords or clobbering the context scope.

Using pseudo-paths like "as" is an abuse of the language that I don't support. It adds unnecessary complications to stuff like Ember's each helper.

Correct me if I'm wrong, ... Still requires the developer to know that the for-each helper takes two params

No one is claiming it doesn't. It's just like forEach(function(item) { ... });. I think it's a lot more intuitive to use established JavaScript params orderings then pseudo-paths that break the handlebars syntax contract.

If a helper injects values...

I think you are missing the point that the helper should not decide the name of the variable. It should be specifiable in the template.

@mmun
Copy link
Member Author

mmun commented Oct 1, 2014

If it's not clear, in the Handlebars case this feature would be built by using data, not some parallel data-like construct. Again, it's just a way of letting the developer decide what to name the yielded values.

@mmun
Copy link
Member Author

mmun commented Oct 1, 2014

I'm not tied to the @ syntax. It was originally chosen so that it would work as an (almost) drop in replacement. It's completely feasible to have block params work without them:

{{#each items |item|}}
  {{item.foo}}
{{/each}}

In fact, since Ember doesn't support @foo variables, we've toyed with the idea of hi-jacking them for a completely different purpose. More on that later...

Ultimately the @ prefix was added to avoid collisions with properties on the context. If you'd have been able to name your own block params I expect that it would not have ever been added.

@guilhermeaiolfi
Copy link

@mmun in your own example you use |key, value| and |item|, it's confusing as @EpicMiller said. The order and the number of params seems to matter.

What if by default it worked as @EpicMiller suggested and you'd have the ability to change the name of them like so:

{{#each items @item=value @index=id}}
    {{@id}} - {{@value.foo}}`
{{/each}}

It seems easier to follow.

@mmun
Copy link
Member Author

mmun commented Oct 1, 2014

What is confusing about it? They're different helpers. [].forEach and [].reduce also have different calback signature.

@guilhermeaiolfi Your proposed syntax doesn't make sense to me. The whole point of this PR is that the value of @item and @index is provided by the helper, not via the parent context (value and id here are part of the parent context which is invoking the each helper).

@guilhermeaiolfi
Copy link

Ops, sorry. My idea is same, but I missed quotes:

{{#each items @item="value" @index="id"}}
    {{@id}} - {{@value.foo}}`
{{/each}}

It's just a way of naming the vars available inside the block: @WHAT_TO_NAME="NAME_INSIDE_THE_BLOCK"

@mmun
Copy link
Member Author

mmun commented Oct 1, 2014

Oh, I see what you mean. Most languages only support yielding ordered values. I think that's simpler than yielding named ones.

{{#each items @item=foo}}
  {{foo.name}}
{{/each}}

vs.

{{#each items |foo|}}
  {{foo.name}}
{{/each}}

That said, your proposal is certainly a valid one.

@amiller-gh
Copy link

I do like the idea of yielding the name at the request of the developer. That way the user can consume the default functionality of the helper if they want, without worrying about custom names, but still have the choice of customizing the helper's default interface if desired. So this:

{{#each people as person}}
  {{person.name}}
{{/each}}

would become

{{#each people}}
  {{@key}} - {{@item.name}}
{{/each}}

or

{{#each people @item=person @key=id}}
  {{id}} - {{person.name}}
{{/each}}

Sorry, literally just re-iterated what guilhermeaiolfi said.

I don't know if the unordered values make sense to me though. Data attributes inside of block helpers don't have an inherent order to the developer using them. To the person making use of the helper it works more like an object/associative array rather than an indexed array.

However, yielding named values runs the risk of getting rather verbose, although its definitely the more legible options over the ordered value options.

@cibernox
Copy link
Contributor

cibernox commented Oct 2, 2014

I have not idea of what kind of syntax tricks handlebars can handle, but even being a ruby developer I find this a bit awkward compared with all the helpers I've seen so far.

I would try to preserve the style we have right now, with something like @EpicMiller sayd:

{{#each people as person}}
  {{person.name}}
{{/each}}

{{#each obj as (name age)}}
  {{name}} has {{age}}
{{/each}}

@stefanpenner
Copy link
Member

some talk about using @ as a namespace mechanism has come up again. I think its a good delimiter for that..

@mmun
Copy link
Member Author

mmun commented Oct 3, 2014

@cibernox If that's what @EpicMiller meant then I'm definitely on board. The syntax is by no means set in stone and as seems like a great choice. I think the parser would need to be modified to support as ... after the hash. I don't think the brackets are necessary either. I also worry that as is not semantically correct in all cases, but I'm not sure of a counter-example.

Here are some examples of the kind of style I'd like to be able to do, using your syntax:

{{#each items tagName="ul" as item}}
  {{#with item.owner as owner}}
    {{item.name}}: {{owner.name}}
  {{/with}}
{{/each}}
{{#each products as product}}
  {{#form-for product as f}}
    <h2>{{product.name}}</h2>
    <p>{{f.input "description"}}</p>
    <p>{{f.submit}}</p>
  {{/form-for}}
{{/each}}
{{#tab-view as t}}
  {{#t.pane as p}}
    {{#p.header}}...{{/p.header}}
    {{#p.content}}...{{/p.content}}
  {{/t.pane}}
  {{#t.pane as p}}
    {{#p.header}}...{{/p.header}}
    {{#p.content}}...{{/p.content}}
  {{/t.pane}}
{{/tab-view}}

@jayphelps
Copy link

I'm not a ruby guy...Help me out here.

How is this different than:

{{#each person in persons}}

Or is it more like destructuring?

{{#each person { name, age } in persons}}
{{#each { name, age } in persons}}

I think I'm completely misunderstanding, even after rereading the ruby docs multiple times.

@jayphelps
Copy link

Maybe I get it now. It's all about the existing helpers losing access to the parent context if they need to also provide their own, right? Hence a new syntax to keep the existing behavior intact when desired + compat?

@mmun
Copy link
Member Author

mmun commented Oct 3, 2014

@jayphelps Yes. {{#each person in persons}} was designed to not lose the parent context. It works, but its a hack which should be standardized. Scoping is very important. ../../foo is not a valid solution. It requires too much mental bookkeeping.

@amiller-gh
Copy link

@mmun , I like it, as definitely seems like the right solution for preventing context switching.
And using @ as a namespace is certainly a concept worth exploring. Do we allow helpers free reign over the @ context in a scope to provide data attributes in the block's context?
so:

{{#each list}}
  {{@index}} - {{title}}
{{/each}}

and:

{{#each list as item}}
  {{item.@index}} - {{item.title}}
{{/each}}

@mmun
Copy link
Member Author

mmun commented Oct 10, 2014

Update: So far we're leaning towards

{{#each items as item}}

and

{{#for-each hash as (value key)}}

Notice that we opted for the bracketed approach when yielding multiple values. We found the syntax {{#for-each hash as value key}} a bit difficult to visually parse. We also think that that yielding multiple values will probably not be very common.

This RFC would still require parser changes to Handlebars. The desired block mustache grammar should look roughly like:

"{{" helperName params? hash? ("as" blockParams)? "}}"

where blockParams = ID | "(" ID+ ")"

Great suggestion @EpicMiller and @cibernox :)

@amiller-gh
Copy link

Still not convinced we need to specifically yield values like that... I think yielding multiple values will be far more common than you think. The default #each helper in handlebars defines @key, @value, @index, @first and @last in its block's scope, all of which are useful in their own right (and all of which I have consistently wanted when coding in ember, there are countless articles online detailing various ways to add them: http://mozmonkey.com/2014/03/ember-getting-the-index-in-each-loops/). There is no good mechanism for doing this with the above syntax if a helper author wants to provide that level of additional data in their block helper.

Rather than specifically yielding values, I like @stefanpenner 's approach of providing the @ context to helpers for all of their context specific values. Its easy to add in htmlbars. The handlebars AST treats data expressions differently: tildeio/htmlbars#98. If htmlbars provides a special treatment for data expressions where they resolve from data defined by the helper - perhaps defined on the options argument - rather than the context the template is rendered in, then the framework can provide a sandboxed mechanism for helpers to define any type of data arguments the author sees fit.

@mmun
Copy link
Member Author

mmun commented Oct 10, 2014

You haven't accounted for nested blocks. This is why you must name your params.

@cibernox
Copy link
Contributor

If it makes the parsing simpler because parenthesis are used for expressions, I would also be happy with any other delimiter like | or <>. I just feels that they should be visually grouped, I don't really care to much about how.

{{#each obj as |key value|}}
{{#each obj as <key value>}}

@amiller-gh
Copy link

Nested blocks:

{{#each building as apartment}}
  {{#each apartment.tennants as roomate}}
    {{roomate.name}} is roomante number {{roomate.@index}} in apartment number {{apartment.@index}}
  {{/each}}
{{/each}}

If you didn't have an as statement preserving the context it would always be in the context of the inner most block

{{#each building}}
  {{#each apartment.tennants}}
    {{name}} is roomante number {{@index}}
  {{/each}}
{{/each}}

@guilhermeaiolfi
Copy link

I think @EpicMiller's suggestion is easier to read and would be easier to undestand specially for very nested blocks. You wouldn't have to come up with different params names like @index_apartment. The values would be kept in the context of the block and available for the inner ones.

@stefanpenner
Copy link
Member

, I like @stefanpenner 's approach of providing the @ context to helpers for all of their context specific values.

i don't remember saying this

@mgenev
Copy link

mgenev commented Mar 23, 2015

@MajorBreakfast
What you wrote here:

<todays-date yields day month year>
  {{day}} / {{month}} / {{year}}
</todays-date>

makes a whole lot more sense than what shipped and is shown in the blog here:

{{#x-customer customer=model birthday=model.birthday as |name age|}}

I fully agree with @donaldpipowitch and @zeppelin that it's confusing and unreadable when there are attributes in between the component name and the 'as'

@knownasilya
Copy link
Contributor

I would disagree, having used the as syntax, it actually is pretty nice, the pipes really help distinguish the yielded values, where as the syntax you mentioned would just be mixed in with everything else, and would create unnecessary confusion.

@mgenev
Copy link

mgenev commented Mar 23, 2015

@knownasilya
This reads well:
{{#x-customer as |name age| customer=model birthday=model.birthday }}
This doesn't:
{{#x-customer customer=model birthday=model.birthday as |name age|}}
Especially since it can become:
{{#x-customer customer=model birthday=model.birthday something=else another=thing more=stuff yet=more attributes=keep piling=and it=doesnt read=well as |name age|}}

@knownasilya
Copy link
Contributor

But this reads even worse:

<todays-date yields day month year required>
<todays-date>

which of these are the yielded attributes? Change it up..

<todays-date required yields day month year>
<todays-date>

What's what? Just trying to parse that visually is a mad headache..
So hopefully you aren't writing your components as one liners if they are that long..

@stefanpenner
Copy link
Member

Just an FYI, we have settled on and I am pretty confident it is finalized as such.

<todays-date as |day month year|>

</todays-date>

@cibernox
Copy link
Contributor

I agree. It's done, merged and released in stable for a while, so there is no point in keep discussing what at the end are just subjective preferences.

It is a know axiom in computer science that there will never be total consensus about what is the best syntax for a given feature.

@knownasilya
Copy link
Contributor

@cibernox not just in computer science 😉

@mgenev
Copy link

mgenev commented Mar 23, 2015

@stefanpenner @cibernox well the reason, i'm bringing it up, is that from the example on the release blog, which is the only place the public has it documented, it really is confusing to the point it makes no sense, even EmberSherpa tried to explain it here https://www.youtube.com/watch?v=8GMeMM0ukYM&t=2345 and basically gave up

@cibernox
Copy link
Contributor

@mgenev can you link the second where embersherpa tryes to explain it? I've found the syntax extremely intuitive (I have ruby background)

@mgenev
Copy link

mgenev commented Mar 23, 2015

Ah sorry, my link didn't work here https://www.youtube.com/watch?v=8GMeMM0ukYM&t=2345, updated above too

@stefanpenner
Copy link
Member

one can think of yielding, as calling a method which calls your callback

<foo-bar as |baz cuz|>

</foo-bar>

is more or less

/* content of fooBar */ = fooBar(function(baz, cuz) {
  return /* content for fooBar */
})

but in the world of HBS templates.

@cibernox
Copy link
Contributor

I think that the problem in that video is that he was trying at the same time to explain block params and the component helper.

In any case <todays-date yields day month year> and <todays-date as |day month year| > are conceptually the same, just a different syntax flavour. I prefer to delimit the yielded values to make them visually grouped and easy to distinguish from regular attributes like required or checked.

@stefanpenner
Copy link
Member

In any case and <todays-date as |day month year| > are conceptually the same, just a different syntax flavour. I prefer to delimit the yielded values to make them visually grouped and easy to distinguish from regular attributes like required or checked.

ya other wise it becomes key-word soup

<my-component require as foo bar baz class="bar" id="foo">

</my-component>

vs

<my-component require as |foo bar baz| class="bar" id="foo">

</my-component>

and more nicely represented as

<my-component require class="bar" id="foo" as |foo bar baz| >

</my-component>

@knownasilya
Copy link
Contributor

@mgenev @cibernox want cleaner examples? Help me here to iron out the component composability guide emberjs/guides#66

@mgenev
Copy link

mgenev commented Mar 23, 2015

I actually like the pipes delimiter a lot.

@stefanpenner why do you think:

<my-component require as |foo bar baz| class="bar" id="foo">

</my-component>

is more nicely represented as

<my-component require class="bar" id="foo" as |foo bar baz| >

</my-component>

since the vars in the pipes refer to what the component yields, not the attributes that precede the 'as' in the 2nd example, in my mind it's a lot more readable the first way

@taras
Copy link

taras commented Mar 23, 2015

@mgenev It was hard for me to explain because like @cibernox said, I was explaining component helper & yield at the same time, but also Ember.computed.oneWay. I underestimated how hard that demo might be to grok.

Ember.computed.oneWay + {{yield oneWayBoundProp}} is a pattern that is useful and should have been explained separately.

@stefanpenner
Copy link
Member

since the vars in the pipes refer to what the component yields, not the attributes that precede the 'as' in the 2nd example, in my mind it's a lot more readable the first way

the last step is just my personal preference, but our syntax allows both. So you can do what suites the situation the best.

@knownasilya
Copy link
Contributor

@taras can you checkout emberjs/guides#66 and maybe leave some feedback about the oneway binding and yields?

@mgenev
Copy link

mgenev commented Mar 23, 2015

@stefanpenner in that case i think a good guide is just fine

@mgenev
Copy link

mgenev commented Mar 23, 2015

btw i'm not just trolling for no reason, even if my communication may come off that way (i'm from the balkans), I really needed clarity because I'm the lead on this book http://www.sanestackbook.com/ and maintain ember demo projects and a stack with hundreds of stars and dozens of forks,

@knownasilya
Copy link
Contributor

@mgenev thanks for that, I've been waiting for someone to integrate Sails and Ember in a nice way (non book link)

@rwjblue rwjblue mentioned this pull request Sep 11, 2015
chancancode pushed a commit that referenced this pull request Oct 27, 2017
pzuraq referenced this pull request in pzuraq/emberjs-rfcs Nov 26, 2018
rwjblue pushed a commit that referenced this pull request Jun 12, 2020
kategengler pushed a commit to kategengler/rfcs-1 that referenced this pull request Jul 31, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet