htmx icon indicating copy to clipboard operation
htmx copied to clipboard

htmx.process not fully applying HTMX behaviors to elements in shadowDom

Open lachdoug opened this issue 1 year ago • 10 comments

I am testing HTMX with web components and have found two issues, which seem to be related to how htmx.process() works.

Issue 1: The hx-on::before-request attribute is ignored when using htmx.process() as outlined in the docs. Note that this could be related to https://github.com/bigskysoftware/htmx/issues/2406

Issue 2: The hx-get attribute is ignored when htmx.process() is called on a web component that is a child of another web component.

The example code below shows five buttons:

  1. Not a component - Created using plain HTML. hx-get and hx-on::before-request both work.
  2. MyComponent - A web component created by following the docs (https://v2-0v2-0.htmx.org/examples/web-components/). hx-get works and hx-on::before-request does not work.
  3. MyComponentAlt - A web component similar to MyComponent, but with htmx.process(buttonEl) instead of htmx.process(root). hx-get and hx-on::before-request both work.
  4. MyButton - A simple button web component. hx-get and hx-on::before-request both work.
  5. MyButtonWrapper - A more complex web component, with one web component inside another. hx-get does not work (and can't tell if hx-on::before-request works).

All five button should behave the same: click to show an alert. Buttons 'Not a component', MyComponentAlt and MyButton all work as expected. MyComponent and MyButtonWrapper do not.

I have tried with both HTMX v1.9.11 and v2.0.0-alpha1, with similar results.

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>HTMX WebComponents</title>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <!-- <script src="https://unpkg.com/[email protected]/dist/htmx.min.js"></script> -->
    <script src="https://unpkg.com/[email protected]/dist/htmx.min.js"></script>
  </head>

  <body>
    <button
      hx-on::before-request="beforeRequestHandler(event)"
      hx-get="#not-a-component"
    >
      Not a component
    </button>
    <hr />
    <my-component></my-component>
    <my-compnent-alt></my-compnent-alt>
    <hr />
    <my-button url="#my-button">MyButton</my-button>
    <my-button-wrapper></my-button-wrapper>

    <template id="my-button-template">
      <style>
        button {
          cursor: pointer;
        }
      </style>
      <button
        hx-get="#"
        hx-boost="true"
        hx-on::before-request="beforeRequestHandler(event)"
      >
        <slot></slot>
      </button>
    </template>

    <template id="my-button-wrapper-template">
      <my-button url="#my-button-wrapper">MyButtonWrapper</my-button>
    </template>

    <script>
      window.beforeRequestHandler = (evt) => {
        evt.preventDefault();
        alert(evt.detail.pathInfo.requestPath);
      };

      customElements.define(
        "my-component",
        class MyComponent extends HTMLElement {
          connectedCallback() {
            const root = this.attachShadow({ mode: "closed" });

            root.innerHTML = `
              <button 
              hx-on::before-request="beforeRequestHandler(event)"
              hx-get="#my-component" 
              >MyComponent</button>`;

            htmx.process(root);
          }
        }
      );

      customElements.define(
        "my-compnent-alt",
        class MyComponentAlt extends HTMLElement {
          connectedCallback() {
            const root = this.attachShadow({ mode: "closed" });

            root.innerHTML = `
              <button
              hx-on::before-request="beforeRequestHandler(event)"
              hx-get="#my-compnent-alt"
              >MyComponentAlt</button>`;

            const buttonEl = root.querySelector("button");

            htmx.process(buttonEl);
          }
        }
      );

      customElements.define(
        "my-button",
        class MyButton extends HTMLElement {
          connectedCallback() {
            const root = this.attachShadow({ mode: "closed" });

            const content = document
              .querySelector(`#my-button-template`)
              .content.cloneNode(true);

            root.replaceChildren(content);

            const url = this.getAttribute("url");

            const buttonEl = root.querySelector("button");

            buttonEl.setAttribute("hx-get", url);

            htmx.process(buttonEl);
          }
        }
      );

      customElements.define(
        "my-button-wrapper",
        class MyButtonWrapper extends HTMLElement {
          connectedCallback() {
            const root = this.attachShadow({ mode: "closed" });

            const content = document
              .querySelector(`#my-button-wrapper-template`)
              .content.cloneNode(true);

            root.replaceChildren(content);
          }
        }
      );
    </script>
  </body>
</html>

I have two questions:

  1. Are the docs wrong? Should it be htmx.process(someElement) instead of htmx.process(root)?
  2. Should htmx.process() work on nested web components?

lachdoug avatar Apr 02 '24 07:04 lachdoug

Issue 2: The hx-get attribute is ignored when htmx.process() is called on a web component that is a child of another web component.

I think this is because the current code in bodyContains() only checks a single level of nesting of shadow DOM.

I sent a pull request #2434 that I think solves the issue. You may want to try it and see if it works for you.

andrejota avatar Apr 03 '24 19:04 andrejota

Thanks andrejota, but still not working with #2434.

lachdoug avatar Apr 03 '24 20:04 lachdoug

Thanks andrejota, but still not working with #2434.

Any chance you could try with mode:"open" shadow DOM?

andrejota avatar Apr 03 '24 21:04 andrejota

Issue 1: The hx-on::before-request attribute is ignored when using htmx.process() as outlined in the docs.

This is indeed #2406.

kgscialdone avatar Apr 04 '24 21:04 kgscialdone

Any chance you could try with mode:"open" shadow DOM?

Thanks for the suggestion. I gave that a go, but no difference.

lachdoug avatar Apr 05 '24 03:04 lachdoug

partially fixed w/ the fix for https://github.com/bigskysoftware/htmx/issues/2406 (hx-on)

I don't know about the nested web components thing, will defer to @kgscialdone who understands web components better than me.

1cg avatar May 15 '24 18:05 1cg

I have two questions:

  1. Are the docs wrong? Should it be htmx.process(someElement) instead of htmx.process(root)

No, the docs are correct. Either will work, and it was actually quite a chore to ensure that calling htmx.process with a ShadowRoot as its parameter will work correctly.

  1. Should htmx.process() work on nested web components?

Yes, and at the moment I'm not entirely sure why it isn't. I'll try to look into it more closely when I have the chance.

kgscialdone avatar May 16 '24 10:05 kgscialdone

I have two questions:

  1. Are the docs wrong? Should it be htmx.process(someElement) instead of htmx.process(root)

No, the docs are correct. Either will work, and it was actually quite a chore to ensure that calling htmx.process with a ShadowRoot as its parameter will work correctly.

  1. Should htmx.process() work on nested web components?

Yes, and at the moment I'm not entirely sure why it isn't. I'll try to look into it more closely when I have the chance.

Is there anything new with this nested shadowroot thing?

I am doing an implementation with Lit, a webcomponents library and it works correctly with the root component

But if any of the nested components want to use HTMX, nothing happens there

NeoTRAN001 avatar Jul 20 '24 18:07 NeoTRAN001

I think the idea is for this to be fixed by #3034

scrhartley avatar Dec 09 '24 16:12 scrhartley

Fixed in 2.0.4

scrhartley avatar Dec 14 '24 01:12 scrhartley