July 4, 2020
A topic that comes up frequently on the Vue Land Discord is checkboxes. As an input control, their selection status is handled differently -- checked
versus value
-- and that can be confusing when starting with v-model. Additionally, since we're dealing with lists, Vue caveats may be involved.
This CodePen demonstrates a set of checkboxes that will manage item selections. There is a top-level checkbox control which is a convenience that allows the items to be checked in bulk. Try different combinations of checkboxes. Notice that when all are checked, the top-level control will automatically become selected.
See the Pen Vue Select/De-select Demo by Carl Walker (@walkerca) on CodePen.
When working with a requirement like this, I prefer to supplement the data with a selected flag rather than manage a separate list of selections. As the data comes in or is created by the user, I use a Vue.set
to set a new property on the list of items. In this case, the list of items is fixed in data()
and the flag is set in mounted()
. The flag could also be set during a retrieval in a Vue Router Navigation Guard.
Here is a listing of the data() and mounted() functions. Notice the use of this.$set
which is short for Vue.set
. This is a Vue Caveat needed when adding a property that Vue doesn't know about. See this CodePen for more information.
data() {
return {
items: [
{ id: 101, name: "Item #1" },
{ id: 102, name: "Item #2" },
{ id: 103, name: "Item #3" }
]
};
},
mounted() {
this.items.forEach( itm => this.$set(itm, "selected", false));
},
This next listing is the HTML from the template section. A v-for
is used to iterate over the list of items. A v-model
binds the selected flag to the checkbox selection state.
<table class="table">
<thead>
<tr>
<th><input type="checkbox" class="checkbox" :checked="allSelected" @click="selectAll"/></th>
<th>ID</th>
<th>Name</th>
</tr>
</thead>
<tbody>
<tr v-for="itm in items" :key="itm.id">
<td><input type="checkbox" class="checkbox" v-model="itm.selected"/></td>
<td>{{ itm.id }}</td>
<td>{{ itm.name }}</td>
</tr>
</tbody>
</table>
The top-level checkbox control is handled differently. That is not bound to a data item but rather uses a computed. When the items change the value of the selected flag, the computed allSelected
is recalculated. It's reactive so the :checked will change as that computed value changes.
computed: {
allSelected() {
return this.items.every(itm => itm.selected);
}
}
The top-level checkbox also has a selectAll method bound to click to support selecting all items in bulk (or de-deselecting).
methods: {
selectAll() {
let all_s = this.allSelected;
this.items.forEach( itm => itm.selected = !all_s );
}
}
When any of the line item checkboxes are selected, their value will be reactively recorded in the items data structure. Additionally, if that last item rounds out a complete selection or de-selection, the allSelected field will be recomputed and drive a change to the display of the top-level control.
There's a watcher used for the display of the selections in the demo that may be of interest. Be sure to read over the code in the CodePen listed.
There are several ways to manage a set of checkbox selections. I prefer to lace my source data with presentation-level flags because it reduces the logic and potentially inconsistent handling throughout my app. Sometimes it's tough to keep two arrays in sync and easier to deal with the hassle at one or two points. When it comes time to process the selection, your array will give you enough information for what you need to do.
By Carl Walker
President and Principal Consultant of Bekwam, Inc