A simple start
I was recently working on a UI element that worked a lot like an accordion. What, you may ask, is an accordion in my eyes?
- The user sees a list of items. Items can be expanded to show details or collapsed to show less information.
- Only one item can be expanded at any given time. Expanding one item will collapse another.
It’s probably reasonable to have a component that represents the whole accordion. As long as there aren’t
a huge number of items to show it’ll be worthwhile to represent each item in the accordion with its own component as well.
For context, here’s the application template:
For the purposes of this example, an item
is just a POJO with a title
and details
, as shown here in the application controller:
Given I’m creating a component for the list and a component to render each item, the real question in my mind is how to keep just one
item open at a time. I did this problem as a kata at Ember Columbus Meetup and we all
came up with similar solutions (which isn’t to say there aren’t more possibilites): have the list component keep track of which item is expanded,
and then pass that information into each accordion item component.
Imagining that each item in the accordion gets its own component, a basic template for the item might look like this:
Here’s the component file.
In order to expand just one item exclusively, each component checks if its item is the active one.
The accordion component manages that piece of state and passes it into the child. The child sends and action back up to the parent
in order for the parent to change its state.
Here’s an ascii diagram of the data down and the actions up.
Here’s an Ember Twiddle of my initial implementation.
The accordion as a pattern
For me the accordion turned out to be an expression of a pattern I see pretty frequently when I work with lists.
- Both the list itself and each position in the list are represented by components.
- Each child component in the list receives both the item it should display as well as the whole list.
- The child component decides what to do with the information, rather than having the parent do those computations.
- Children send an action to the parent to change any shared state.
By generalizing a little, component hierarchies that look quite different in the UI might end up behaving in very similar fashion.
The unfamiliar becomes familiar! Now I can look at translating the pattern into a more reusable component.
Separating behavior and rendering with {{yield}}
I started out with a generic idea but quickly got specific in the implementation, and it shows. My accordion only renders one kind of item, and other than passing it a
different list of items in there’s no real way for it to be used in other contexts. That’s an okay start, though! On the plus side,
the accordion encapsulates everything away from its caller. It only takes one parameter so it’s easy to invoke, and it’ll serve as a good boundary for testing.
I feel like creating a new component is always a tradeoff between encapuslation and indirection.
Right now both the rendering logic and the mechanics of the accordion itself are hidden away. It’s nice as a caller because the
accordion-list
is only exposing one parameter to the outside world, but as a reader it’s frustrating to have to navigate a whole
hierarchy of black boxes to understand what’s going on behind the scenes.
What I really want is something to take care of the accordiony behavior of the UI without enforcing how I render the parts.
Ideally the caller could render the list and specify how the items look the items in one fell swoop.
In Ember, when a component needs to let its caller provide it with content, the {{yield}}
helper is the thing to use.
The guides have some good examples, and I’ll
be going over the useage here too.
From this point on I’ll make a few versions of the accordion-list
component. I’ll call the first one block-accordion
.
Here’s a first shot at what I’d want the API to look like:
I’ve pretty much just inlined the old accordion-item
template into the calling context now. I worked backwards from the template
to decide that block-accordion
will have to yield the item
and isExpanded
into the block.
Here’s the accordion rendering accordion-item
behind the scenes:
Maybe I can get rid of the accordion-item
component
entirely? accordion-item
contained the logic for isExpanded
, and if I decided to nix it I’ve got a few choices.
- I could use a handlebars helper to check if the activeItem is the same as item ala
ember-truth-helpers and put the logic into the template. Since
it’s just an equality checkisExpanded=(eq item activeItem)
would be easy to reason about. - I could calculate
isExpanded
for each item inside the accordion component, and then somehow yield it. - I could keep
accordion-item
and yieldisExpanded
from there.
For now I’m going to keep the accordion-item
around. It’s serving as a glorified helper for the moment, but it’s a good example of yielding
content to a nested component, and it’ll be easy to remove at the end if it still feels redundant.
Now the accordion itself doesn’t know anything about how the actual items are rendered, and leaves it up to the caller to specify.
That includes implementing the action to toggle the active item, and when to hide or show the details. As a reader, it’s hard to tell what
portions of the template are devoted to the accordion itself and what is for specifics of the header and the item. It’d be nice if the accordion
could also provide those behaviors to the caller and still allow for flexibility in rendering the items. As of Ember 2.3 there are some declarative
ways to do exactly that!
Encapsulating behavior with contextual components
As of Ember 2.3, a component can yield other components in its block params.
The guides
provide a few examples, but I’ll be going into it in a litle more detail here. Coming from a rails background,
I was used to building markup using form helpers that yield a form builder to a block. Something
like this:
Inside the block, person_form.text_input
is a function that’s going to render some of its own markup. The form builder essentially exposes
its own DSL for building forms.
This is totally possible in contemporary Ember! I’ll go over the two building blocks: the hash helper, and contextual components.
The hash helper
The hash helper allows you to create hash inside a template.
Rather than having to yield a long list of parameters you can simply yield a hash and access the params by name.
Contextual components
A contextual component
is a component that was created with the component helper and yielded as
a parameter. When a caller invokes the yielded component they can specify extra options, just like with a normal component. Here’s a simple version of the
rails form builder implemented in handlebars as an example.
Not that for the text-input
component, the model
is passed in by the form-for
component, but the field
is passed in later. Also note that the
internal details of how the submit-form
component’s action is handled are completely hidden from the caller. I could make calling the form
builder even nicer by using positional params,
but this example shows how much you can accomplish only using templates.
If I apply the same methodology to the accordion, I can provide some declarative, semantic components that will make it harder
to use the accordion incorrectly. I’d like to have the accordion take care of expanding an item when a user clicks. I’d also like
to encapsulate the logic for hiding and showing the details based on the isExpanded
value. I think I can use two new contextual
components to do that!
I’ve changed my items to be a list of blog posts:
Here’s the template for rendering the new accordion:
From the caller’s point of view, I think this is much improved. Everything the caller needs to maintain the accordion itself has
been yielded as a component. The header
component takes care of toggling the active item, and the details
component
hides or shows the provided content. The two contextual components also provide clear buckets for me to enter the custom
markup for my blog posts. I was able to render a totally different kind of item without having to change the accordion at all,
and I don’t get lost reading through it either. How is this working behind the scenes?
Admittedly this is harder to read through than the first two versions of the accordion. contextual-accordion
yields two params: the item to render, and then a
hash with three members. isExpanded
is the yielded parameter from accordion-item
, and then there are two contextual components. As components go, there’s nothing
special about them. accordion-header
gets its click action set when it’s invoked from accordion-item
.
click
is one of the many events that
Ember.Component
inherits from Ember.View
that you can register actions on.
As for the header template:
The template itself renders another div
with a class used for styling, and that’s it!
The details template has only a little more content:
accordion-details
holds an if
based on isExpanded
, which got passed in by the accordion.
In my case the components themselves are very simple, but by wiring them together inside the accordion
component they can expose my intent to the
caller without also exposing their implementation details. Here’s a link to the updated twiddle
Recap: From single-use to generic
I started with two components, accordion-list
and accordion-item
. If someone wanted to change how the list was rendered, they would’ve had to make completely new components.
By changing the accordion components to yield values, I made something that was much more flexible, but the component didn’t provide much guidance to other developers. The contextual
version of the accordion is still flexible, but it also provides a toolbox that represents its domain. On the negative side, the internals of the component itself are less straightforward than
the other two implementations, and the number of files has gone up. There’s some extra indirection but I feel like the extra power and guidance that the contextual components provide are more than
worth it.