htmx.process not fully applying HTMX behaviors to elements in shadowDom
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:
- Not a component - Created using plain HTML.
hx-getandhx-on::before-requestboth work. - MyComponent - A web component created by following the docs (https://v2-0v2-0.htmx.org/examples/web-components/).
hx-getworks andhx-on::before-requestdoes not work. - MyComponentAlt - A web component similar to MyComponent, but with
htmx.process(buttonEl)instead ofhtmx.process(root).hx-getandhx-on::before-requestboth work. - MyButton - A simple button web component.
hx-getandhx-on::before-requestboth work. - MyButtonWrapper - A more complex web component, with one web component inside another.
hx-getdoes not work (and can't tell ifhx-on::before-requestworks).
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:
- Are the docs wrong? Should it be
htmx.process(someElement)instead ofhtmx.process(root)? - Should
htmx.process()work on nested web components?
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.
Thanks andrejota, but still not working with #2434.
Thanks andrejota, but still not working with #2434.
Any chance you could try with mode:"open" shadow DOM?
Issue 1: The hx-on::before-request attribute is ignored when using htmx.process() as outlined in the docs.
This is indeed #2406.
Any chance you could try with
mode:"open"shadow DOM?
Thanks for the suggestion. I gave that a go, but no difference.
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.
I have two questions:
- Are the docs wrong? Should it be
htmx.process(someElement)instead ofhtmx.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.
- 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.
I have two questions:
- Are the docs wrong? Should it be
htmx.process(someElement)instead ofhtmx.process(root)No, the docs are correct. Either will work, and it was actually quite a chore to ensure that calling
htmx.processwith aShadowRootas its parameter will work correctly.
- 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
I think the idea is for this to be fixed by #3034
Fixed in 2.0.4