Conditionally Wrapping Components with Ember.js

By: Brandon Blaylock
Residential block, aerial view

Dynamically choosing what component to render in Ember is straightforward and well documented. We just reach for Ember’s component helper. The component helper works really well if we need to dynamically render an inline component. But is there a way to easily achieve something similar with block-level components? It turns out, Ember makes this pretty easy as well. We just need to know where to look.

Recently, I was working on a component as part of an internal addon that we share between several apps. The component was pretty simple. Our template looked something like this:

<h3>
 You are using:
 <span class={{this.featureNameClass}}>
   {{@featureName}}
 </span>
 !
</h3>

This is a simplified example, but it is not too far from our real component. Of course nothing stays simple for long. In our case, we received a software feature request that would require us to allow consumers of our addon to include an optional tooltip around the featureName. One approach we can take is to include a condition wrapping the content:

You are using:

{{#if @includeTooltip}}
 <ToolTip>
   <span class={{this.featureNameClass}}>
     {{@featureName}}!
   </span>
 </ToolTip>
{{else}}
   <span class={{this.featureNameClass}}>
     {{@featureName}}!
   </span>
{{/if}}

This does what we want. But in real life, most components include a bit more markup, classes, or other properties than we do in our example. So while this will work, it will also require a lot of duplication.

While we know that we likely should reach for Ember’s component helper, it is not immediately obvious how to use it as a block-level component. However, as is often the case, we can get some help from the official Ember component guides. There we see this example code:

{{#let (component this.componentName) as |Post|}}
 <Post @post={{post}} />
{{/let}}

This looks really close to what we need. What we will want to do is render either our tooltip component, or nothing. To accomplish this, we will use a blank component, like the one Scott Batson used to yield components in multiple places. The blank component that Scott created looks like this:

import Component from '@ember/component';

export default Component.extend({
  tagName: ''
});

And the template is just a yield block:

{{yield}}

This component does not render any markup at all. It is also worth mentioning that I am working on the same codebase as Scott, so I am reusing his blank component. I will set up my template to choose between the empty component and our ToolTip component. To do this we can set up a computed property that returns the name of the component that we want to render.

 ComponentName: computed('includeTooltip', function() {
   return this.includeTooltip ? 'tool-tip' : 'blank-component';
 }),
You are using:

{{#let (component this.ComponentName) as |MaybeToolTip|}}
 <MaybeToolTip>
   <span class={{this.featureNameClass}}>
     {{@featureName}}!
   </span>
 </MaybeToolTip>
{{/let}}

So if we pass in a truthy includeTooltip attribute, we pass the name tool-tip; otherwise, we will pass the name blank-component. So, the component helper will turn this name into a component reference while our let block assigns it to the value MaybeToolTip, which we can then use just as we would any component reference. Finally, we will render our wrapper component. But, we can simplify this code even more. I recently learned from Sergio Arbeo that angle bracket components can accept either a component reference or a component’s name. That means we can do away with the component helper and use a let block by itself:

{{#let this.ComponentName as |MaybeToolTip|}}

But wait, there’s more. We can simplify even further. With Angle Bracket syntax we can do away with the let block altogether. So our template can end up looking like this:

You are using:
<this.ComponentName>
 <span class={{this.featureNameClass}}>
   {{@featureName}}!
 </span>
</this.ComponentName>

That’s it. I’m sure there are other ways to achieve conditional block-level components. But this is how I have it working and in our case, it works like a charm.

DockYard is a digital product agency offering custom software, mobile, and web application development consulting. We provide exceptional professional services in strategy, user experience, design, and full stack engineering using Ember.js and Elixir. With a nationwide staff, we’ve got consultants in key markets across the United States, including Seattle, San Francisco, Los Angeles, Denver, Chicago, Austin, New York, and Boston.