I'm all about Ember.js recently

Using Dependency Injection to Write Better Tests

Testing is given much emphasis in the Ember.js community, and testing tools have showed steady progress to reduce the cost of writing tests of all types.

Lauren Tan wrote a great post about how Dependency Injection (DI) can be used to decouple a parent component from the internals of its child components. One of the gains of doing so is that the parent component becomes more focused and thus easier to test.

In this post, I’m doing something similar, although much simpler. I want to show you how to use DI in a simple helper function to make it easier to test.

Just your ordinary, run-of-the-mill function

Although the helper is an Ember (template) helper, the concepts could be very easily transferred to other frameworks, libraries and even languages.

I recently had to modify a normalizeText helper function that looked like this:

1
2
3
4
5
6
7
8
// tests/unit/helpers/normalize-text-test.js
import Ember from 'ember';

export function normalizeText([text]) {
  let normalizedEOLs = text.trim().replace(/(?:\r\n|\r|\n)/g, '</p><p>');
  let noEmptyParagraphs = normalizedEOLs.replace(/(<p><\/p>)/g, '');
  return Ember.String.htmlSafe("<p>" + noEmptyParagraphs + "</p>");
}

(I realize the above code does not handle a text value of undefined or null. The real code does but I want to keep the code examples to the minimum necessary to get my point across.)

Comparing objects to objects

Its test was quite simple and straightforward:

1
2
3
4
5
6
7
8
9
10
// tests/unit/helpers/normalize-text-test.js
import { normalizeText } from '../../../helpers/normalize-text';
import { module, test } from 'qunit';

module('Unit | Helper | normalize-text');

test('it works', function(assert) {
  let normalizedText = normalizeText(["The brown fox\r\njumped over the quick rabbit.\n"]);
  assert.equal(normalizedText, "<p>The brown fox</p><p>jumped over the quick rabbit.</p>");
});

The problem with that test is that we compare two Handlebars.SafeString instances (returned by Ember.String.htmlSafe) which are different even if the strings they wrap, their value, is the same:

1
2
3
let s1 = Ember.String.htmlSafe("sid transit gloria mundi");
let s2 = Ember.String.htmlSafe("sid transit gloria mundi");
s1 === s2 // => false

We’re, however, interested in the equality of the strings. If only there was a way to replace that pesky Ember.String.htmlSafe call from the call site…

DI to the rescue

This is exactly what Dependency Injection can help us do. Instead of hard-coding that “sanitizer” function dependency, the function could take it as a parameter so that callers could inject it. Usually DI examples use (and thus inject) class names or object instances but it is important to realize that the injected param could be very “primitive”, like a simple function.

So here is how I rewrote the function:

1
2
3
4
5
6
7
8
9
10
11
// app/helpers/normalize-text.js
import Ember from 'ember';

export function normalizeText([text], params={}) {
  let { sanitizer=Ember.String.htmlSafe } = params;
  let normalizedEOLs = text.trim().replace(/(?:\r\n|\r|\n)/g, '</p><p>');
  let noEmptyParagraphs = normalizedEOLs.replace(/(<p><\/p>)/g, '');
  return sanitizer("<p>" + noEmptyParagraphs + "</p>");
}

export default Ember.Helper.helper(normalizeText);

Notice how easy ES2015 destructuring makes the assignment of the sanitizer function:

1
let { sanitizer=Ember.String.htmlSafe } = params;

If no sanitizer key was present in params, then it will have a value of Ember.String.htmlSafe, the default behavior.

The call from the test can now override the default behavior of sending the normalized text through Ember.String.htmlSafe by passing in a “no-op” sanitizer function:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// tests/unit/helpers/normalize-text-test.js
import { normalizeText } from '../../../helpers/normalize-text';
import { module, test } from 'qunit';

function leaveAsIs(text) {
  return text;
}

module('Unit | Helper | normalize-text');

test('it works', function(assert) {
  let normalizedText = normalizeText(["The brown fox\r\njumped over the quick rabbit.\n"], { sanitizer: leaveAsIs });
  assert.equal(normalizedText, "<p>The brown fox</p><p>jumped over the quick rabbit.</p>");
});

We’re now comparing simple strings which place nicely with assert.equal (with ===), and our test now passes.

Non-testing benefits

Code modifications introduced for the sake of testing usually also improve the non-testing aspect. Here, we made it possible to pass any function before we return the normalized text. We could, for example, use this to replace the <p> tags with <span>s, if we so wish.

Rock and Roll With Ember.js 2.6 Is Released

With some delay, but I just sent an update to all of the Rock and Roll with Ember.js customers. The book brings the app in sync with Ember, Ember Data and Ember CLI 2.6. Among a few other improvments it:

  • Makes sure the new Ember welcome page is removed before generating the application template: #196
  • Adds a version string to the book’s title and also in the Preface so that the reader knows whether they are reading the latest version: #174

The list of all accomplished things can be seen here

Complex Component Design in Ember - Part 4 - Use the Hash Helper

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:

<!-- 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
// package.json
{
  "name": "ember-cli-autocomplete",
  "version": "0.0.0",
  "dependencies": {
    (...)
    "ember-hash-helper-polyfill": "0.1.0"
  },
}

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.

Rock and Roll With Ember.js 2.5 Is Released

I published a new version of the Rock and Roll Ember.js book, and the related application. It now runs on Ember, Ember Data and Ember CLI ~2.5.0.

More importantly, I made other improvements that serve to improve clarity and reduce the number of new things the reader has to absorb at each step, which I think is hugely important for an efficient, non-frustrating learning process.

  1. The biggest change (and simplification) is that I no longer sort the songs from the get-go. To do so, I needed to use the SortableMixin and later, when that was gone, an ArrayProxy. This resulted in other simplifications, like not having to use (and maintain) a jsbin for that code snippet that used the “global” Ember application building style and iterated on the magical sortedContent property.

  2. I also improved the flow of the Components chapter, rearranged some sections, explained a few things that help comprehension and moved a few things that only add to the learning burden.

  3. I created an Appendix, called “Encore” to further the rock analogy. I felt (and got matching feedback) that on some occasions there were too many “sidebar” explanations (called “Backstage” sections in the book), that either weren’t important enough to warrant holding up the flow of explanation or lacked context. I moved these sections into the Encore where interested readers can learn about these topics when they see fit.

  4. Last, but not least, I went through the book and built the application from scratch to see that everything still works. I also applied git tags at the end of each chapter so that readers of the middle- and high-tier packages can skip to each chapter in the code in a very simple way, using git checkout.

(There were some other changes, the whole list of which you can see here.)

This is the most significant update since I published the Ember 2 version of the book last October and I believe following the book (and the building of the app) became even easier.

If this piqued your interest, you can download a sample chapter below.

Complex Component Design in Ember - Part 3 - Replace the Observer

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

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:

JPJ is too good to be replaced

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// addon/components/auto-complete-option.js
import Ember from 'ember';

export default Ember.Component.extend({
  (...)

  didBecomeSelected: Ember.observer('isSelected', function() {
    let isSelected = this.get('isSelected');
    if (isSelected) {
      this._selectItem();
    }
  }),

   _selectItem() {
    let item = this.get('item');
    this.get('on-click')(item, this.get('label'));
  }
});

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// addon/components/auto-complete.js
import Ember from 'ember';

export default Ember.Component.extend({
  (...)
  selectOption: function(event) {
    event.preventDefault();
    const focusedIndex = this.get('focusedIndex');
    if (Ember.isPresent(focusedIndex)) {
      this.set('selectedIndex', focusedIndex);
      this.send('selectOption', this.get('selectedOption'));
    }
    this.set('isDropdownOpen', false);
  },

  selectedOption: Ember.computed('selectedIndex', 'options.[]', function() {
    return this.get('options').objectAt(this.get('selectedIndex'));
  }),
});

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:

1
2
3
4
5
6
7
8
9
10
11
12
13
// addon/components/auto-complete.js
import Ember from 'ember';

export default Ember.Component.extend({
  (...)
  actions: {
    inputDidChange(value) {
      this.get('on-input')(value);
      this.set('focusedIndex', null);
      (...)
    }
  }
});

Next, let’s look at how the selectOption action needs to change:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// addon/components/auto-complete.js
import Ember from 'ember';

export default Ember.Component.extend({
  (...)
  _displayForOption(option) {
    const displayProperty = this.get('displayProperty');
    return option.get(displayProperty);
  },

  actions: {
    selectOption(option) {
      let inputValue = this._displayForOption(option);
      this.get('on-select')(option);
      this.set('isDropdownOpen', false);
      this.set('inputValue', inputValue);
    },
    (...)
  }
});

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// addon/components/auto-complete-option.js
import Ember from 'ember';

export default Ember.Component.extend({
  tagName: 'li',
  classNames: 'ember-autocomplete-option',
  classNameBindings: Ember.String.w('isSelected:active isFocused:focused'),

  item: null,
  'on-click': null,
  isFocused: false,
  isSelected: false,

  click() {
    this.get('on-click')(this.get('item'));
  }
});

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:

<!-- addon/templates/components/auto-complete.hbs -->
{{yield isDropdownOpen
        inputValue
        options
        focusedIndex
        selectedIndex
        (action "toggleDropdown")
        (action "selectOption")
        (action "inputDidChange")}}

Then, the each loop should iterate through options, and not through matchingArtists as before:

<!-- tests/dummy/app/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|}}
  <div class="input-group">
    {{auto-complete-input
        value=inputValue
        on-change=onInput
        type="text"
        class="combobox input-large form-control"
        placeholder="Select an artist"}}
    {{#auto-complete-list
        isVisible=isDropdownOpen
        class="typeahead typeahead-long dropdown-menu"}}
      {{#each options as |option index|}}
        {{#auto-complete-option
            item=option
            on-click=onSelect
            isFocused=(eq focusedIndex index)
            isSelected=(eq selectedIndex index)}}
          <a href="#">{{option.name}}</a>
        {{/auto-complete-option}}
      {{else}}
        <li><a href="#">No results.</a></li>
      {{/each}}
    {{/auto-complete-list}}
    {{#auto-complete-dropdown-toggle on-click=toggleDropdown class="input-group-addon dropdown-toggle"}}
      <span class="caret"></span>
    {{/auto-complete-dropdown-toggle}}
  </div>
{{/auto-complete}}

The bug at the beginning of the post is now gone:

JPG too-good-to-go bug fixed

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.

Rock and Roll With Ember.js - Now on 2.4 and With Code Diffs

I have just sent an updated version of Rock and Roll with Ember.js to my readers. The app now runs on Ember 2.4.3.

The biggest change in this release is that I now leverage code diffs in code snippets, where this makes understanding changes easier. It looks like this in the pdf version:

Colored code diffs

Since the last release was more than two months ago and I constantly improve things, there is a whole slew of other changes that you can see here.

If you are not yet a reader yet and want to have an always up-to-date Ember guide book, sign up below to get a sample chapter:

Rock and Roll With Ember.js Demo - a Public Ember.js Example Project

I have a book called Rock and Roll with Ember.js that has an accompanying application we develop throughout the book. I also maintain a demo version of the same app which has been open-source since its inception. However, that demo app has not received updates for a while now so I decided to do something about this and spent some time this week on making it a state-of-the-art Ember 2 application.

Here are the main developments I have made:

  • Upgraded it to use the latest Ember, Ember Data and Ember CLI versions, 2.3.0.
  • Used ember-cli-mirage to seed the app with data and handle “backend” requests. (Look, ma’, no backend!) ember-cli-mirage is fantastic addon that lets you set up route handlers, fixtures, factories and many more to manage your seed data and mock your server responses, both in development and tests. This was the first time I seriously used it and I have grown to like it a ton! The author, Sam Selikoff, helped out tremendously and had an amazing turnaround on a few questions/issues. I used the latest beta version, 0.2.0-beta.7, which you should definitely check out and give feedback to Sam.
  • Made it a modern, idiomatic Ember app. It’s not just Ember, but also Javascript that evolves at a neck-breaking space (although to a lesser extent). I used the not-at-all elementary ember-watson to modernize the Ember app and applied a few manual tweaks for some of the Javascript parts, like using destructuring and let and const instead of var.
  • Deployed it to be publicly accessible. Leveraging the most excellent PageFront, the app is now deployed to their platform. You can see it in action at https://rarwe-demo.pagefrontapp.com. This was not even a task, I only needed to issue two commands, one to install the add-on and one to deploy it.

The source lives on Github, at balinterdi/rarwe-demo.

As Mirage can also be used in production (although it’s probably not common to do that), you can check out the “production” app, with the same seed data I used in development, and play around with it.

As I mentioned in the introduction, the full version of the app is developed chapter by chapter in the Rock and Roll with Ember book. You can download a sample chapter below:

How to Make an Ember Module Resolve as Another One

I wanted to write another short, and hopefully useful, post just as I did recently for binding the style attribute.

No configuration is simpler than no configuration

About a month ago I was working to add authorization to the Rock and Roll with Ember application. I used my favorite addon, Torii, to help with that and opted to do the authorization via the google-oauth2-bearer provider. To restore the session, Torii looks up the application (Torii) adapter, but the session initialization and closing code used the google-oauth2-bearer adapter. So I had two separate files, which I was not happy about and I did not want to merge everything into the application adapter, as it does not give a hint about its contents then.

My idea was to make it possible to use another adapter to restore the session from, via a configuration option. Matthew Beale hinted at a solution that removes the need for a configuration option and since I haven’t seen this before, I want to share it with you.

Import from target module, then reexport

The Ember resolver is the piece that maps qualified full names (like route:blog or controller:bands) to module names.

In my case, Torii makes the resolver look up torii-adapter:application to fetch the session from and I wanted this to be resolved to torii-adapter:google-oauth2-bearer. In the Ember CLI project, that is equivalent of having the app/torii-adapters/application.js file export what is exported by app/torii-adapters/google-oauth2-bearer.js.

When phrased like this, the solution is near and I am somewhat embarrassed it took me a few attempts to arrive at this.

So the solution is to import in app/torii-adapters/application.js what app/torii-adapters/google-oauth2-bearer.js exports and then reexport it:

1
2
3
4
// app/torii-adapters/application.js
import GoogleOAuth2BearerAdapter from './google-oauth2-bearer';

export default GoogleOAuth2BearerAdapter;
1
2
3
4
// app/torii-adapters/google-oauth2-bearer.js
export default Ember.Object.extend({
  (...)
});

Voila, we have “tricked” the resolver without adding any configuration (and thus complexity) to the addon.

Complex Components in Ember.js - Part 2 - Towards a More Reactive Component

This is part 2 of my Complex Component Design series. Here are the posts in the series:


In the previous part of this series, the implementation of the main user flows were explained in detail. I ended the post by saying that I was not content with the implementation for several reasons, the most crucial of which was that parent components needed to be passed down to children, so that children can register themselves with their parent. That, in turn, allowed parents to reach their children and call methods on them directly instead of using events, actions and data bindings for communication. In this post, we’ll see how to get rid of these and replace them with more reactive solutions.

Remove the need for direct access to the input

Currently, the autocomplete component (the parent) yields itself to its children. auto-complete-input binds its own autocomplete attribute to it so that it can register itself with its parent when inserted:

<!-- tests/dummy/app/templates/index.hbs -->
{{#auto-complete
      on-select=(action "selectArtist")
      on-input=(action "filterArtists")
      class="autocomplete-container" as
        |autocomplete isDropdownOpen inputValue
         toggleDropdown onSelect onInput|}}
  <div class="input-group">
    {{auto-complete-input
        autocomplete=autocomplete
        value=inputValue
        on-change=onInput
        type="text"
        class="combobox input-large form-control"
        placeholder="Select an artist"}}
    (...)
  </div>
(...)
{{/auto-complete}}
1
2
3
4
5
6
7
8
9
10
11
// addon/components/auto-complete-input.js
import Ember from 'ember';

export default Ember.TextField.extend({
  autocomplete: null,

  registerWithAutocomplete: Ember.on('didInsertElement', function() {
    this.get('autocomplete').registerInput(this);
  }),
  (...)
});

This is needed when the item is autocompleted and the autocompleted segment is pre-selected so that the user can type over it if it’s not the item they had in mind:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// addon/components/auto-complete.js
export default Ember.Component.extend({
  (...)
  actions: {
    inputDidChange(value) {
      (...)
      Ember.run.scheduleOnce('afterRender', this, function() {
        (...)
        const firstOption = this.get('list.firstOption');
        if (firstOption) {
          const autocompletedLabel = firstOption.get('label');
          this.set('focusedOption', firstOption);
          this.get('on-select')(firstOption.get('item'));
          this.set('inputValue', autocompletedLabel);
          Ember.run.next(() => {
            this.get('input.element').setSelectionRange(value.length, autocompletedLabel.length);
          });
        }
      });
    }
  }
});

On the very last line, the component accesses the input directly, to select (and highlight) the portion of the item that was autocompleted. That’s why we need the whole registration process.

Since inputDidChange is triggered from the auto-complete-input component, we could get rid of this direct coupling if there was a way to react to the action’s result in the auto-complete-input itself. That way is called closure actions.

Fire, but don’t forget

As opposed to the fire-and-forget nature of “ordinary” (aka. element) actions, closure actions provide a way to react to the action’s outcome at the source, where the action was fired from.

Since closure actions are functions, they can have return values. If the action triggers an async action, it’s best to return a promise from the upstream handler to which the event source can attach its handler to.

Let’s see how that works in our case.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// addon/components/auto-complete.js
export default Ember.Component.extend({
  (...)
  actions: {
    inputDidChange(value) {
      this.get('on-input')(value);
      this.set('isDropdownOpen', true);
      return new Ember.RSVP.Promise((resolve, reject) => {
        (...)
        Ember.run.scheduleOnce('afterRender', this, function() {
          const firstOption = this.get('list.firstOption');
          if (firstOption) {
            const autocompletedLabel = firstOption.get('label');
            this.set('focusedOption', firstOption);
            this.get('on-select')(firstOption.get('item'));
            this.set('inputValue', autocompletedLabel);
            Ember.run.next(() => {
              resolve({ start: value.length, end: autocompletedLabel.length });
            });
          }
        });
      });
    }
  }
});

The code did not change a lot, but now a promise is returned on line 8. It is resolved on 18, where start and end designate the cursor positions of the selection.

The action handler in the auto-complete-input component needs to be modified to set the selection higlight itself:

1
2
3
4
5
6
7
8
9
10
11
// addon/components/auto-complete-input.js
import Ember from 'ember';

export default Ember.TextField.extend({
  valueDidChange: Ember.on('input', function() {
    const value = this.$().val();
    this.get('on-change')(value).then(({ start, end }) => {
      this.get('element').setSelectionRange(start, end);
    });
  })
});

Calling on-change will call the above inputDidChange function. Instead of firing the (element) action and forgetting about it, we now call the (closure) action and then “wait” for the resulting promise to be resolved. Once it does, we set the selection range.

We could now remove all the registration code and the passing down of the autocomplete instance to the input component.

Remove the need for direct access to the list options

There is still another instance of the same. It serves to give access to the autocomplete component to the auto-complete-option, through the auto-complete-list.

<!-- tests/dummy/app/templates/index.hbs -->
{{#auto-complete
      on-select=(action "selectArtist")
      on-input=(action "filterArtists")
      class="autocomplete-container" as |autocomplete isDropdownOpen inputValue
                                         toggleDropdown onSelect onInput|}}
  <div class="input-group">
    {{auto-complete-input
        value=inputValue
        on-change=onInput
        type="text"
        class="combobox input-large form-control"
        placeholder="Select an artist"}}
    {{#auto-complete-list autocomplete=autocomplete isVisible=isDropdownOpen class="typeahead typeahead-long dropdown-menu" as |list|}}
      {{#each matchingArtists as |artist|}}
        {{#auto-complete-option
            id=artist.id
            label=artist.name
            item=artist
            list=list
            on-click=onSelect
            activeId=selectedArtist.id}}
          <a href="#">{{artist.name}}</a>
        {{/auto-complete-option}}
      {{/each}}
    {{/auto-complete-list}}
    (...)
  </div>
{{/auto-complete}}

I am not copying all the registration code here as it’s very boilerplatey. Each option, when inserted into the DOM, registers itself with its list, while the list registers itself with the auto-complete component. The latter has an options property to access the options:

1
2
// addon/components/auto-complete.js
options: Ember.computed.readOnly('list.options')

This access is needed to be able to cycle through the options by using the cursor keys and then select one of them by using the return key. Here is the code that handles keypresses (more precisely, keydowns):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// addon/components/auto-complete.js
export default Ember.Component.extend({
  (...)
  keydownMap: {
    8:  'startBackspacing', // backspace
    13: 'selectOption',  // return
    27: 'closeDropdown', // escape
    38: 'focusPrevious', // up key
    40: 'focusNext', // down key
  },

  handleKeydown: Ember.on('keyDown', function(event) {
    const map = this.get('keydownMap');
    const code = event.keyCode;
    const method = map[code];
    if (method) {
      return this[method](event);
    }
  }),
  (...)
});

This is pretty simple so far. If a key we care about was pressed, we call the appropriate method to handle it. Let’s see how focusing works:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// addon/components/auto-complete.js
export default Ember.Component.extend({
  (...)
  options: Ember.computed.readOnly('list.options'),

  focusPrevious: function(event) {
    event.preventDefault();
    const focused = this.get('focusedOption');
    let index = this.get('options').indexOf(focused);
    if (this.get('isDropdownOpen')) {
      index = index - 1;
    }
    this.focusOptionAtIndex(index);
  },

  focusNext: function(event) {
    event.preventDefault();
    let index = 0;
    const focused = this.get('focusedOption');
    if (focused) {
      index = this.get('options').indexOf(focused);
      if (this.get('isDropdownOpen')) {
        index = index + 1;
      }
    }
    this.focusOptionAtIndex(index);
  },

  focusOptionAtIndex: function(index) {
    const options = this.get('options');
    if (index === -1) {
      index = options.get('length') - 1;
    } else if (index === options.get('length')) {
      index = 0;
    }
    const option = this.get('options').objectAt(index);
    if (!option) {
      return;
    }
    this.focusOption(option);
  },

  focusOption: function(option) {
    const focused = this.get('focusedOption');
    if (focused) {
      focused.blur();
    }
    this.set('focusedOption', option);
    option.focus();
  },
  (...)
});

focusPrevious and focusNext make sure that the focused index is kept within the bounds of the avaiable number of options and then focus the previous (or next) one by calling option.focus() directly (line 49).

There is one more key press concerning related to options, the return key. It should select the currently focused option, if there is one:

1
2
3
4
5
6
7
8
9
10
11
12
13
// addon/components/auto-complete.js
export default Ember.Component.extend({
  (...)
  options: Ember.computed.readOnly('list.options'),
  selectOption: function(event) {
    event.preventDefault();
    const focused = this.get('focusedOption');
    if (focused) {
      this.send('selectItem', focused.get('item'), focused.get('label'));
    }
    this.set('isDropdownOpen', false);
  },
});

This code also leverages the access to the options, indirectly through this.get('focusedOption'). Furthermore, it assumes that each option has an item and label properties. Not stellar.

It won’t be a piece of cake to get rid of direct coupling in all of these, so let’s get to it.

Change the focused option without accessing the options

In the first step, we’ll change the focused option without directly commanding the options to focus/unfocus. We’ll then tackle selecting the focused option.

We can use simple data binding to have the focused option available. By maintaining and yielding a focusedIndex in the “control center”, the autocomplete component, autocomplete-option components can bind to it and know whether they are focused or not.

Here is how the templates need to change:

<!-- addon/templates/components/autocomplete.hbs -->
{{yield isDropdownOpen
        inputValue
        focusedIndex
        selectedIndex
        (action "toggleDropdown")
        (action "selectItem")
        (action "inputDidChange")}}
<!-- tests/dummy/app/templates/index.hbs -->
{{#auto-complete
      on-select=(action "selectArtist")
      on-input=(action "filterArtists")
      options=matchingArtists
      displayProperty="name"
      class="autocomplete-container" as |isDropdownOpen inputValue
                                         focusedIndex selectedIndex
                                         toggleDropdown onSelect onInput|}}
  <div class="input-group">
    {{auto-complete-input
        value=inputValue
        on-change=onInput
        type="text"
        class="combobox input-large form-control"
        placeholder="Select an artist"}}
    {{#auto-complete-list
        isVisible=isDropdownOpen
        class="typeahead typeahead-long dropdown-menu" as |list|}}
      {{#each matchingArtists as |artist index|}}
        {{#auto-complete-option
            label=artist.name
            item=artist
            on-click=onSelect
            isFocused=(eq focusedIndex index)
            isSelected=(eq selectedIndex index)}}
          <a href="#">{{artist.name}}</a>
        {{/auto-complete-option}}
      {{else}}
        <li><a href="#">No results.</a></li>
      {{/each}}
    {{/auto-complete-list}}
    (...)
  </div>
{{/auto-complete}}

Note the new focusedIndex and selectedIndex attributes, yielded by the top-level component that isFocused and isSelected in the auto-complete-option are bound to.

The eq helper comes from ember-truth-helpers and will evaluate to true if its params are equal which is exactly what we want.

The autocomplete component needs to change to manage the new indexes instead of setting its focusedOption and calling option.set directly:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// addon/components/auto-complete.js
export default Ember.Component.extend({
  (...)
  optionsLength: Ember.computed.readOnly('options.length'),
  focusPrevious: function(event) {
    event.preventDefault();
    const currentIndex = this.get('focusedIndex');
    let newIndex;
    if (Ember.isNone(currentIndex)) {
      newIndex = this.get('optionsLength') - 1;
    } else if (currentIndex === 0) {
      newIndex = this.get('optionsLength') - 1;
    } else {
      newIndex = currentIndex - 1;
    }
    this.set('focusedIndex', newIndex);
    this.set('isDropdownOpen', true);
  },

  focusNext: function(event) {
    event.preventDefault();
    const currentIndex = this.get('focusedIndex');
    const lastIndex = this.get('optionsLength') - 1;
    let newIndex;
    if (Ember.isNone(currentIndex)) {
      newIndex = 0;
    } else if (currentIndex === lastIndex) {
      newIndex = 0;
    } else {
      newIndex = currentIndex + 1;
    }
    this.set('focusedIndex', newIndex);
    this.set('isDropdownOpen', true);
  },

  selectOption: function(event) {
    event.preventDefault();
    const focusedIndex = this.get('focusedIndex');
    if (Ember.isPresent(focusedIndex)) {
      this.set('selectedIndex', focusedIndex);
    }
    this.set('isDropdownOpen', false);
  },
});

That is simpler and less intrusive than before. (Setting isDropdown to true has been added as before the option’s focus method did the opening).

What’s missing is for the selected item to be sent to the outer world (in other words, for the selectItem to be triggered). Before, it was done by sending the selectItem action with the focused option’s item and label (see line 9 in the last snippet of the previous section) but we can no longer indulge in accessing the options directly. Consequently, it was replaced by setting the selectedIndex to the focusedIndex (see line 40 above).

The problem now is that selectItem needs to be called with the item and the label (the name of the selected artist to be set as the input’s value) and only the selected auto-complete-option component has that knowledge. So we need to set up a way for the auto-complete-option components to know when they become selected and then call that action. As these components are not the source of the event that lead to an option being selected by key press, we choose to use an observer:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// addon/components/auto-complete-option.js
import Ember from 'ember';

export default Ember.Component.extend({
  tagName: 'li',
  classNames: 'ember-autocomplete-option',
  classNameBindings: Ember.String.w('isSelected:active isFocused:focused'),

  label: null,
  item: null,
  'on-click': null,
  isFocused: false,
  isSelected: false,

  didClick: Ember.on('click', function() {
    this._selectItem();
  }),

  didBecomeSelected: Ember.observer('isSelected', function() {
    const isSelected = this.get('isSelected');
    if (isSelected) {
      this._selectItem();
    }
  }),

  _selectItem() {
    const item = this.get('item');
    this.get('on-click')(item, this.get('label'));
  }
});

Line 21 and 22 is where the option realizes it has become the selected option, and then calls the corresponding (closure) action on line 28.

We’re done, we got rid of all the direct passing of component instances, registrations and direct property access and method calling. Even though we’re Demeter compliant, there are things that could be improved.

In the next episode…

One of these things is the observer. Observers fell out of favor some time ago, and for a good reason. They can be over-eager and lead to scenarios where it is hard to see what’s going on. To prove my point, let me show you a bug I’ve just accidentally introduced. I call it the “JPJ is too good to be replaced” bug:

JPJ is too good to be replaced

(The code for this series is publicly available on Github here. I’ve tagged where we are now with ccd-part-two.)

So we’re not done yet. In the next post of the series, we’re going to fix that bug by replacing the observer and make other worthy improvements. Stay tuned!