htmx icon indicating copy to clipboard operation
htmx copied to clipboard

Lit Light DOM Components Are Removed After hx-swap="morph:outerHTML"

Open RohiNof opened this issue 1 month ago • 8 comments

Hi, I’m using HTMX with morph in my project. I created a custom Lit component using Light DOM by overriding:

createRenderRoot() {
  return this
}

Problem: When using:

hx-swap="outerHTML"

everything works fine, and the component keeps its internal DOM structure.

However, when using:

hx-swap="morph:outerHTML"

the internal DOM disappears. Only the custom element tag remains:

Before swap:

<r-api-combobox id="someId" ...>
  <div 
    <wa-input ...>
      ...
    </wa-input>
    <div class="error-message" ...></div>
    <input type="hidden" name="bankId" value="2">
  </div>
</r-api-combobox>

After swap:

<r-api-combobox id="someId" ...></r-api-combobox>

It seems that morph does not preserve the Light DOM children of custom elements, unlike a normal outerHTML swap.

Question: Is there any configuration or solution to prevent Light DOM children from being removed when using hx-swap="morph:outerHTML" with Lit custom elements?

RohiNof avatar Dec 03 '25 08:12 RohiNof

And even more importantly: Make it work for version 4, since we will soon switch to the 4 alpha to prepare for the GA release.

We're using HTMX on https://app.reai.no (that is also where we use both web components from webawesome.com and our own custom web components created with Lit)

Example web component datepicker:

r-date-picker.js

cc @RohiNof :)

gregjotau avatar Dec 03 '25 10:12 gregjotau

Idiomorph.defaults.callbacks.beforeNodeMorphed = (oldNode, newNode) => {
    // Only handle custom elements (Lit components)
    if (!oldNode.tagName?.includes('-')) return;
    
    // Copy attributes from newNode to oldNode
    for (const attr of newNode.attributes) {
        if (oldNode.getAttribute(attr.name) !== attr.value) {
            oldNode.setAttribute(attr.name, attr.value);
        }
    }
    
    // Remove attributes that don't exist in newNode
    for (const attr of [...oldNode.attributes]) {
        if (!newNode.hasAttribute(attr.name)) {
            oldNode.removeAttribute(attr.name);
        }
    }
    
    // Skip the rest of morphing (children)
    return false;
};

you might be able to try something like this that will add a global callback to idiomorph that will block all custom elements being morphed but still modify the host container attributes if needed.

For htmx4 we don't have full idiomorph callbacks but I want to instead implement custom configs for morphSkip and morphSkipChildren so that you can manually set css selectors to whatever elements you want to retain their state or their children's state during morph. would be good to test out the proposed change i just made for this that you can set htmx.morphSkipChildren to a selector for your web components and it might solve your issue. Note in htmx4 it is outerMorph swap style

MichaelWest22 avatar Dec 04 '25 03:12 MichaelWest22

@MichaelWest22 thanks for getting back to us. Probably a stupid question, but shouldn't idiomorph maybe be able to handle this situation without a workaround / config to skip?

What is the rationale for why a workaround should be needed here? Is there cases where you actually want morphing to remove the child nodes of custom web components that render the component in light DOM?

@RohiNof will test out your proposed solution and get back to you if it works :) We will also test the V4 solution once it is out, so just ping us in that case.

gregjotau avatar Dec 04 '25 05:12 gregjotau

@MichaelWest22 Thanks for your response. Your solution works.

RohiNof avatar Dec 04 '25 09:12 RohiNof

@MichaelWest22 you can ping us when https://github.com/bigskysoftware/htmx/pull/3573 is out in a new alpha, then we will test it :)

gregjotau avatar Dec 04 '25 11:12 gregjotau

@gregjotau latest alpha just shipped for htmx 4 with this included. Full documentation not live on https://four.htmx.org/ yet but you can read the details here: https://github.com/bigskysoftware/htmx/pull/3582/files

MichaelWest22 avatar Dec 10 '25 00:12 MichaelWest22

The built-in morphing is sufficient for most applications. The [idiomorph extension](@/extensions/idiomorph.md) provides a more advanced algorithm for complex DOM transformations.

I guess since we're using idiomorph now, is it a better idea to maybe just continue using that, would we then be able to ues the same approach as above?

gregjotau avatar Dec 10 '25 04:12 gregjotau

Well I have yet to write the idiomorph htmx4 extension yet actually... I should probably remove that idiomorph reference till we have sorted this part out.

There should be no real need to stick with Idiomoprh with htmx4 unless you are already using some of the advanced callbacks to customize your behavior beyond what htmx4 morph configs can do. There is different input handling in idiomorph as well where it overwrites input values instead of preserving them and apps that depend on this may have issues. We also used new swap styles for built in morph so you will be able to implement both in an application if you need to transition.

MichaelWest22 avatar Dec 10 '25 10:12 MichaelWest22