Problem/Motivation

When an HTMX response contains JavaScript libraries that are not already present on the page, Drupal can trigger behavior attachment before those assets finish loading.

The affected scripts are eventually loaded and executed, but any Drupal behaviors they define are registered too late for the behavior attachment pass associated with the HTMX response. Their attach() methods are therefore never called for the inserted content.

Steps to reproduce

  1. Create an HTMX request whose response adds a JavaScript library that is not already loaded on the current page.
  2. Have that library define a Drupal behavior.
  3. Load the response into the page through HTMX.
  4. Observe that the JavaScript file loads and executes, but the behavior's attach() method is not called.

Expected result

The htmx:drupal:load event should fire only after all assets from the response have finished loading. The subsequent call to Drupal.attachBehaviors() should include behaviors defined by those assets.

Actual result

The htmx:drupal:load event can fire while the new assets are still loading. Behaviors defined by those assets miss the attachment pass.

Cause

In core/misc/htmx/htmx-assets.js, htmx:beforeRequest stores an already resolved promise in requestAssetsLoaded.

During htmx:beforeSwap, asset loading is chained onto that promise:

requestAssetsLoaded.get(detail.xhr).then(() => Drupal.htmx.addAssets(data));

The chained promise is discarded. The WeakMap still contains the original resolved promise. Consequently, htmx:afterSettle reads the resolved promise and triggers htmx:drupal:load without waiting for Drupal.htmx.addAssets().

Proposed resolution

Store the chained promise back in requestAssetsLoaded so that htmx:afterSettle waits for the assets:

const assetsLoaded =
  requestAssetsLoaded.get(detail.xhr) || Promise.resolve();

requestAssetsLoaded.set(
  detail.xhr,
  assetsLoaded.then(() => Drupal.htmx.addAssets(data)),
);

Add regression coverage using a delayed asset-loading promise. The test should verify that htmx:drupal:load is not triggered before the promise resolves and is triggered after it resolves.

Related issue

This was exposed by a modeler extension loaded inside an HTMX overlay. A compatibility fix is available in modeler merge request !107.

Issue fork drupal-3617895

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

jurgenhaas created an issue. See original summary.

jurgenhaas’s picture

Status: Active » Needs review

The failing tests are unrelated. The MR is now NR.

jurgenhaas’s picture

Component: javascript » ajax system
fathershawn’s picture

Related issues: +#3555916: Update to htmx 4

The effected code is refactored in #3555916: Update to htmx 4 and that implementation needs to be checked with this bug in mind.

latent’s picture

yeah the htmx4 proposed version already uses proper async behavior to handle this properly. The only possible edge case is js assets that were ES modules that always load later which could impact both versions but I can't see a valid reason to use these with Drupal behavior attachment.

smustgrave’s picture

@jurgenhaas think this should be closed in favor of the HTMX4 ticket?

fathershawn’s picture

Version: main » 11.x-dev

@smustgrave The v4 ticket will only fix this behavior in D12 - I'm changing the version here to D11