Complex Component Design in Ember - Part 4 - Use the hash helper

26 May 2016

This is the fourth and final part of my Complex Component Design series. Here are the preceding posts:

You can find the code for this post on Github.


After our last refactoring, the ember-cli-autocomplete component no longer uses observers. However, the list of parameters the outermost, container component, auto-complete returns is now unwieldily long:

 1<!-- tests/dummy/templates/index.hbs -->
 2{{#auto-complete
 3          on-select=(action "selectArtist")
 4          on-input=(action "filterArtists")
 5          options=matchingArtists
 6          displayProperty="name"
 7          class="autocomplete-container" as |isDropdownOpen inputValue options
 8                                             focusedIndex selectedIndex
 9                                             toggleDropdown onSelect onInput|}}
10
11  (...)
12{{/auto-complete}}

Not only does that look clumsy, it also makes refactoring more difficult and one always constantly have to flip between the component's template (where params are yielded from) and the template where the component is used to see if the position of values match. So how can improve this?

Components as functions

To understand several concepts about components, consider them functions. Putting aside the fact that they can also emit DOM elements, you call them with a list of arguments, usually, though not exclusively, a collection of key-value pairs. The component then does some internal stuff and returns a value from its template via the yield keyword.

Our current case is another instance when treating them as functions can help us find the solution. Ask yourself: what would you do if the return value of a function you wrote grew to a long list of arguments? You would convert the return value to a key-value collection, such as a hash, wouldn't you?

Well, in Ember's component land, we can do this by using the hash helper, introduced in Ember 2.3. It takes a list of key-value pairs at invocation time and outputs an object (a hash) with them:

1{{#with (hash firstName='Mike' lastName='McCready' instrument='guitar') as |musician|}}
2  Hello, I'm {{musician.firstName}} {{musician.lastName}} and I play the {{musician.instrument}}.
3{{/with}}

We can use the hash helper to bring some sanity to the return value of auto-complete parameters. It currently looks like this:

 1<!-- addon/templates/components/auto-complete.hbs -->
 2{{yield isDropdownOpen
 3        inputValue
 4        options
 5        focusedIndex
 6        selectedIndex
 7        (action "toggleDropdown")
 8        (action "selectOption")
 9        (action "inputDidChange")}}

So we introduce the hash helper to get the following:

 1<!-- addon/templates/components/auto-complete.hbs -->
 2{{yield (hash
 3    isOpen=isDropdownOpen
 4    inputValue=inputValue
 5    options=options
 6    focusedIndex=focusedIndex
 7    selectedIndex=selectedIndex
 8    toggleDropdown=(action "toggleDropdown")
 9    onSelect=(action "selectItem")
10    onInput=(action "inputDidChange"))}}

Modifying call sites

Now that the component's return value has changed, we should not forget to modify the callers, the downstream components that use that value:

 1<!-- tests/dummy/app/templates/index.hbs -->
 2{{#auto-complete
 3      on-select=(action "selectArtist")
 4      on-input=(action "filterArtists")
 5      items=matchingArtists
 6      displayProperty="name"
 7      class="autocomplete-container" as |params|}}
 8  <div class="input-group">
 9    {{auto-complete-input
10        value=params.inputValue
11        on-change=params.onInput
12        type="text"
13        class="combobox input-large form-control"
14        placeholder="Select an artist"}}
15    {{#auto-complete-list
16        isVisible=params.isOpen
17        class="typeahead typeahead-long dropdown-menu"}}
18      {{#each params.options as |option|}}
19        {{#auto-complete-option
20            index=option.index
21            on-click=params.onSelect
22            isFocused=(eq params.focusedIndex option.index)
23            isSelected=(eq params.selectedIndex option.index)}}
24          <a href="#">{{option.value}}</a>
25        {{/auto-complete-option}}
26      {{else}}
27        <li><a href="#">No results.</a></li>
28      {{/each}}
29    {{/auto-complete-list}}
30    {{#auto-complete-dropdown-toggle on-click=params.toggleDropdown class="input-group-addon dropdown-toggle"}}
31      <span class="caret"></span>
32    {{/auto-complete-dropdown-toggle}}
33  </div>
34{{/auto-complete}}

Instead of the long list of parameters, auto-complete now yields a single hash parameter (called params above), whose keys are used in the child components (params.isOpen, params.options, etc.)

Polyfill it

Since we want our component to be usable not only in Ember >=2.3 applications, where the hash helper is built in, we should add the ember-hash-helper-polyfill, which makes the hash helper available in earlier Ember versions, as a dependency of the addon:

 1// package.json
 2{
 3  "name": "ember-cli-autocomplete",
 4  "version": "0.0.0",
 5  "dependencies": {
 6    (...)
 7    "ember-hash-helper-polyfill": "0.1.0"
 8  },
 9}

Wrapping up

That wraps up my Complex Component Design in Ember.js series. Our component improved by each post and I think we now have a pretty flexible and thus reusable component. The main purpose of the series, however, is education, so I hope that I was able to transfer some of the knowledge I've acquired by building components.

If you would like to read the whole series as a pdf, just give my your email address below and I'm sending it to you.

Share on Twitter