Flexible Components in Ember.js
I've spent quite a few years working with Ember.js and a pattern I often see repeated is the 'yielded component' pattern.
I'm going to share what that is and why (in many cases), it's an anti-pattern.
Let's get some things out of the way first.
- This post is a series of examples and opinions about how I think things should be. Because it's my opinions and my experience, you many not find them correct or useful.
- None of the things I describe should be considered absolutes, you are free to ignore the suggestions.
So with that out of the way, let's look at some code.
A breadcrumb component
Imagine we want to be able to present the user with an overview of where they are in our app and the ability to move around a hierarchy.
For example Home > Computer Hardware > Motherboards
The user is able to click on each of the links to navigate around the application.
There are many ways to achieve this and one pattern I've seen is to create a component that yields sub components
{{yield
(hash
Divider=(component "breadcrumbs/divider")
Link=(component "breadcrumbs/link")
)
}}
This Component is then used something like this
<Breadcrumbs as |Breadcrumb|>
<Breadcrumb.Link @href="/home">
Home
</Breadcrumb.Link>
<Breadcrumb.Divider />
<Breadcrumb.Link @href="/parts/hardware">
Computer Hardware
</Breadcrumb.Link>
<Breadcrumb.Divider />
<Breadcrumb.Link @href="/parts/hardware/motherboards">
Motherboards
</Breadcrumb.Link>
</Breadcrumbs>
So we have a collection of components that have very little behaviour, but may carry significant styling needs. From the code we have expressed we can fairly easily understand what's going on.
Why is this an anti-pattern?
The main objection I have have is that the yielding
of components in this example does very little improve readability and requires the user to know a bit too much about how to use the component.
A consumer must know that the top level component yields these sub components, which isn't too bad here, but once you add a few more it starts to get painful.
Another reason this is an anti pattern, is that it doesn't add any behaviour to the sub components. There's no data being passed along, so why not just invoke the sub components directly?
I'd argue the below version is just as clear as the yielded version.
<Breadcrumbs>
<Breadcrumb::Link @href="/home">
Home
</Breadcrumb::Link>
<Breadcrumb::Divider />
<Breadcrumb::Link @href="/parts/hardware">
Computer Hardware
</Breadcrumb::Link>
<Breadcrumb::Divider />
<Breadcrumb::Link @href="/parts/hardware/motherboards">
Motherboards
</Breadcrumb::Link>
</Breadcrumbs>
What might better look like?
One thing that might seem obvious is that we're producing an output that has some regular features.
- Breadcrumbs contain a
href
and sometext
- Each Breadcrumb is followed by a
Divider
, except the final item
So perhaps we should give the component a data structure and let it build things for us?
Let's describe the items in terms of class
es
export class BreadcrumbItem {
constructor({ href, text }) {
this.href = href;
this.text = text;
}
}
export class Divider {
divider = true;
}
So we could build a data structure
breadCrumbItems = [
new BreadcrumbItem({ href: "/home", text: "Home" }),
new Divider(),
new BreadcrumbItem({ href: "/parts/hardware", text: "Hardware" }),
new Divider(),
new BreadcrumbItem({
href: "/parts/hardware/motherboards",
text: "Motherboards",
}),
];
And update the template to iterate over the collection to produce our output
{{#each @breadCrumbItems as |bci|}}
{{#if bci.divider}}
<Breadcrumbs::Divider />
{{else}}
<Breadcrumbs::Link @href={{bci.href}}>
{{bci.text}}
</Breadcrumbs::Link>
{{/if}}
{{/each}}
Most seasoned developers will look at breadCrumbItems
with a twitching eyebrow because this whole Divider
business seems unnecessary.
So now we could improve the situation by building the dividers automatically.
In our backing class we can map
over the items and automatically add the dividers.
export default class BreadcrumbsComponent extends Component {
get lastItemIndex() {
return this.args.breadCrumbItems.length - 1;
}
get withDividers() {
return this.args.breadCrumbItems.flatMap((item, index) => {
if (index === this.lastItemIndex) return item;
return [item, new Divider()]
});
}
}
We could also implement this logic directly in the template if we were so inclined. I prefer it in the backing class, but wouldn't die on that hill.
We now have a clear expectation that consumers will provide a collection of BreadcrumbItem
s and this reduces the risk that someone will make a mistake by missing some important object property.
Consumers also don't need to care about how to render the component, just give it the data and let it do its thing.
A new requirement
Someone in the business decides that you now need a double chevron, but only in a few places, so now what do we do?
A common instinct is to add some conditional logic that encapsulates this variant, but this is generally the wrong thing to do.
A much better way of dealing with this situations is provide an escape hatch. Allow users to provide a block, an in that situation, we don't try to programmatically generate the output, we leave it up to the user.
{{#if (has-block)}}
{{yield}}
{{else}}
{{#each this.withDividers as | bci |}}
{{#if bci.divider}}
<Breadcrumbs::Divider />
{{else}}
<Breadcrumbs::Link @href={{bci.href}}>
{{bci.text}}
</Breadcrumbs::Link>
{{/if}}
{{/each}}
{{/if}}
And now the consumer can output the (erratically) formatted breadcrumbs.
<Breadcrumbs>
<Breadcrumb::Link @href="/home">
Home
</Breadcrumb::Link>
<Breadcrumb::Divider />
<Breadcrumb::Divider />
<Breadcrumb::Link @href="/parts/hardware">
Computer Hardware
</Breadcrumb::Link>
<Breadcrumb::Divider />
<Breadcrumb::Link @href="/parts/hardware/motherboards">
Motherboards
</Breadcrumb::Link>
</Breadcrumbs>