I am trying make a custom binding to allow knockout to work with the jquery datatables plugin and I have an interesting problem. The binding itself isn't really the problem it is an issue that arises with the way the datatable works.
- DataTables 1.10
- Knockout 3.4
Here is the scenario:
Data and elements are all bound together via a custom knockout binding. Everything works as expected except when an observable view model object is modified but the html element that the object is tied to is not currently on the page. How does this happen? Well the when you have the datatable configured for paging and you have more items than fit on a single page, the datatable only puts the elements that exist in that page of data in the DOM. As you navigate pages it swaps out the html elements.
So if you modify an observable object and that object is tied to html elements on a different page of the datatables object, what happens is the binding from the object to the html element no longer works. It works if you change the html element when it is in view. The value gets pushed to the object. However, once you modify the object and the html element is not in view you can no longer modify the object and have the value push to the html element.
I believe this happens because when the observable object goes to push the value to the html element and it's not there, it can't re-subscribe to the html element so it no longer can send notifications.
What I want to do is put in a draw callback into the DataTables control that looks at the data and html element and determines if that subscription is broken or not. This will allow me to fix broken subscription as rows are swapped out when the user is paging. Is it possible to determine if the subscription is broken and "fix it" using the knockout API.
My question really is about knockout not the jquery datatables plugin and what I'm trying to do. I just thought it was necessary to provide context to why an element might be missing at a certain point when the observable gets updated.
Any help would be appreciated!!
Update Using the answer below I did solve my problem but not as efficiently as I would have hoped because I still don't have a way to detect if the subscription is broken. For now I just remove the bindings and reapply them when the DataTable pages like so:
rowCallback: function (row, data, index) {
var koContext = ko.contextFor(row);
ko.cleanNode(row);
ko.applyBindings(koContext, row);
}
** Update 2 ** For everyone who may encounter this here is the full binding I created
/*
* Knockout Binding that allows for the developer to generate content in a datatable
* that is bound to view model objects via knockout. Based on: https://github.com/zachpainter77/DatatablesForEach-Custom-Knockout-Binding
*/
ko.bindingHandlers.dtForeach = {
page: 0,
init: function (element, valueAccessor, allBindings, viewModel, bindingContext) {
var options = ko.unwrap(valueAccessor());
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, options.data, allBindings, viewModel, bindingContext);
},
update: function (element, valueAccessor, allBindings, viewModel, bindingContext) {
var options = ko.unwrap(valueAccessor()), key = 'DataTablesForEach_Initialized';
var dtData = ko.unwrap(options.data);
var dtOptions = ko.unwrap(options.dataTableOptions);
var tableElement = $(element).closest('table');
/*
* Destroy old table and reinitialize it with
* new data
*/
var table = tableElement.DataTable();
if (dtOptions !== undefined && dtOptions.paging) {
ko.bindingHandlers.dtForeach.page = table.page();
}
table.destroy();
ko.bindingHandlers.foreach.update(element, options.data, allBindings, viewModel, bindingContext);
var bdtOptions = ko.bindingHandlers.dtForeach.setupOptions(dtOptions);
table = tableElement.DataTable(bdtOptions);
if (bdtOptions.paging) {
if (table.page.info().pages - ko.bindingHandlers.dtForeach.page == 0)
table.page(--ko.bindingHandlers.dtForeach.page).draw(false);
else
table.page(ko.bindingHandlers.dtForeach.page).draw(false);
}
if (!ko.utils.domData.get(element, key) && (options.data || options.length))
ko.utils.domData.set(element, key, true);
return { controlsDescendantBindings: true };
},
setupOptions: function (options) {
/*
* Make a deep copy of options because DataTables code
* sometimes changes settings and we want to make some modifications for
* this binding
*/
var bdtOptions = {};
if (options !== undefined)
$.extend(true, bdtOptions, options);
/*
* Need to register a row callback so that bindings are maintained
* for rows that are not visible when paging
*/
if (bdtOptions.paging) {
bdtOptions.rowCallback = function (row, data, index) {
var koContext = ko.contextFor(row);
ko.cleanNode(row);
ko.applyBindings(koContext, row);
}
}
return bdtOptions;
}
};