Problem/Motivation

When an HTMX response includes new JavaScript files (e.g. from SDC components), Drupal.attachBehaviors() is never called for the newly loaded behaviors. As a result, behaviors registered by HTMX-loaded scripts are never attached to the swapped-in content.

This is a follow-up to #3578459: htmx:drupal:load fires before JS assets are loaded due to unchained promise in htmx:beforeSwap which fixed the unchained promise in htmx-assets.js. With that fix applied, loadjs correctly waits for script files to be loaded before firing htmx:drupal:load.

However, two issues remain:

  1. The htmx.on('htmx:drupal:load') handler in htmx-behaviors.js does not fire at all (see debug evidence below), meaning Drupal.attachBehaviors() is never called for HTMX-loaded content via the intended mechanism.
  2. Even if it did fire, loadjs resolves its promise on the script's load event, which may fire before the browser has executed the script contents. Behaviors registered by those scripts would not yet exist on Drupal.behaviors.

In our case, Alpine.js components in HTMX-loaded content still work because we use a custom bridge that defers Alpine's MutationObserver during swaps (Alpine.deferMutations() on htmx:beforeSwap, Alpine.flushAndStopDeferringMutations() on htmx:drupal:load). Alpine then self-initializes new x-data elements independently of Drupal.attachBehaviors().

However, any component using vanilla Drupal.behaviors with once() — will silently fail when loaded via HTMX, since attachBehaviors() is never called on the new content.

Steps to reproduce

  1. Create an SDC component with a Drupal.behaviors registration in its JS file:
    // my_module/components/my-component/my-component.js
    ((Drupal, once) => {
      Drupal.behaviors.myComponent = {
        attach(context) {
          once('my-component', '[data-selector="my-component"]', context).forEach((el) => {
            // Initialize component
          });
        },
      };
    })(Drupal, once);
    
  2. Load that component via an HTMX request (e.g. a dialog loaded with data-hx-get, using _htmx_route)
  3. The behavior's attach() is never called on the new content

Debug evidence

Three console.log statements were added:

  1. In the component script, right before Drupal.behaviors registration:
    // my-component.js, before Drupal.behaviors assignment:
    console.log('script executed, behaviors keys:', Object.keys(Drupal.behaviors).length);
    
  2. In core/misc/htmx/htmx-assets.js, right before htmx.trigger() (line 146):
    console.log('firing htmx:drupal:load, myComponent in Drupal.behaviors:',
      'myComponent' in Drupal.behaviors,
      'Object.keys(Drupal.behaviors).length:', Object.keys(Drupal.behaviors).length);
    
  3. In core/misc/htmx/htmx-behaviors.js, inside the htmx:drupal:load handler (line 14):
    console.log('htmx-behaviors: htmx:drupal:load received');
    

Console output:

// 1. Component script executes, behavior not yet registered:
script executed, behaviors keys: 20

// 2. htmx-assets.js fires event, behavior IS registered:
firing htmx:drupal:load, myComponent in Drupal.behaviors: true, Object.keys(Drupal.behaviors).length: 21

// 3. htmx-behaviors.js log NEVER appears — the handler does not fire

Manually calling Drupal.behaviors.myComponent.attach(document) from the console works and finds the element.

Proposed resolution

Add a queueMicrotask() call in htmx-assets.js after htmx.trigger('htmx:drupal:load') to call attachBehaviors() directly:

// htmx-assets.js, in htmx:afterSettle handler:
htmx.trigger(detail.elt.parentNode, 'htmx:drupal:load');
queueMicrotask(() => {
  Drupal.attachBehaviors(detail.elt.parentNode, drupalSettings);
});

This works around both issues: the non-firing htmx:drupal:load handler, and the script execution timing.

Note: Placing queueMicrotask inside the htmx:drupal:load handler in htmx-behaviors.js was also tested but does not fix the issue — the handler itself never fires.

Remaining tasks

  • Investigate why htmx.on('htmx:drupal:load') in htmx-behaviors.js does not receive the event fired by htmx.trigger() in htmx-assets.js
  • Review and test the proposed queueMicrotask workaround
  • Consider whether a more robust solution exists

User interface changes

Introduced terminology

API changes

Data model changes

Release notes snippet

Issue fork drupal-3581317

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Comments

niklan created an issue. See original summary.

niklan’s picture

Title: Behaviors registered by HTMX-loaded scripts are not attached because attachBehaviors runs before script execution completes » Drupal.attachBehaviors() is never called for HTMX-loaded content when response includes new scripts 

The new title better reflects findings during testing: the htmx:drupal:load handler in htmx-behaviors.js does not fire at all, so attachBehaviors() is never called for HTMX-loaded content — not just a timing issue with script execution.

niklan’s picture

Status: Active » Closed (duplicate)

Whoops, it appears to be fixed in the main branch by #3560659: HTMX Drupal behaviors are not applied when swapped element is body

Now that this issue is closed, review the contribution record.

As a contributor, attribute any organization that helped you, or if you volunteered your own time.

Maintainers, credit people who helped resolve this issue.

niklan’s picture