Skip to content

Instantly share code, notes, and snippets.

@tomdale
Last active May 30, 2019 14:53
Show Gist options
  • Save tomdale/bedb77662b19529f59154ec55e2f4a21 to your computer and use it in GitHub Desktop.
Save tomdale/bedb77662b19529f59154ec55e2f4a21 to your computer and use it in GitHub Desktop.

Forwarding Named Blocks in Glimmer

Use Case

A component that "forwards" passed blocks to a child component. For example, imagine we have a component called MiniCard that accepts a named block called header. It is invoked like this:

<MiniCard>
  <:header>
    My Card
  </:header>
</MiniCard>

The implementation of MiniCard uses the has-block helper to emit additional header-related DOM elements only in the case that the header block is provided:

{{#if (has-block "header")}}
  <div class="mini-card-header">
    {{yield to="header"}}
  </div>
{{/if}}

Next, we want to implement a component called JobCard that uses MiniCard. JobCard also takes an optional named block called header, but wants to pass it along to MiniCard rather than yield the block itself.

Named blocks are (intentionally for the MVP) not exposed as first-class values. In order for a component to "pass" a named block to a child component, it must create a new named block and yield the passed block within the new block. For example, JobCard defines a new header named block when invoking MiniCard, and yields its own header block inside:

{{!-- JobCard.hbs --}}
<MiniCard>
  <:header>
    {{yield to="header"}}
  </:header>
</MiniCard>

Problem

This solution works so long as the header block is always provided. However, in this case, the header block was intended to be optional, because different DOM output is emitted by MiniCard if the header block is not present. In this case, the MiniCard will always render the additional <div class="mini-card-header"> element, even if it is unwanted because no header block was passed to the parent JobCard.

Workaround

One workaround is to check for the existence of the named block in a conditional in the parent component template, invoking the child component with the named block if the passed block exists and without the named block if it doesn't. For example:

{{!-- JobCard.hbs --}}
{{#if (has-block "header")}}
  <MiniCard>
    <:header>
      {{yield to="header"}}
    </:header>
  </MiniCard>
{{else}}
  <MiniCard />
{{/if}}

While this solves the problem, it requires tedious, verbose boilerplate code to accomplish. It also makes bugs more likely, as both component invocations (with and without the block) must be updated if the arguments passed to the component change.

Worse, using this approach when there are multiple optional blocks creates a combinatorial explosion of boilerplate. Each permutation of possible block configurations would need to be handled in the template, quickly leading to a verbose, confusing, hard-to-maintain template.

Strawman

Syntactic support for passing an existing in-scope named block as a named block to a child component.

{{!-- JobCard.hbs --}}
<MiniCard @title="Hello world">
  <:header from="header" />
</MiniCard>

Importantly, named blocks provided via the from syntax preserve "undefinedness." For example, if no named block was provided to the parent component that matches the name specified in from="blockName", {{has-block "blockName"}} would evaluate to false in the child component.

For example:

{{!-- Parent.hbs --}}
Parent has-block: {{has-block "blockName"}}
<Child>
  <:blockName from="blockName" />
</Child>

{{!-- Child.hbs --}}
Child has-block: {{has-block "blockName"}}

Invoking Parent like this:

<Parent />
<Parent>
  <:blockName>
    Hello
  </:blockName>
</Parent>

Produces the following output:

Parent has-block: false
Child has-block: false
Parent has-block: true
Child has-block: true
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment