Pass argument to yielded component

Hi,

I’m trying to build a component which content must be customizable by the user. This component is an input which display results underneath. (you can see that as simplified power-select).

I managed to let the user customize the result list by doing the following :

myComponent.hbs :

<ul>
	{{#each this.items as |item|}}
		<li {{action 'selectItem' item}}>
			{{yield item}}
		</li>
	{{/each}}
</ul>

And then i can do :

{{#myComponent
    items=(action 'fetchItems')
    onChange=(action 'foo') as |item| }}
    <span>{{item.name}}</span>
{{/myComponent}}

That is working fine.

My problem is that my component is a little bit more complex than that and i’d like to have multiple part that the user can customize. For exemple if my component is now :

<ul id="list1">
	{{#each this.items as |item|}}
		<li {{action 'selectItem' item}}>
			{{yield item}}
		</li>
	{{/each}}
</ul>
....
<ul id="list2">
	{{#each this.otheritems as |otheritem|}}
		<li>
			{{yield otheritem}}
		</li>
	{{/each}}
</ul>

How do i let the user customize the render of the different list’s items ?

So i was thinking of doing something like :

myComponent.hbs :

<ul>
	{{#each this.items as |item|}}
		<li {{action 'selectItem' item}}>
			{{yield (hash results=(component 'myComponent/results'))}}
		</li>
	{{/each}}
</ul>

And use it like that :

{{#myComponent
    items=(action 'fetchItems')
    onChange=(action 'foo') as |part| }}
    {{#part.results as |item|}}
        <span>{{item.name}}</span>
    {{/part.results}}
{{/myComponent}}

But i cannot figure out how to pass the item to myComponent/results when yielded by the “base” component Any idea ?

But i cannot figure out how to pass the item to myComponent/results when yielded by the “base” component Any idea ?

The component helper can accept args so you can “partially apply” args, or “curry” them, to your component when you yield it. So in your code:

-			{{yield (hash results=(component 'myComponent/results'))}}
+			{{yield (hash results=(component 'myComponent/results' item=item))}}
                                                                     ^ results component receives 'item' as arg named 'item' 

A few more notes:

Yielding multiple things

There are two primary ways to yield multiple things to a block. You can yield them as individual “positional block params” or you can slap them all in a hash. Both methods are useful for different things but they’re more or less equivalent other than that with hashes you get names and the order doesn’t matter.

// hashes
{{yield (hash thing1=this.thing1 thing2=this.thing2 thing3=this.thing3)}}

// block params
{{yield this.thing1 this.thing2 this.thing3}}

// both
{{yield this.thing1 (hash thing2=this.thing2 thing3=this.thing3)}}

Component Args

The pattern you’re using above, adding args to the child component as you yield it, is the core idea behind “contextual” components. You’re providing the consumer with a (partially) configured child component that’s ready to render however they wish. It can allow you to provide powerful APIs that are still simple and clutter-free. It’s worth knowing a little more about what the component helper is capable of.

The helper looks like this (as you know):

(component "my-component" arg1=value1 arg2=value2)

It allows you to bind/curry all, some, or none of the components args. The component returns a “CurriedComponentDefinition” (I forget if that’s the exact name but it’s something like that), basically a component that has some/all/none args applied and ready to be rendered. The args that you curry with the helper can always be clobbered later on which is great but note that it means you can’t force a user to use them. You can curry args at multiple layers, so you can add args later on and re-yield it if you want.

Named Blocks

In Ember 3.25 “named blocks” were introduced. This lets you yield to multiple blocks from the same parent. This feature is polyfilled back to Ember 3.12. You can also approximate named blocks earlier than that by using hashes and “empty components”. To do this you’d make a component called “blank-template” or something (which is, of course, a blank template), yield it contextually in a hash, and then render it wherever you want:

// my-card.hbs
<div class="card">
  <div class="header">
    {{yield (hash header=(component "blank-template"))}}
  </div>
  <div class="body">
    {{yield (body header=(component "blank-template"))}}
  </div>
</div>

// usage
{{#my-card as |card|}}
  {{#card.header}}
    my header content here
  {{/card.header}}

  {{#card.body}}
    my body content here
  {{/card.body}}
{{/my-card}}

Modern Patterns

Since you’re using curlies instead of angle bracket syntax I’m assuming you’re on a fairly old version of Ember. In more modern Ember we’re moving away from dynamic component resolution, so all of the above still applies except the component helper is becoming less necessary except to curry args, and instead of passing strings to it you’d usually pass a component proper e.g.:

{{yield (component MyComponent arg1=value1)}}

Also important to note is that with Glimmer components there’s unfortunately. no way to curry attributes via the component helper.

2 Likes

Thank you so much for this great answer ! This will help me a lot :pray:

@dknutsen i’m still a bit confused on how i get to use the parameter i pass to the “blank-template” (im using ember 3.14 but the component is extending an old one with the handlebar synthax.

If i start from your previous exemple, and add a parameter “item” in the body

// my-card.hbs
<div class="card">
  <div class="header">
    {{yield (hash header=(component "blank-template"))}}
  </div>
  <div class="body">
    {{yield (hash body=(component "blank-template" item=item))}}
  </div>
</div>

//blank-template.hbs
{{yield}}

How do i access this parameter ? I tried yielding it from blank-template like this :

//blank-template.hbs
{{yield item}}


// usage
{{#my-card as |card|}}
  {{#card.header}}
    my header content here
  {{/card.header}}

  {{#card.body as |res|}}
    my body content here with {{res.item.name}}
  {{/card.body}}
{{/my-card}}

without success , no error , but the name is blank. If i {{log res}} it is undefined

I think the problem in your example is that the arg you’re passing to the child doesn’t actually exist in the parent context. But also the blank-template example is most useful if you’re trying to use named blocks without the ability to use the polyfill (or if you’re trying to do some really crazy contextual behavior). Since you’re on 3.14 you could use the named blocks polyfill instead which I’d probably recommend.

But the main question is about contextual args, so let’s tackle that. My favorite example for this is a select component because those are concise but a perfect fit for this sort of pattern. (Also I’m going to use angle bracket syntax since that’s modern and you can support it, but it’s easy to toggle back and forth between them if you need). So an HTML select looks like this…

<select>
  <option value="first">First Value</option>
  <option value="second" selected>Second Value</option>
  <option value="third">Third Value</option>
</select>

Obvious things to note are:

  • you have a select tag
  • each option tag has both a value and a label
  • the selected option has the “selected” attribute

Now there are a couple ways we could write a component to do this. For example we could pass in all the options upfront, and then let the component render everything:

// my-select.hbs
<select {{on "change" (pick "target.value" @onChange)}}>
  {{#each options as |optionObject|}}
    <option
      value={{optionObject.value}}
      selected={{eq @value optionObject.value}}
    >
      {{optionObject.label}}
    </option>
  {{/each}}
</select>

// usage:
<MySelect
  @value={{this.selectValue}}
  @onChange={{this.selectChanged}}
  @options={{array
    (hash value="first" label="First Value")
    (hash value="second" label="Second Value")
    (hash value="third" label="Third Value")
  }}
/>

NOTE: i used the pick helper from ember-composable-helpers for brevity but this could just as easily be a backing class action to extract target.value from the event and pass it up to this.args.onChange.

So here we’re passing in our options as an array of POJOs (this could also be done from the backing class), and we set the selected attribute if the option value is the same as the select value arg. Pretty straightforward.

But now let’s say we want to still support this nice “render everything for me” use case, but ALSO allow the user to completely control the rendering. This is where the contextual component pattern can help us.

First, we can extract the “option” tag into a component. Then we can pass the option label, option value, and current selection to it contextually. Then we can yield it out. And we can put in some fancy checks to allow the user to use it in either “mode”, blockless or block-form.

So our code could look something like this:

// my-select-option.hbs
{{!--
  This is the option component. It takes 3 args:
  - optionValue: the "value" of the option
  - optionLabel: the "label" for the option, falls back to value if not given
  - currentOption: the value of the "currently selected option" in the select
--}}
<option
  value={{@optionValue}} 
  selected={{eq @currentOption @optionValue}}
  ...attributes
>
  {{! if we are given a label, use it, otherwise fall back to rendering the value as the label }}
  {{or @optionLabel @optionValue}}
</option>

// my-select.hbs
<select {{on "change" (pick "target.value" @onChange)}}>
  {{#if (has-block)}}
    {{! if the user wants to customize their options, just yield }}
    {{yield (component "my-select-option" currentOption=@value)}}
  {{else}}
    {{! otherwise render the options for them }}
    {{#each @options as |theOption|}}
      <MySelectOption
        @optionValue={{or theOption.value theOption}}
        @optionLabel={{theOption.label}}
        @currentOption={{@value}}
      />
    {{/each}}
  {{/if}}
</select>

Now it can be used in multiple ways:

// blockless with "simple options"
<MySelect
  @value={{this.selectValue}}
  @onChange={{this.selectChanged}}
  @options={{array "first" "second" "third"}}
/>

// blockless with "option objects", same as before!
<MySelect
  @value={{this.selectValue}}
  @onChange={{this.selectChanged}}
  @options={{array
    (hash value="first" label="First Value")
    (hash value="second" label="Second Value")
    (hash value="third" label="Third Value")
  }}
/>

// block-mode
<MySelect
  @value={{this.selectValue}}
  @onChange={{this.selectChanged}}
  as |Option|
>
  <Option @value="first" @label="First Value" />
  <Option @value="second" @label="Second Value" />
  <Option @value="third" @label="Third Value" />
</MySelect>

But wait! We can give the user even more flexibility in block mode. We could let them render arbitrary content in the options instead of just a lame ol’ string label. All we need to do is optionally yield in the option component too. So if we change the option component to this:

// my-select-option.hbs
{{!--
  This is the option component. It takes 3 args:
  - optionValue: the "value" of the option
  - optionLabel: the "label" for the option, falls back to value if not given
  - currentOption: the value of the "currently selected option" in the select
--}}
<option
  value={{@optionValue}} 
  selected={{eq @currentOption @optionValue}}
  ...attributes
>
  {{#if (has-block)}}
    {{yield @optionValue}}
  {{else}}
    {{! if we are given a label, use it, otherwise fall back to rendering the value as the label }}
    {{or @optionLabel @optionValue}}
  {{/if}}
</option>

Now the user can do even crazier stuff, like this:

// block-mode
<MySelect
  @value={{this.selectValue}}
  @onChange={{this.selectChanged}}
  as |Option|
>
  <Option @value="first">
    <div class="colored-square-blue"></div>
    <span>First Option</span>
  </Option>
  <Option @value="second">
    <div class="colored-square-red"></div>
    <span>Second Option</span>
  </Option>
  <Option @value="third">
    <div class="colored-square-green"></div>
    <span>Third Option</span>
  </Option>
</MySelect>

Neato!

1 Like

Oh and just for kicks, one thing that helps me when working with blocks/yield is always thinking of them like functions. The syntax can get a little confusing since it’s unusual but if you can translate it to function calls you can always figure out what’s going on.

So let’s take the final select we have above, and represent it using pseudocode functions (this is all imaginary so don’t take it too seriously) instead of hbs syntax. Let’s imagine, hypothetically that the component is a function, and to pass args we just provide a hash as the first argument to that function.

// simple version
MySelect({
  value: this.selectValue
  onChange: this.selectChanged
  options: [ "first" "second" "third"]
});

And since the primary purpose of a component is to emit DOM matter, we just imagine that this returns some HTML or JSX-ish looking stuff. Let’s say the inside looks something like this:

function MySelect({ value, onChange, options }, maybeBlock) {
  return pretendRenderHTML(`
    <select {{on "change" (pick "target.value" @onChange)}}>
      if (maybeBlock) {
        maybeBlock(component("my-select-option" currentOption=value));
      } else {
        for (const theOption in options) {
          MySelectOption({
            optionValue: theOption.value || theOption,
            optionLabel: theOption.label,
            currentOption: value,
          });
        }
      }
    </select>
  );
}

But let’s look at the block mode version because that’s more interesting. This is basically the same, except that we’re treating the block as just another argument passed to the “function” (aka component). But the block is itself a function, and the “block params” are arguments to that function.

MySelect({
  value: this.selectValue
  onChange: this.selectChanged
}, (Option) => {
  return pretendRenderHTML(`
    Option({ value: "first", label: 'First Value' })
    Option({ value: "second", label: 'Second Value' })
    Option({ value: "third", label: 'Third Value' })
  `);
});

So now you can see how this might all be wired up with functions. The block which we give to MySelect is actually easily represented as a function, and MySelect calls that function with the args that become the “block params” (in this case the option component). For completeness we’ll go ahead and define weird pseudocode for the inside of my-select-option too:

function MySelectOption({ optionValue, optionLabel, currentOption }, maybeBlock) {
  return pretendRenderHTML(`
    <option
      value={{optionValue}} 
      selected={{eq currentOption optionValue}}
    >
      if (maybeBlock) {
        maybeBlock(optionValue);
      } else {
        optionLabel || optionValue
      }
    </option>
  `);
}

With named blocks it’s similar, it’s just instead of the one block each component could take a hash of blocks:

MyCardComponent(args, { headerBlock, bodyBlock });

So something like:

MyCardComponent(args, {
  header: () => {
    return pretendRenderHTML(` ... my header content `);
  },
  body: () => {
    return pretendRenderHTML(` ... my body content `);
  }
});

Anyway… your mileage may vary but if my head ever starts spinning with blocks and contextual stuff I always can always find some clarity by mentally translating them into function semantics.

Thank you again for your valuable answers.

After a lot of refactoring , i managed to rewrite my component as a glimmer one and i can now use named blocks which are way cleaner !

Awesome! Glad you got it working, and can use named blocks too! They’re really nice.