OK so I know a few variations on this question have been asked already, across the various versions and APIs of Vue... But I haven't been able to figure it out so here's the context as to why I think mine is different:
I'm trying to build some components which:
<form>
s (again, not in Vue apps).One challenge with this is that Vue Web Components use shadow DOM, and forms don't automatically traverse shadow roots to look for inputs: So making the form actually see and submit the components' inner data is not automatic.
It seems like there's some hope as detailed in this helpful blog post: A new ElementInternals API and element-internals-polyfill NPM package by which components can indicate data up to forms. Implementing a "form-associated custom element" requires setting a static readonly boolean property (easy enough) but also linking something like:
// (`this` == the custom HTMLElement itself)
const _internals = this.attachInternals();
_internals.setFormValue(value);
Problem is, I'm really struggling to figure out where I can hook in to have access to both:
<my-custom-element>
, not just some ref()
in the template), andvalue
...So far I'm mostly using Vue's composition and script setup
APIs which admittedly feel like they make this even harder: For example onMounted
doesn't define this
at all. But even using the equivalent options API mounted: () => {}
I see this.$el
seems to be the first element in the template/shadow root, not the parent custom element that owns the shadow root.
I also looked at going the other way - starting from the created CustomElement class and trying to work back through to useful Vue data & hooks... But couldn't find a way here either:
import { defineCustomElement } from "vue";
import MyCustomComponent from "./components/MyCustomComponent.ce.vue"
const MyCustomElement = defineCustomElement(MyCustomComponent);
class MyCustomElementFormAssoc extends MyCustomElement {
static get formAssociated() {
return true;
}
constructor(initialProps?: Record<string, any> | undefined) {
super(initialProps);
const _internals = this.attachInternals();
// But here the component isn't even mounted yet - this._instance doesn't
// exist and presumably reactive state doesn't either, so can't do:
// _internals.setFormValue(someValueState);
}
}
customElements.define("my-custom-element", MyCustomElementFormAssoc);
So while in general, in line with other Vue 3 answers "there is no single root element and we should use refs instead", in my case I'm specifically trying to access the Custom Element defining the component - not the element(s) inside the template. The rendered DOM looks something like:
<my-custom-element class="this-one-is">
#shadow-root (open)
<div class="custom-element-template-can-have-multiple-roots"></div>
<div class="but-these-are-not-the-elements-im-looking-for"></div>
</my-custom-element>
Does anybody know how it can be done?
Agree this is a bad code smell and a signal to evaluate whether Vue is really a good fit for the use case in general: Hacking around with hybrid Web Components that aren't quite native but aren't quite Vue either is likely to be a maintenance burden even if it works today.
But needs must - and my current workaround for this is to track back from some reference element in the template (doesn't really matter what) via DOM, like this:
// (MyCustomComponent.ce.vue script setup)
import { ref } from "vue";
const someTemplateRef = ref();
onMounted(() => {
const hostNode = someTemplateRef.value.getRootNode()?.host;
});