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:
- The
htmx.on('htmx:drupal:load')handler inhtmx-behaviors.jsdoes not fire at all (see debug evidence below), meaningDrupal.attachBehaviors()is never called for HTMX-loaded content via the intended mechanism. - Even if it did fire,
loadjsresolves its promise on the script'sloadevent, which may fire before the browser has executed the script contents. Behaviors registered by those scripts would not yet exist onDrupal.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
- Create an SDC component with a
Drupal.behaviorsregistration 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); - Load that component via an HTMX request (e.g. a dialog loaded with
data-hx-get, using_htmx_route) - The behavior's
attach()is never called on the new content
Debug evidence
Three console.log statements were added:
- In the component script, right before
Drupal.behaviorsregistration:
// my-component.js, before Drupal.behaviors assignment: console.log('script executed, behaviors keys:', Object.keys(Drupal.behaviors).length); - In
core/misc/htmx/htmx-assets.js, right beforehtmx.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); - In
core/misc/htmx/htmx-behaviors.js, inside thehtmx:drupal:loadhandler (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')inhtmx-behaviors.jsdoes not receive the event fired byhtmx.trigger()inhtmx-assets.js - Review and test the proposed
queueMicrotaskworkaround - Consider whether a more robust solution exists
User interface changes
Introduced terminology
API changes
Data model changes
Release notes snippet
Issue fork drupal-3581317
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:
- 3581317--behaviors-registered
changes, plain diff MR !15198
Comments
Comment #2
niklanThe new title better reflects findings during testing: the
htmx:drupal:loadhandler in htmx-behaviors.js does not fire at all, soattachBehaviors()is never called for HTMX-loaded content — not just a timing issue with script execution.Comment #4
niklanWhoops, it appears to be fixed in the main branch by #3560659: HTMX Drupal behaviors are not applied when swapped element is body
Comment #6
niklanComment #7
niklan