Knockout.js 3.2 custom binding for DataTables 1.10x

Knockout.js 3.2 custom binding for DataTables 1.10x

KungFuCoderKungFuCoder Posts: 8Questions: 1Answers: 0

I forked the work that Chad Mullins had done, along with some others that did not quite work right and made a bunch of fixes to it. The updated code can be found here:

https://github.com/JohnBergman/knockout.datatables

I am not a knockout guru, but the code works for what I am using it for, and I am happy to help anyone that has trouble with it; simply leave me an post in the git repository or here.

Hopefully this helps others, I have not really found direct posts that claim to have it fully working.

John

Replies

  • cwong3cwong3 Posts: 1Questions: 0Answers: 0

    Hi John!

    Would love to get some example code to figure out how to use this binding handler. I'm pretty new to Knockout so I'm not sure how to implement this. Currently I'm using Knockout to create tables in my web application, and I'd love to get the automated pagination from DataTables to work with my existing table that is using data binding by row. Any help is appreciated. Thanks!

    Chris

  • chrisknollchrisknoll Posts: 1Questions: 0Answers: 0

    I took a look at your fork, it seems you removed the AMD wrapper for the code, not making it accessable by an amd loader (without including some legacy JS wrapper plugin from curl.js or require.js).

    Was there something wrong with the AMD loader code? Did it cause a conflict?

  • noelynnoelyn Posts: 1Questions: 0Answers: 0

    Hello,

    I'm using Knockout and DataTables for a few months now but I can't bind them properly, e.g. when adding, editing or deleting a row, I would always need to refresh the page to update the table. Do you have some example that can help me with this? Thanks!

  • relentlesslylearningrelentlesslylearning Posts: 5Questions: 2Answers: 0
    edited January 2015

    Chad Mullins appears to have dropped off the face of the earth, well, his domain expired 11 days ago and thus his article explaining his binding code seems to be gone. Because of this anybody unfamiliar with binding an observable array to a datatable is going to struggle with using your code John. It would be greatly appreciated if you could provide some basic examples and explanation.

  • KungFuCoderKungFuCoder Posts: 8Questions: 1Answers: 0

    Sorry I've been extremely busy. I removed the AMD wrapper because it was causing a conflict with my specific implementation.

    Once you bind to the grid, you should be able to add/remove objects from the observable collection to have them show up.

    This binds to the knockout observable collection testAssets that contains a list of simply objects with the properties of name and shortDescription.

    Here is an example binding markup

            <div class="col-md-12">
              <div class="table-responsive">
                <table class="table table-bordered table-advance table-hover" id="Test-DataTable" data-bind="dataTable: {
                                          dataSource: testAssets,
                                          rowTemplate: 'testAssetListTemplate',
                                          gridId: testeAssetGrid',
                                          deferRender: true,
                                          columns:
                                              [
                                                  {'name':'name' },
                                                  {'name':'shortDescription' },
    
                                              ]
                                        }">
                  <thead>
                    <tr>
                      <th> Name</th>
                      <th>Short Description</th>
                    </tr>
                  </thead>
                  <tbody></tbody>
                </table>
              </div>
    
              <script type="text/html" id="testAssetListTemplate">
                <td class="highlight">
                  <span data-bind="html: name"></span>
                </td>
                <td><span data-bind="html: shortDescription"></span></td>
              </script>
            </div>
    

    This is very much a work in progress, I am working through another issue right now, and once I have things stable in the binding again, I'll probably post another update to it

  • zachpainter77zachpainter77 Posts: 22Questions: 1Answers: 1
    edited May 2015

    I used the information found on this forum to create my own version of the custom binding. Although to get it to work I needed to add two callbacks into the knockout foreach default binding... I added beforeRenderAll and afterRenderAll callbacks that allow the binding to destroy the datatable, then add the rows through the original knockout foreach binding and then call the afterRenderAll callback that reinitializes the datatable. This seemed like the easiest way to get knockout to work with datatables... I created a jsFiddle here: --> http://jsfiddle.net/zachpainter77/cn4tL656/30/

    If you like this binding please help me to get the knockout changes into the next version thanks..

  • zachpainter77zachpainter77 Posts: 22Questions: 1Answers: 1
    edited June 2015

    Updated Fiddle: --> http://jsfiddle.net/zachpainter77/z8us2aej/

    ko.bindingHandlers.DataTablesForEach = {
                init: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
                     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, allBindingsAccessor, viewModel, bindingContext) {
    
                    var value = ko.unwrap(valueAccessor()),
                    key = "DataTablesForEach_Initialized";
    
                    var newValue = function () {
                        return {
                            data: value.data || value,
                            beforeRenderAll: function (el, index, data) {
    
                                if (ko.utils.domData.get(element, key)) {
                                    
                                    $(element).closest('table').DataTable().destroy();
                                }
                            },
                            afterRenderAll: function (el, index, data) {
                                $(element).closest('table').DataTable(value.options);
                            }
    
                        };
                    };
    
                    ko.bindingHandlers.foreach.update(element, newValue, allBindingsAccessor, viewModel, bindingContext);
    
                    //if we have not previously marked this as initialized and there is currently items in the array, then cache on the element that it has been initialized
                    if (!ko.utils.domData.get(element, key) && (value.data || value.length)) {
                        ko.utils.domData.set(element, key, true);
                    }
    
                    return { controlsDescendantBindings: true };
                }
            };
    

    Updated Fiddle: --> http://jsfiddle.net/zachpainter77/z8us2aej/

  • bhanubhanu Posts: 4Questions: 2Answers: 0

    deferRender: true, and html bind worked my problem

  • lemonlionlemonlion Posts: 1Questions: 0Answers: 0
    edited November 2015

    Unfortunately Knockout seems to be very slow in implementing Zach's custom event "afterRenderAll". I've tweaked his binding so it will work with Knockout out of the box without the need for a custom event. The only breaking change I made was switching the casing of the name to lower camel case to be more consistent with other bindings. It will work with his jsfiddle above if you switch the data-bind in the tbody from "DataTablesForEach" to "dataTablesForEach"

    ko.bindingHandlers.dataTablesForEach = {
        page: 0,
        init: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
            valueAccessor().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, allBindingsAccessor, viewModel, bindingContext) {
            var value = ko.unwrap(valueAccessor()),
            key = 'dataTablesForEach_Initialized';
            var newValue = function () {
                return {
                    data: value.data || value,
                    afterRender: function () {
                        var htmlTable = $(element).closest('table');
                        var itemsRenderedCount = $(htmlTable).find("tbody").children().length;
                        if (itemsRenderedCount === value.data().length) {
                            value.options = value.options || {};
                            value.options.destroy = true;
                            var table = $(htmlTable).DataTable(value.options);
                            if (value.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);
                                }
                            }
                        }
                    }
                };
            };
    
            ko.bindingHandlers.foreach.update(element, newValue, allBindingsAccessor, viewModel, bindingContext);
    
            //if we have not previously marked this as initialized and there is currently items in the array, then cache on the element that it has been initialized
            if (!ko.utils.domData.get(element, key) && (value.data || value.length)) {
                ko.utils.domData.set(element, key, true);
            }
    
            return { controlsDescendantBindings: true };
        }
    };
    
  • zachpainter77zachpainter77 Posts: 22Questions: 1Answers: 1
    edited November 2015

    @lemonlion I like the way your heading with this binding!! good job! however, I can't seem to get it to work. Do you have a jsfiddle or something that shows it in action? Thanks.

    EDIT: I finally got it working, however this solution does not work when you remove an item from the knockout bound array. I suspect it is because the 'afterRender' callback is only called after an item's bound html is rendered and not when removed. I would say that this would be an easy fix if we just did the same thing after knockout removes the item but the knockout foreach binding has no afterRemove call back. There is a before remove but this won't work because if we initialize datatables before knockout has done it's thing the row won't be removed since datatables breaks the bindings by rewriting the bound html.

  • zachpainter77zachpainter77 Posts: 22Questions: 1Answers: 1
    edited November 2015

    Also the events to the knockout foreach binding should be added to version 3.5 or 4.0
    pull request #1856

  • philwithphilwith Posts: 1Questions: 0Answers: 0

    Great work with this binding @zachpainter77. I started using it on a project and it worked nicely in Chrome. However in IE and Firefox, I get long running script warnings with larger datasets (~1000 rows). I also noticed IE in particular was using a ton of memory and eventually would crash after a few updates to the underlying data. Sadly I don't have enough javascript knowledge to work out what I would have to tweak to solve this memory issue. Have you ever tried it with a big dataset?

  • zachpainter77zachpainter77 Posts: 22Questions: 1Answers: 1

    @philwith Yes, I've crashed chrome with a data-set of 90,000 + records... lol... the truth is , that knockout is slow... at least the mapping is.. there are ways to get around that and map to observables more effeciently for sure. But what I ended up doing was implementing a server side paging for that particular project. I had to create a searchCriteria knockout bound viewModel and then I create the basic paging ui functions like previous and next and whatnot then I pass the criteria to .Net MVC and call a stored procedure that implements my paging and it works great!

  • zachpainter77zachpainter77 Posts: 22Questions: 1Answers: 1
    edited November 2015

    Here is an update to the binding... This binding will work on vanilla knockout version 3.4! Thanks to Michael Best one of the top knockout devs for helping me with this.. :)

    ko.bindingHandlers.dataTablesForEach = {
        page: 0,
        init: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {        
            valueAccessor().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 options = ko.unwrap(valueAccessor()),
                key = 'DataTablesForEach_Initialized';
            ko.unwrap(options.data); // !!!!! Need to set dependency      
            ko.bindingHandlers.foreach.update(element, valueAccessor, allBindings, viewModel, bindingContext);
            (function() {
                console.log(options);
                var table = $(element).closest('table').DataTable(options.dataTableOptions);
                if (options.dataTableOptions.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) && (options.data || options.length))
                ko.utils.domData.set(element, key, true);
            return { controlsDescendantBindings: true };
        }
    }; 
    
    

    Here is how to use the binding:

    <tbody data-bind="dataTablesForEach: {data: people, dataTableOptions: {
                              jQueryUI: true,
                              scrollY: 250,
                              paging: true,
                              dom: 'rtip',
                              columns:[
                                  {width: '30px'},
                                  {width: '100px'},
                                  {width: '100px'},
                                  {width: '100px'},
                                  {width:'100px'}
                              ]
                          }
                      }">
    

    See it in action! JSFIDDLE

  • sandrelosandrelo Posts: 1Questions: 0Answers: 0
    edited February 2016

    Hello,

    This has been super super helpful. However, when I hide a column using:

    columnDefs: [{ visible: false, targets: 0 }]

    I get an error on the table.destroy()(see snippet below) saying style is missing every second Add click. every time it fails the hidden column shows up and when I click add again two rows show up and the Id column (column 0) is hidden again.

    var table = $(element).closest('table'String).DataTable();
    ko.bindingHandlers.dataTablesForEach.page = table.page();
    table.destroy();

    I think its because it trys to destroy the id column (the column im hiding) and cant find it.

    I am very new to knockout js and datatables and im probably missing something obvious.

    Thank in advance

    Also, I noticed when you edit a value and try to sort that column, it sorts based on the old value until you add a new row. I'm still learning but would something like table.columns().adjust().draw() after each edit refresh the table?

  • israel3000israel3000 Posts: 1Questions: 0Answers: 0

    It is any way to handle server processing with knockoutjs? I am trying to do so with no luck so far.

  • sohel3csedusohel3csedu Posts: 3Questions: 1Answers: 0

    Is there any way to enable button plugin for this custom binding?
    If someone already has done it pls give me some tips.

    thanks

  • demarilydemarily Posts: 7Questions: 2Answers: 0

    How would i go about using the initComplete option? In that option i create jquery.tabs() and filter values from one of the columns into the tabs. It basically just counts the number of times each value shoes up.

This discussion has been closed.