I am using the knockout-binding suggested here for DataTables. However, when using the FixedColumns extension (which clones the original datatable to a new one) I lose the binding between the new datatable and the existing viewmodel/bindingContext.
For instance, having a selection checkbox on a fixed column to select items from the table will not behave as expected.
The binding looks like this:
ko.bindingHandlers.dataTablesForEach = {
page: 0,
init: function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
var binding = ko.utils.unwrapObservable(valueAccessor());
if (binding.options.paging) {
binding.data.subscribe(function(changes) {
var table = $(element).closest('table').DataTable();
ko.bindingHandlers.dataTablesForEach.page = table.page();
table.destroy();
}, null, 'arrayChange');
}
var nodes = Array.prototype.slice.call(element.childNodes, 0);
ko.utils.arrayForEach(nodes, function(node) {
if (node && node.nodeType !== 1) {
node.parentNode.removeChild(node);
}
});
return ko.bindingHandlers.foreach.init(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext);
},
update: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
var binding = ko.utils.unwrapObservable(valueAccessor()),
key = 'DataTablesForEach_Initialized';
var table;
if (!binding.options.paging) {
table = $(element).closest('table').DataTable();
table.destroy();
}
ko.bindingHandlers.foreach.update(element, valueAccessor, allBindings, viewModel, bindingContext);
table = $(element).closest('table').DataTable(binding.options);
if (binding.options.paging) {
if (table.page.info().pages - ko.bindingHandlers.dataTablesForEach.page === 0) {
table.page(--ko.bindingHandlers.dataTablesForEach.page).draw(false);
} else {
table.page(ko.bindingHandlers.dataTablesForEach.page).draw(false);
}
}
if (!ko.utils.domData.get(element, key) && (binding.data || binding.length)) {
ko.utils.domData.set(element, key, true);
}
return {
controlsDescendantBindings: true
};
}
(See the full working example).
Wow... That is a tough question. I'm thinking that you would either need to make the html available before the bindings are applied, or you would need to clone your data and apply the bindings to that. The first option is out of the question since the fixedColumns plugin gets called when the datatable is initialized. which is happening inside this binding, and that happens when ko.applyBindings is called. Paradoxical.. :)
I also wanted to add that you are not losing your bindings because of fixed columns. The bindings are still there. It's just that the new html that the plugin creates floats above the original binded html.
I did however get this hack to work...
Add this after applybindings is called...
Edit: Needed to bind the event to the document since fixed column plugin rewrites elements on the fly depending on the page.
$(document).on('click','.DTFC_LeftBodyWrapper input[type="checkbox"]',function(){
var value = $(this).attr('value');
$('.dataTables_scrollBody').find('input[value="'+value+'"]').click(); });
I figured if you cant bind the cloned html then you can force it to interact with the original html... It's really hacky but it works.. ¯_(ツ)_/¯
forked working example CODEPEN
I've ended up with a binding that reapplies the knockout context to the cloned table (i.e. the fixed columns table) after its creation:
import $ from 'jquery';
import ko from 'knockout';
import 'datatables.net';
import 'datatables.net-fixedColumns';
const defaultOptions = {
deferRender: true,
paging: true
};
ko.bindingHandlers.datatables = {
init: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
const $element = $(element),
binding = ko.unwrap(valueAccessor()),
options = binding.options || {};
$.extend(true, options, defaultOptions);
if (binding.rowTemplateId && binding.data) {
// bind the header first
ko.applyBindingsToDescendants(viewModel, $element.find('thead')[0]);
setupTableBody($element, binding, bindingContext);
if (ko.isObservable(binding.data)) {
// destroy and build the table again when the data changes
binding.data.subscribe(() => {
$element.DataTable().destroy();
$element.find('tbody').remove();
setupTableBody($element, binding, bindingContext);
initializeDataTable(element, options);
}, null, 'arrayChange');
}
}
initializeDataTable(element, options);
ko.utils.domNodeDisposal.addDisposeCallback(element, () => {
$(element).DataTable().destroy();
});
return {
controlsDescendantBindings: true
};
}
};
function initializeDataTable(element, options) {
const table = $(element).DataTable(options);
if (options.fixedColumns) {
// we need to apply the context to the cloned table for the first time
setTimeout(() => {
applyBindingsToClonedRows(element, options.fixedColumns);
}, 0);
// register handler to fix the cloned table column width
// when the table is (re)drawn
table.on('draw.dt', (event) => {
$(event.target).DataTable().fixedColumns().relayout();
});
// register handler to fix the cloned table binding context
// when the table is (re)drawn
table.on('draw.dt.DTFC', (event) => {
applyBindingsToClonedRows(event.target, options.fixedColumns);
});
}
}
function setupTableBody($element, binding, bindingContext) {
// render each element of the body with the template
let tbody = $element.find('tbody')[0];
if (!tbody) {
tbody = document.createElement('tbody');
$element.append(tbody);
}
ko.renderTemplateForEach(ko.unwrap(binding.rowTemplateId), binding.data, {}, tbody, bindingContext);
}
function applyBindingsToClonedRows(originalTable, fixedColumnsOptions) {
const $table = $(originalTable);
const rows = $table.find('tbody>tr');
if (fixedColumnsOptions.leftColumns) {
const clonedRows = $table.parent().parent().parent().find('.DTFC_LeftBodyWrapper .DTFC_Cloned').find('tbody>tr');
for (let i = 0; i < rows.length; i++) {
ko.applyBindings(ko.contextFor(rows[i]), clonedRows[i]);
}
}
if (fixedColumnsOptions.rightColumns) {
const clonedRows = $table.parent().parent().parent().find('.DTFC_RightBodyWrapper .DTFC_Cloned').find('tbody>tr');
for (let i = 0; i < rows.length; i++) {
ko.applyBindings(ko.contextFor(rows[i]), clonedRows[i]);
}
}
}
I've created a github repo that also contains some examples since it might help someone else.
If you feel like this answers the question please up vote and I'll mark it as an answer - it surely answers my initial question but I would appreciate some feedback.
Please notice that this is ES2015 code so you'll need to transpile it.