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

Nested Glimmer Syntax + attr/component-centric slots #203

Closed
wants to merge 6 commits into from

Conversation

machty
Copy link
Contributor

@machty machty commented Jan 20, 2017

Rendered

By submitting feedback to this (likely to be controversial) RFC, you are implicitly agreeing to the statement below:

I, Ember enthusiast, have, to the best of my efforts, fully absorbed the nuances of the {{yield}} vs {{component}} debate, and I understand machty's concerns about the fragmentation that would occur if we continued to build out the {{yield}} API in such a way that "named yields" were not effortlessly interchangeable with the attr=(component ...) pattern.

If you're interested in more real-time discussion of this RFC, head to the #topic-forms channel in the Ember Community Slack.

@rtablada
Copy link
Contributor

I like the idea of nested syntax in templating.
However my two reservations are this (as you pointed out in your RFC):

  1. The component helper is a pretty loaded terminology (even though I agree that it could be smarter by being able to render arbitrary templates as "DOM stuff" instead of requiring it to be a named component or component instance)
  2. I think for single block components the yield keyword is much easier to understand for students and starting devs. I think changing this drastically in the way we teach components would be a big strike against this. It also strikes a pretty different feel compared to existing block structures like for statements (actually this was a big selling point for things like Smoke and Mirrors where complex behavior looked like a regular for loop rather than a strange map function).

Instead of just reiterating what's already pointed out in the RFC and echoing it back, here's maybe some quick thoughts on ways around this:

  1. Still shim the component helper in the way described, listening to Godfrey's discussion on Glimmer 2 this more closely mimics what Glimmer considers a "component" anyway. But! A new helper has appeared! I'm thinking maybe child or display (hey it's late and I'm bad at naming things). This helper would be the same as the component changes listed but are an alias for better understanding of what is happening when displaying fragments with named attrs.

  2. Continue teaching yield for where it is appropriate and primary component outlets. This matches the props.children that is common when using JSX. I think that we then have a separate teaching section on using the nested glimmer syntax. I think there are even cases where you would want to use both a yield and the new nested syntax. And with that I will put what I think could be a really cool improvement on the mdl-table component:

{{#mdl-table header=(|data| 
  {{#mdl-table-heading}}Id{{/mdl-table-heading}}
  {{#mdl-table-heading}}Name{{/mdl-table-heading}}
) content=rows as |row|}}
  {{#mdl-table-col}}
    {{row.id}}
  {{/mdl-table-col}}
  {{#mdl-table-col}}
    {{row.name}}
  {{/mdl-table-col}}
{{/mdl-table}}

Compare this to the existing syntax which requires parent component support and child shouldRegisterToParent which IMO is a MUCH more difficult mental model to understand:

{{#mdl-table content=rows as |row|}}
  {{#mdl-table-col label='Id'}}
    {{row.id}}
  {{/mdl-table-col}}
  {{#mdl-table-col label='Name'}}
    {{row.name}}
  {{/mdl-table-col}}
{{/mdl-table}}

@sclaxton
Copy link

I think there is some real cognitive dissonance in our UI programming model that you're getting at here: we want to treat components like any arbitrary data that can passed around in templates but also like markup that adds semantic structure to our templates. This proposal pushes us more towards the react model of treating all components and markup like data that can be passed around arbitrarily, whereas yield and named slots pretty much keeps us where we currently are just with slightly extended syntax and capability. Additionally, with this RFC we will basically nix nesting components the same way we nest html and will also have the freedom to intermingle data attributes and nested markup assigned to attributes, neither of which I'm crazy about.

Personally, I'm not convinced that going to one extreme of our cognitive dissonance about components being markup vs. data just for consistencies sake of having OneTrue(TM) model is a great idea. Instead, maybe we can manage the dissonance with established, recognizable patterns like contextual components and the like?

@machty
Copy link
Contributor Author

machty commented Jan 20, 2017

@sclaxton

with this RFC we will basically nix nesting components the same way we nest html and will also have the freedom to intermingle data attributes and nested markup assigned to attributes

There is no "same way as html" to pass multiple chunks of markup to an element. There is only a moving target of a Web Component's RFC that many are not particularly enthusiastic/optimistic about. Slots require syntactical extension just as this RFC does.

As for data/markup intermingling: we already have this with attr=(component ...), and it is one of the better / more powerful features added to Ember in the last 2 years. Allowing it to mature into supporting template blocks that close over scope does not fundamentally change Ember's story of data/markup-intermingling. And in my personal experience, my team and my apps have not benefited from Ember's attempts to draw a sharp line between markup and data (quite the opposite, to be honest).

I do not share your optimism that the dissonance can be managed and tamed by recognizable patterns; Ember already has too many split-brained APIs where a problem can be solved 90% of the way by one pattern/feature and 91% of the way by another separate/feature with ever-so-slightly different semantics; having to explain why/when to use {{yield}} vs why/when to use {{component}} is just one example among many, and I think we're overdue for some hard-lined decisions about whether such separate but similar abstractions are really benefiting the ecosystem.

@sclaxton
Copy link

sclaxton commented Jan 20, 2017

@machty

There is no "same way as html"

I guess I meant "in a similar way to html", meaning in the same spirit.

There is only a moving target of a Web Component's RFC

There is more precedent for slot syntax than just web components. The following essentially has the same block syntax/semantics (sans block params) as my proposal for slots:

{{#if something}}
  {{! stuff }}
{{else if somethingElse}} 
  {{! other stuff }}
{{else}} 
  {{! more stuff }}
{{/if}}

This is something that everyone in Ember uses always.

As for data/markup intermingling: we already have this with attr=(component ...)

I would say that's a different level of intermingling than this RFC. attr=(component ...) is treating template as data, yes, but at least it doesn't structurally intermingle data and markup. By structural intermingling I mean nesting data as if it had a hierarchical HTML-like structure, but it doesn't in fact have that structure. This RFC is essentially relying on the formatting of templates to make them readable rather than the actual syntactic/semantic structure.

having to explain why/when to use {{yield}} vs why/when to use {{component}}

I'm not sure I've ever encountered anyone asking when to use {{yield}} over {{component}} or vice versa. That's confusion that just doesn't exist, at least in my sphere of people working with Ember. They are different tools that accomplish different use cases. Why does there have to be one tool that accomplishes all use cases?

@machty
Copy link
Contributor Author

machty commented Jan 21, 2017

@sclaxton (we've probably discussed most of this in the Slack channel but good to share here)

I 100% agree that your (and some others') slots proposals look way more familiar and Ember-y than what I'm proposing. I also agree that the fact that this proposal is such a shift from the evolutionary momentum of certain Ember patterns is one of the major drawbacks, and certainly shouldn't be taken lightly.

attr=(component ...) is treating template as data, yes, but at least it doesn't structurally intermingle data and markup

The debatable word here is "structural"; I think we'd do ourselves a disservice to limit our concept of "structural" to that which happens to be <div>nested</div> between the opening and closing tags vs within the opening tag itself (as an attr). All acknowledgments that I know I'm proposing a change in conventions aside, I don't think there's a strong distinction between both concepts if we have the syntax to support the latter.

Why does there have to be one tool that accomplishes all use cases?

So that we don't have to teach as many concepts and so that more things seamless fit together in Ember. Multiple concepts require translation layers between wherever you bridge between the two concepts. The RFC makes a strong case that building off of yield would fragment the ecosystem between attr=(component) and yield in a way that could only be patched over by boilerplate translation layers between positional params and attrs.

@webark
Copy link

webark commented Jan 21, 2017

Would you say the place of partials could come into play here? Cause speaking just for me, and i know it's really just sugar in actuality, but if you where to just pull out these blocks and put them into a sperate partial file, i'm able to swallow the concept easier. Would you consider the partial/separate file approach able to live alongside the more inline approach? That way it might fulfil the desires of the "practicalists" while being the "spoon full of sugar" for the "purists"?

@machty
Copy link
Contributor Author

machty commented Jan 21, 2017

@webark Partials I think are generally considered an anti-pattern, since the way they're essentially passed the entire template scope makes them very brittle and difficult to reuse in different contexts.

Also, the attr=(component 'x-foo') API that we support today is already an established example of "moving the template blocks into a separate file", and that approach would be able to work seamlessly alongside what I'm proposing. But 1) there's no inherent virtue in Ember forcing you to keep templates in separate files if you're not reusing that code in a separate place in your app, and 2) the ability for blocks to close over template scope within the same file (and yet be provided additional data via block params) is an extremely nice pattern that the community has already coalesced around.

@les2
Copy link
Contributor

les2 commented Jan 21, 2017

@machty I bet someone skilled enough with AST transforming stuff could create an addon that doesn't even modify glimmer or anything and implement this proposal.

The addon would need to:

  1. Process all handlebars files looking for instances of the new syntax.
  2. Transform the new syntax by generating a component hbs file and adding that to the broccoli tree

For example, the generated components could be registered using a name like:

component:<source-component>-ember-cli-component-slots-component-<N>

where <source-component> is the name of the component the slot occurs in and <N> is the position of the occurrence in that file.

Then we could start using this today! This is awesome!

@webark
Copy link

webark commented Jan 21, 2017

@machty i totally get that it's a perception thing. I understand that partials are considered an "anti-pattern" but it's currently the only way to do code reuse in a given template. And i thought partials weren't passed the whole scope in, but grabbed and executed in place.

Cause for me, even though i understand the power of it, it looks like poor form to be passing html into an attribute. I get that's it's basically the same thing as passing a component using the helper, but it just looks kind of like old callback hell, that we all (and i can imagine for you specifically) shutter at a bit when we see. I'm just trying to think of ways that we can keep the power of this pattern, but have it look better (at least to me).

@machty
Copy link
Contributor Author

machty commented Jan 21, 2017

@les2 I thought of something similar but got stuck on the following: how would you pass in the closed-over scope?

{{title}}
{{#x-foo
    header=(|data|
      I too can access {{title}} via closed-over scope
      (even if i hadn't previously referenced it
      outside of this template block).
      But if broccoli moved me to another file, how
      would it have access to `{{title}}` (and `{{data}}`)?
    )
/}}

@machty
Copy link
Contributor Author

machty commented Jan 21, 2017

@webark Do you consider the following to be callback heck?

{{x-foo as |a|}}
  {{x-bar as |b|}}
    {{x-woot as |c|}}
       <p>Hello! {{a}} {{b}} {{c}}</p>
    {{/x-woot}}
  {{/x-bar}}
{{/x-foo}}

Because that's valid (and community-validated/adopted) syntax today and solves many use cases for which only one/main/default template block needs to be passed, but it's still semantically the same degree of "callback-y-ness" and closed over scope as what this RFC proposes.

@machty
Copy link
Contributor Author

machty commented Jan 21, 2017

@webark / @sclaxton

Here's an experiment where my attr/component-centric semantics are combined with the slot syntax that @sclaxton came up with in his RFC. Instead of the recursively-nestable syntax that I proposed (e.g. header=(|a| <h1>...</h1>), we'd use a slot syntax that looks more like traditional yielded template blocks, but as per my proposed semantics they'd still be passed into the component as attrs rather than in the magical hidden inaccessible-to-js namespace of blocks, and they'd be rendered with {{component}}.

This is a translation of the Recursive Nesting example from my RFC.

{{#x-foo other=data
         :header as |a|}}
  I am a fragment {{a.title}}
  <span>Woot</span>
{{:body as |a|}}
  I am a fragment {{a.title}}
  <span>Woot</span>
  
  {{#x-foo other=data
    :header as |b|}}
      I am a fragment {{b.title}}
      <span>Woot</span>
    {{:body as |b|}}
      I am a fragment {{b.title}}
      <span>Woot</span>
    {{:footer as |b|}}
      I am a fragment {{b.title}}
      <span>Woot</span>
    {{/x-foo}}
    
{{:footer as |a|}}
  I am a fragment {{a.title}}
  <span>Woot</span>
{{/x-foo}}

It's hard to tell if what I'm proposing is semantically scary or only syntactically scary, so maybe this experiment will help answer the question.

(fwiw if we like this, we'd probably need to come up with a reasonable angle-bracket syntax equivalent)

edit: major use case that this misses

Using bold headers because I keep forgetting this important use case: if we used this syntax then there'd be no way to use slot syntax to express an array of blocks, which is a strong use case for certain components that let you, say, define all the columns in a table component. Thanks to @mmun for pointing this out.

@mmun
Copy link
Member

mmun commented Jan 22, 2017

How do your syntax changes fit into vanilla Handlebars that doesn't have a concept of components or yielding?

@machty
Copy link
Contributor Author

machty commented Jan 22, 2017

I don't know.

@les2
Copy link
Contributor

les2 commented Jan 22, 2017 via email

@sclaxton
Copy link

sclaxton commented Jan 23, 2017

@les2 I'm doing more or less exactly this for my slots RFC. Look out for the polyfill in the next week or two once I get the testing solid ;)

Would be great if we could get syntax polyfills for both so we can get people to use them for experimental projects and such to get user-land feedback.

@machty
Copy link
Contributor Author

machty commented Jan 23, 2017

@les2 Ohhh so you're saying if I had a template block that looked like

  {{x-foo bar=(|a|
    <p>{{a.stuff}} {{closedOverData}}</p>
  )}}

Then the template block would be extracted verbatim into an anonymous component file and the above template would transform to:

  {{x-foo bar=(component 'anonymous-polyfill-123'
                              a=a
                              closedOverData)}}

I think that should actually work? That's really awesome if it does. @sclaxton how bout a bake off once the polyfill(s) come(s) together?

Example of recursive nesting polyfil translation
  {{x-foo bar=(|a|
    {{hello}}
    {{x-foo bar=(|b|
      {{a}} {{b}} {{borf}}
    )}}
  )}}

turns into:

  {{x-foo bar=(component 'anonymous-polyfill-123'
                 hello=hello
                 borf=borf
  )}}
// anonymous-polyfill-123.hbs
{{hello}}
{{x-foo bar=(component 'anonymous-polyfill-456'
                a=a
                borf=borf
)}}

// anonymous-polyfill-456.hbs
{{a}} {{b}} {{borf}}

TL;DR the broccoli plugin would basically have to start
from the innermost template, parse the AST, and collect
every possible property reference and pass it in.

@sclaxton
Copy link

@machty lets do this 😂

@machty
Copy link
Contributor Author

machty commented Jan 24, 2017

Gonna take a mental break from this RFC for a while, but got a polyfill working w the help of @sclaxton (based on @les2's idea) and got to try out a few things:

image

This RFC has many warts and is likely incompatible a lot of the optimizations Glimmer has planned for static blocks (such that based solely on the syntax for rendering a component, the compiler should be able to know which blocks have been passed), but let this RFC serve as a representation of what might be possible with a more dynamic / attrs-based approach.

@ming-codes
Copy link

There's something similar in Ember that already handles this use case: named outlet. It's the same idea. You have multiple slot/outlet in your template and you want to fill those slots/outlet with another template. You can do this as part of Route#renderTemplate hook with Route#render. Both of which IMO is underutilized.

Another idea I had was about extending the{{outlet}} helper to allow block form.

{{#outlet "main" as |model queryParams|}}
{{/outlet}}

@eccegordo
Copy link

@machty thanks for putting this RFC together. I stumbled into this by following my nose. Speaking from personal experience the nested template problem is something that many users might need but don't know how to describe the problem. It is amazing how many different implementations are out there solving similar but slightly different use cases.

I would humbly submit that an important first step is some really clarifying examples or demo project showing why this RFC (and the others) is important. Need to see tangible working demos of the pattern put into practice. A hallmark of your previous efforts is the super cogent documentation, ala ember concurrency, and all those jsbins you created for query params. If feasible, it would be helpful to reference a comprehensive ember cli demo project. Hopefully this could even be a way to hone the api. Otherwise I fear a lot of ink will be spilled and many of us will still be confused.

@machty
Copy link
Contributor Author

machty commented Mar 8, 2017

Thanks for the feedback @eccegordo; a lot of conversations have taken place since this RFC with the team working on Glimmer, and the present state of things is that something much closer to a "slot" / named block syntax is more likely to land. This proto-RFC has some details, and I recently presented at the Ember NYC meetup about the current state of things, and it's pretty out of sync with the ideas/direction of this RFC, so I think it's probably time to close.

Rest assured there will be a million interactive examples as we get closer to the finish line.

@machty machty closed this Mar 8, 2017
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

8 participants