Ember 'Toggle All' Checkbox

I recently read an interesting article by Mark Przepiora titled Ember.js Recipes: Checkboxable Index Pages Using itemController. Mark makes several good points regarding the logical separation between controllers and models in Ember. He shows how to identify use cases when it’s appropriate to leverage an itemController to wrap each item in a collection. Here is his demo which shows a simple implementation for a UI structure with a list of checkbox items and a delete button.

I thought this would be a fun starting point for a quick post of my own, so I took Mark’s idea and slightly expanded upon it by adding a “Toggle Select All” checkbox. Below I’ll show my demo and highlight some of the more interesting bits of code. Check it out.

The templates are pretty straightforward, as I only made a few small changes from the original example. First, I’m setting the itemController inside the {{#each}} helper rather than inside the controller. Second, I’m using an {{else}} block helper to render some content when there are no checkboxes remaining.

<!-- index.hbs -->
{{#if model}}
  ...
  {{#each model itemController="color"}}
    <li>...</li>
  {{/each}}
  </ul>
{{else}}
  <strong>Please Add A Color...</strong>
{{/if}}

Here’s where things get a little more fun. You can see my App.IndexController with comments describing each of the main code blocks and properties.

App.IndexController = Ember.ObjectController.extend({
  /**
   Simply a placeholder array for each child `itemController`
  */
  toggles: function(){ return Ember.A([]) }.property(),

  /**
   This computed property acts as both a setter and a getter. Check
   out the docs for more information on this type of computed property:
     http://emberjs.com/guides/object-model/computed-properties/#toc_setting-computed-properties
     http://emberjs.com/guides/getting-started/toggle-all-todos/
  */
  allChecked: function(key, value){
    if (arguments.length === 1) /* get */ {
      /* Executes `if` block on get when the user toggles any of the individual `itemController` checkboxes */
      var toggles = this.get('toggles');
      return toggles && toggles.isEvery('isChecked')
    } else /* set */ {
      /* Executes `else` block on set when the user toggles the actual "Toggle Select All" checkbox */
      this.get('toggles').setEach('isChecked', value);
      return value;
    }
  }.property('toggles.@each.isChecked'),

  /* Get the total number of selected checkboxes */
  allSelectedItems: Ember.computed.filterBy('toggles', 'isChecked', true),
  totalSelectedCount: Ember.computed.alias('allSelectedItems.length'),

  actions: {
    /* Called when each child `itemController` is initialized (initial state/dynamically added) */
    registerToggle: function(toggle) {
      this.get('toggles').addObject(toggle);
    },
    /* Called when each child `itemController` is deleted and destroyed from the view render tree */
    deregisterToggle: function(toggle) {
      this.get('toggles').removeObject(toggle);
    },
    add: function() {
      var color = COLORS[Math.floor(Math.random() * COLORS.length)];
      this.get('model').pushObject({color: color, id: ID++});
    },
    remove: function() {
      var allSelectedItems = this.get('allSelectedItems').mapBy('content');
      this.get('model').removeObjects(allSelectedItems);
    }
  }
});

Finally, you can see my App.ColorController which acts as an itemController for each object in the model array.

App.ColorController = Ember.ObjectController.extend({
  isChecked: false,

  colorId: function() {
    return 'checkbox' + this.get('id');
  }.property('id'),

  /**
   When a checkbox is initially or dynamically added, this declarative init handler will register the checkbox on its `parentController`
  */
  registerOnParent: function() {
    this.send('registerToggle', this);
  }.on('init'),

  /**
   When a checkbox is deleted, this hook will be called to deregister on the `parentController`
  */                                              
  willDestroy: function() {
    this.send('deregisterToggle', this);
  }
});