Ember Composable Helpers
Simplify your templates using these declarative template helpers.
Summary
Installation
ember install ember-composable-helpers
Final template:
{{#each-in (group-by activeGroupBy (filter-by 'size' activeSize model)) as |group items|}}
...
{{/each-in}}
Transcript
In this video, we're going to talk about refactoring your app using the Ember Composable Helpers addon.
Ember apps often have dynamic interface elements that let users explore their data. Here, we're looking at a shopping app that's displaying some clothes. We can see on the left that users can group clothes by type (we have shirts and pants) or color (blue, green and red). They can also filter by size - regular or tall.
Traditionally an interface like this might be built using a series of computed properties. Let's look at how this page was implemented.
On the left, we have this application's controller and on the right our template. The controller has a few pieces of state that it needs in order to render: the list of clothing items (referenced by the model
property), the activeGroupBy
option, and the activeSize
filter. The computed properties then use this state to transform the clothing items array: filteredItems
returns a subset of the original list, and groupedItems
takes that filtered list, and transforms it into an object whose keys are the groups and whose values are the items belonging to each group.
The template then uses the standard each-in
helper to iterate over the keys and values of the groupedItems
object. Finally, it renders each group's items to the page.
We can also see how the interface elements themselves update the controller's state. For example, clicking on the "Color" button sends an action to the controller, which updates the activeGroupBy
property. The computed properties are then recalculated, and as a result the interface re-renders.
Ember's Computed Properties work well here, but there's a lot of room for developers to make errors: the dependent keys for each property must be just right; there's a lot of boilerplate for these fairly generic transforms; and most of the code here is imperative and focused on implementation.
Let's see how we can use some simple, declarative template helpers from the Ember Composable Helpers library to clean up our code.
First, we'll install the library by running
ember install ember-composable-helpers
Next, we'll pull up the documentation. Looking at the index of helpers, we see that the library includes both a group-by
helper and a filter-by
helper, which is exactly what our app needs. Let's start with group-by
.
The docs show us that group-by
takes two arguments: the property used to form each group, and the original ungrouped array of objects. Note that in our app, the user can actually change which property is being used to form each group, so we'll be using that dynamic property here, instead of a static string.
Let's go ahead and make the change. We'll replace groupedItems
with the group-by
helper, passing in the activeGroupBy
property and our array of filteredItems
.
- {{#each-in groupedClothingItems as |group items|}}
+ {{#each-in (group-by activeGroupBy filteredClothingItems) as |group items|}}
<h2>{{group}}</h2>
<ul>
{{#each items as |item|}}
<li>
<img>{{item.url}}</img>
<p>{{item.name}}</p>
<p>{{item.price}}</p>
</li>
{{/each}}
</ul>
{{/each-in}}
Note that we're grouping the filteredItems
array here, just like in our computed property, so that our filtering functionality still works.
With that change, we can see that our app renders, and the grouping functionality works - changing the group updates the template. The filtering functionality is also preserved. We can now go back to our controller and delete our groupedItems
computed property. We already have a simpler controller and a more declarative template.
Now let's replace the filteredItems
computed property. We'll use the filter-by
helper. This helper takes a property to filter on, a value that satisfies the filter, and the original unfiltered array. We want to filter on size
using the activeSize
property, so we'll end up with something like this:
(filter-by 'size' activeSize model)
This says we want to filter the original model by looking at each item's size property, and seeing if it matches the current value of activeSize
- which can be either regular or tall.
Now, your first instinct here might be to assign this filtered list to a local property using the {{#with}}
helper, and then passing that result into our (group-by)
helper:
{{#with (filter-by 'size' activeSize model) as |filteredClothingItems|}}
{{#each-in (group-by activeGroupBy filteredClothingItems) as |group items|}}
...
{{/each-in}}
{{/with}}
This solution works, but we can do better. The Ember Composable Helpers library is built with composability in mind. This means that helpers can be used in conjunction with each other, which can save us from having to do things like creating temporary variables.
Instead of using {{#with}}
, let's leverage the power of subexpressions and compose filter-by
with group-by
directly:
{{#each-in (group-by activeGroupBy (filter-by 'size' activeSize model)) as |group items|}}
Note that nested helpers evaluate right-to-left: first we filter the list by activeSize
, and then we group the resulting array by activeGroupBy
.
Looking at our app, we see that everything works as expected - meaning we can delete our filteredItems
computed property. We've now eliminated both of our original computed properties, and we're left with a declarative template that's easy to read, change, and most important, relies on an addon to do its heavy lifting.
As with most things in programming, there are tradeoffs involved when using helpers to move controller or component logic into templates. In our example, we only needed two helpers; but you can imagine a series of computed properties that are much harder to replace.
Computed properties are still extremely useful in Ember applications. They're best suited for domain-heavy or complex transformations. You have the full power of JavaScript, and they're easy to debug and write unit tests for.
However, writing generic computed properties just to group or sort an array is unnecessary and error-prone. In these situations, Ember Composable Helpers is a great fit - and it's really fun to use! In fact, it's so fun that you'll need to be careful not to get carried away and start programming in your templates. The key is to find isolated, generic transforms you've written yourself, and replace them with battle-tested library code that you don't have to maintain.
So give it a shot in your own app! Install the library, and see how you can make your templates simpler and more declarative.`