Ember.js Recipes: Checkboxable Index Pages Using itemController

A very common UI design pattern is to display an index of items with checkboxes beside them, so that we can select some items and (for example) delete them in bulk.

We may be tempted to implement our template like this:

1
2
3
4
5
<ul>
  {{#each}}
    <li>{{input type="checkbox" checked=selected}} {{name}}</li>
  {{/each}}
</ul>

This certainly works. Each item in the collection will get a selected boolean property, which we can use to filter the collection of selected items.

But there’s something about this that should make us feel gross: each item in the collection is likely a model record, which we are polluting with a new selected property. Suddenly the model layer cares about what is strictly a controller-layer concern. The selected property will persist on the records even when a user leaves the route and comes back to it later. Not only are we abusing the separation between controllers and models, but we are potentially adding nonlocal behaviour to our application.

Luckily, Ember provides a relevant mechanism. If we define an itemController property on an array controller, each item in the collection will be wrapped in an instance of the specified object controller.

Let’s define a CheckboxableItemController:

1
2
3
App.CheckboxableItemController = Ember.ObjectController.extend({
  selected: false
});

Now we can tell Ember to use it for each item in the array controller:

1
2
3
App.FoosIndexController = Ember.ArrayController.extend({
  itemController: 'checkboxableItem'
});

Now it’s each instance of CheckboxableItemController that gets a selected property, rather than the model it wraps. For good measure, let’s see how we can define computed properties for retrieving the list/count of selected items, and an action for removing them.

1
2
3
4
5
6
7
8
9
10
11
12
App.FoosIndexController = Ember.ArrayController.extend({
  itemController: 'checkboxableItem',
  selectedItems:  Ember.computed.filterBy('', 'selected', true),
  selectedCount:  Ember.computed.alias('selectedItems.length'),

  actions: {
    removeSelected: function() {
      var selectedItems = this.get('selectedItems').toArray();
      this.removeObjects(selectedItems);
    }
  }
});

Note our use of Ember.computed.filterBy. Critically, we are filtering over '' (i.e. the controller itself), and not 'content'. This is because 'content' is a reference to the underlying collection of items, and not the wrapped ones.

Here is the relevant JS Bin.

Have a happy hacking day. □

Comments