Reloading externally updated DOM data

Reloading externally updated DOM data

henrikleionhenrikleion Posts: 3Questions: 1Answers: 0

https://jsfiddle.net/wt627fL0/1/

I'm trying to integrate DataTables into an htmx website.
The table data is initially loaded from DOM. When deleting or updating a row in the table, htmx replaces the row with a new tr element returned from the server, but I can't refresh the Datatable with the new DOM.

When debugging, I can see that the tr is being replaced properly, but then I want to invalidate the Datatable, reload the data from DOM and re-draw Datatable to get the css classes for select feature added.

I've tried a few combinations of invalidate and draw:
1. DataTable().rows().invalidate('dom').draw();, leads to the data being reverted back to the previous state, overwriting what htmx just fetched from the server
2. DataTable().rows().draw();, same thing
3. otherwise, if I just DataTable().rows().invalidate('dom')or do nothing, the DataTables select classes are not applied, and I can no longer click to select the row.

As I understand, whole idea with ``ìnvalidate('dom') ``` is to reload data which has been externally modified - what am i missing here?

Thanks for an awesome library!

Best regards,
Henrik

This question has an accepted answers - jump to answer

Answers

  • allanallan Posts: 63,676Questions: 1Answers: 10,497 Site admin

    htmx replaces the row with a new tr element returned from the server,

    That's going to be a problem for DataTables. It expects to have full control over the DOM. It will manipulate the DOM to apply sorting, ordering, paging, etc. For example, if you have paging enabled and more than 10 rows in the table, it will only have the 10 needed actually in the DOM. HTMX can't know that, so we end up with a conflict. I'm not sure how the two could really work together to be honest. It might be possible if the rows have ids and you updated that way, but it isn't something I've tried - DataTables has its own internal reference to the TR and its cells, so if they are replaced, all bets are off.

    Realistically you only want one library controlling the DOM for the table. If you can get the data alone and tell HTMX to leave the table alone, that's probably the best way.

    Allan

  • henrikleionhenrikleion Posts: 3Questions: 1Answers: 0

    But isn’t this exactly the point of the invalidate method - to scrap all internal references and recalculate them based on what’s currently in the html?
    Just like when the datatable was first applied on the html table when page was first loaded.

  • kthorngrenkthorngren Posts: 21,443Questions: 26Answers: 4,974
    edited October 2023

    But isn’t this exactly the point of the invalidate method - to scrap all internal references and recalculate them based on what’s currently in the html?

    My understanding is rows().invalidate() will only update rows that exited in the DOM when initialized. It won't add or remove rows which is what you are doing when replacing a tr. I built this simple example to deomonstrate:
    https://live.datatables.net/mohiwogu/1/edit

    Click the Replace Aston's TR button and the row is replaced and rows().invalidate() is executed. Since the original row is removed and a new row is added Datatables doesn't update its cache with that new row. You can see this be searching for replaced.

    Click the Run with JS button to start over then click Update Ashton. The cell within the existing row is updated and rows().invalidate() works. Search for updated to verify.

    It would be a bit drastic but you could use destroy() or destroy and reinit Datatables if you want Datatables to scrap all internal references and recalculate.

    Possibly you can use Datatables API's, instead of HTMX, to update the table as explained in this FAQ.

    I'm not familiar with HTML but could you change this code:

        action: function(e, dt, node, config) {
          let rows = dt.rows('.selected');
          rows.every(function(rowIdx, tableLoop, rowLoop) {
            let bNumber = this.node().id.slice(3) //row id is "tr_0012345", so slice first 3 chars
            htmx.ajax('DELETE', '/bnumber/' + bNumber, {
              target: '#' + this.node().id, //htmx target is #tr_012345
              swap: 'outerHTML' //htmx replaces the whole tr element with a new from server
            })
          });
    

    to something like this to stop the HTML update?

        action: function(e, dt, node, config) {
          let rows = dt.rows('.selected');
          rows.every(function(rowIdx, tableLoop, rowLoop) {
            let bNumber = this.node().id.slice(3) //row id is "tr_0012345", so slice first 3 chars
            htmx.ajax('DELETE', '/bnumber/' + bNumber, {})
          });
          rows.remove().draw();
    

    Or instead of using htmx.ajax use jQuery ajax() to send requests to your API's and use the success function to utilize the Datatables APIs to update the table.

    Kevin

  • allanallan Posts: 63,676Questions: 1Answers: 10,497 Site admin
    Answer ✓

    Yes, exactly as Kevin says. Depending on the "direction" it will either:

    1. Read the new values from the DOM elements for a row (i.e. for when you've changing the innerHTML for a cell). That reads the DOM values into the DataTables internal store, or
    2. Read the new values from the data object and write them into the DOM (allowing external updates of the data object).

    invalidate() does not scan for new elements, removed element or replaced ones. The rows.add(), rows().remove() methods would be used to perform those actions.

    Consider paging - if DataTables and HTMX both controlled the DOM and the user clicked the DataTable control to flick to page 2, HTMX would have no idea where the previous elements have gone (since they are removed from the DOM) when it next does an update (i.e. a data change), and would redraw the table. DataTables wouldn't know that had happened, and still think it is correctly showing page 2's data. Cached filtering values would be out of date, as would sorting, etc.

    Without a full integration between the two, there is simply no way for two libraries to control and manipulate the same DOM. This is true for all, not just DataTables and HTMX.

    In Vue we use v-once to make Vue leave the table alone once it has been rendered. Let DataTables control the DOM. If HTMX has a similar option and you can feed it data, then that would work just fine.

    I do need to make some time to play with HTMX sometime!

    Allan

  • henrikleionhenrikleion Posts: 3Questions: 1Answers: 0

    Thanks a lot for the in-depth explanation. The purpose and function of invalidate() is now clear.

    What I ended up doing was the following:

    window.addEventListener("DOMContentLoaded", (event) => {
      //Listen for htmx events on table rows and update the DataTable internal DOM with
      // the swapped content.
      document.body.addEventListener('htmx:afterSwap', event => {
        if (event.target.localName == 'tr' || event.target.localName == 'tbody') {  
          let table = $('#datatable').DataTable()
          let verb = event.detail.requestConfig.verb;
          switch (verb) {
            case 'delete':
              var trSelector = '#'+event.target.id;
              table.row(trSelector).remove();
              table.draw('full-hold');  //i.e. stay on page
              break;
            case 'put':
              var swappedTdArray = getTdArrayFromTr(event.target.id);
              var trSelector = '#'+event.target.id;
              table.row(trSelector).data(swappedTdArray);
              table.draw('full-hold');  //i.e. stay on page
              break;
            case 'post':
              var location = event.detail.xhr.getResponseHeader("Location");
              var trId = calculateTrIdFromLocation(location)
              var swappedTdArray = getTdArrayFromTr(trId);
              table.row.add(swappedTdArray);
              table.draw('full-reset');  //i.e. recalculate paging 
              break;
          }
        }
      });
    
      function getTdArrayFromTr(trId) {
        var trDom = document.getElementById(trId);
        var tdNodeList = trDom.querySelectorAll('td');
        var tdArray = Array.from(tdNodeList).map(function(td) {
          return td.innerHTML.trim();
        });
        return tdArray;
      }
    

    It fills our needs. We don't have enough data to warrant server-side processing, and htmx provides us with all the dynamics we need. By having htmx talking care of data manipulation in the datatable, we can reuse e.g. error handling and user confirmation from htmx also here.
    In fact, these methods bridging data tables and htmx are pretty much the only custom javascript we have in the whole application.

    /Henrik

  • allanallan Posts: 63,676Questions: 1Answers: 10,497 Site admin

    That looks like a really nice way of doing it. Thanks for sharing it with us!

    Allan

  • rmbertolinormbertolino Posts: 1Questions: 0Answers: 0

    you can publish the method calculateTrIdFromLocation(location)?

  • allanallan Posts: 63,676Questions: 1Answers: 10,497 Site admin

    @rmbertolino - That post was from a year ago, so I'm not sure you'll get an answer. However, we can probably figure it out. What is the value of the location variable that you have? It probably just parses out the id and then that can be used with a row selector (row()) to get the required element.

    If you can link to your page showing the data data you are working with, that would be useful.

    Allan

Sign In or Register to comment.