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?

1
  • 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. Commented Feb 28, 2022 at 20:30

1 Answer 1

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;
});
Sign up to request clarification or add additional context in comments.

3 Comments

For some reason, that doesn't seem to work in this demo. Can you fork that StackBlitz to demonstrate the solution?
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 that I think should show both
Aren't you getting the The target element is not a form-associated custom element error? Here's a fork of your fork.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.