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

Named Blocks #226

Merged
merged 1 commit into from
Jul 28, 2017
Merged

Named Blocks #226

merged 1 commit into from
Jul 28, 2017

Conversation

machty
Copy link
Contributor

@machty machty commented May 5, 2017

Rendered

I presented some of the ideas behind this RFC at the EmberNYC meetup; here's a video of that. (The proposed syntax has since changed though)

@alexlafroscia
Copy link
Contributor

alexlafroscia commented May 6, 2017

What are your thoughts on the difference between this proposed syntax and the "real" WebComponents v1 concept of "named slots"? The idea there is that your component includes something like:

<!-- my-component -->
<div class="a-wrapper">
  <slot name="a">
</div>
<div class="b-wrapper">
  <slot name="b">
</div>
<div class="default-wrapper">
  <slot>
</div>

and would be used like:

<my-component>
  <span slot="a">I end up in a-wrapper</span>
  <span slot="a">I also end up in a-wrapper</span>

  <span slot="b">I end up in b-wrapper</span>

  <span>I end up in default-wrapper</span>
</my-component>

I'm not sure how important it is to try to approximate the WebComponents version of the same idea, but it might be worth thinking about.

For more information, the MDN docs on <slot>:
https:https://developer.mozilla.org/en-US/docs/Web/HTML/Element/slot

@machty
Copy link
Contributor Author

machty commented May 8, 2017

@alexlafroscia we might very well support the web components version in the future, but as it stands, it's not very composable; for instance, you wouldn't be able to forward a slot to another component for rendering (whereas in this RFC you can just pass along the block to whomever since the blocks are just @args).

Also, a lot of people (myself included) aren't fans of the behavior that everything that isn't selected out of your "block" because the "default" block.

@ef4
Copy link
Contributor

ef4 commented May 8, 2017

I presume that the lack of any Javascript-side API for the block values is intentional because we don't have any designed public API for them. It may be good to say so explicitly, and we should make sure the implementation marks any methods on them as private.

Is <as> future-proof against the HTML spec? I guess we think its mandatory arg name is sufficient?

The RFC focuses on the multi-block use case, but the first thing I would use this for is actually block capture of the default block, like for

{{#to-elsewhere named="my-modal"}}
  Hello world
{{/to-elsewhere}}

by grabbing @default and sending it through a service. This is obviously a dynamic case that cannot be statically optimized -- it seems the implementation will need to be able to do the fast static thing when possible but still have a slower dynamic path.

This proposal sneaks @ arguments into curly components. That has the benefit of avoiding collision with any arguments named default or else on existing components. But I think we need to explain how you access them from Javascript. this.args.default? this['@default']?

@courajs
Copy link

courajs commented May 11, 2017

How is this related to glimmer's @args? I can't tell whether it's a completely different usage of the same symbol, or whether it's actually the same exact concept but used only in this specific place.
If it's related, can we add a note to How We Teach This?
If it's not, can we add a note to Drawbacks about overloading @ and potential confusion?

@cibernox
Copy link
Contributor

cibernox commented May 12, 2017

I'll leave my feelings of the currently proposed API in handlebars, as I think that the usage of as is confusing.

Traditionally we have been using as to precede the pipes on blocks. I understand as like a way of saying "the yielded things are now known by these names". A "naming" keyword.

I find the {{as @header |h|}} syntax backwards on that sense. Reasons:

  • The pipes are not preceded by as, as they used to.
  • as is the first keyword and is not followed by pipes.
  • as is defining a block but it's not preceded by #
  • Is <as in angle-brackets syntax safe/good-idea? This is a very minor concern tho, as at the end of the day our templates will be compiled and as is not going to be on the html.

Could we use a different keyword? I'm thinking on the block keyword, preceded by a #, and keep using as to precede the pipes?

This:

{{#x-foo as |a|}}
  @default block {{a}}
{{as @header |h|}}
  @header block. Referenced via @header in x-foo's layout
{{as @footer}}
  @footer block. No block params.
{{/x-foo}}

would become

{{#x-foo as |a|}}
  @default block {{a}}
{{#block @header as |h|}}
  @header block. Referenced via @header in x-foo's layout
{{#block @footer}}
  @footer block. No block params.
{{/x-foo}}

The semantincs of the proposal seem reasonable, is the syntax what seemed unaligned with what we have today.

@machty
Copy link
Contributor Author

machty commented May 13, 2017

We've taken in a lot of feedback over the last few days and it's clear that the <as>-based separator syntax is simply too awkward / ugly, and that we can do better. TL;DR, we're leaning more towards a syntax that looks as follows:

<x-modal>
  <@header>
    <span class="special-header">
      Header Text
    </span>
  </@header>

  <@main as |c|>
    <p>Modal Content {{foo}}</p>
    <button onclick={{c.close}}>
       Close modal
    </button>
  </@main>
</x-modal>

This syntax has become a front runner because:

  • It clarifies that @header and @main are just args that you're passing into the component along with another of the other "simpler" values that are passed into the component
  • Pre-existing syntax highlighters / auto-indenters handle it much more gracefully
  • It avoids a lot of the overhead/boilerplate of making you type <template> or <block>

There's still quite a bit to resolve but we're feeling pretty confident about this syntax. If you're interested in mulling over alternative syntaxes, make sure you take a look at the graveyard of alternative syntaxes that have been considered as part of this RFC.

@csantero
Copy link

@machty I'm guessing the following syntax would be invalid? (block params on the component's tag when in multi-block mode)

<x-modal as |x y z|>
  <@header>
    ...
  </@header>

  <@main as |c|>
    ...
  </@main>
</x-modal>

@mmun
Copy link
Member

mmun commented May 13, 2017

@csantero That's right.

@jonnii
Copy link

jonnii commented May 13, 2017

@machty do you envision people being able to do something like:

<x-modal @header=(component ...)>
</x-modal>

or maybe?

<@namedblock>
  shenanigans
</@namedblock>

<x-modal @header=@namedblock>
</x-modal>

@rwjblue
Copy link
Member

rwjblue commented May 13, 2017

@jonnii:

<x-modal @header=(component ...) />

Yes, the idea is that you can interchange between a closure component, a named block, and even a plain string.

So you might start with:

<x-modal @header="Some header text" />

Then realize you need more HTML for the header and update to:

<x-modal>
  <@header>
    <div class="hero">
      <img src={{heroImageSrc}}>
      Some header text
    </div>
  </@header>
</x-modal>

Then you might decide that you need an actual component to back the header and refactor to:

<x-modal @header={{component 'my-special-header' headerImage=heroImgSrc}} />

In all of these examples, the layout of x-modal might simply be:

{{@header}}

This same interchangeability does not exist (within this RFC) if you need to yield block params or hash arguments into the renderable. Currently, passing block params and positional params is only allowed for either named blocks or components, and passing hash arguments is only allowed with closure components. For example, invoking with positional params like this:

{{@header someParam}}

The above would issue a runtime error if @header was a string (my first snippet above).

This is an open question called out in the current RFC text, and could be even be addressed in future RFC's if we decide on the more conservative approach here...

@chadhietala
Copy link
Contributor

The latest proposal seems to imply that we should declarative way of defining blocks. Today we contextually and implicitly define them:

<x-modal>
  {{! Implicit block definition}}
</x-modal>

In the proposal we effectively are defining macros that could expand into declarative calls to something like a {{def-block}} helper. For instance:

<x-modal>
  <@header>
    <span class="special-header">
      Header Text
    </span>
  </@header>

  <@main as |c|>
    <p>Modal Content {{foo}}</p>
    <button onclick={{c.close}}>
       Close modal
    </button>
  </@main>
</x-modal>

Could be expanded/de-sugared into the following.

{{#def-block header}}
    <span class="special-header">
      Header Text
    </span>
{{/def-block}}

{{#def-block main as |c|}}
    <p>Modal Content {{foo}}</p>
    <button onclick={{c.close}}>
       Close modal
    </button>
{{/def-block}}

<x-modal @header=header @main=main />

This is out of the scope for this RFC but may paint how the implementation may play out and potentially how blocks could be reused in the future.

@cibernox
Copy link
Contributor

My only concern with <x-foo><@header></@header></x-foo> is, and this is probably a question for someone involved in the design of glimmer components, glimmer intends to allow devs to use dynamic tag names. Although if that will really be possible, the syntax would probably be <{{@prop-containing-a-tag-name}}></{{@prop-containing-a-tag-name}}>

@rwjblue
Copy link
Member

rwjblue commented May 13, 2017

@cibernox yes, if that sort of syntax is what we want for dynamic tagname it would be <{{@something}}></{{@something}}> and so it would not conflict with defining named blocks under the current proposal.

@joukevandermaas
Copy link

I'm slightly confused because Ember currently does not have @ syntax for component arguments (unlike GlimmerJS). I cannot find an RFC for this syntax anywhere. To the best of my knowledge it was just introduced to GlimmerJS? This RFC seems to assume it will be introduced to Ember, but shouldn't there be an RFC for it first then?

Does this mean this RFC depends on the new syntax landing in Ember first? Or does this RFC only apply to GlimmerJS until the syntax lands?

@machty
Copy link
Contributor Author

machty commented May 22, 2017

@joukevandermaas shipping this RFC means fleshing out a basic idea of @arg semantics with curly components. It's looking most likely that you can use @arg= notation with "curly" component invocations, and that it'll be in a separate namespace from classic arg=s. This approach does mean there will be an open question as to when to use arg= vs @arg=, and I think the best answer is "only use it for blocks" when you're using curly components.

@luxzeitlos
Copy link

Do I understand it correctly, that this will basically will work exactly like this, and just uses a different syntax to specify block-arguments?

And will we also get a way to use this with a helper?

@machty
Copy link
Contributor Author

machty commented May 27, 2017

@luxferresum it will hit 99% of the use cases of that other RFC, but is a much more familiar/iterative/achievable API enhancement.

What does "use this with a helper" mean?

@knownasilya
Copy link
Contributor

What's the state of this RFC?

@machty
Copy link
Contributor Author

machty commented Jun 19, 2017

@knownasilya still under active development. Will follow up after this coming weekend's core team F2F, but here are a few mostly-agreed-upon developments that haven't yet been reflected in the RFC:

  1. We recognize the distinction between component invocation syntax and component type: we have {{curly}} syntax, which can render Ember Components AND Glimmer Components, and we have <angle> syntax which can only render Glimmer Components.
  2. With curly syntax, the format for passing "args" is and will remain k=v; {{x-foo @k=v}} will be a syntax error. If you're rendering an Ember Component, k=v will, as always, translate to a property set/binding on the Ember.Component instance. If you're rendering a Glimmer component with curly syntax, k=v will translate to passing in an arg that is referenceable as {{@k}} in the glimmer component layout template.
  3. We're sticking with {{#x-foo}} <@namedBlock>hey</@namedBlock> {{/x-foo}} as the multi-block curly syntax; for EComponents, this sets the namedBlock property on the component instance and will be renderable as {{namedBlock}} in the template layout. For Glimmer components: {{@namedBlock}}.
  4. Classic "single-block" invocation for curlies will not set main/else/default/inverse properties on the component instance; those should continue to be rendered with {{yield to="inverse"}}/etc.

These seem to be the best compromises for backwards compatibility but by no means final and hopefully we can flesh it out a lot more over the F2F; will be updating the official RFC shortly.

@machty
Copy link
Contributor Author

machty commented Jun 26, 2017

I just posted a pretty major update to the RFC, mostly fleshing out the details as to how Ember/Glimmer semantics diverge in some cases, and how component invocation syntax works with both component types. I feel pretty good about where it's at, but I really haven't gotten a chance to share the full details with the rest of the core team so keep in mind that some of the details are far from settled. (But please provide whatever feedback comes to mind!)


Hence, the blocks provided in classic single-block syntax should also
be exposed as properties (Ember) and args (Glimmer), and should have
conventional, meaningful names names: instead of "default" (which is a
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

names names

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

names named names

combinatoric mess of block syntax + `if hasBlock` checks to forward
blocks through to the inner component)

I propose an opt-in API; any Ember Component that wants `main`/`else`
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mean components with default and inverse blocks like today?
Do I understand correctly that you propose to extend all of them with a snippet below? Sorry if I misunderstood.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. Basically, you could create a component like this, and just extend any of your components from that one.


How should we handle `{{@header some blockparam data}}`
when `@header` is a non-"callable", simple value like a string? Should
we loudly error? Should it just ignore those "extra" args and render
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about having a debug flag, like the ones in config/environment.js, and by default fail silently. But like the transitions debugging flags, output if the flag is set.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addons will be using this syntax with its intended semntics and I don't think there'd be a way to prevent a config flag from falsely enabling logs on code that doesn't belong to you.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should be an error IMO, similar to when you run {{component <something>}} and <something> cannot be resolved to a known component.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this can be compared to the component case, which fails for the same reason injecting an unknown service should fail. This is IMO more like how {{yield}} no-ops when hasBlock is false; we don't require you to wrap all your yields in {{#if hasBlock}} because, well, that would be miserable.

The `reopenClass` syntax s clunky, and I wouldn't be surprised if people
wanted to use unified renderable syntax all over the place. So is there
a less painful way to opt-in? Could it just be a property set on the
prototype rather than the class object?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed on the clunkiness of reopenClass


This might be desirable in the future, particularly for use cases
involving flex-ish layouts where the component changes behavior /
appearance based on whether blocks on passed in.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is pretty important for anything ambitious...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the future we'll likely support local block declarations (so that you can pass the same block in to multiple components / multiple args of the same component; with something like this in place you could do something like {{x-layout footer=(if showFooter localFooterBlock)}}. It's not great, but it's workable til we feel out all these ambitious use cases.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about an {{is-block helper that returns true if the first argument is a block, false otherwise? Example:

{{#if (is-block @localFooterBlock)}}
  {{@localFooterBlock}}
{{/if}}

If the consumer did not pass in localFooterBlock, or if localFooterBlock is e.g. a string, this would not render anything.

I have already built a similar helper for the biggest app I work on, where in certain cases we allow people to optionally pass components into other components:

{{#if (is-component localFooterBlock)}}
  {{component localFooterBlock}}
{{/if}}

This generally works pretty well as a pattern.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, that'd work, but the idea behind unified renderable syntax is that all you should really have to do is check if the arg/property is truthy or not; it's either present or it's not, and you shouldn't have to check for the presence of a specific type of renderable (be it a block or component factory), as that would arbitrarily force anyone invoking your component to use only specific kinds of renderables.

Then again... the only way to build components that support universal renderables is to commit to always passing args to universal renderables as both positional params and named k=v pairs, which, is kinda weird/bad?

Elsewhere in the RFC I demonstrated this as {{header headerTitle title=headerTitle}}, but maybe that's ridiculous to expect people to do, or maybe it only makes sense for addon authors to do, and app maintainers can just choose/prefer invocation styles that are consistent with the rest of the app? Will have to think on this...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, thinking on this more, I think what we can expect is that if we include named block param syntax as part of this RFC (and we probably should), then there will be a natural/organic shift towards using named KV args when universality is desirable. In other cases like control flow (e.g. a fancy-each) component, yielding a positional param or two will remain a nice block-centric API for those kinds of use cases. And maybe in some cases there will be a mixture of positional and named params but I think generally speaking the API nudges folk toward named KV pairs, which is good.

@@ -307,6 +308,61 @@ case, nor would I want to have to introduce all of the `is-block` or
guard.
```

#### Named block params
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@machty
Copy link
Contributor Author

machty commented Jul 14, 2017

We're likely to FCP this on Monday. Per core team feedback I've refined a few things to clarify the Ember-centric nature of this RFC; spelling out a basic Glimmer roadmap is important but some details are likely to change over time, and hence Glimmer suggestions in this RFC should be considered non-normative.

@oligriffiths
Copy link

oligriffiths commented Jul 14, 2017

@machty To clarify, is the reason you're not going with:

{{#x-modal}}
  {{@header}}
    My header
  {{/@header}}
{{/x-modal}}

For the curly syntax, because the {{@header}} is invalid hbs syntax? It does seem a bit weird to mix the angle and curly syntax within a curly component...

@machty
Copy link
Contributor Author

machty commented Jul 14, 2017

@oligriffiths {{@header}}{{/@header}} as a (proposed) syntax means "render the @header arg here and pass the block containing "My header" to it.

There's a few reasons we went with angle brackets + @:

  1. That'll (most likely) be the syntax for Glimmer component named blocks
  2. {{@header}}{{/@header}} already has a meaning (as mentioned above)

@oligriffiths
Copy link

Correct me if I'm wrong, but {{@header}} is only used in glimmer components, to render an argument.

Given that {{@header}} is invalid HBS syntax anyway (afaik), why not choose another invalid HBS character and keep it curly, like:
{{~header}}my header{{/~header}} or {{$header}}my header{{/$header}}

@tomdale
Copy link
Member

tomdale commented Jul 28, 2017

@knownasilya @machty Sorry, I should have provided an example:

<x-modal>
  <@ember-power/select>
    Header {{title}}
  </@ember-power/select>
</x-modal>

How do you disambiguate between whether @ember-power/select is a component invocation in the implicit main block, or a named block named @ember-power/select?

@knownasilya
Copy link
Contributor

Maybe named blocks can't have a /?

@machty
Copy link
Contributor Author

machty commented Jul 28, 2017

We discussed this RFC in today's core team call, and decided it makes sense to merge it, with the caveat that since there are still open questions regarding the full story for Glimmer component invocation, there may be some slight changes (mostly likely syntactical, if any) along to way to shipping this feature, but that it didn't make sense to indefinitely block implementation of named blocks in Ember on waiting for 100% of the Glimmer story to fall into place.

@tomdale's example above highlights a possible case where syntax might need to shift, but such cases might also be addressed by other means (e.g. perhaps an import mechanism or something along those lines).

@machty machty merged commit b38ec77 into emberjs:master Jul 28, 2017
@rwjblue
Copy link
Member

rwjblue commented Jul 28, 2017

👏 🎉

@machty - Great work shepherding this through the RFC process. Thank you for working so hard on it!

@locks
Copy link
Contributor

locks commented Jul 28, 2017

👏 👏

@chriskrycho
Copy link
Contributor

Is there a ticket tracking implementation of this feature?

@mmun
Copy link
Member

mmun commented Jun 14, 2018

@chriskrycho No, named blocks needs some more design work to deal with angle bracket invocation.

There's also some vague concerns about the idea and approach in general. The current proposal for named blocks doesn't have an answer for cases where you want to have multiple blocks with the same name or for situations where being able to nest blocks would be useful.

@oliverlangan
Copy link

@mmun, can you elaborate a bit on the concerns, or direct us towards those discussions? Once the RFC was accepted, we decided to hold off on some refactoring until it lands—and if there is a chance that it may never be implemented we will have to explore other options.

@sandstrom
Copy link
Contributor

@oliverlangan There is https://github.com/ciena-blueplanet/ember-block-slots which may be a viable alternative, while you wait for named blocks.

@yarigpopov
Copy link

@sandstrom ember-block-slots seems to be abandoned and doesn't work for latest versions of Ember.

@aalimovs
Copy link

@chilicoder we're using ember-block-slots in 3.5 with ember-cli-shims, works great.

@sandstrom
Copy link
Contributor

For anyone stumbling on this, implementation of yieldable named blocks is tracked here:
emberjs/rfc-tracking#44 (comment)

SergeAstapov added a commit to SergeAstapov/rfcs that referenced this pull request Dec 6, 2022
wagenet added a commit that referenced this pull request Dec 6, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet