createdCell doesn't fire when row data changes

createdCell doesn't fire when row data changes

JakeMeiergerdJakeMeiergerd Posts: 1Questions: 1Answers: 0

TL;DR: The column.createdCell callback doesn't fire if I manually change row data, or call invalidate(), I feel like it should, but if not, is there a way I can prevent that cell from being re-rendered when row data changes, or is it possible to do DOM manipulation from the column.render callback, somehow?.

I've got a server-side datable setup as part os a custom JQuery widget, as follows: https://pastebin.com/ixEA5mkp

The key here is I've got a couple of columns that aren't displaying data, they're displaying buttons for the user. To accomplish this, I'm using the column.createdCell callback for the column to format the buttons, and attach event handlers. One of the buttons pulls up a dialog to allow the user to edit the row data, which will fire off an AJAX request to update the backing record. What I'd like to do is not have to refresh the whole table page from the backend, when I can just update the row by hand. However, when I update the row data, either via row.data() or by editing the data object and calling row.invalidate(), column.render fires, which wipes the contents of the cells, and the column.createdCell callback doesn't fire, so I don't get the chance to re-build them.

In my mind, since the createdCell callback receives cellData and rowData as parameters, that implies that if those change, the callback should re-fire, despite its name. I can't use the column.render callback, as far as I'm aware, because the DOM for the cell hasn't been built yet, therefore I have nothing to attach event handlers to. I can also just re-query the whole table page, if I really have to. I was just hoping there'd be an easier way.

Answers

  • kthorngrenkthorngren Posts: 21,344Questions: 26Answers: 4,954
    edited February 2018

    I would suggest using rowCallback, in place of createdCell, to update the columns with the buttons.

    Kevin

  • adjenksadjenks Posts: 22Questions: 6Answers: 0

    I too would like to be able to call the createdCell function on an individual cell.

  • moishe1927moishe1927 Posts: 1Questions: 0Answers: 0
    edited September 2018

    i have created a function that is refreshing my row, and calling createdCell again.

    function RefreshRow(row) {
        row.invalidate();
        row.context[0].aoColumns.filter(function (f) { return f.fnCreatedCell != null; }).forEach(function (o) {
            o.fnCreatedCell($(row.node()).find('td').eq(o.idx), row.data()[o.data], row.data(), row.index(), o.idx);
        });
    }
    
    
  • simoncrowdersimoncrowder Posts: 3Questions: 0Answers: 0

    RefreshRow function works perfectly, half my code is just hacks to get datatables to behave, I'm very disappointed with the quality of the library.

  • allanallan Posts: 63,535Questions: 1Answers: 10,475 Site admin

    Sorry to hear that. If you have a link to the page you are working on, I'd like to take a look so I can see how you are using DataTables and the issues you've run into so I can try and improve the library.

    The thing with columns.createdCell is that it will only ever run once and it is only intended to run once. If you need to call it again, then give the columns.createdCell option a function that is already defined, and you can just call that again when you change the content of the cell:

    function modifyCell( ... ) {
      ...
    }
    
    // Then later:
    {
      createdCell: modifyCell
    }
    
    // And still later:
    table.cell(...).data();
    modifyCell( ... );
    

    Does that make more sense? I guess it will depend a little on what it is exactly you are trying to do. If you are able to link to a page that would be great.

    Allan

  • simoncrowdersimoncrowder Posts: 3Questions: 0Answers: 0

    Actually, the hacky refresh function above doesn't work if you have a column set to visible: false, presumably the .find for o.idx is off by one.

    The code I have, whenever a row is added, automatically adds buttons and other bits based on data attributes. So you can add a delete column to a table with a simple attriute in the html rather than having to spend ages wiring it up in every page.

    If you dynamically add a new row to the table or edit (replace) an existing row, I need the createdCell function to run, or some other function to run, that will add the necessary stuff.

    I was doing it a different way before, hooking into the table refresh function, but this created some problems with dynamically altering the datatable so now I just call a function in createdCell to set up the row. I needed the function to run every time.

    Maybe I can replace it with render, I'm not sure. But it's a lot of effort to go back and change everything now. It's a shame you can't hook into things after the table has loaded because it would be so much easier to add modifications to the table than directly hacking html and crawling through parents looking for TRs etc.

  • allanallan Posts: 63,535Questions: 1Answers: 10,475 Site admin

    The code I have, whenever a row is added, automatically adds buttons and other bits based on data attributes.

    Are you using a delegated event for that, or actually modifying the HTML rather than relying on Javascript for it? If you are able to link to the page, or even just show me your code, I'll be happy to take a look at it and either suggest improvements or learn myself how I can improve DataTables.

    Thanks,
    Allan

  • simoncrowdersimoncrowder Posts: 3Questions: 0Answers: 0
    edited August 2019

    replacing createdCell with render, render doesn't have a reference to the cell as a parameter so that didn't work. Also setting a column to visible false doesn't work with the refresh. And if a column is set to display: none, doing .row.add ignores the column so all your data is shifted across one and doesn't go into the hidden column.

    This is a nightmare. I'm not really sure how to even paste an example because it's so monstrous. But let's break it down since I have 3 days to finish this...

    When the table first loads, it's literally a <table> and all the data is in it, and I initialise it with .DataTable().

    I want one column to be the 'delete' column in this table, and most of my other tables, so to do this I wrote a function called setDataTableDeleteColumn which looks like this, called from CreatedCell since I need it to run not just when the table is initialised, but also if it's edited (a row is replaced) or added to (a row is added).

    Originally I did indeed hook into the draw event but for some reason I hit a problem with that so I had to abandon it, but I can't remember what the problem was, I suspect it was because the data-attributes on the elements were lost when the table redrew or something.

    The last cell of my table row looks like:

    <td data-parameter-customFieldId="@field.someId"></td>

    function setDataTableDeleteColumn(cell, action, data) {
        if ($(cell).find(".delete-column").length == 0) {
            var button = $(cell).append("<a class='delete-column w-100 h-100 d-block' href='#'><i class='fal fa-trash-alt'></i></a>");
    
            $(button).on('click', function () {
                let table = $(this).closest("table").DataTable();
    
                if (!data) {
                    data = {};
                }
    
                for (var i = 0; i < cell.attributes.length; i++) {
                    if (cell.attributes[i].name.startsWith('data-parameter')) {
                        var parameter = cell.attributes[i].name.substring(15);
                        data[parameter] = cell.attributes[i].value;
                    }
                }
    
                if (confirm("Are you sure you wish to delete this record?")) {
                     $.ajax({
                        url: action,
                        type: 'DELETE',
                        cache: false,
                        context: this,
                        data: data
                     }).done(function (result) {
                         if (result == true) {
                             $(this).closest("tr").addClass("@(ViewContext.RouteData.Values["controller"].ToString().ToLower())Background").fadeOut(600, function () {
                                 table.row($(this).closest('tr')).remove();
                                 table.draw();
                             });
                         }
                         else {
                             var initialColour = $(this).closest("tr").css("background-color");
                            $(this).closest("tr").css("background-color", "red").animate({ "background-color": initialColour }, 600, function () { $(this).css('background-color', ""); });
                         }
                     }).catch(function () {
                         var initialColour = $(this).closest("tr").css("background-color");
                         $(this).closest("tr").css("background-color", "red").animate({ "background-color": initialColour }, 600, function () { $(this).css('background-color', ""); });
                    });
                }
    
                event.preventDefault();
                event.stopPropagation();
                return false;
            });
    

    this is how I set it up:

                    { "aTargets": [6], "createdCell": function (cell, cellData, rowData, rowIndex, colIndex) {
                        setDataTableDeleteColumn(cell, "@Url.Action("DeleteItem", "Ajax")");
                        }
                    }]}
    

    You can see that basically I want to add this delete button to most of my tables with a simple one line declaration, and it mostly works with the hack to cause the createdCell to run every time, just the visible problem remains because I don't want to show the IDs of my items. Maybe I can pile on another hack to set the width to 0 or something.

  • allanallan Posts: 63,535Questions: 1Answers: 10,475 Site admin

    Right - if I might suggest a simplification of your code there. Don't use createdCell for simply adding event handlers. It isn't required and as you are seeing it isn't efficient. Use a delegated event handler like in this events example.

    So you might have:

    $('#myTableId').on('click', 'tbody a.delete-column', function () {
      var rowData = table.row( $(this).closest('tr') ).data();
    
      if (confirm...) {
       ...
      }
      
      return false;
    } );
    

    Then use a renderer to create the a for the column.

    That will be faster (both for development and running of code), smaller, I would say more maintainable and will actually work :).

    Allan

This discussion has been closed.