I'm all about Ember.js recently

Select in Ember With Multiple Selection

A few weeks ago I wrote a blog post about how to do a select in Ember 2, that seemed to be popular. I also received good comments about advanced versions of the same problem, namely how the solution would have to change to deal with the case if the items to select from are objects and how to tackle multiple selections. I thus decided to do a Part 2, showing a solution for these cases. Comment are welcome, as always.

Multiple selection with simple strings as items

Let’s tackle the easier problem first, being able to select more than one items, but the items are simple string values. The values will serve both as the value and the content of the options.

I added some extra Bootstrap markup and a list to see which items are selected:

<div class="container">
  <div class="row">
    <div class="col-sm-8">
      <h2>Select some bands</h2>
      <select style="height:100px" class="form-control" multiple onchange={{action "selectBand"}}>
        {{#each bands as |bandChoice|}}
        <option value={{bandChoice}} selected={{include selectedBands bandChoice}}>{{bandChoice}}</option>
        {{/each}}
      </select>
    </div>
    <div class="col-sm-4">
      {{#if selectedCount}}
        <h2>Selected bands ({{selectedCount}})</h2>
      {{else}}
        <h2>Selected bands</h2>
      {{/if}}
      <ul class="list-group">
        {{#each selectedBands as |band|}}
          <li class="list-group-item">{{band}}</li>
        {{else}}
          <li class="list-group-item">No band selected.</li>
        {{/each}}
      </ul>
    </div>
  </div>
</div>

I added the multiple attribute to the select tag to allow multiple selections. Not much has changed from the earlier example. When the user selects an option, whether in a way that clears the earlier selection (simple click) or adds to it (ctrl/cmd + click), the onchange event is fired, and our selectBand handler will handle it. We expect that handler to set selectedBands so that the list of selected bands gets updated correctly. So let’s see the controller:

export default Ember.Controller.extend({
  bands: ['Pearl Jam', 'Tool', 'Long Distance Calling', 'Led Zeppelin'],

  selectedBands: [],

  selectedCount: Ember.computed.reads('selectedBands.length'),

  actions: {
    selectBand(event) {
      const selectedBands = Ember.$(event.target).val();
      this.set('selectedBands', selectedBands || []);
    }
  }
});

For multiple selections, jQuery, aliased as Ember.$, returns an array of the selected options values as the select’s value, so all we have to do is assign this to the selectedBands property. In case nothing is selected, val() returns null, so we guard against transferring this to selectedBands by defaulting to an empty array.

There is one more thing you might have noticed, and that is the include helper in the template. We want to mark the option as selected if its value is included in the selectedBands:

<select style="height:100px" class="form-control" multiple onchange={{action "selectBand"}}>
  {{#each bands as |bandChoice|}}
  <option value={{bandChoice}} selected={{include selectedBands bandChoice}}>{{bandChoice}}</option>
  {{/each}}
</select>

The include helper is not provided by Ember but it is rather easy to write ourselves:

import Ember from 'ember';

export function include(params) {
  const [items, value] = params;
  return items.indexOf(value) > -1;
}

export default Ember.Helper.helper(include);

That is all there is to it:

Multiple selection

Multiple selection with objects as items

This is just a tad more difficult, as we cannot directly have objects as options values. Let’s assume that these objects have a property that identifies them unambiguously (which is a fair assumption to make), usually referred to as id:

import Ember from 'ember';

export default Ember.Controller.extend({
  bands: [
    Ember.Object.create({ id: "1", name: 'Pearl Jam', formedIn: 1990 }),
    Ember.Object.create({ id: "2", name: 'Tool', formedIn: 1991 }),
    Ember.Object.create({ id: "3", name: 'Long Distance Calling', formedIn: 2003 }),
    Ember.Object.create({ id: "4", name: 'Led Zeppelin', formedIn: 1970 })
  ],
  (...)
});

We’ll use the id as the option value and display the name:

(...)
<select style="height:100px" class="form-control" multiple onchange={{action "selectBand"}}>
  {{#each bands as |bandChoice|}}
    <option value={{bandChoice.id}} selected={{include selectedBandIds bandChoice.id}}>{{bandChoice.name}}</option>
  {{/each}}
</select>
(...)

On the controller, we collect the id of each selected band, and if we need to display their names, we simply make the mapping between these two:

export default Ember.Controller.extend({
  (...)
  selectedBandIds: [],

  selectedBands: Ember.computed('selectedBandIds.[]', function() {
    return this.get('selectedBandIds').map((bandId) => {
      return this.get('bands').findBy('id', bandId);
    });
  }),
  (...)
});

bands.findBy is our makeshift store service, which allows us to find an object in a collection by its id. If we used Ember Data, it would become store.findRecord('band', bandId) or store.peekRecord('band', bandId). The only other difference from before is that we set selectedBandIds instead of selectedBands in the action handler:

export default Ember.Controller.extend({
  (...)
  actions: {
    selectBand(event) {
      const selectedBandIds = Ember.$(event.target).val();
      this.set('selectedBandIds', selectedBandIds || []);
    }
  }
});

Comments