Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Web Components don't survive hx-swap=morph #57

Open
luontola opened this issue Jul 5, 2024 · 1 comment
Open

Web Components don't survive hx-swap=morph #57

luontola opened this issue Jul 5, 2024 · 1 comment

Comments

@luontola
Copy link

luontola commented Jul 5, 2024

I have an htmx page with a form. Some form elements depend on other form elements, and the page has some web components which also depend on the form, so I want to swap all of them when the form is changed. I use Idiomorph to maintain form element focus.

But when I use hx-swap="morph:outerHTML", the web components stop working after the swap. None of the web components' callback methods are called when the swap happens. It seems like the original web component instance is never notified that it was removed from the DOM, and no new web component instance is initialized. With hx-swap="outerHTML" things work fine.

Example app

  1. Save this file as server.py
from string import Template

from flask import Flask, request

app = Flask(__name__)


@app.route("/", methods=['GET', 'POST'])
def home():
    message = "world"
    if "message" in request.form:
        message = request.form['message']
    html = Template("""
<script src="https://unpkg.com/[email protected]/dist/htmx.min.js"></script>
<script src="https://unpkg.com/[email protected]/dist/idiomorph-ext.min.js"></script>
<script type="module">
class HelloWorldElement extends HTMLElement {
  constructor() {
    super();
    console.log("constructor")
  }

  connectedCallback() {
    console.log("connectedCallback")
    const message = this.getAttribute("message");
    const root = document.createElement("p");
    root.textContent = "Hello " + message + " from Web Component";    
    this.appendChild(root)
  }

  disconnectedCallback() {
    console.log("disconnectedCallback")
  }
  
  adoptedCallback() {
    console.log("adoptedCallback")
  }
  
  attributeChangedCallback() {
    console.log("attributeChangedCallback")
  }
}
customElements.define('hello-world', HelloWorldElement);
</script>

<div id="root">

<form method="post" hx-post="/" hx-trigger="input" hx-select="#root" hx-target="#root" hx-ext="morph" hx-swap="morph:outerHTML">
<label>Message: <input type="text" id="message" name="message" value="$message"></label>
</form>

<p>Hello $message from HTML</p>

<hello-world message="$message"></hello-world>

</div>
""").substitute(message=message)
    print(html)
    return html
  1. Run the app with the commands:
pip install Flask
flask --app server.py --debug run
  1. Open the app at http://127.0.0.1:5000/

  2. Write something to the input field

Expected result

  • The page should show the messages "Hello something from HTML" and "Hello something from Web Component"

Actual result

  • The page only shows "Hello something from HTML"
  • The page source contains the element <hello-world message="something"></hello-world> without any of its dynamically generated child elements (it's like the web component was not re-initialized after the swap)
  • The only log statements on the javascript console are "constructor" and "connectedCallback" from the original full page load (it's like the original web component instance wasn't notified that its DOM node was destroyed/recreated/updated)

Workaround

In my app I was able to use hx-select-oob to update the web component portion of the page without morphing, and only morph the form which didn't contain web components.

@hanomu
Copy link

hanomu commented Jul 12, 2024

Hi just providing some helpful info here because I ran into a somewhat similar issue with ion icons and idiomorph merging. In my case it was because ion icon web component adds a "hydrated" class after the icon is loaded so during the merge this change is lost (resulting in invisible icons). This isn't a fault of idiomorph but a consideration that needs to be taken when employing any web component with idiomorph merging.

In the case of your hello-world element, you're creating a child element in the connectedCallback which will be lost on merge.

If you don't have access to the web component code an appropriate workaround for this specific case would be adding { callbacks: { beforeNodeRemoved: (removedNode) => return removedNoded.parentNode.nodeName !== 'HELLO-WORLD' } } to your idiomorph config, this will keep your dynamically added child elements during merge. However, the hello-world element as defined in your sample code will not update on attribute changes because it's missing the requisite static observedAttributes = ['message']; in your class definition so attributeChangedCallback will never be called accordingly. I'm assuming this is just an oversight in the example code and the real web component is correctly implemented.

If you have access to the web component code you could consider using shadow DOM for child nodes because these aren't affected by merge.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants