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

Functionality clarification - {{yield}}ing in multiple places #17240

Closed
LevelbossMike opened this issue Nov 30, 2018 · 9 comments
Closed

Functionality clarification - {{yield}}ing in multiple places #17240

LevelbossMike opened this issue Nov 30, 2018 · 9 comments

Comments

@LevelbossMike
Copy link
Contributor

LevelbossMike commented Nov 30, 2018

Hi and sorry for the strange title.

I have come across this blog-article that suggest a way to implement "multiple"-yields in a component.

https://dockyard.com/blog/2018/11/26/how-to-yield-an-ember-component-in-multiple-places

This is a very powerful pattern and allows entirely new component abstractions - before embracing I want to make sure that this is intended behavior and it won't break in future releases before using in my own applications.

The blogpost suggests a clever solution to the problem at hand (wanting to define named blocks in a component) and it really works the way the author describes. Several things are surprising in the code example provided:

https://ember-twiddle.com/034c04f68f0c3082d95104586e52f1e4?openFiles=templates.components.fancy-toolbar.hbs%2C

  1. Component invocation seems to work by yielding out a string
  2. hashes of multiple yields seem to be merged together and given to all yields

Example for 1.

{{!-- x-parent --}}
{{yield 'x-wat'}}

{{!--application.hbs --}}
{{#x-parent as |c|}}
  {{c}}
  {{!-- will render x-wat-component --}}
{{/x-parent}}

I thought this only worked if we were using the component-helper. I'm pretty certain that was needed in the past but this might have changed along the way. Is this intended behavior or only works by accident?

Example for 2 - directly taken from the blog post

https://ember-twiddle.com/034c04f68f0c3082d95104586e52f1e4?openFiles=templates.components.fancy-toolbar.hbs%2C

To me it's very surprising that hashes are being merged from multiple yields. It really doesn't fit the mental model that I had for ember-templates but that is most likely because I was missing something. If that is intended behavior I suppose we should clarify this in the guides. I can't remember ever coming across this functionality in the past.

Playing around with this a bit I found another surprising behavior.

  1. Calling yielded components as a block works even if the componets aren't defined. Example:
{{!-- x-parent --}}
{{yield (hash ui=(hash wat='x-wat')}}

{{!-- application.hbs --}}
{{#x-parent as |p|}}
  {{!-- this will simply not render anything --}}
  {{#p.ui.doesNotExist}}
    We won't render this
  {{/p.ui.doesNotExist}}
{{/x-parent}}

this seems to be treated the same way as an undefined property but will throw when we try to render a component like usual:

{{#x-parent as |p|}}
  {{!-- won't throw but not render anything --}}
  {{#p.ui.doesNotExist}}
    We won't render this
  {{/p.ui.doesNotExist}}
{{/x-parent}}```

{{!-- there's no component called `wat` but this won't throw --}}
{{#wat}}
  yo
{{/wat}}

{{!-- this throws because no component called `x-wat` exists --}}
{{x-wat}}

This is nice because it allows patterns like this where you define a loading-component and a loaded-state component side-by-side but it's still surprising.

{{!-- loading-component.hbs --}}
{{#if @isLoading}}
  {{yield (hash ui=(hash loading='x-loading'))}}
{{else}}
  {{yield (hash ui=(hash loaded='x-loaded'))}}
{{/if}}


{{!-- application.hbs --}}
{{#loading-component isLoading=true as |c|}}
  {{#c.ui.loading}}
     loading ui goes here
  {{/c.ui.loading}}
  {{#c.ui.loaded}}
    loaded ui goes here
  {{/c.ui.loaded}}
{{/loading-component}}

I guess this works because the rendering engine tries to look up a value instead of a helper but this is surprising behavior still.

TLDR;

https://dockyard.com/blog/2018/11/26/how-to-yield-an-ember-component-in-multiple-places discusses surprising behavior of Ember's templating layer. This allows powerful abstractions for components but it's not clear if this only works by "accident" or is intended behavior. Before embracing this pattern we should make it clear if this behavior will be supported in future releases.

  1. yielding a string and rendering it as a component will render a component with the string's name
  2. hashes from multiple yields will be merged into all yields
  3. it is possible to call undefined yielded values as component-blocks but despite calling undefined components in templates will throw yielding undefined values and calling them in block format won't throw.
@LevelbossMike
Copy link
Contributor Author

LevelbossMike commented Nov 30, 2018

After thinking about this some more I think I get why this works.

  • hashes are not merged into one yield

We are actually yielding out two times and rendering the block two times but because of the fact that blocks for properties that aren't yielded out will be ignored we will only render the ui once.

So this seems like it is supposed to work that way. But this raises the question if we should do it this way? Are there any drawbacks when yielding out multiple times from one component? Is this future proof?

I realized that this might be something better discussed in discuss happy to move the discussion there if here's not the right place.

@Windvis
Copy link
Contributor

Windvis commented Nov 30, 2018

Relevant discuss post: https://discuss.emberjs.com/t/using-yield-for-multiple-nested-components/9126/2

I'm also interested to hear if this is the intended behaviour and can be considered safe to use, since we use it in multiple projects already 😁.

@pixelhandler
Copy link
Contributor

@LevelbossMike What I've observed is that the Ember docs to provide a commitment to behavior, I tagged this as a Documentation issue. Is the mutliple yield is not documented I'd say it's not guarateed for the future. Perhaps the first step would be to contribute to the guides a section on components with the yield tricks that merge the hash so one could leverage multiple yields.

cc/ @emberjs/learning-team-managers

@rwjblue
Copy link
Member

rwjblue commented Dec 7, 2018

Component invocation seems to work by yielding out a string

This seems pretty surprising, and I believe does not work in Ember 3.6.0. This was fixed by #17135.

hashes of multiple yields seem to be merged together and given to all yields

I realize that this is how it seems, but is actually not what is happening. The yielded block is actually being invoked twice and combined with the bug mentioned above (fixed in 3.6) means that the "alternate" is simply ignored (as if you had done {{some-thing-undefined}}).

@pixelhandler
Copy link
Contributor

pixelhandler commented Dec 7, 2018

@LevelbossMike yesterday I worked on a component which at first glance it seemed like a good idea to use multiple yields, but after working on it for a while a single yield was fine. What I did was create multiple nested components for the HTML structure I wanted to use and those components each had their own yield. And I yielded a hash of named components (and specific properties needed in the context) so that the template which invoked the component could access those components/properties. In that case it seemed "Contextual Components" turned out to work fine.

I'm curious if you have tried a different approach, e.g. "Contextual Components" vs. a solution for multiple yields.

@rwjblue
Copy link
Member

rwjblue commented Dec 8, 2018

I believe that the initial question has been addressed (the specific technique in the blog post is essentially taking advantage of a bug that has been fixed), so I'm going to close this for now (though I'm super happy to continue to the conversation).

@rwjblue rwjblue closed this as completed Dec 8, 2018
@noslouch
Copy link

@rwjblue I have a question around the multiple yielded blocks. Even if they're empty, is that a potential performance bottleneck?

The technique described in the blog post can still be used if we include the component keyword in the template, rather than trying to invoke a component from a string.

@Techn1x
Copy link

Techn1x commented Jul 14, 2021

The technique described in the blog post can still be used if we include the component keyword in the template, rather than trying to invoke a component from a string.

I've found the same, it still works on Ember 3.24 it seems when using component helper. Modified the example from the OP;

// my-component.hbs
{{#if @isLoading}}
  <div class="loading">
    {{yield (hash loading=(component 'x-loading'))}}
  </div>
{{else}}
  <span class="loaded">
    {{yield (hash loaded=(component 'x-loaded'))}}
  </span>
{{/if}}
<MyComponent as |me|>
  <me.loading> loading... </me.loading>
  <me.loaded> loaded!! </me.loaded>
</MyComponent>

Can we rely on this behaviour when using the component helper instead? It's such a handy way to write components

@Techn1x
Copy link

Techn1x commented Jul 14, 2021

Can we rely on this behaviour when using the component helper instead? It's such a handy way to write components

That being said, I've managed to re-write my code to use the new yieldable named blocks (Ember 3.25+). Here's what that looks like, in case anyone finds it useful;

// my-component.hbs
{{#if @isLoading}}
  <div class="loading">
    {{yield (component "load-indicator") to="loading"}}
  </div>
{{else}}
  <span class="loaded">
    {{yield to="loaded"}}
  </span>
{{/if}}
<MyComponent>
  <:loading as |LoadIndicator|>  loading... <LoadIndicator /> </:loading>
  <:loaded> loaded!! </:loaded>
</MyComponent>

You can yield components instead of where I've used loadingPercent too

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

6 participants