htmx.ajax requests promise fulfilled too early
When using htmx.ajax the returned promise is fulfilled before htmx finishes all DOM manipulations.
Here is an example. One would think that the displayed box is blue after everything finished but it is red. Why? Because htmx copies some classes from the old elements to the new ones after the ajax promise is fulfilled.
- html is loaded and the box is gray
- js gets executed and the ajax call is made
- the ajax promise is fulfilled and the color is switched to blue
- htmx calls some settle methods and overwrites the changes made in step 3.
- the box has the wrong color
index.html
<html>
<head>
<script src="https://cdnjs.cloudflare.com/ajax/libs/htmx/2.0.2/htmx.min.js"></script>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="my-8 flex flex-col gap-8 justify-center items-center">
<h1>Basic example</h1>
<main id="main">
<div id="box" class="w-64 h-32 bg-gray-200"></div>
</main>
<script>
window.htmx.ajax("GET", "./main.html", {
source: "#main",
target: "#main",
swap: "innerHTML",
}).then(() => {
console.log("promise resolved");
document.getElementById("box").classList.remove("bg-red-200");
document.getElementById("box").classList.add("bg-blue-200")
});
</script>
</body>
</html>
main.html
<div id="box" class="w-64 h-32 bg-red-200"></div>
My current workaround is using htmx:afterSettle, but this is inconvenient, because the data flow is now broken and we need to associate the data with the corresponding settle event.
var boxState;
document.addEventListener("htmx:afterSettle, (e) => {
if (e.detail.pathInfo.requestPath === "./main.html") {
// do stuff with boxState
}
});
// somewhere else
boxState = state;
window.htmx.ajax("GET", "./main.html", {
source: "#main",
target: "#main",
swap: "innerHTML",
});
Here the log with htmx.logAll():
htmx.esm.js:895 htmx:confirm <div id="container" class>…</div> {target: div#container, elt: div#container, path: './container.html', verb: 'get', triggeringEvent: undefined, …}
htmx.esm.js:895 htmx:configRequest <div id="container" class>…</div> {boosted: undefined, useUrlParams: true, formData: FormData, parameters: Proxy(FormData), unfilteredFormData: FormData, …}
htmx.esm.js:895 htmx:validateUrl <div id="container" class>…</div> {url: URL, sameHost: true, boosted: undefined, useUrlParams: true, formData: FormData, …}
htmx.esm.js:895 htmx:beforeRequest <div id="container" class>…</div> {xhr: XMLHttpRequest, target: div#container, requestConfig: {…}, etc: {…}, boosted: undefined, …}
htmx.esm.js:895 htmx:beforeSend <div id="container" class>…</div> {xhr: XMLHttpRequest, target: div#container.htmx-request, requestConfig: {…}, etc: {…}, boosted: undefined, …}
htmx.esm.js:895 htmx:xhr:loadstart <div id="container" class>…</div> {lengthComputable: false, loaded: 0, total: 0, elt: div#container.htmx-request}
htmx.esm.js:895 htmx:beforeProcessNode <body class="h-full w-full m-0"><doctype html>…</doctype><script type="module" src="/@vite/client"></script><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1"><div id="container" class>…</div><script type="module" src="/src/renderer.ts?t=1724265622306"></script></doctype></body> {elt: body.h-full.w-full.m-0}
htmx.esm.js:895 htmx:load <body class="h-full w-full m-0">…</body> {elt: body.h-full.w-full.m-0}
htmx.esm.js:895 htmx:xhr:progress <div id="container" class>…</div> {lengthComputable: true, loaded: 103, total: 103, elt: div#container.htmx-request}
htmx.esm.js:895 htmx:beforeOnLoad <div id="container" class>…</div> {xhr: XMLHttpRequest, target: div#container.htmx-request, requestConfig: {…}, etc: {…}, boosted: undefined, …}
htmx.esm.js:895 htmx:beforeSwap <div id="container" class>…</div> {shouldSwap: true, serverResponse: '\x3Cscript type="module" src="/@vite/client">\x3C/script…iv id="box" class="w-64 h-32 bg-red-200"></div>\r\n', isError: false, ignoreTitle: undefined, selectOverride: undefined, …}
htmx.esm.js:895 htmx:beforeCleanupElement <div id="box" class="w-64 h-32 bg-gray-200"></div> {elt: div#box.w-64.h-32.bg-gray-200}
htmx.esm.js:895 htmx:beforeCleanupElement " " {elt: text}
htmx.esm.js:895 htmx:beforeCleanupElement " " {elt: text}
htmx.esm.js:895 htmx:afterSwap <div id="container" class>…</div> {xhr: XMLHttpRequest, target: div#container.htmx-request.htmx-settling, requestConfig: {…}, etc: {…}, boosted: undefined, …}
htmx.esm.js:895 htmx:afterRequest <div id="container" class>…</div> {xhr: XMLHttpRequest, target: div#container.htmx-settling, requestConfig: {…}, etc: {…}, boosted: undefined, …}
htmx.esm.js:895 htmx:afterOnLoad <div id="container" class>…</div> {xhr: XMLHttpRequest, target: div#container.htmx-settling, requestConfig: {…}, etc: {…}, boosted: undefined, …}
- renderer.ts:19 promise resolved
htmx.esm.js:895 htmx:xhr:loadend <div id="container" class>…</div> {lengthComputable: true, loaded: 103, total: 103, elt: div#container.htmx-settling}
htmx.esm.js:895 htmx:beforeProcessNode <script type="module" src="/@vite/client"></script> {elt: script}
htmx.esm.js:895 htmx:load <script type="module" src="/@vite/client"></script> {elt: script}
htmx.esm.js:895 htmx:beforeProcessNode <div id="box" class="w-64 h-32 bg-red-200"></div> {elt: div#box.w-64.h-32.bg-red-200}
htmx.esm.js:895 htmx:load <div id="box" class="w-64 h-32 bg-red-200"></div> {elt: div#box.w-64.h-32.bg-red-200}
htmx.esm.js:895 htmx:afterSettle <div id="container" class>…</div> {xhr: XMLHttpRequest, target: div#container, requestConfig: {…}, etc: {…}, boosted: undefined, …}
Hey, as you have noticed, this is due to settling, that you can find some explanations about in the docs here and here.
If you want to get rid of settling altogether, you can set defaultSettleDelay to 0 (defaults to 20 ms), see the configuring htmx doc.
Hope this helps!
I understand what is happening (but not the reason why) and I found a workaround for what I want to do, but I still think that this a bad API choice. As a user you would expect that htmx has finished everything when the promise is fulfilled.
Yeah, this appears to be what was causing my bug as well. I have something like this which initializes the Javascript of server-side rendered components.
document.addEventListener('htmx:afterSwap', initializeComponents);
The initializeComponents method relies on checking CSS classes on these components, and it just as described in the settle docs, old classes are on the new elements, unless I wait for some arbitrary setTimeout delay.
You know what would really help in the documentation, is a chart of how all the different events relate to each other, similar to the Vue lifecycle diagram.
I also take issue with the structure here, it seems reversed.
- https://htmx.org/docs/ - This is basically a reference/usage guide, not complete documentation. "Docs" suggests to me it should document literally everything I could possibly encounter in the library. For example, I can't find the "afterSettled" event listed anywhere on this page.
- https://htmx.org/reference/ - This functions a little more like API documentation. The title "Reference" is misleading, it is not a reference guide.
Love that chart idea @Giwayume , it would certainly help newcomers to understand all those phases and when the various events occur.
Documentation improvements are always welcome, so if you have suggestions, please feel free to open PRs! (if you see different parts to improve, please always open a PR for each specific part so it's easier to discuss, review & merge them as we may not agree with every change)
It's true that the wording can be confusing here... The docs are maybe more like a "user guide", but I'm not fluent enough in english to determine what word fits best here honestly