make bodyContains() handle possibly nested shadow root's
Description
make bodyContains() handle the case where elt might be inside nested shadow dom's
Testing
Tested manually with and without nested shadow dom's
Checklist
- [x] I have read the contribution guidelines
- [x] I have targeted this PR against the correct branch (
masterfor website changes,devfor source changes) - [x] This is either a bugfix, a documentation update, or a new feature that has been explicitly approved via an issue
- [x] I ran the test suite locally (
npm run test) and verified that it succeeded
no opinion on this @kgscialdone do you have one?
Definitely needs tests if it is to be accepted though.
This is... acceptable. Recursive checks like this are gross but there isn't really a better way.
@kgscialdone , @1cg - I propose to solve this via htmx extension. That way its up to the user to iterate and fetch the host node.
function getRootNode(elt) {
// custom logic to iterate nodes and return host
const node = /* ... */
return node;
}
htmx.defineExtension("body-contains-elem", {
bodyContains: function (elt) {
const node = getRootNode(elt);
return document.body.contains(node);
}
})
The revised bodyContains would be:
function bodyContains(elt) {
let exists = undefined;
// process extension
withExtensions(asElement(elt), function (extension) {
exists = extension.bodyContains(elt);
})
if (exists === true || exists === false) {
return exists;
}
// original implementation
const rootNode = elt.getRootNode && elt.getRootNode();
if (rootNode && rootNode instanceof ShadowRoot) {
return getDocument().body.contains(rootNode.host);
} else {
return getDocument().body.contains(elt);
}
}
Let me know if this solution is acceptable, I can raise PR for it.
@innovation-stack I'm unclear why solving this with an extension would be preferable to simply solving it, even if the solution is a little gross. Could you elaborate on why?
@kgscialdone - For performance reasons. In my case, I am building a query-builder web component, which can be contain deeply nested components. Here's an illustration:
Few notes:
- Card component can be deeply nested, upto any level of hierarchy
- In my query-builder, any deeply nested child component can refer its root via
builderproperty. I want to leverage this fact as part ofbodyContainsimplementation. - As per solution proposed in PR, calling
bodyContainson any deeply nested child web component have to iterate over all parent web components, which will incur performance.bodyContainsis being called multiple times during render and API calls.
Proposed solution:
- Using
body-contains-elemextension, I can completely bypass all parent node traversals and simply use thebuilderproperty to refer to root component.
Let me know your thoughts on this. Thank you.
@innovation-stack I see where you're coming from, thank you for the explanation. However, I don't think that relegating a fix for this to extensions is the right approach from a logistical point of view, as it puts the impetus on developers using HTMX to handle the problem rather than on HTMX itself.
That said, on further inspection of the use cases of bodyContains, it may be worth looking into whether it actually needs to be "does the <body> contain this element", or if all use cases would be fine also including elements in the <head>. I can't determine that offhand at the moment, so will need to inquire of @1cg for more information, but if it's the case that it doesn't matter whether it's in the <body> or the <head> in all use cases, the whole function could be replaced with the following and avoid the whole issue:
function documentContains(elt) {
return elt.getRootNode({ composed: true }) === getDocument()
}
@kgscialdone : Thank you for the explanation. I agree with your solution. Its elegant. Let me know what's the final verdict on the approach.
Got Carson's approval and closing this in favor of #3034. Thanks for diving on this everyone.