1

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:

  1. Are internally complex enough that building with Vue rather than just native web components is helpful, but...
  2. Will run outside Vue context on the page (not in a Vue app), so are packaged as Web Components / Custom Elements from Vue, and...
  3. Implement data inputs that will be used inside <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:

  • The mounted DOM element (the one above the shadow root, i.e. <my-custom-element>, not just some ref() in the template), and
  • Reactive state of the component to get value

...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?

dingus
  • 655
  • 1
  • 7
  • 18
  • Sounds like you want to stuff the Home Depot (native) into an IKEA (Vue) factory, and sell it as a Home Depot Component. Have you calculated how much time you will have to spend when Vue hits versions 4,5,6, etc? and you **must** refactor your code. Versus how much time you now have to spend on 100% Home Depot, which will run without any _required_ changes for another 27 JS years. – Danny '365CSI' Engelman Feb 28 '22 at 20:30

1 Answers1

1

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;
});
Thomas Urban
  • 4,649
  • 26
  • 32
dingus
  • 655
  • 1
  • 7
  • 18
  • For some reason, that doesn't seem to work in this [demo](https://stackblitz.com/edit/vue3-vite-typescript-starter-lqpqck?file=src%2FApp.vue). Can you fork that StackBlitz to demonstrate the solution? – tony19 Mar 06 '22 at 05:33
  • 2
    Hey yes sure - the difference is I was accessing the host node from *within* the component whereas you're looking from outside. Here's [a fork](https://stackblitz.com/edit/vue3-vite-typescript-starter-zmf4fb?file=src/components/HelloWorld.ce.vue) that I think should show both – dingus Mar 29 '22 at 09:31
  • Aren't you getting the `The target element is not a form-associated custom element` error? Here's [a fork](https://stackblitz.com/edit/vue3-vite-typescript-starter-j9ubx4?file=src%2Fcomponents%2FHelloWorld.ce.vue) of your fork. – t0byman Mar 07 '23 at 15:47