vuejs/vue-web-component-wrapper

Add support for Vue 3 with web-component-wrapper

aranoe opened this issue ยท 48 comments

I wanted to migrate my Vue 2 project to Vue 3.
The problem is, that the wrap-function is still using and expecting the Vue-instance, which is why i get the following error:

vue-wc-wrapper.js?b803:172 Uncaught TypeError: Vue is not a constructor

The main.js:

import { createApp } from "vue";
import App from "./App";
import wrap from "@vue/web-component-wrapper";

const app = createApp(App);

const WrappedElement = wrap(app, app);
// Tried several other things
// const WrappedElement = wrap(app, App);
// const WrappedElement = wrap({}, app);
// const WrappedElement = wrap({}, App);
// const WrappedElement = wrap(app);

window.customElements.define("my-name", WrappedElement);

This makes total sense, since global Vue doesn't exist anymore.
Any work around on this?

I started working on the port of the wrapping library, but we also need to add back some capabilities to the Vue-3-compatible versions of vue-loader (& rollup-plugin-vue) in order to be able to inject styles into shadowRoot.

So this is being worked on, but it will still be some time before it's ready.

micru commented

Hi, any news on this? Would be really great to have this abillity

This will still take a while, mostly because we still need to add shaoowRoot CSS injection back into the build tooling. Without that, this lib is pretty useless.

when we can expect vue 3 support? do you have any specific idea? days? weeks? months?

weeks to months. The work I described will take some time and has a bit lower priority at this moment while we get core more stable etc.

I have published a Vue3 compatible version of this package @ https://www.npmjs.com/package/vue3-webcomponent-wrapper. It supports reactive attributes, events & slots. Feel free to use it if you are not keen on shadow-root.
Also please find a demo app that uses my wrapper @ https://github.com/sreenaths/vue3-webcomponent-wrapper-demo

If you have found an issue with the wrapper, please submit a PR on https://github.com/cloudera/hue/tree/master/desktop/core/src/desktop/js/vue/wrapper

How can I use router or store on a Vue web component if those should be declared on the main.js file?

I have published a Vue3 compatible version of this package @ https://www.npmjs.com/package/vue3-webcomponent-wrapper. It supports reactive attributes, events & slots. Feel free to use it if you are not keen on shadow-root.
Also please find a demo app that uses my wrapper @ https://github.com/sreenaths/vue3-webcomponent-wrapper-demo

If you have found an issue with the wrapper, please submit a PR on https://github.com/cloudera/hue/tree/master/desktop/core/src/desktop/js/vue/wrapper

i try the demo and it looks like you dont have a shadow root any plans on that ? @sreenaths

@DrMabuse23 Sorry that I don't have immediate plans for shadow root .

@nunofhfontes Was that question regarding vue3-webcomponent-wrapper?

How can I use router or store on a Vue web component if those should be declared on the main.js file?

You can use it like this:

const createAppWrapper: CreateAppFunction<Element> = (component) => {
  return createApp(component)
  .use(store)
  .use(router)
}

const webComponent = wrapper(RouterViewWrapper, createAppWrapper, h);

@sreenaths

how can i publish a component and use it in another project ? not whole app, just one component. thanks

@BlahaMarek
You could use webpack and build everything into a vanilla javascript file. Publish that file in an NPM package, and load that file in another project using a script tag or import "path/to/file".

For instance following is how we package a er-diagram component.

  1. Wrap the component - https://github.com/cloudera/hue/blob/master/desktop/core/src/desktop/js/components/er-diagram/webcomp.ts
  2. Build the file using webpack - https://github.com/cloudera/hue/blob/master/webpack.config.npm.js#L95
  3. Publish - https://github.com/cloudera/hue/blob/master/package.json#L162
  4. Load using a script tag - https://github.com/cloudera/hue/blob/master/tools/examples/components/er-diagram-demo/index.html#L4

Note:
If the other project uses webpack then you could skip the build part all together. Just publish the source as an NPM package and configure the bundler in the other project to bundle everything.

Guys, maybe move this discussion about vue3-webcomponent-wrapper package to its repository and keep this issue about official package. Are we have any news about official package? Any progress?

@LinusBorg: Do you have any update for us on the Vue 3 support?
Currently deciding whether to use Vue 2 or Vue 3 for a project where we we will create web-components.
Thanks the response and of course your work.

@TFohrer I would recommend you to start with Vue2 and @vue/composition-api and put Vue3 on hold for now. It's working, it's great and all that, but it's not ready for production use. Besides the web component wrapper, Test-utils and DevTools still have work to be done. There is a fork for the wrapper that works with Vue3, but @vue/composition-api is really good and would serve as an in between steps toward Vue3. You can even utilize @vue/component-api as an alternative to Vuex (IMHO you do not need vuex with vue3, as long as the Vuex DevTools are not critical to you).

When Vue3 is production ready, all you need to do is to replace "@vue/composition-api" with "vue" in your imports and you are good to go.

@LinusBorg
I'm sorry to mention you suddenly.
I opened a draft PR (#109) to provide Vue3 users the same function as the current wrap() for Vue2.

I want you to check it if you have time ๐Ÿ™

@TatsuyaYamamoto I tried your repository and got this for v3 (vue3-webcomponent-wrapper works for me but no shadowDom, no styles):

Failed to construct 'HTMLElement': Please use the 'new' operator, this DOM object constructor cannot be called as a function.

Vue 3 will have built-in support for web component wrappers in 3.2. you can already test it in the current beta.

Documentation will come with the stable release.

Guessing what you did wrong: defineCustomElement() expects a compiled component

So You don't replace defineComponent in an SFC. You import an SFC, then wrap it:

import MyComponent from './MyComponent.vue'

const myWebComponent = defineCustomElement(MyComponent)

customElements.define('my-web-component', myWebComponent)

Thanks a lot! I also found a tweet about it.
Tried it and looks working.
The only thing missing so far is that it doesn't add <style> tags to ShadowRoot for some reason.
Though $style variable is available and filled-in correctly.

In this func styles is undefined in component:

    _applyStyles() {
            if (this._def.styles) {

That needs some special treatment in the build tooling (vue-loader etc), which will come with the 3.2 release.

@LinusBorg I know the docs are still kind of "TBD"; however, is it possible to bundle a small Vue 3 app as its own custom component with defineCustomElement whereby you can still utilize Vuex and Vue Router within the "app" ? It doesn't look possible, but there's a real good chance I'm missing something.

@adamdehaven I don't think that's possible, and I would be interested in asking why you would want to wrap a whole SPA in a custom element. that's not really the normal use case for custom elements.

@LinusBorg And that's good approach. Hovewer, with vue-custom-element I do allow it. There are cases like e.g. chat app that you handle to client and he/she just inset HTML tag into page (e.g. <Chat>) and it will work. Regards.

@LinusBorg similar to the chat example mentioned above, I have a passwordless auth app that would be ideal to embed as a custom element (especially for use within other frameworks). The app has internal routing via Vue Router to handle the webauthn flow and utilizes Vuex.

@karol-f is vue-custom-element compatible with Vue 3?

@adamdehaven it's not. I had plans to do it but as Vue 3 support Custom Elements out of the box I don't think it's necessary.

I see how that's nice for the client, but at the same time, the difference between that and instead having to call one function and pass it a selector to mount a Vue app is rather small, don' you think?

<div id="chat-app"></div>
<script>
ChatApp.start('#chat-app')
</script>

And if you really need the approachability of the custom element, why not use a simple custom element to mount an actual Vue app inside of it?

const MyChatApp extends HTMLElement {
  connectedCallback() {
    const app = createApp(App)
    app.mount(this)
  }
}

customElements.register('my-chat-app', MyChatApp)

Does it really need shadow DOM? does it need to have props and events?

All that being said, You likely can use Vuex and the router (with memoryHistory) in a custom element (though I haven't tried out), but not through the usual app.use() plugin mechanism. you can still import the store and router where you need them, or provide them from the App.vue component downwards etc.

@LinusBorg I understand your point of view. However there are some things to consider:

  • we might not know when chat app html div will be present in DOM (e.g. if we want to use our app inside other framework) - so JS init won't solve all cases
  • we might want to pass customizations or reactively change props to our chat app - that's why simple Custom Element won't be enough

But anyway I agree with Your choice. Vue-custom-element had different assumptions so it works such way. This approach is not better or worse.

Good points. As ai said there are probably workarounds to make it work. The focus of defineCustomElement() is on smaller reusable components though.

Vue 3.2 is here but I still get [vue] ERROR Vue 3 support of the web component target is still under development. when trying to build for web components as target..

@leonheess there is built in solution already - https://blog.vuejs.org/posts/vue-3.2.html

@karol-f That doesn't enable me to build as a web component for others to use, does it?

@karol-f That doesn't enable me to build as a web component for others to use, does it?

You can https://v3.vuejs.org/guide/web-components.html#tips-for-a-vue-custom-elements-library

If this helps anyone, I managed to build a web component following documentation for Vue3 custom elements and SFC, using --target lib, see https://v3.vuejs.org/guide/web-components.html#sfc-as-custom-element
I only called generated script .umd.js for web component implementation on client page, no need to call .css file generated.

vue-cli-service build --target lib --inline-vue --name my-custom-element --dest dist/ce ./src/wc-main/MyCustomElement.ts

Vue-cli documentation is not clear enough about the current status of build with --target wc with Vue3.
Up to this date, doc is not accurate. Since build for web components won't not work with Vue 3 and latest vue-cli version (next version 5.0.0-rc.2), blocking condition in file /cli-service/lib/commands/build/resolveWcConfig.js

Is there a vue-cli update scheduled for compatibility with build --target wc and Vue3 ?

@karol-f

You can https://v3.vuejs.org/guide/web-components.html#tips-for-a-vue-custom-elements-library

Not sure how that link helps. In Vue 2 I can do vue-cli-service build --target wc --name x --mode production how do I replicate that with Vue 3?

Not sure how that link helps. In Vue 2 I can do vue-cli-service build --target wc --name x --mode production how do I replicate that with Vue 3?

#93 (comment)

@karol-f That's not the same. It drops all scoped styles outside of the main/App file for example.

@leonheess I made a PR to use custom elements without shadow dom (which skip global styles).

Here's how to use it in your project until it's merged : vuejs/core#4314 (comment)

A little late to the discussion, but we have currently the problem with vue3, that we want to move to a microfrontend structure and want to wrap our microfrontend-vue3-apps as web components.
This is working well as long as we do not need anything that relies on the vue app instance (like vue-router or pina, e.g.).

A real world example: A customer microfrontend displaying a list of customers and a details page for a single customer where we need a subrouting ("/customers/", "/customer/:id") and maybe want to store the current customer in a pinia store.

The workaround above is probably a way to solve this, but I would recommend that there is an official way to solve this issue (or at least a section in the documentation about it). Especially regarding competitiveness with other reactive frameworks where this is not a problem at all and officially supported.

Hi Ho,
I created a plugin that attempts to solve this long waiting problem
https://www.npmjs.com/package/vue-web-component-wrapper
With this Vue Web Component Wrapper, it's now possible to create microfrontend Vue applications that can be used anywhere on any page

@EranGrin Thank you very much for this plugin ! We are in the middle of a Vue2 -> Vue3 migration and need to embed some of our vue3 packages as web components into our vue2 client. So far so good, CSS and deep css is well injected, and we were able to inject i18n + pinia as well as our own components as plugins !

I'm trying to use slots in the components but it doesn't seem to be working:

Slot code inside the component:

<slot name="custom">No content passed!!</slot>

How it is called:

<my-component> <div slot="custom"> Lorem ipsum dolor sit amet consectetur adipisicing elit. Maxime magnam architecto nihil modi voluptatem vero nam, eligendi itaque harum, sapiente, ipsam voluptates perferendis? Quos fugiat, molestias asperiores pariatur explicabo vel! </div> </my-component>

When I open the page where the custom element is called, only the default text 'No content passed!!' appears

xewl commented

@busella73 Please refer to the docs. This ticket ain't the place to ask this. Use: <template v-slot:custom></template>.

Maybe i don't explain myself correctly. I use:

"vue": "^3.3.4",
"vue3-webcomponent-wrapper": "^0.2.0"

This is the component that i build as web component:

<script setup>

</script>

<template>
    <div class="kb-flex kb-justify-center kb-font-sans kb-bg-lime-200">
        <div class="kb-container">
            <div class="kb-text-2xl kb-font-bold">Named Slot </div>
            <slot name="custom">No content passed!!</slot>
            <div>Lorem ipsum dolor sit amet consectetur adipisicing elit. Quasi maiores aperiam ratione. Debitis eius non
                perspiciatis sapiente consectetur natus corrupti dolores ratione praesentium illo sed, atque, nobis
                accusamus velit maiores?</div>
        </div>
    </div>
</template>

this is the script i use to build it:

import { createApp, h } from "vue";
import wrapper from "vue3-webcomponent-wrapper";
import My-component from "./components/My-component.vue";

const CustomElement = wrapper(My-component, createApp, h);
window.customElements.define("my-component", CustomElement);

and this is the command i use to run the script:

vue-cli-service build --mode production --inline-vue --target lib --name my-component --dest wc/my-component ./src/my-component.js
this is the html file where i use the web component:

<!DOCTYPE html>
<html lang="en">

  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>detail demo</title>
    <script src="../wc/my-component/my-component.umd.js"></script>
  </head>

  <body>
    <my-component>
      <template v-slot:custom>
        <div>
          Lorem ipsum dolor sit amet consectetur adipisicing elit. Maxime magnam architecto nihil modi voluptatem 		vero nam,
          eligendi itaque harum, sapiente, ipsam voluptates perferendis? Quos fugiat, molestias asperiores pariatur
          explicabo
          vel!a
        </div>
      </template>
    </my-component>
  </body>

</html>

If i use the element is rendered as a document fragment and is not visible on the page, if i use

the element is rendered at the end of the page and not used as slot. I think this is an issue.

Any update on this? I want the user to be able to change the styling of my custom element with their CSS, but with shadow DOM it doesn't seem to be possible?

@onurusluca you can use CSS var on the :root and then use them inside the shadow DOM

:root {  
  --clr-primary: #ee6352;
  --clr-secondary: #d16e8d;
  }

Inside the web component

.my-class {
 background-color: var(--clr-primary)
}