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

Multiple yields #43

Closed
wants to merge 1 commit into from
Closed

Conversation

jamiebuilds
Copy link


# Drawbacks

N/A
Copy link
Member

Choose a reason for hiding this comment

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

when composing multiple components, naming collisions become quite possible.

Copy link
Author

Choose a reason for hiding this comment

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

In terms of multiple layers of components?

{{#my-component}}
  {{#content-for "body"}}
    {{#other-component}}
      {{#content-for "body"}}
        Hello
      {{/content-for}}
    {{/other-component}}
  {{/content-for}}
{{/my-component}}

I see no reason why these wouldn't be localized to the current context they are in. Can you give an example of when this would be a problem?

@mmun
Copy link
Member

mmun commented Apr 2, 2015

What happens if two components supply content to the same name? Is the content replaced? or appended to? What happens if one of those components is then removed?

I'm also kind of dubious about the name yield since the operation is async. That said, Rails uses yield :name for this use case (link). An alternative name would be outlet.

@jamiebuilds
Copy link
Author

@mmun providing content-for is localized to the component it is within. For a more in-depth example see: https://gist.github.com/thejameskyle/d16583c478c0dadd48df Is this what you meant?

@mmun
Copy link
Member

mmun commented Apr 2, 2015

@thejameskyle So the content-for must be supplied immediately in the component's block? That seems troublesome. e.g.

{{#foo-component}}
  {{#bar-component as |b|}}
    {{#content-for "body"}}
      {{b.baz}}
    {{/content-for}}
  {{/bar-component}}
{{/foo-component}}

In this case I want to yield back content to foo-component with data from bar-component.

@mmun
Copy link
Member

mmun commented Apr 2, 2015

This problem was one of the initial motivations for block params. You could imagine yielding a helper on a block param that was scoped to a component, like

{{#foo-component as |f|}}
  {{#bar-component as |b|}}
    {{#f.content-for "body"}}
      {{b.baz}}
    {{/f.content-for}}
  {{/bar-component}}
{{/foo-component}}

@jamiebuilds
Copy link
Author

What would be the use case for that?

Also could that be solved as:

{{#foo-component as |foo|}}
  {{#bar-component as |bar|}}
    {{#content-for foo.body}}
      {{bar.baz}}
    {{/content-for}}
  {{/bar-component}}
{{/foo-component}}

@mmun
Copy link
Member

mmun commented Apr 2, 2015

The use case I'm interested in is Routable components, e.g. the index component nested inside the application component.

{{!-- templates/application.hbs --}}
{{yield name="header"}}
<div>{{yield}}</div>
{{!-- templates/index.hbs --}}
{{#content-for "header"}}<h1>This is the header</h1>{{/content-for}}
This is the Index.

Essentially a template based API around the this.render method commonly used in Route#renderTemplates.

@mmun
Copy link
Member

mmun commented Apr 2, 2015

These use cases are obviously a bit different, but I bring it up because they are fighting for the same content-for name.

I think your RFC would be better implemented as a Handlebars language feature. Basically you are naming a block and passing it into the component. It is better to have the named block accessed statically instead of as a side effect of running {{yield}} (Imagine if the component never even calls {{yield}}, the named blocks will never be seen/registered).

I'm thinking something like & to denote references to blocks. The syntax would be similar to how {{else}} splits a "block" into two blocks. In fact, this approach could be used to desugar {{else}} into &else. Modifying your example a bit...

{{!-- templates/component/foo-component.hbs --}}
<div class="foo">
  <div class="foo-body">
    {{exec &header}}
  </div>
  <div class="foo-aside">
    {{exec &aside}}
  </div>
</div>
{{!-- templates/component/bar-component.hbs --}}
<div class="bar">
  <div class="bar-header">
    {{exec &header}}
  </div>
  <div class="bar-aside">
    {{exec &aside}}
  </div>
</div>
{{!-- templates/example-1.hbs --}}
{{#foo-component}}
  Foo body
  {{#bar-component}}
    Bar body
  {{&header}}
    Bar header
  {{&aside}}
    Bar aside
  {{/bar-component}}
{{&aside}}
  Foo aside
{{/foo-component}}

The main point is that the named blocks are known statically. The actual syntax for defining the blocks is open for bike shedding.

@mmun
Copy link
Member

mmun commented Apr 2, 2015

Paging señor @wycats.

@balinterdi
Copy link

This is definitely something I have come across and would like to see implemented in Ember one way or another.

Even just moderately complex components have some markup that is inherent for the component then something that should be customized in each case, then again some common markup, then customizable again, etc.

An example in our project is an image uploader that shows the currently uploaded image, an upload link/button, a progress bar, an icon to remove the current image. Some of these are only present in some cases.

Here is one instance where we use said component:

<!-- app/templates/components/image-uploader.hbs -->
<div class="u-margin-bottom-sm">
  <a class="link u-margin-right-sm js-upload-button">{{text}}</a>
  {{yield}}
</div>
<p class="u-text-xs u-text-color-light">You can also drag and drop a file from your computer.</p>
<!-- app/templates/user/preferences.hbs -->
{{#image-uploader class="u-align-bottom" text="Upload new photo"}}
  <a {{action 'removePicture'}} class="link {{if isRemovingProfilePicture 'disabled'}}">
    {{if isRemovingProfilePicture "Removing..." "Remove"}}
  </a>
{{/image-uploader}}

Not being able to yield multiple times is something I can get around but being able to do it would make code more DRY and intuitive and components' code more intuitive/readable.

@jamiebuilds
Copy link
Author

The main point is that the named blocks are known statically. The actual syntax for defining the blocks is open for bike shedding.

@mmun I'd be totally fine with that change or any implementation detail. I don't care so much about syntax or naming, but rather that there is a solid way of writing templates like this.

@workmanw
Copy link
Contributor

workmanw commented Apr 2, 2015

❤️ the concept. I definitely have a need for this.

I'm a little hung up on the content-for helper idea. I liked @stefanpenner's gist about yielding a helper.

Either way: 👍

@tim-evans
Copy link

I'm with @workmanw on using helpers that are yielded from components. I believe @rwjblue had an example of this, specifically with a rails like form-for component. Any input on this @rwjblue ?

@mmun
Copy link
Member

mmun commented Apr 4, 2015

@tim-evans We need to work out some kinks. The primary concern is that a helper call like {{f.input}} should not fallback to looking for a global helper called "f.input". I'm excited for that feature too, but probably need to wait until after Glimmer and maybe Ember 2.0 given our milestones, unless someone else wants to take a stab.

@tim-evans
Copy link

Agreed, @mmun That makes a lot of sense.

@balinterdi balinterdi mentioned this pull request Apr 13, 2015
@MiguelMadero
Copy link

Today we're doing something similar by using nested and related components.

Borrowing from the provided example:

{{#my-component}}
  <p>Here is some body text</p>

  {{#content-for "footer"}}
    <p>Here is some footer text</p>
  {{/content-for}}
{{/my-component}}

We would have something like:

{{#my-component}}
  <p>Here is some body text</p>

  {{#my-component-footer}}
    <p>Here is some footer text</p>
  {{/my-component-footer}}
{{/my-component}}

my-component-footer is a completely separate component. By convention we use the name of the parent component, followed by the name of the "section" that identifies this. For the consumer it's all nice and easy. The implementation however, could be simplified a bit maybe with some Mixins or custom base classes, but the pattern is:

  1. On the child component (my-component-footer) override render to make it a no-op.
  2. On the child components init, find the parent component and give it a reference to the child component this.nearestOfType(MyComponent).set('_footer', this)
  3. On the parent component (my-component) simply grab that template {{view templateBinding='_footer.template'}}

Rewriting the original example:

<div class="my-component">
  <div class="my-component-body">
    {{yield}}
  </div>

  {{#if _footer}}
    <div class="my-component-footer">
      {{view templateBinding='_footer.template'}}
    </div>
  {{/if}}
</div>
import MyComponent from 'tyrion/components/my-component';
export default Ember.Component.extend({
    onInit: function () {
        var myComponent = this.nearestOfType(MyComponent);
        Ember.assert('my-component-footer can only be used inside a my-component', myComponent);
        myComponent.set('_footer', this);
    }.on('init'),

    render: function () {
        // intentionally left as no-op so we don't render anything
    }
});

@rwjblue
Copy link
Member

rwjblue commented Jun 7, 2015

We have been discussing this use-case in depth at the Ember core team face to face, and we would like to move towards using static slots as opposed to dynamic slots. We'll be submitting an RFC for the static slot concept and would love your feedback on where that falls down for your use case.

@rwjblue rwjblue closed this Jun 7, 2015
@jamiebuilds jamiebuilds deleted the multiple-yields branch June 7, 2015 22:12
@machty
Copy link
Contributor

machty commented Jul 30, 2017

FYI this has been superseded by the now-merged Named Blocks RFC.

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.

9 participants