Problem/Motivation
Htmx has a new version of the library with some nice improvements that we will definitely want to take advantage of: The fetch()ening
See also: htmx 4: migration
HTMX 4.0 Timeline
htmx 4.0.0 stable release occurred on August 28, 2026
Summary of the work offered in the MR:
- The bundled htmx.js and htmx.min.js are updated to v4.0.0. This version is a complete re-write and includes a number of breaking changes
- Our integration JS has been refactored to account for javascript changes.
- The
Htmxfactory class exposes the Htmx API to our attribute rendering process and has been refactored to account for changes in that API. Deprecation notices for removed methods were handled in a separate issue. - The
HtmxRequestInfoTraithas been refactored to account for changes in both the names and data format of Htmx headers. FormBuilderpreviously directly accessed headers that are removed in v4.0.0. It has been refactored to use the new header info provided byHtmxRequestInfoTrait.- Tests have been refactored where needed to align with these changes.
This MR is larger than ideal but all these changes are closely coupled to the full rewrite of HTMX. A review is suggested as follows:
- Review the change record
- Review the code in parts
- The bundled htmx js is changed along with core.libraries.yml, package.json, and yarn.lock
- The javascript files in Drupal in misc/htmx and big_pipe.commands.js
- The PHP files in Drupal/Core/HTMX
- The FormBuilder changes and the test changes.
Remaining tasks
Move to 12.x branch when it opensper policy.Review the htmx 2 => 4 migration guide and adjust our code as needed.Update to htmx 4.0.0Refactor our JavaScript integrationRefactor our PHP attribute and header factories and any supporting code.Refactor our tests
User interface changes
none
Introduced terminology
none
API changes
There are changes to the Htmx class to align with the API changes. This class is mutable by policy.
Data model changes
Release notes snippet
HTMX has been upgraded to version 4. As this contains some backward-incompatible changes, sites with custom code that uses HTMX should review the change record.
| Comment | File | Size | Author |
|---|---|---|---|
| #56 | index.html_.zip | 713.37 KB | fathershawn |
Issue fork drupal-3555916
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:
- 3555916-update-to-htmx
changes, plain diff MR !13668
Comments
Comment #3
fathershawnComment #4
fathershawnComment #5
fathershawnComment #6
fathershawnComment #7
fathershawnComment #8
fathershawnComment #9
catchGiven the overall timings this looks like something we should try to get into 12.0.0
Comment #10
nod_worked out the problems to get things working with that new branch. This is now behaving like our current ajax framework. Assets needs to be loaded before DOM replacement happens.
Comment #11
nod_Got an assist from @latent with the code: https://gist.github.com/MichaelWest22/7ca5b715b368ec1289352d4eeb310182
This moves the code to an htmx extension, and we might be able to use an extension to declare new swap styles to manage modals and messages.
Comment #13
fathershawnComment #14
fathershawnComment #15
fathershawnI'm going to update the HTMX object with changes for 4.x. I'm leaving the rebase for @nod_ as the javascript files are branching from an earlier version of those files and I don't want to try to interpret his work.
Comment #16
fathershawnReposting a question from Slack for wider discussion:
Comment #17
longwaveAdding to #3449808: [12.x] [meta] Requirements for tagging 12.0.0-beta1 after discussions in Slack where HTMX 4 should be stable shortly before we release Drupal 12.
Comment #18
fathershawnRebased the feature branch onto the current head of main. htmx-assets.js should be re-checked to verify that I preserved the refactor but brought in appropriately the bug fixes in that file since the feature branch was forked.
Refactored the
Htmxclass to account for the two new attribute modifiers:inheritedand:appendAI Note: I refactored the first htmx attribute building method and then tasked my JetBrains AI agent to repeat the pattern for the remaining methods.
Comment #19
fathershawnI can't get the yarn.lock to update without adding dev dependencies.
Comment #20
fathershawnThe backend tests now pass. JS tests are failing - I very likely missed something rebasing the JS on changes in
main.Comment #21
fathershawnAll tests passing - waiting for updated releases of htmx 4
Comment #22
catchOnce there's a beta or rc of htmx 4 we could update to that in core on the expectation we'll be able to update again to stable prior to our own stable release.
Comment #23
fathershawn@catch This MR is currently on 4.0.0-beta2. Michael has improvements coming in the next tag that include restoring
data-hx-equally supported ashx-so I'm waiting for the next tag and will update accordingly.Comment #24
fathershawnWe received an pre-release of beta3 from the HTMX team. I copy/pasted the code into this branch and returned our markup strategy to
data-hx-syntax. All tests still pass.The branch is conflicted with
main. I'm going to rebase and resolve and then mark this issue for an initial review.Comment #25
fathershawnComment #26
needs-review-queue-bot commentedThe Needs Review Queue Bot tested this issue. It no longer applies to Drupal core. Therefore, this issue status is now "Needs work".
This does not mean that the patch necessarily needs to be re-rolled or the MR rebased. Read the Issue Summary, the issue tags and the latest discussion here to determine what needs to be done.
Consult the Drupal Contributor Guide to find step-by-step guides for working with issues.
Comment #27
fathershawnI'll update this branch again when the new htmx release is tagged.
Comment #28
fathershawnBeta3 is released with new features that need to be accounted for in our code.
From Drupal Slack:
Comment #29
fathershawnComment #30
fathershawnComment #31
fathershawnComment #32
fathershawnRebased and resolving conflicts in asset aggregation tests.
Comment #33
fathershawnThree un-related tests are failing. If someone who has permission (I no longer can?!) can re-run the failing tests that would be super: https://git.drupalcode.org/project/drupal/-/merge_requests/13668/pipelines
Comment #34
fathershawnAll tests now passing
CR created
Comment #35
fathershawnComment #36
needs-review-queue-bot commentedThe Needs Review Queue Bot tested this issue. It fails the Drupal core commit checks. Therefore, this issue status is now "Needs work".
This does not mean that the patch necessarily needs to be re-rolled or the MR rebased. Read the Issue Summary, the issue tags and the latest discussion here to determine what needs to be done.
Consult the Drupal Contributor Guide to find step-by-step guides for working with issues.
Comment #37
fathershawnThe only failing test is php 8.6 which is allowed
Comment #38
godotislatePHP 8.6 should have been resolved upstream recently with #3588024: PHP 8.6 - Functions mb_regex_encoding() and mb_ereg() are deprecated. You may need to rebase.
Comment #39
fathershawnThanks @godotislate! Rebased onto HEAD of main.
Comment #40
fathershawnNow all tests pass including php 8.6
Comment #41
fathershawnComment #42
needs-review-queue-bot commentedThe Needs Review Queue Bot tested this issue. It fails the Drupal core commit checks. Therefore, this issue status is now "Needs work".
This does not mean that the patch necessarily needs to be re-rolled or the MR rebased. Read the Issue Summary, the issue tags and the latest discussion here to determine what needs to be done.
Consult the Drupal Contributor Guide to find step-by-step guides for working with issues.
Comment #43
fathershawnThe bot reset this issue due to issues about ckeditor 5 files, but they are not part of this MR.
Comment #44
godotislateBot silenced.
Comment #45
longwaveAdded some review questions.
Comment #46
fathershawnResponded to all the comments. I resolved those that were simple feedback. I gave my opinion on those asking about how we should handle our methods that prepare attributes or headers that are now unsupported. I left those open for possible further discussion.
I also commented on the question about
Document.parseHTMLUnsafe. Input on that question from more experienced Javascript devs is very welcome!All tests still passing.
Comment #47
longwaveComment #48
longwaveDiscussed with @fathershawn and @Michael West in the HTMX meeting in Slack. There are some backward incompatible changes here, and because of name clashes and some large architectural changes it's not trivial to provide full forward/backward compatibility. Given that HTMX adoption is still low I think best-effort is okay here for the Drupal 12 cycle; I added a release note snippet so we can inform end users, and if we can get Rector rules into core (or somewhere else) to help automate custom code upgrades that would be amazing.
Comment #49
fathershawnBased on both comments on the MR and the discussion referenced in #48, I'm resolving the open comments.
Comment #50
fathershawnRefactoring for changes in
hx-on. CR adjusted.Comment #51
fathershawnBack to NR - all tests passing.
Comment #52
fathershawnComment #53
smustgrave commentedHaven't done it but may want to drop no-needs-review-bot as there MR needs rebase.
I ignored the changes in assets as assuming there's nothing custom there.
The CR https://www.drupal.org/node/3583674 is great! Wonder though since there's breaking changes if more code example could help? Overkill maybe?
1 question on the MR but really it's a good MR, I really like that you comment on a lot of the changes. I'd say like 99% there from what I can tell.
Comment #54
fathershawnResolved merge conflict and updated htmx to the new RC (beta5)
Comment #55
fathershawnNightwatch
toolbarApiTestfails - I don't have permission to re-run tests but I don't see how that would be related.Comment #56
fathershawnThat test passes locally. Uploaded a zipped Nightwatch report.
Comment #57
smustgrave commentedStarted a new pipeline and indeed seems random. All threads appear to be addressed.
Am removing no-needs-review-bot since moving to RTBC.
Comment #58
needs-review-queue-bot commentedThe Needs Review Queue Bot tested this issue. It fails the Drupal core commit checks. Therefore, this issue status is now "Needs work".
This does not mean that the patch necessarily needs to be re-rolled or the MR rebased. Read the Issue Summary, the issue tags and the latest discussion here to determine what needs to be done.
Consult the Drupal Contributor Guide to find step-by-step guides for working with issues.
Comment #59
smustgrave commentedWeird I'll put the tag back but guess it depends when a core committer gets to it if it'll be kicked back for a reroll.
Comment #60
fathershawnTest
testInstallProfileConfigOverwritefailed and seems unrelated.Comment #61
longwaveAdded some review feedback. Some items in the review were spotted by Claude Code.
Comment #62
fathershawnI updated the clientside and serverside code around getting the triggering element name based on feedback. An unrelated test is failing:
Drupal\Tests\settings_tray\FunctionalJavascript\SettingsTrayBlockFormTest. I don't have permission to re-run tests.Comment #63
fathershawnVerified the observation from @longwave at https://git.drupalcode.org/project/drupal/-/merge_requests/13668#note_13...
Added a test to check for the presence of the
_wrapper_formatquery parameter, which does in fact fail.Comment #64
fathershawnAdded a fix for wrapper format - test passing locally.
Comment #65
fathershawncore/tests/Drupal/FunctionalJavascriptTests/Core/Htmx/HtmxDynamicFormTest.phpfailed but passes locally.Comment #66
fathershawnAll tests passing again
Comment #67
fathershawnAll threads resolved. All tests passing.
Comment #68
fathershawnhtmx 4-beta6 is out. There is this new section (or at least new to me!) in the migration guide: https://four.htmx.org/docs#whats-new We need to re-check that all are accounted for in our code.
Comment #69
fathershawnThese are new and need added methods in the
Htmxfactory class.hx-actionhx-method. Supports progressive enhancement via nativeaction/methodfallbackhx-methodmethodandformmethod)hx-configkey:valuesyntax)Comment #70
fathershawnFixed up the branch and updated HTMX 4 to beta-6. The CR needs some updating and we need another deprecation child issue
Comment #71
fathershawnComment #72
fathershawnAll tests now passing.
Comment #73
fathershawnWe missed hx-status - need to add a method for that.
Comment #74
fathershawnAdded with test - tests passing
Comment #75
fathershawnUpdated the CR.
Comment #76
catchI think we could update to an HTMX beta in main asap, that way once we hit Drupal 12's beta/rc we're updating to more stable releases than introducing a big change late in the cycle. This is on the basis that HTMX 4 is likely to be at an rc or stable release in December when we actually release 12.0.0 which I'm not sure what the timeline is looking like at the moment. But I'd rather go ahead here than be locked into HTMX 3 until 2030.
Comment #77
fathershawn@catch I agree. Further, my discussions with HTMX maintainers give me confidence that stable release will land before we are out of D12-beta.
Comment #78
fathershawnAlso, a bit of HTMX humor: We would be stuck on 2.0 as there is not and never will be a 3.0.
Carson Gross
Comment #79
cosmicdreams commentedSo where is this work?
1. Tests pass (good)
2. So we're ready for a full critical review, yea?
Here are some comments / things I've found:
htmx 2 sent that header — `'HX-Trigger': getRawAttribute(elt, 'id')`. htmx 4 does not. In the newly vendored file the request headers are set at lines 450–476 and are `HX-Request`, `HX-Source`, `HX-Current-URL`, plus `HX-Target` and `HX-Request-Type` conditionally. `HX-Trigger` is gone.
So `$hx_trigger` is always `NULL`, `$hx_target !== $hx_trigger` is always true, and the condition can no longer be false. The out-of-band `form_build_id` swap is now added unconditionally, including in the whole-form-swap case the code deliberately intends to skip, where the build identifier already arrives in the main swap.
This merge request already handles the same rename correctly in `HtmxRequestInfoTrait::getHtmxSource()`, so `FormBuilder` looks simply to have been missed. The comment block at `FormBuilder.php:782` referencing "Headers `HX-Target` and `HX-Trigger`" needs the same update.
Comment #80
fathershawnThank you! Nice finds!! I'll give it a tune up and put it back to NR.
Comment #81
fathershawnmainTest failures seem unrelated to this MR.
Comment #82
godotislateCKEditor5AllowedTagsTest and EntityReferenceWidgetTest are likely random. OpenTelemetryPerformanceTest might be a legitimate failure that needs adjustment in the performance numbers. We've updated the process to do that: https://www.drupal.org/node/3618676
Comment #83
fathershawnThank you for the performance link - only noticed the first two tests so I'll get the metric updated for the third
Comment #84
fathershawnEntityReferenceWidgetTest is failing, but it is also failing locally on main?
Comment #85
fathershawnFound and reverted an unintended change. All tests passing.
Comment #86
cosmicdreams commentedTwo issues are still present on the current head:
core/misc/htmx/htmx-assets.js:57still returnsdoc.documentElement.outerHTML, so every response reaches htmx as a full document.processTarget === nullfallback inhtmx_after_swapis unchanged.Green tests are expected here rather than reassuring. Nothing in the suite exercises
hx-swap="outerHTML", and inner-style swaps are unaffected becausehtmx.js:1354setsstripfor them and compensates by accident. Measured in Chrome against 4.0.0-beta6, clicking a control withhx-swap="outerHTML":The cause is
#makeFragmentathtmx.js:1079keying off the first tag. It seeshtml, sofragment.firstElementChild.tagName === 'BODY'andhtmx.js:1352silently upgradesouterHTMLtoouterSync, which copies attributes from the body wrapper onto the target and wipes the target's own attributes.Fixing that alone exposes the second item. With the one-line change applied, a real
outerHTMLswap detachesctx.target, soctx.target.parentElementisnull, the fallback passes the detached node tohtmx.trigger(), andhtmx.js:1572redirects the event todocument.Drupal.attachBehaviorsthen receivesdocumentinstead of the swapped content:These need handling together, since fixing the first turns the second from masked into live. Also worth checking out-of-band swaps, as
htmx.js:1111defaults them toouterHTML.Happy to supply a FunctionalJavascript test that fails on the current head and passes with the change, if that is the most useful next step.
Comment #87
fathershawnTL:DR The issue with partials and full documents was an upstream bug. The HTMX team is amazing and @latent has already fixed it and @1cg has committed it: outerHTML swap with body fragment corrupts non-body targets
I'll summarize my understanding of the issue after a Slack discussion with @latent and reading their issue and fix:
We always return a typical HTML document. Even our minimal response includes a
<body>tag. Strictly speaking, a body element is required but it does not have to be a<body>tag. HTMX 4 had been written to expect a<body>tag to not be present on an outerHtml swap unless a<body>tag swap was in play. The linked issue fixes this and has more detail but before body attributes were bleeding into targets on on an outerHtml swap.The second issue gets at the attaching and detaching of drupalBehaviors. My understanding is that our system expects to call
Drupal.detachBehaviorson elements about to be removed andDrupal.attachBehaviors.In v.4 the
htmx:before:swapevent detail contains a task queue stored as an array. This is built inHtmx.swapand my LLM has correctly summarized the structure as:There are some swap strategies that do not modify their target elements: the various before & after strategies, textContent, none. For the remaining elements, the target is modified and we should pass the target to detach by modifying our existing processor to loop the tasks and check the strategy (See extension events):
Current MR
Concerning attachment, I think our existing logic could work as now the modified element is inserted our modified without behaviors and going up a level if available should be the right attachment context.
Current MR
Those who are monitoring this issue with deeper experience in core and/or JavaScript please comment about what I may be missing here?
Comment #88
fathershawnI further wonder if we should move our
attachBehaviorsto thehtmx:after:settle event?Comment #89
fathershawnFrom #htmx in Slack
- Carson Gross
Comment #90
fathershawnAn additional HTMX attribute is available:
hx-querythat supports a new HTTP verbQUERYproposed by the IETF: RFC 10008: The HTTP QUERY MethodAdding this to
\Drupal\Core\Htmx\HtmxComment #91
fathershawnAll tests passing!!
I examined the existing Ajax API and how it deals with behaviors. In that context every operation has an identified wrapper element which becomes the context for any
querySelectormethods. So I've improved our process of finding the nearest ancestor using newly available data coming from HTMX 4. The default context isdocument, which is the default context for attach and detach, and is used if there is no parent.Comment #92
fathershawnI also improved the Nightwatch tests to verify every swap method works with behaviors attach/detach.
Comment #93
fathershawnComment #94
catchJust increasing priority.
Comment #95
fathershawnComment #96
fathershawnComment #97
cosmicdreams commentedFrom my perspective, only one tiny thing left, that we can handle in a follow-up
One gap: nothing asserts the invariant that rewrite establishes. I grepped the
branch and found no test covering behavior attach scope.
I have two FunctionalJavascript tests that cover it:
testOuterHtmlSwapDoesNotAttachToDocument
testOutOfBandSwapAttachesBehaviors
They pass against the current head, and both fail if behaviorTarget() is made
to return a detached parent, so they discriminate rather than pass vacuously.
They need a small recorder added to the test_htmx behavior and two routes on
HtmxTestAttachmentsController.
Happy to push them to the merge request, or open a follow-up if you would
rather not expand scope now. No objection to RTBC either way.
Comment #98
cosmicdreams commentedI created the follow-up issue #3620247: Add test coverage for Drupal behavior attachment scope after HTMX swaps
Comment #99
longwaveThere is a merge conflict. Excited to land this ASAP though; I think this has been through enough rounds of review here and any minor edge cases can be handled in followups.
Comment #100
fathershawnMerge conflict resolved - All required tests passing.
Comment #101
fathershawnComment #102
cosmicdreams commentedWhile I still think this work is ready to go, I have some quips about the change record. Mostly additions / extra things to call out:
Our own custom events changed shape, and htmx.org will not tell anyone. The Events section points at the upstream migration guide, but
htmx:drupal:loadandhtmx:drupal:unloadare ours, and their payload changed fromdetail.elttotarget. Any contrib listener written as:now receives
undefinedand fails silently. This deserves a before/after block, it is the one change in the set that produces no error at authoring time and no upstream documentation.The extension registration API changed, not just
hx-ext. The table says "include extension script directly", buthtmx.defineExtension()is gone in favour ofhtmx.registerExtension(name, extension), and the extension shape is now a flat object whose keys are hook names (htmx_config_request,htmx_before_swap,htmx_before_response, and so on). Anyone who shipped an htmx 2 extension needs the new shape, not just a different way to load it. Change notices usually help devs transition from the old state to the new. We should think about what kind of guidance is actually needed hereMorph is available and it will silently break
once(). Morph is not a breaking change, so it is reasonably out of scope — but people will read "htmx 4" and reach forinnerMorph/outerMorph. Morph synchronises attributes, and an attribute on the existing node that is absent from the response markup gets removed. The server never emitsdata-drupal-once, so a surviving element keeps every bound listener while losing its processed marker, and the nextattachBehaviorsdouble-binds. The only protected prefix today isconfig.morphIgnore = ["data-htmx-powered"]. Worth a warning plus a pointer atmorphIgnoreor thehtmx:before:morph:attrhook until core protects it centrally.There is now a 60 second default request timeout.
defaultTimeout: 60000, previously unlimited. Any module issuing long running htmx requests starts failing at one minute with no code change on its side.No builder method for
hx-morph-skiporhx-morph-skip-children. Both are new htmx 4 attributes, and theHtmxclass has no corresponding methods. Since the convention is thathx-*attributes never appear raw in Twig, there is currently no supported way to fence a region from morphing. We might want to make some kind of mention to morph in the change notice.History behaviour changed by default. The table notes that
hx-historywas removed with an extension provided, but not that the default itself changed: htmx 4 no longer caches the DOM, so back navigation re-fetches from the server. That is a behavioural change for anyone relying on restore-from-cache, and it interacts with form build identifiers and cross-site request forgery tokens in ways worth a sentence.Comment #103
fathershawnThanks @cosmicdreams for a close reading of the change record. I definitely do not want to hold up the commit, and I do want the CR to be as helpful as possible.
#1) I'm not sure what to say about this and with the caveat that I'm better in PHP than JS, before and after we pass an instance of Element to the htmx.trigger method
#2) Added encouragement for custom JS developers to go read the htmx change doc.
#3) If Drupal devs have properly coded
once.remove()in their detach methods, our code will detach and re-attach before and after morphing swaps.#4 & #6) I've added an unordered list of all the breaking changes listed in the upstream change guide so that if someone has made use of those features in their custom code, they know to look at the upstream doc.
#5) Good catch! As @longwave notes in #99 I created a follow-up: #3620407: Add missing HTMX 4 morph attributes
Comment #104
longwaveAdded some small feedback points, following both a manual review and a review by Claude Fable 5.1.
Comment #105
fathershawn@longwave I left one thread open for further thoughts from you and addressed all the other feedback.
Comment #106
cosmicdreams commentedI agree, after the license fix. Looks good.
Comment #107
fathershawnRandom test failure: Tests.navigationDisplaceTest
Comment #109
longwaveNice work here everyone, I am so happy that we will get this into the Drupal 12 release.
Committed and pushed 2aa5aede43f to main. Thanks!
Also published the change record.
Comment #111
longwave