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!