Subscribe on changes!

devtools support for defineCustomElement

avatar
Aug 16th 2021

What problem does this feature solve?

Thank you for the great work on the defineCustomElements. I'm really enjoying this feature, and I'm sure many others so so as well.
Unfortunately the devtools are currently unable to find any components created with defineCustomElements.
From a quick glance it seems they are initialized with the .mount() method of an app, which defineCustomElements does not make use of.
It would be a really nice addition if we were able to debug our components with the vuejs devtools.

What does the proposed API look like?

I'm not that in-depth with vue behind the scenes. Maybe it would be possible to hook into connectedCallback and disconnectedCallback to initialize devtools?

avatar
Aug 16th 2021

cc @Akryum

avatar
Jun 24th 2022

I'm far from in-depth with the core functionality of vue. But I did some digging and posting it here in case it is helpful. 🙈

The dev tools isn't showing since no apps are registered with the dev tools. If you mount a regular vue app on the same page it will show normally, but only have the regular app registered. Can see the apps registered on window.__VUE_DEVTOOLS_GLOBAL_HOOK__.apps

Regular vue registers the apps on the mount() function on createApp in runtime-core project apiCreateApp.ts

if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
    app._instance = vnode.component
    devtoolsInitApp(app, version)
}

It is then removed from devtools on unmount

 if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
    app._instance = null
    devtoolsUnmountApp(app)
  }

As @soultice mentions it looks natural to put it as part of connectedCallback() and disconnectedCallback() of the runtime-dom apiCustomElement.

The devtools methods aren't exported from runtime-core, so can't call them unless changed.

The app param is of type App, which is the return of createApp. Which is as I see it rather far from the custom element. The version param is as far as I see, used to determine which backend to use in dev tools. (Vue 1-3).

As I see it either the customElement needs to be wraped or transformed to an app type to be registered as a regular vue app is. Or a different backend is required for dev tools to handle customElements. (Registering the app will still require some work)

avatar
Feb 7th 2023

my solution to whom ever who will need this @NielsJorck your comment was a big help, thank you

import { VueService } from "../../registries/services/vue.service";
import { ComponentInternalInstance, defineCustomElement as vueDefineCustomElement, render, version, VNode, devtools } from "vue";
const Fragment = Symbol('Fragment');
const Text = Symbol('Text');
const Comment = Symbol('Comment');
const Static = Symbol('Static');

export function wrapperVueCustomElement(customElement: ReturnType<typeof vueDefineCustomElement>, vueService: VueService) {
  class VueCustomElement2 extends customElement {
    private rootVNode: VNode | null = null;

    constructor(initialProps: any) {
      super(initialProps);
      console.debug("constructed VueCustomElement2:tagName:", this.tagName);
      //create div inside shadow root append id root to it
      const div = document.createElement('div');
      this.shadowRoot!.appendChild(div);
      div.id = "root";

      //@ts-ignore
      this._update = function () {
        //@ts-ignore
        this.rootVNode = this._createVNode();
        render(this.rootVNode, div);
      }
    }

    connectedCallback() {
      console.debug("connectedCallback", this);
      vueService.CurrentRoot = this.shadowRoot;
      // vueService.CurrentApp._container = this.shadowRoot;
      super.connectedCallback();
      const rootContainer = this.shadowRoot?.firstElementChild as HTMLElement;
      vueService.CurrentApp._container = rootContainer;
      //@ts-ignore
      rootContainer.__vue_app__ = vueService.CurrentApp;
      {
        vueService.CurrentApp._instance = this.rootVNode?.component as ComponentInternalInstance;
        devtools.emit('app:init', vueService.CurrentApp, version, {
          Fragment,
          Text,
          Comment,
          Static
        });
      }
      vueService.StyleMountService.mountStyles(vueService.CurrentWebComponent.host, vueService.TeleportRef);
    }

    disconnectedCallback() {
      console.debug("disconnectedCallback");
      vueService.CurrentRoot = null;
      vueService.destroy();
      // @ts-ignore
      render(null, this.CurrentApp._container);
      {
        vueService.CurrentApp._instance = null;
        devtools.emit('app:unmount', vueService.CurrentApp);
      }
      // @ts-ignore
      delete vueService.CurrentApp?._container?.__vue_app__;
      vueService.CurrentApp._container = null;
      super.disconnectedCallback();
    }
  }

  return VueCustomElement2;
}

The line devtools.emit('app:init', vueService.CurrentApp, version, {Fragment, Text, Comment, Static}); is emitting an event to mount the Vue devtools to the browser. The event is named "app:init" and it takes in arguments the current app instance (vueService.CurrentApp), the version of Vue being used, and the symbols for Fragment, Text, Comment, and Static.

The line devtools.emit('app:unmount', vueService.CurrentApp); is emitting an event to unmount the Vue devtools from the browser. The event is named "app:unmount" and it takes in argument the current app instance (vueService.CurrentApp).

you can see the results in the images image

image

avatar
Mar 1st 2023

hi DBTK1990, Could you explain just a bit how to use your script ! my entire app is mounted in an shadowdom target and my expectation is that I can use the VueDevTool to inpect Vue component, is your script aim for that ?

avatar
Mar 6th 2023

yes it is but you dont need all of it to do it, you can just emit the event with the current app do you have discord?

you can use this event with the vue app

devtools.emit('app:init', vueService.CurrentApp, version, { Fragment, Text, Comment, Static });

On Wed, 1 Mar 2023 at 12:37, larry @.***> wrote:

hi DBTK1990, Could you explain just a bit how to use your script ! my entire app is mounted in an shadowdom target and my expectation is that I can use the VueDevTool to inpect Vue component, is your script aim for that ?

— Reply to this email directly, view it on GitHub https://github.com/vuejs/core/issues/4356#issuecomment-1449835833, or unsubscribe https://github.com/notifications/unsubscribe-auth/AEXKOFCPHCSKQTD6TLYX5V3WZ4RH7ANCNFSM5CIBEKLQ . You are receiving this because you commented.Message ID: @.***>

avatar
Mar 7th 2023

Hi DBTK1990, my Discord userName : _larry_

As I said, my entire app is attached to a shadowRoot tag, as shown in the simplified code below, I just added your code by replacing vueService.CurrentApp by the instance of my application app.

VueDevtools works in this config (Pinia ok, cool!), but there is something that doesn't work which is very practical in a teamwork, it's to be able to inspect the tree of Vue components (mouse hover) and highlight them in the browser. This makes it possible to understand the nesting of the components. Unfortunately, it still does not work ;(

//--- all the bootstrap code here ....
const app = createApp(App);
//---- Shadow
let targetShadow = document.getElementById("app");
const shadowRoot = targetShadow!.attachShadow({ mode: 'open' });

//--- Append Vue app
const appNode = document.createElement("div");
shadowRoot.append(appNode);

//--- Mount Vue.js App
app.mount(appNode);

//--- Your code
devtools.emit('app:init', app, version, {
          Fragment,
          Text,
          Comment,
          Static
    });
...
avatar
Oct 22nd 2023

Hello, I created a plugin to support web-component for vue3 https://www.npmjs.com/package/vue-web-component-wrapper in the latest version, I had to solve the same issue.

I solved it like this

 // Add support for Vue Devtools
      if (process.env.NODE_ENV === 'development' && window.__VUE_DEVTOOLS_GLOBAL_HOOK__) {
        const root = document.querySelector(elementName);
        app._container = root;
        app._instance = inst;
        
        const types = {
          Comment: Symbol('v-cmt'),
          Fragment: Symbol('v-fgt'),
          Static: Symbol('v-stc'),
          Text: Symbol('v-txt'),
        };
        
        window.__VUE_DEVTOOLS_GLOBAL_HOOK__.emit('app:init', app, app.version, types);
        window.__VUE_DEVTOOLS_GLOBAL_HOOK__.Vue = app;
      }