When using a JavaScript library that makes a lot of modifications to the DOM, the modified DOM being saved into history is usually undesirable and can lead to bugs

Problem

When using a JavaScript library that makes a lot of modifications to the DOM, the modified DOM being saved into history is usually undesirable and can lead to bugs. Carson explains how to avoid it.

Solution

In order to make back button support work as expected , it’s important to undo all DOM changes that your JS widgets made to the DOM on load, so that htmx takes a snapshot of the pristine html that was initially rendered, before the JS widgets did their respective DOM mutation tasks.

This allows the following flow to happen:

  1. On html load (either from full page refresh or htmx page fragment load), initialise the loaded html with JS widgets as needed, in a load event handler

  2. on navigating to another page, just before htmx takes a snapshot of the current DOM, clean-up/destroy all the extra mark-up that the JS widgets created, inside a htmx:beforeHistorySave event handler

  3. now when the user clicks the back button, htmx will restore the previously saved DOM state and then fire the load event again, which will trigger the event handler defined in step 1 above and your JS widgets will created again… resulting in a properly functioning “previous” html page

Here’s what this looks like in practice in our htmx apps:

  1. We have the following catch-all load event handler in the head section of our base html template, where we initialise all the JS widgets we use in the app.

    Each of the functions in this handler then checks if the target element contains any html elements that should be initialised with the particular JS widget and then proceeds to do the initialisation as needed:

    htmx.onLoad(function(target) {
    
      initializeButtonsets(target);
      initializeDatatables(target);
      initializeSelect2(target);
      initializeMultiSelects(target);
      initializeTinyMCE(target);
      initializeDatePickers(target);
      ...
    });
    
  2. On the flipside of this, we also listen for the htmx:beforeHistorySave event, where we teardown/destroy/undo all the DOM changes the JS widgets did on page load (full disclaimer - we still use a ton of jQuery-based JS widgets, which we are slowly busy migrating to vanilla JS equivalents):

    // Destroy widgets added to the DOM, before taking the history snapshot
    // in order for the widgets to be created again without problems on back button clicks
    htmx.on("htmx:beforeHistorySave", function() {
    
      $('#content .buttonset').buttonset('destroy');
      $('#content table.standard, #content table.readOnly').dataTable().fnDestroy();
      $('#content .select2, #content .select2-allowClear, #content .select2-focus').select2('destroy');
      $('#content select.multiselect').multiselect('destroy');
      if (window.tinymce) tinymce.remove();
      $('#content input.hasDatepicker').datepicker('destroy');
      ...
    
    });