This is the fourth and final part of my Complex Component Design series. Here are the preceding posts:
- Intro
- Part 1 - Analyzing User Flows
- Part 2 - Towards a more reactive component
- Part 3 - Remove the observer
- Part 4 - Use the hash helper
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:
<!-- tests/dummy/templates/index.hbs -->
{{#auto-complete
on-select=(action "selectArtist")
on-input=(action "filterArtists")
options=matchingArtists
displayProperty="name"
class="autocomplete-container" as |isDropdownOpen inputValue options
focusedIndex selectedIndex
toggleDropdown onSelect onInput|}}
(...)
{{/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:
{{#with (hash firstName='Mike' lastName='McCready' instrument='guitar') as |musician|}}
Hello, I'm {{musician.firstName}} {{musician.lastName}} and I play the {{musician.instrument}}.
{{/with}}
We can use the hash
helper to bring some sanity to the return value of auto-complete
parameters. It currently looks like this:
<!-- addon/templates/components/auto-complete.hbs -->
{{yield isDropdownOpen
inputValue
options
focusedIndex
selectedIndex
(action "toggleDropdown")
(action "selectOption")
(action "inputDidChange")}}
So we introduce the hash
helper to get the following:
<!-- addon/templates/components/auto-complete.hbs -->
{{yield (hash
isOpen=isDropdownOpen
inputValue=inputValue
options=options
focusedIndex=focusedIndex
selectedIndex=selectedIndex
toggleDropdown=(action "toggleDropdown")
onSelect=(action "selectItem")
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:
<!-- tests/dummy/app/templates/index.hbs -->
{{#auto-complete
on-select=(action "selectArtist")
on-input=(action "filterArtists")
items=matchingArtists
displayProperty="name"
class="autocomplete-container" as |params|}}
<div class="input-group">
{{auto-complete-input
value=params.inputValue
on-change=params.onInput
type="text"
class="combobox input-large form-control"
placeholder="Select an artist"}}
{{#auto-complete-list
isVisible=params.isOpen
class="typeahead typeahead-long dropdown-menu"}}
{{#each params.options as |option|}}
{{#auto-complete-option
index=option.index
on-click=params.onSelect
isFocused=(eq params.focusedIndex option.index)
isSelected=(eq params.selectedIndex option.index)}}
<a href="#">{{option.value}}</a>
{{/auto-complete-option}}
{{else}}
<li><a href="#">No results.</a></li>
{{/each}}
{{/auto-complete-list}}
{{#auto-complete-dropdown-toggle on-click=params.toggleDropdown class="input-group-addon dropdown-toggle"}}
<span class="caret"></span>
{{/auto-complete-dropdown-toggle}}
</div>
{{/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 2 3 4 5 6 7 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.