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

htmx 2.0.0 breaks <template> elements inside Web Components #2702

Closed
luontola opened this issue Jul 6, 2024 · 5 comments
Closed

htmx 2.0.0 breaks <template> elements inside Web Components #2702

luontola opened this issue Jul 6, 2024 · 5 comments
Labels
2.0 bug Something isn't working

Comments

@luontola
Copy link

luontola commented Jul 6, 2024

When a htmx response contains <template> elements inside a Web Component element, htmx 1.9.12 adds them to the DOM, but htmx 2.0.0 removes them. This breaks web components which use that method to receive data from the server.

Example app

  1. Save this file as server.py
from flask import Flask

app = Flask(__name__)


@app.route("/")
def home():
    # Works with https://unpkg.com/[email protected]/dist/htmx.min.js
    # Doesn't work with https://unpkg.com/[email protected]/dist/htmx.min.js
    return """
<script src="https://unpkg.com/[email protected]/dist/htmx.min.js"></script>
<script type="module">
class HelloWorldElement extends HTMLElement {
  constructor() {
    super();
  }

  connectedCallback() {
    const template = this.querySelector("template");
    console.log("template:", template, template?.content.textContent)
  }
}
customElements.define('hello-world', HelloWorldElement);
</script>

<div hx-get="/lazy" hx-swap="outerHTML" hx-target="this" hx-trigger="load">
</div>

</div>
"""


@app.route("/lazy")
def lazy():
    return """
<hello-world>
<template>Lazy loaded content</template>
</hello-world>
"""
  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. Open the JavaScript console and see what is printed there on page load

Expected result

  • The code prints template: <template>​…​</template>​ Lazy loaded content
  • This is what happens on htmx 1.9.12

Actual result

  • The code prints template: null undefined
  • This is what happens on htmx 2.0.0
luontola added a commit to luontola/territory-bro that referenced this issue Jul 6, 2024
Why:
- Fixes lazy loading the territory-list-map. htmx 2.0.0 breaks web
  components which contain <template> elements: the template element
  isn't added to DOM. This leads to the map showing no data.
  See bigskysoftware/htmx#2702
@Telroshan Telroshan added bug Something isn't working 2.0 labels Jul 7, 2024
@kubeden
Copy link
Contributor

kubeden commented Jul 8, 2024

Hey, after some investigation, I found out the following function added to 2.0.0 which does not exist in 1.9.12:

      // oob swaps
      findAndSwapOobElements(fragment, settleInfo)
      forEach(findAll(fragment, 'template'), /** @param {HTMLTemplateElement} template */function(template) {
        findAndSwapOobElements(template.content, settleInfo)
        if (template.content.childElementCount === 0) {
        // Avoid polluting the DOM with empty templates that were only used to encapsulate oob swap
          template.remove()
        }
      })

You can see here that there is a comment "// Avoid polluting the DOM with empty templates that were only used to encapsulate oob swap"

Now, I tested your code and upon encapsulating the "Lazy loaded content" inside a <p> tag, it is being correctly returned.

I am assuming the <template> is considered empty if there is no tag inside.

A fix to this might be to instead of having a plaintext in your <template/>, to instead have text inside a tag (p h2 etc.)

Plaintext inside the template:

image image

P tag inside the template:

image image

Otherwise the function might need to be deleted or change this line:

if (template.content.childElementCount === 0) {

To (or similar)... not sure about this, though. Someone from the core team should jump into the discussion.

if (template.content && template.content.textContent && template.content.textContent.trim().length > 0) {

@luontola
Copy link
Author

luontola commented Jul 8, 2024

Does htmx create some of its own templates and use them for oob swap? It seems dangerous to find and remove arbitrary template elements, instead of just those that htmx has created for a specific purpose. Maybe the oob swap templates could be tagged with a custom class or attribute for identification, and only delete those?

@kubeden
Copy link
Contributor

kubeden commented Jul 8, 2024

Continuing the discussion with more investigation

The following function seems to be taking care of OOB elements:

findAndSwapOobElements(fragment, settleInfo)

function findAndSwapOobElements(fragment, settleInfo) {
    forEach(findAll(fragment, '[hx-swap-oob], [data-hx-swap-oob]'), function(oobElement) {
      if (htmx.config.allowNestedOobSwaps || oobElement.parentElement === null) {
        const oobValue = getAttributeValue(oobElement, 'hx-swap-oob')
        if (oobValue != null) {
          oobSwap(oobValue, oobElement, settleInfo)
        }
      } else {
        oobElement.removeAttribute('hx-swap-oob')
        oobElement.removeAttribute('data-hx-swap-oob')
      }
    })
  }

Now back to oob swaps again:

      // oob swaps
      findAndSwapOobElements(fragment, settleInfo)
      forEach(findAll(fragment, 'template'), /** @param {HTMLTemplateElement} template */function(template) {
        findAndSwapOobElements(template.content, settleInfo)
        if (template.content.childElementCount === 0) {
        // Avoid polluting the DOM with empty templates that were only used to encapsulate oob swap
          template.remove()
        }
      })

We are looping through each occurance of the fragment 'template' from the response of the findAll function (see htmx official doc) so it seems htmx is indeed looping through all elements of some type:
image

Maybe if the findAndSwapOobElements() is assigned to a variable to introduce a second check (if HTMX modified the template) before removing the OOB template, that would help?

forEach(findAll(fragment, 'template'), /** @param {HTMLTemplateElement} template */function(template) {
    let wasModified = findAndSwapOobElements(template.content, settleInfo)
    if (wasModified && template.content.childNodes.length === 0) {
      // Only remove if HTMX modified it and it's completely empty
      template.remove()
    }
  })

But then... I think this is starting a different discussion that maybe needs a new issue opened.

So it seems we are back to the initial workaround or "fix":

🚧 Workaround: don't leave plaintext inside <template>
✅ Fix: check if there is at least one character before removing (not sure if that's a good practice... I need suggestions here); if someone approves this direction, I would be happy to open a pull request.

@kubeden
Copy link
Contributor

kubeden commented Jul 10, 2024

Should I tag someone from the maintainers team? Or should I directly start PR to kickoff a discussion inside?

I'm new to this whole open source contributing thing. 🥲

@Telroshan
Copy link
Collaborator

Hello @kubeden, haven't had much time lately to investigate issues, a bugfix PR would be very welcome as per our contribution guidelines:

Correspondingly, it is fine to directly PR bugfixes for behavior that htmx already guarantees, but please check if there's an issue first, and if you're not sure whether this is a bug, make an issue where we can hash it out..

So if you feel like it, sure go for it!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
2.0 bug Something isn't working
Projects
None yet
Development

No branches or pull requests

4 participants