Thursday, October 23, 2014

Tri-state checkboxes on the web (with AngularJS)

This topic deserves an update. A couple years ago I tried some wacky things to simulate tri-state checkboxes on the web. And sometime after that post I found and shared Chris Coyer's page on indeterminate checkboxes.
Now I’ve got native tri-state checkboxes working cleanly using an AngularJS directive I put together. Now I can just use an html element to place them on my pages. Here’s an example where I’ve used them, in a “contacts manager” type tool:
tri-state-checkbox-example
The “tall people” box is already checked because all the selected contacts are already in that group. The two indeterminate checkboxes are in such a state because a subset of the selected contacts are in those groups. (And BTW, all those names and numbers are randomly generated. No real data here.)
Chris's information explained how to use javascript to natively put a checkbox into its indeterminate state. Namely you do this:
var checkbox = document.getElementById("mycheckbox");
checkbox.indeterminate = true;
Or if you are using jQuery or jqLite:
$("#mycheckbox").prop("indeterminate", true);
So we do the latter inside our Angular directive when the state is supposed to be indeterminate. I created an “api” variable as a gateway to the directive. You set the api ‘state’ to –1 for indeterminate, 0 for unchecked, and 1 for checked. The directive looks like this:
angular.module('app').directive('tricheckbox', function() {
 return {
  restrict: 'A',
  transclude: false,
  scope: {
   api: '&api',
  },
  link: function(scope, element, attrs) {
   var api = scope.api();
   scope.uid = 'chk_' + api.id;
   var el = angular.element(element);
   var checked = (1==api.state) ? " checked=\"checked\"" : "";
   var chkbox = $("<input type='checkbox' id=\"" + scope.uid + "\"" + checked + ">");
   var clickHandler = function() {
    api.state = chkbox.prop('checked') ? 1 : 0;
    scope.$apply();
   };
   if ( -1==api.state ) {
    chkbox.prop('indeterminate', true);    
   }
   chkbox.on('click', clickHandler);
   element.on('$destroy', function() {
    chkbox.off('click', clickHandler);
   });
   el.append(chkbox);
   var label = $("<label for=\"" + scope.uid + "\"><span>" + api.name + "</span></label>");
   el.append(label);
  },
 };
});
 
And the HTML looks like this:
<ul>
 <li x-ng-repeat="g in menuGroups">
  <!-- tricheckbox api needs g.id=(unique), g.name=(any string), g.state=(-1,0,1) -->
  <div x-tricheckbox x-api="g"></div>
 </li>
</ul>
And here’s what the menuGroups data structure looked like:
[
  {
    "id":3500031,
    "name":"daves-i-know",
    "state":-1
  },
  {
    "id":3500032,
    "name":"freds",
    "state":0
  },
  {
    "id":3500044,
    "name":"snazzy dresser",
    "state":-1
  },
  {
    "id":3500045,
    "name":"tall people",
    "state":1
  }
]