Open lngr opened 3 years ago
wow, that's an interesting case...
We would want to check the element just before settle and see if any new classes were added... :thinking:
Can you give me a more concrete use case to help me think about it? What's that "second" class doing and how did it get there?
Yes, the example in the bug report was a bit condensed for the sake of demonstration.
We actually replace a custom element, which happens to be a Web Component (e.g., <my-dialog id="replaced" open>
). Once the web component is connected to the DOM (connectedCallback
is run, which happens in or immediately after the swap), it runs custom initialization code and adds visual style classes to itself. That's where the second
class comes from.
However, I think the bug may also happen with non-web components. For example, it would argue it's rather common to run some specificy Javascript code once the element has been swapped in. In particular, we rely heavily on the eventing mechanism with HX-Trigger:
, and adding custom classes from Javascript triggered by HX-Trigger
is quite common.
Note that in our case, I already was able to implement a workaround: I'm not adding custom classes (second
), but a specific custom attribute, which we can then target from CSS via my-dialog[my-attribute]
. However, that's not always possible due to the way CSS selectors work, and the web component might be a third-party component, where such changes are impossible.
This is the logic where the attribute cloning happens:
thar be dragons... :cold_sweat:
I'll take a look
We're running against this same issue. Perhaps a more concrete usecase is where we want to replace content of a sidepanel with tabs. The tabs from different htmx requests have the same id's. We use AlpineJS to keep the active tab state and we use a bind on the class property to set the right tab as active.
So this is a dumbed down example of what that looks like:
<div id="side-content"
x-data="{
activeTab: null
}"
>
<div class="tab-content">
<div id="tab1" class="tab-pane" :class="{ 'active': activeTab === $el.id }" title="Tab 1"></div>
<div id="tab2" class="tab-pane" :class="{ 'active': activeTab === $el.id }" title="Tab 2"></div>
</div>
</div>
Now I left out the meganism to switch the activeTab value, but imagine we've loaded this side-content once and a tab was activated. We then use htmx to retrieve the sidecontent of another item which uses the same layout and same tab id's.
The same tab that was active before immidiatly gets the active
state again, but after settle that active class is removed and AlpineJS is not aware of that change as it already assumed the class bind to be up to date.
Running into this as well when using web components.
Any classes added in connectedCallback will be overwritten after settling.
I believe htmx also "steals" style. Alpine'sx-show
doesn't work and here is a demo of it at https://jsfiddle.net/mange85/u3q76z5o/7/
Removing id="someId"
makes the problem go away.
We are aware of https://htmx.org/extensions/alpine-morph/ but we don't want to retain Alpine's state.
I can confirm that this is indeed happening.
When some third party code asynchronously applies changes on specific attributes before the HTMX settling is done, they will be reverted by HTMX when the settling process starts. In most cases, that means 20ms after the element is created.
It happens for the following attributes:
There are several fixes for now:
htmx.config.defaultSettleDelay
to zero. This would mean that the HTMX class swapping process will all be done synchronously. This will work for you if you don't care about CSS transitions in your app. You could also maybe try to overwrite htmx.config.attributesToSettle
with an empty array (not documented but it could allow disabling this whole behavior as well) ?htmx:load
to manually set the missing attributes if you know exactly which ones you need.htmx:beforeSwap
, htmx:afterSwap
and their out of band variants with htmx:afterSettle
in order to hook up some custom logic for attributes recovery. That would certainly be absolutely terrible performance wise because you would have to observe changes and store attributes state for many nodes. It would be nice to have an htmx:beforeSettle
step so that we can achieve this kind of customization more easily.I came across this issue in the context of alpinejs as well.
There's a corresponding discussion on the alpinejs repository, so it seems like there are at least some people confused by this behavior.
Here's a Codepen demonstrating the issue and some of the mentioned workarounds: https://codepen.io/shimikano/pen/MWREmPO
Funny enough hx-on::after-settle="window.Alpine.start()"
fixes it.
So Alpine may be confused?
@andryyy I like that approach, however, as documented, Alpine.start()
should not be called more than once per page.
I tried your workaround and on the surface, the result looked good. However, a warning is displayed in the console:
Alpine Warning: Alpine has already been initialized on this page. Calling Alpine.start() more than once can cause problems.
@shimikano Oh, missed that.
A second DOM update fixes the issue.f too.
hx-on::after-swap="event.detail.elt.innerHTML=event.detail.elt.innerHTML"
Well, don’t know if that’s a fix, but a funny workaround.
I think I have an associated issue, too.
I have this custom Alpine directive (for an auto-growing text field):
document.addEventListener("alpine:init", () => {
Alpine.directive("grow", (el) => {
el.style.resize = "none";
el.style.overflow = "hidden";
el.style.height = "auto";
el.addEventListener("input", () => {
el.style.height = "auto";
el.style.overflow = "hidden";
el.style.height = el.scrollHeight + "px";
});
});
});
It works fine with a textarea that is initially rendered on the page, but the style assignments are not respected when the textarea is swapped into the page later on using HTMX. When I use a little timeout the styles are applied correctly.
I believe I have the same issue. I implemented a hack with alpine to solve this
<textarea id="txt" @textarea-updated.window="$el.style.height = $el.scrollHeight + 'px'"></textarea>
response.headers['HX-Trigger-After-Settle'] = "textarea-updated"
return textarea
I trigger an event after settle that resizes the area when I replace it. For my case I also have to set the cursor, but that is fine to do in the alpine x-init.
The transition behaviour described here has an unintended side effect, which breaks our application. The same use case worked with Intercooler.js, but for other reasons we must migrate to HTMX. In particular HTMX removes classes on replaced DOM elements that have been added to the swapped DOM element during the settle period.
Assume you have a target as follows:
We are going to replace
#target
'sinnerHTML
viahx-post
/hx-swap
with the following HTML returned by the server:Now assume after the
#replaced
element has been swapped into the DOM but before the DOM is settled (beforehtmx-settling
is removed),#replaced
has gained another classsecond
. Then, after the settle delay, HTMX sets theclass
attribute to the value originally obtained from the server (here:"first"
) and therefore effectively removes thesecond
class from#replaced
.If would therefore be great if there was a way to disable the settling mechanism during specific swaps.