How To Create an Ember 2.0-Compatible Select Box

The advent of Ember 2.0 has brought with it a slew of good things, and certainly purged itself of a lot of cruft. The deprecation of Ember.View and Object/Array Controllers, lots of syntactical sugaring and decluttering, etc., made the act of upgrading a large Ember project from 1.0 to 2.0 a relatively time-consuming, albeit worthy, endeavor.

While going through the upgrade process, one of the challenges was the fact that the Ember.Select global and {{view 'select'}} view helper were deprecated. Most other common input elements (e.g. Text field/area, checkbox) are covered by the built-in {{input}} and {{textarea}} helpers, but there is no love built-in for the select box.

There are some alternatives and suggestions provided in the Ember community, but I was a little disappointed that a {{select}} or helper wasn’t provided by default. As it turns out, creating my own {{select}} helper turned out to be easier than I thought.

Some things I wanted in this component were:

  • Placeholder/prompt support
  • Action handling (willChange and didChange would be nice)
  • Customizable CSS classes
  • Support for optgroup tags

Let’s write some code.

First things first, we need to build the HTML/Handlebars for our basic select component:

<select disabled={{disabled}} class="{{class}}" {{action 'change' on='change'}}>
  {{#if prompt}}
    <option disabled selected={{is-not selection}}>
      {{prompt}}
    </option>
  {{/if}}
  {{#each content as |item|}}
    <option value={{read-path item optionValuePath}}>
      {{read-path item optionLabelPath}}
    </option>
  {{/each}}
</select>

This gives us the framework for a basic HTML select box. We can dynamically disable the box, add CSS classes, trigger change actions, support placeholders/prompts, and support a full set of content as an array of hashes or straight strings.

The optionLabelPath and optionValuePath properties allow us to specify the label and value key names if the option is a hash. The is-not, read-path, and read-path helpers are pretty self-explanatory but you can find them here if interested.

Next, we need some JavaScript to back the template:

import Ember from 'ember';

export default Ember.Component.extend({
  content: [],
  prompt: null,
  optionValuePath: 'value',
  optionLabelPath: 'label',
  
  init: function () {
    this._super(...arguments);
    if (!this.get('content')) {
      this.set('content', []);
    }
  },

  actions: {
    change: function () {
      let selectedIndex = this.$('select')[0].selectedIndex;
      let content = this.get('content');
      
      // decrement index by 1 if we have a prompt
      let hasPrompt = !!this.get('prompt');
      let contentIndex = hasPrompt ? selectedIndex - 1 : selectedIndex;
      let _selection = content[contentIndex];
  
      this.sendAction('willChangeAction', _selection);

      if (this.get('optionValuePath')) {
        this.set('selection', _selection[this.get('optionValuePath')]);
      } else {
        this.set('selection', _selection);
      }

      this.sendAction('didChangeAction', _selection);
    }
  }
});

This code is quite simple. It basically handles a change action triggered from the template, calculates the selected value based on the selectedIndex, and fires off a willChangeAction and didChangeAction. These actions can be really useful for warning the application when the value is about to change and making any necessary preparations.

The one thing that is missing is support for <optgroup> tags. I wanted to be able to pass in an array of group objects, like this:

[{ label: "Group 1", value: "group1" },
 { label: "Group 2", value: "group2" }]

and arrange an array of select content objects by their specified group, like this:

[{ label: "Group 1 Opt 1", value: "opt1", group: "group1" },
 { label: "Group 1 Opt 2", value: "opt2", group: "group1" },
 { label: "Group 2 Opt 1", value: "opt3", group: "group2" },
 { label: "Group 2 Opt 2", value: "opt4", group: "group2" }]

in order to create a select box that looks like this:

To implement this grouping functionality, we’ll need to add some magic to the template. We need to first iterate over all groups, and then iterate over all options within that group and add them to the DOM. Here is a snippet:

{{#each groups as |group|}}
  <optgroup label={{group.label}}>
    {{#each (content-for-group content group.value optionGroupPath) as |item|}}
      <option value={{read-path item optionValuePath}} selected={{is-equal (read-path item optionValuePath) selection}}>
        {{read-path item optionLabelPath}}
      </option>
    {{/each}}
  </optgroup>
{{/each}}

As you can see, we added a couple of things here.

  • An optionGroupPath property which defines the path to the group key in the content hash.
  • A new content-for-group helper, which filters the content using the current group’s key.

export default Ember.Helper.helper(function([content, group, contentGroupKey]) {
  return content.filterBy(contentGroupKey, group);
});

Note that this update now requires us to use groups (if there are no groups provided, it will not display anything), which is not what we want. To solve this problem, I ended up just wrapping the {{#each}} logic in a conditional like this:

{{#if groups}}
  <!-- do the group logic -->
{{else}}
  <!-- do the basic logic -->
{{/if}}

I don’t particularly love this solution, but it was the best I could come up with at the time.

Putting it all together.

Now that we have everything wired up, we can put it to the test! Let’s build a basic controller and a corresponding template:

export default Ember.Controller.extend({
  shouldDisable: false,
  myGroups: [{ label: "Group 1", value: "group1" },
             { label: "Group 2", value: "group2" }],
  myContent: [{ label: "Group 1 Opt 1", value: "opt1", group: "group1" },
              { label: "Group 1 Opt 2", value: "opt2", group: "group1" },
              { label: "Group 2 Opt 1", value: "opt3", group: "group2" },
              { label: "Group 2 Opt 2", value: "opt4", group: "group2" }]
});

{{select
  content=myContent
  groups=myGroups
  selection=selectedOption
  disabled=shouldDisable
  optionLabelPath="label"
  optionValuePath="value"
  optionGroupPath="group"
  prompt="Select an option"
  class='my-ember-select-widget'
  willChangeAction='someAction'
  didChangeAction='anotherAction'
}}

Voila! You now have a very flexible, customizable, Ember 2.0-compatible select box to use as you wish. Happy Ember-ing!

P.S. See this gist for all code used/referenced.

Conversation
  • mAhnaz says:

    Thank you for this article

  • NotReally says:

    Thanks, but comments like “this is pretty simple” and “these are quite self explanatory” do not help to understand the article and are huge put offs.

  • Comments are closed.