Complex Component Design in Ember - Part 3 - Replace the observer
08 April 2016
This is part 3 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 - Replace the observer
- Part 4 - Use the hash helper
You can find the code for this post on Github.
In the last post, we refactored towards a more reactive component and got pretty far. However, we established that we'd still have to remove the observer that was also causing a weird bug:

Event origin and data owner components are different
The reason we introduced an observer was that we wanted to trigger an action when one of the options was selected via cycling through them and hitting the return key on the focused option. Since the necessary data for that event was contained in the auto-complete-option component but the source of the keyboard event was the auto-complete component, we couldn't simply trigger the action with the right data from the event source.
We fixed this by using an observer so that even though the event that should trigger the action was fired "upstream", in the auto-complete component, we could react to this change "downstream", in the appropriate auto-complete-option whose isSelected property became true:
1// addon/components/auto-complete-option.js 2import Ember from 'ember'; 3 4export default Ember.Component.extend({ 5 (...) 6 7 didBecomeSelected: Ember.observer('isSelected', function() { 8 let isSelected = this.get('isSelected'); 9 if (isSelected) { 10 this._selectItem(); 11 } 12 }), 13 14 _selectItem() { 15 let item = this.get('item'); 16 this.get('on-click')(item, this.get('label')); 17 } 18});
Our analysis of the cause already hints at the solution. We could move the knowledge of which option is selected up to the auto-complete component and then, when the user hits the return key to select the focused option, trigger the action with the data that we have at our disposal.
Centralized power in auto-complete
Changes in components
We will maintain the selected option in auto-complete and trigger the selectItem action when one of them is selected via a return key event (I skipped the code snippet that calls selectOption for return):
1// addon/components/auto-complete.js 2import Ember from 'ember'; 3 4export default Ember.Component.extend({ 5 (...) 6 selectOption: function(event) { 7 event.preventDefault(); 8 const focusedIndex = this.get('focusedIndex'); 9 if (Ember.isPresent(focusedIndex)) { 10 this.set('selectedIndex', focusedIndex); 11 this.send('selectOption', this.get('selectedOption')); 12 } 13 this.set('isDropdownOpen', false); 14 }, 15 16 selectedOption: Ember.computed('selectedIndex', 'options.[]', function() { 17 return this.get('options').objectAt(this.get('selectedIndex')); 18 }), 19});
On line 11, we call the selectOption action (renamed from selectItem) with the (new) selected option. selectedOption is simply the option that has the selectedIndex.
Independently of the current selectOption refactor, let's fix a nasty bug by making sure to reset the focusedIndex when the input changes:
Next, let's look at how the selectOption action needs to change:
1// addon/components/auto-complete.js 2import Ember from 'ember'; 3 4export default Ember.Component.extend({ 5 (...) 6 _displayForOption(option) { 7 const displayProperty = this.get('displayProperty'); 8 return option.get(displayProperty); 9 }, 10 11 actions: { 12 selectOption(option) { 13 let inputValue = this._displayForOption(option); 14 this.get('on-select')(option); 15 this.set('isDropdownOpen', false); 16 this.set('inputValue', inputValue); 17 }, 18 (...) 19 } 20});
One of the things that has changed is that it now only receives one argument, option as the label of the option can now be computed internally, from within the component.
That means that the label now does not need to be passed to the auto-complete-option components and that its action that gets triggered when the user clicks on it needs to be adjusted:
1// addon/components/auto-complete-option.js 2import Ember from 'ember'; 3 4export default Ember.Component.extend({ 5 tagName: 'li', 6 classNames: 'ember-autocomplete-option', 7 classNameBindings: Ember.String.w('isSelected:active isFocused:focused'), 8 9 item: null, 10 'on-click': null, 11 isFocused: false, 12 isSelected: false, 13 14 click() { 15 this.get('on-click')(this.get('item')); 16 } 17});
You can see I removed the observer and that I only send the item (not the label, see the very first code example) in the action handler to comply with the new API of the selectOption action.
Changes in templates
Let's see how the templates need to change to accommodate that change.
First of all, the template of the auto-complete component needs to yield the options to be consumed downstream. Let's also not forget to rename selectItem to selectOption:
Then, the each loop should iterate through options, and not through matchingArtists as before:
The bug at the beginning of the post is now gone:

In the next episode...
We now have a working, state-of-the-art component design with no coupling between the sub-components and no observers. One thing that is not ideal, though, is the number of parameters the auto-complete components yields (see last code snippet).
Just as you wouldn't have a method with 7 or 8 positional parameters, you don't want a component that yields that many properties matched by position. So in the next installment of this series, we'll use the hash helper to transform that long list into keyed parameters.