Code Locket Menu

Keeping checkbox state while using AJAX powered DataTables

Blog post created on 2022-08-07

JavaScript

A lot of the admin pages over at Spesati.com are driven by DataTables with server side processing. These have always worked great and provide a reasonably good interface for quick UI prototyping, especially once you have some of the groundwork done.

Recently, I had to build a UI that allowed our staff to select one or more orders (DataTable rows) simultaneously to generate a "multi order picking" email (topic for a separate post!). My default thinking was to simply add a checkbox for each row as I suspected this would be native DataTable functionality, and indeed, there is a pre-built extension. However, it wasn't as quick as I initially expected.

Spesati multi order picking table

Server side processing troubles

The extension works perfectly when loaded with client side processing (all data available in the browser). So for example, if you check a row, then paginate forward and back again, your row will still be selected. This is not true when using server side processing. In fact, with the AJAX integration, the local DataTable is only aware of the "current page" and whenever pagination or search is used, any prior row selection is immediately lost. To my surprise, there was no ready made code snippet to make this work either so I had to build something quick.

The first objective was to avoid changing the backend AJAX endpoint that returned the order information as I wanted to make this work with client side code changes only. To avoid changing the data format, I added the extra column for the checkboxes at the end of the table along with explicitly specifying the ID of each row (in this case the order number). Here was my additional configuration for the DataTable object:

columnDefs: [
    {
        targets: -1,
        data: null,
        defaultContent: '',
        orderable: false,
        className: 'select-checkbox'
    }
],
rowId: 0,
select: {
    style:    'os',
    selector: 'td:last-child'
}

The additional columnDefs object specifies that the last column is to be used for the checkboxes. rowID with a value of 0 is explicitly asking DataTables to use the first column returned by the AJAX endpoint as the id attribute for the HTML table rows. The select object was taken as is from the official documentation. Note that I also had to add the additional column in the table header with an empty value.

With the above code I managed to get a working checkbox for each row (just like the screenshot shown above) but with no state kept on pagination or search. For this latter part I had to add some JavaScript. First a simple array to keep state:

var checked = [];

Next, we can hook into the native select event to update our array and keep track of which rows are selected:

dTable.on("select", function(e, dt, type, indexes) {
    var rowData = dTable.rows(indexes).data().toArray();
    for(let i = 0; i < rowData.length; i++) {
        // Delete first to avoid duplicates
        checked = checked.filter(e => e !== rowData[i][0]);
        checked.push(rowData[i][0]);
    }
})

The select event proved to be quite verbose especially as you can use ctrl and shift keyboard keys to make multiple selections faster, and each time, the event is fired for every selected row. To avoid the array having duplicate items, I simply clean the array on each iteration. Note that dTable is the name of the DataTable variable.

We can now do the same on the deselect event:

dTable.on("deselect", function(e, dt, type, indexes) {
    var rowData = dTable.rows(indexes).data().toArray();
    for(let i = 0; i < rowData.length; i++) {
        checked = checked.filter(e => e !== rowData[i][0]);
    }
});

With the above code, we now have a list of selected rows by id that persists across pagination and search. The last step is to simply check any page or search result for the selected rows, and if any are found, select them. To do this DataTable exposes a drawCallback configuration option that allows us to pass a function that will be called whenever a draw action is completed:

drawCallback: function() {
    // Make copy so select event does not change order of array
    var checkedCp = [...checked];
    for(let i = 0; i < checkedCp.length; i++) {
        dTable.row("#" + checkedCp[i]).select();
    }
}

Note that I make a quick copy of the checked array before updating the selections as selecting the rows will result in... the select event being triggered changing the array under our feet causing undesirable behaviour.

Wrapping it up

With the above code in place, creating a submit button that submitted the contents of the checked array was relatively straightforward and just like that, with not many lines of code at all, we now have a multi row select with DataTables powered by server side processing! Hopefully this will be useful for anyone else wanting to do the same.