Matt Sweetman

Latest articles

Indeterminate checkboxes in Angular

Checkboxes

By default Angular doesn't provide a binding mechanism for the checkbox indeterminate state, meaning you can't do something like this:

<input type="checkbox" indeterminate="true"> <!-- Doesn't work :( -->

There's actually a good reason for this, and that's because it's not actually settable via regular element attributes. You can however set the indeterminate state via JavaScript:

var checkbox = document.getElementById('my-checkbox');
checkbox.indeterminate = true; // Does work :)

This means wherever you need an indeterminate checkbox you'll have to make sure the surrounding component has access to the DOM node and keeps it up to date. Repeatedly writing this wherever you have a checkbox is not ideal, we want to keep things DRY, we want to abstract it away and expose it just like a regular attribute. Then we can bind to it in our template just like any other attribute.

In order to do this we're going to use an Angular directive, but a special kind of directive that can only be applied to element attributes. This can be achieved by setting restrict: 'A' in the directive config. These type of directives allow you to bolt-on functionality to an existing elements, or even other directives, while retaining access to everything the element already has.

The indeterminate attribute directive

This is a complete example so you can copy the directive below into your own application and use it straight away (you might want to change the module namespace first).

angular.module('myModule').directive('indeterminate', function() {
  return {
    // Restrict the directive so it can only be used as an attribute
    restrict: 'A',

    link(scope, elem, attr) {
      // Whenever the bound value of the attribute changes we update
      // the internal 'indeterminate' flag on the attached dom element
      var watcher = scope.$watch(attr.indeterminate, function(value) {
        elem[0].indeterminate = value;
      });

      // Remove the watcher when the directive is destroyed
      scope.$on('$destroy', function() {
        watcher();
      });
    }
  };
});

An important thing to note is we don't define a custom scope in the directive. This means the directive shares whatever scope is currently available on the element. This is important for two reasons: firstly an element can only have one scope, so this directive won't create a new scope and overwrite the existing one; secondly it means we have access to the existing scope and all the properties available on it. While the second point isn't useful in this example it can be quite powerful in other situations.

To use the directive just bind to the indeterminate attribute (remember that the checked attribute is bound by ng-model):

<!-- The value of indeterminate="" can be bound to any angular expression -->
<input type="checkbox" ng-model="data.isChecked" indeterminate="data.isIndeterminate">

Where is this useful?

The most common use for an indeterminate checkbox is a "select all" at the top of a list of selectable items. If all of the items are selected then the checkbox is ticked, but if only some of the items are selected then the checkbox is indeterminate.

To set the state of a "select all" checkbox you need just two boolean variables. In the example below we're going to call them allItemsSelected and someItemsSelected. Let's also assume you have an array of item objects called $scope.myItems and each item inside has a selected boolean. A couple of simple array methods can help us calculate the selection states:

angular.module('myModule').controller('MyController', function($scope) {

  // We can use the ES5 'every' function to check whether each item in our array
  // has a selected property that equals true
  $scope.allItemsSelected = $scope.myItems.every(function(item) {
    return item.selected === true;
  });

  // We use the same technique to determine if only some of the items are selected
  $scope.someItemsSelected = !$scope.allItemsSelected && $scope.myItems.some(function(item) {
    return item.selected === true;
  });

});

And now we can use them in our corresponding template:

<div ng-controller="MyController">
  <input
    type="checkbox"
    class="select-all"
    ng-model="allItemsSelected"
    indeterminate="someItemsSelected">
</div>