Standard HTML checkboxes have some superpowers in Aurelia, but I’m always astonished to find out when one of my customers isn’t taking full advantage of them. In addition to the standard checked
and indeterminate
attributes, checkboxes and radio buttons have a model
bindable attribute that handles some pretty powerful use cases. By combining all these features, we can create a table with selectable rows and a select all checkbox at the top.
First, we’ll start by creating a basic checkbox and some radio buttons. Then, we’ll use the model
binding to make the rows of a table selectable. Finally, we’ll use the bindings to add a select all checkbox to the top of our table.
A Standard Checkbox
The standard HTML checked
property is a boolean attribute. When you bind it to a variable, the result is a boolean value. Let’s bind to a variable canSort
which toggles the ability to sort.
table.html
<label>
<input type="checkbox" checked.bind="canSort" />
Enable Sorting
</label>
This syncs the canSort
variable to the checked
attribute and state of the checkbox. When the checkbox is checked, canSort === true
. When it is unchecked, canSort === false
.
A Standard Radio Button
Radio buttons also have a checked property, but the default value is on
or off
. If we changed the example above to type="radio"
, we would have canSort === 'on'
or canSort === 'off'
. Radio buttons are more useful in conjuction with a value
binding. When value
is bound, the bound checked
variable will receive the bound value
when it is checked.
table.html
<label>
<input type="radio" value="none" checked.bind="sorting" /> none
</label>
<label>
<input type="radio" value="ascending" checked.bind="sorting" /> ascending
</label>
<label>
<input type="radio" value="descending" checked.bind="sorting" /> descending
</label>
This syncs sorting
to the value of the value
binding. When the “ascending” radio button is toggled, sorting === 'ascending'
.
In this case, it would be more useful to bind the sorting
variable to integers 0
, 1
, and -1
so that we could use them in an Array.sort
method call; however, the value
binding is limited to strings! Aurelia includes a model
binding on checkboxes and radio buttons that works identically to the value
binding but supports all JavaScript values. Let’s use that instead:
table.js
sortings = [
{ label: 'none', value: 0 },
{ label: 'ascending', value: 1 },
{ label: 'descending', value: -1 }
];
table.html
Sorting:
<label repeat.for="sort of sortings" if.bind="canSort">
<input type="radio" model.bind="sort.value" checked.bind="sorting" /> ${sort.label}
</label>
Now, when we toggle ‘ascending’, sorting === 1
, and likewise for the other radio buttons.
Selecting Items in an Array
If you include the model
binding on a checkbox, then you can bind checked
to an array and it will add values to the array when checked and remove them when unchecked. This makes it easy to track a list of selected items.
table.js
// We define an array that will be bound to the `checked` binding of our selection checkboxes.
selected = [];
// And we have an array of objects that will get added to and from the selection.
items = [
{ value: 2 },
{ value: 1 },
{ value: 3 }
];
table.html
<table>
<tbody>
<tr repeat.for="item of items">
<td>
<!-- When the checkbox is checked, the `selected` array will contain `item`. When unchecked, `item` will be removed from `selected`. -->
<input type="checkbox" checked.bind="selected" model.bind="item" />
</td>
<td>${item.value}</td>
</tr>
</tbody>
</table>
The Select All Checkbox
Here’s the trick that most people don’t know about. Let’s add a checkbox to the top of the table that will be (1) checked when all items are selected, (2) unchecked when no items are selected, and (3) indeterminate when some items are selected. indeterminate
is a boolean attribute, just like checked
, and therefore it can be bound just like any other attribute.
table.html
<table>
<thead>
<tr>
<th>
<input type="checkbox" <!-- -->
<!-- We want the checkbox to be checked when the selected array contains all the items in the items array.
We can take a shortcut and just compare lengths. You can bind anything here so long as it is true when the
two arrays are equal. Since this is an expression and not a value, the default two-way binding will not
work since you cannot assign to an expression. So, we ask Aurelia for a one-way binding only. -->
checked.one-way="selected.length === items.length"
<!-- We want the checkbox to be indeterminate when the selected array contains some but not all items in the
items in array. Just like with the `checked` binding, we take the shortcut of comparing array lengths. Again
you can bind anything here so long as its true when selected includes some but not all of the elements in
items. Indeterminate is a one-way binding, so we can just use the standard bind syntax. -->
indeterminate.bind="selected.length > 0 && selected.length < items.length" />
</th>
<th>value</th>
</tr>
</thead>
</table>
Now when we check checkboxes in our table the select all checkbox will update based on our selection. The select all checkbox does not yet add or remove items from the selected
array, though, so let’s add that next. Since we are binding to expressions for both checked
and indeterminate
, it would be difficult to handle this behavior with a binding. Instead, let’s handle it by listening for the change
event on our select all checkbox.
table.html
<table>
<thead>
<tr>
<th>
<input type="checkbox"
checked.one-way="selected.length === items.length"
indeterminate.bind="selected.length > 0" <!-- -->
<!-- `$event.target`, the target of the event, is the checkbox. When checked, we want `selected` to contain
all the items in `items`, or `items.slice()`. When unchecked, we want `selected` to be an empty array. -->
change.delegate="selected = $event.target.checked ? items.slice() : []" />
</th>
<th>value</th>
</tr>
</thead>
</table>
Now, clicking the checkbox will select or deselect all the items in the table.
Demo
Notes
As a Custom Element
I don’t love the syntax for the select all checkbox. Since I’m never using an array value for the model
binding in practice, I like to create a checkbox custom element that interprets an array-valued model
binding with the select all behavior.
table.js
items = [
{ value: 'a' },
{ value: 'b' },
{ value: 'c' }
];
selected = [];
table.html
<!-- Checking this checkbox will add all the items from `items` to the `selected` array. Unchecking it will remove
everything from `items`. Adding one but not all items from `items` to `selected` will set the checkbox state to
indeterminate. -->
<my-checkbox checked.bind="selected" model.bind="items" />
I have enough of these in a typical application that the time it takes writing a rock-solid component is justified.
Links
Full Working Demo
Aurelia 2 Checkbox / Radio Button RFC
StackOverflow question which inspired this post
Aurelia Checkbox Binding Docs