UNPKG

vue-web-component-wrapper

Version:

A Vue 3 plugin that provides a web component wrapper with styles, seamlessly integrating with Vuex, Vue Router, Vue I18n, and supporting Tailwind CSS and Sass styles.

561 lines (445 loc) 18 kB
<p align="center"> <h1 align="center">vue-web-component-wrapper</h1> </p> <p align="center"> <em>Transforming full-fledged Vue3 applications into reusable web components</em> </p> <p align="center"> <img src="https://img.shields.io/badge/license-MIT-green" alt="License MIT"> <img src="https://img.shields.io/badge/version-1.7.7-blue" alt="version 1.7.7"> <img src="https://img.shields.io/badge/maintained-yes-brightgreen" alt="maintained yes"> </p> <hr> ## Introduction **vue-web-component-wrapper** is a powerful Vue 3 plugin designed to transform full-fledged Vue applications into reusable web components (custom elements). These web components can be integrated into any website, enhancing flexibility and reusability. ## Why Use vue-web-component-wrapper? As of now, Vue 3 does not support the creation of full applications as web components out of the box. This plugin aims to solve this problem by providing a simple and easy-to-use solution for creating web components from Vue applications. It also provides support for Vue ecosystem plugins such as [Vuex](https://vuex.vuejs.org/), [Pinia](https://pinia.vuejs.org/), [Vue Router](https://router.vuejs.org/), [Vue I18n](https://vue-i18n.intlify.dev/), and [VeeValidate](https://vee-validate.logaretm.com/v4/). ## Demo Check out these demo projects to see **vue-web-component-wrapper** in action: - **Webpack Implementation**: [Webpack Demo Project](https://stackblitz.com/edit/vue-web-component-wrapper?file=README.md&startScript=webpack-demo) - **Vite.js Implementation**: [Vite Demo Project](https://stackblitz.com/edit/vue-web-component-wrapper?file=README.md&startScript=vite-demo) ## Documentation See the [Documentation](https://erangrin.github.io/vue-web-component-wrapper) for more details. ## Key Features - **Vue Plugins Compatibility**: Seamlessly integrates with Vue ecosystem plugins like Vuex, Vue Router, and Vue I18n. - **CSS Framework Support**: Works with popular CSS frameworks such as Tailwind CSS, Bootstrap, Vuetify, Element Plus, and more. - **CSS Preprocessor Support**: Allows the use of CSS preprocessors like SCSS and LESS. - **Scoped CSS**: Supports scoped CSS in your components. - **Shadow DOM Support**: Encapsulates styles and scripts to prevent clashes with the rest of your application. - **Vue DevTools Support**: Compatible with the Vue DevTools browser extension. - **Slot and Named Slot Support**: Define and use slots and named slots within web components. - **v-model Support**: Improved support for two-way data binding using the `v-model` architecture. - **Event Emitting Support**: Emit and handle custom events from web components. - **Provide/Inject Support**: Pass data from parent to child components using `provide` and `inject`. - **Disable Removal of Styles on Unmount**: Control the removal of styles upon component unmount to solve issues with CSS transitions. - **Disable Shadow DOM**: Option to disable Shadow DOM for web components. - **Replace `:root` with `:host`**: Optionally replace `:root` selectors with `:host` in your CSS to ensure styles are correctly scoped within the Shadow DOM. - **Async Initialization**: Option to delay the initialization until its Promise resolves. - **Loader Support**: Support for loader spinner elements until the application is fully initialized. - **Hide slot content until the component is fully mounted**: Option to hide the content of named slots until the web-component is fully mounted. ## CSS Frameworks Examples - **Tailwind CSS**: [Demo](https://stackblitz.com/edit/vue-web-component-wrapper?file=README.md&startScript=tailwind-demo) - **UnoCSS**: [Demo](https://stackblitz.com/~/github.com/EranGrin/element-plus-unocss-web-component?file=src/style.css:L1-L2) - **Vuetify**: [Demo](https://stackblitz.com/~/github.com/EranGrin/vuetify-web-component-wrapper) - **Element Plus**: [Demo](https://stackblitz.com/~/github.com/EranGrin/element-plus-unocss-web-component?file=src/style.css:L1-L2) - **Bootstrap**: [Demo](https://stackblitz.com/~/github.com/EranGrin/bootstrap-demo-webcomponent) For more details, see the [Documentation](https://erangrin.github.io/vue-web-component-wrapper). ## Installation ```bash npm install vue-web-component-wrapper # or yarn add vue-web-component-wrapper # or pnpm add vue-web-component-wrapper ``` ## Usage To create a web component using **vue-web-component-wrapper**, follow the steps below: ### 1. Import the Necessary Modules In your entry file, import the required modules: ```javascript import App from './App.vue'; import tailwindStyles from './assets/tailwind.css?raw'; import { createWebHashHistory, createRouter } from 'vue-router'; import { createI18n } from 'vue-i18n'; import { createStore } from 'vuex'; import { createPinia } from 'pinia'; import { defaultRoutes } from './main.routes.js'; import { store } from './store/index.js'; import { defineCustomElement as VueDefineCustomElement, h, createApp, getCurrentInstance, } from 'vue'; import { createWebComponent } from 'vue-web-component-wrapper'; ``` ### 2. Set Up the Instances and Plugins Configure your Vuex/Pinia store, Vue Router, and other Vue plugins: ```javascript export const pluginsWrapper = { install(GivenVue) { const Vue = GivenVue; // Vuex const createdStore = createStore(store); Vue.use(createdStore); // Or Pinia const pinia = createPinia(); Vue.use(pinia); // Vue Router const router = createRouter({ history: createWebHashHistory(), routes: defaultRoutes, }); Vue.use(router); // Vue I18n const i18n = createI18n({ locale: 'en', fallbackLocale: 'en', }); Vue.use(i18n); }, }; ``` ### 3. Create Your Web Component Use `createWebComponent` to create your web component. Specify your root Vue component, the element name, any plugins, and CSS framework styles: ```javascript createWebComponent({ rootComponent: App, elementName: 'my-web-component', plugins: pluginsWrapper, cssFrameworkStyles: tailwindStyles, VueDefineCustomElement, h, createApp, getCurrentInstance, disableStyleRemoval: false, // default is false disableShadowDOM: false, // default is false replaceRootWithHostInCssFramework: false, // default is false loaderAttribute: 'data-web-component-loader', // default is 'data-web-component-loader' hideSlotContentUntilMounted: true, // default is false }); ``` #### Options Explained - **rootComponent**: The root component of your Vue application. - **elementName**: The tag name for your custom web component (must contain a hyphen and be lowercase). - **plugins**: Vue plugins to use in your application. - **cssFrameworkStyles**: Global CSS or SCSS styles your application needs. - **VueDefineCustomElement**: The `defineCustomElement` function from Vue. - **h**: The `h` function from Vue. - **createApp**: The `createApp` function from Vue. - **getCurrentInstance**: The `getCurrentInstance` function from Vue. - **disableStyleRemoval**: Disable removal of styles on unmount (useful for CSS transitions). - **disableShadowDOM**: Disable Shadow DOM for web components. - **replaceRootWithHostInCssFramework**: Replace `:root` selectors with `:host` in your CSS styles. - **asyncInitialization**: Accepts a function that returns a Promise. - **loaderAttribute**: Defines the attribute used to mark loader spinner (default is `data-web-component-loader`). - **hideSlotContentUntilMounted**: Hide the content of named slots until the component is fully mounted. - **nonce**: Content Security Policy (CSP) nonce for your web component. ### asyncInitialization The `asyncInitialization` option accepts a function that returns a Promise. The custom element waits for this Promise to resolve before completing its initialization. This is useful for performing asynchronous tasks (e.g., API calls, dynamic imports) before the app mounts. #### Example Usage ```javascript const asyncPromise = () => { return new Promise((resolve) => { setTimeout(() => { resolve() }, 1000) }) } createWebComponent({ rootComponent: App, elementName: 'my-web-component', plugins: pluginsWrapper, cssFrameworkStyles: tailwindStyles, VueDefineCustomElement, h, createApp, getCurrentInstance, asyncInitialization: asyncPromise, // default is Promise.resolve() loaderAttribute: 'data-web-component-loader', hideSlotContentUntilMounted: true, // default is false }); ``` ### loaderAttribute The `loaderAttribute` option defines the attribute used to mark loader spinner elements in your custom element's DOM. Elements with this attribute will be removed automatically once the component is fully mounted. ```html <my-web-component class="my-web-component" > <!-- named slot --> <div class="customName" data-web-component-loader slot="customName"> <div class="spinner"></div> </div> </my-web-component> <style> .spinner { border: 4px solid rgba(0, 0, 0, 0.1); border-left-color: #4a90e2; /* Customize spinner color if needed */ border-radius: 50%; width: 30px; height: 30px; animation: spin 1s linear infinite; margin: auto; } @keyframes spin { to { transform: rotate(360deg); } } </style> ``` ### hideSlotContentUntilMounted The `hideSlotContentUntilMounted` option hides the content of named slots until the component is fully mounted. - By using the `hidden` attribute on the slot element, the content will be hidden until the component is fully mounted, and the web component wrapper will remove the `hidden` attribute once the component is fully mounted. - This could be break the layout of your application, if you use the `hidden` attribute internally in your application. - If you want to use the `hidden` attribute internally in your application, you can set the `hideSlotContentUntilMounted` option to `false`. ```html <my-web-component> <!-- named slot --> <div class="customName" hidden slot="customName">I am a custom named slot </div> </my-web-component> ``` ### replaceRootWithHostInCssFramework The `replaceRootWithHostInCssFramework` option replaces all occurrences of `:root` with `:host` in your `cssFrameworkStyles`. This is useful when working with CSS variables defined on `:root`, ensuring they are properly scoped within the Shadow DOM. #### Example Usage ```javascript createWebComponent({ rootComponent: App, elementName: 'my-web-component', plugins: pluginsWrapper, cssFrameworkStyles: tailwindStyles, VueDefineCustomElement, h, createApp, getCurrentInstance, replaceRootWithHost: true, }); ``` ### cssFrameworkStyles The `cssFrameworkStyles` option imports the CSS of your CSS framework or any other global CSS styles your application needs. By setting `replaceRootWithHostInCssFramework` to `true`, any `:root` selectors in your styles will be replaced with `:host`, ensuring correct scoping within the web component. ### nonce The `nonce` option is used to set a Content Security Policy (CSP) nonce for your web component. This is useful when your application uses inline scripts or styles, as it allows you to specify a unique nonce value that can be used to whitelist the inline content. ### 4. Build Your Application Tested bundlers to build the web-component application. ## Bundler Configurations <details> <summary>Vite Configuration</summary> ### Vite.js Configuration Here's a sample Vite configuration. Vite.js handles asset files like `.css` and `.scss`, and media files, importing them as usual. Vue files are parsed using the official [@vitejs/plugin-vue](https://github.com/vitejs/vite-plugin-vue/tree/main/packages/plugin-vue). ```javascript import { defineConfig } from 'vite'; import vue from '@vitejs/plugin-vue'; export default defineConfig({ build: { sourcemap: 'inline', }, plugins: [ vue({ customElement: true, }), ], }); ``` #### `main.js/ts` In your main file, import the CSS framework with `?inline`: ```javascript // Fonts are not loaded with ?inline; import font CSS in App.vue import style from './style.css?inline'; ``` #### `App.vue` Workaround for fonts: ```html <style> @import url('https://fonts.googleapis.com/css2?family=YourFont'); header { @apply font-sans; } main { @apply font-sans; } </style> ``` </details> <details> <summary>Webpack Configuration</summary> ### Webpack Configuration Here's a sample webpack configuration to handle `.vue`, `.css`, and `.scss` files: ```javascript const path = require('path'); const { VueLoaderPlugin } = require('vue-loader'); const HtmlWebpackPlugin = require('html-webpack-plugin'); module.exports = { mode: 'production', entry: './src/main.js', output: { path: path.resolve(__dirname, 'dist'), filename: 'my-web-component.js', }, module: { rules: [ { test: /\.(vue|ce\.vue)$/, loader: 'vue-loader', options: { customElement: true, }, }, { test: /\.(css|scss)$/, oneOf: [ { resourceQuery: /raw/, use: [ 'to-string-loader', 'css-loader', 'postcss-loader', { loader: 'sass-loader', options: { sassOptions: { indentedSyntax: false, }, }, }, ], }, { use: [ 'style-loader', 'css-loader', 'postcss-loader', { loader: 'sass-loader', options: { sassOptions: { indentedSyntax: false, }, }, }, ], }, ], }, { test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, loader: 'file-loader', options: { name: 'assets/[name].[hash:7].[ext]', }, }, ], }, plugins: [ new VueLoaderPlugin(), new HtmlWebpackPlugin({ template: './public/index.html', }), ], resolve: { alias: { vue$: 'vue/dist/vue.esm-bundler.js', }, extensions: ['.js', '.vue', '.json'], }, }; ``` #### `main.js/ts` Import the CSS framework with `?raw`: ```javascript import style from './style.css?raw'; ``` </details> <details> <summary>Vite + Rollup Configuration</summary> ### Vite + Rollup Configuration This configuration provides enhanced build options using Vite with Rollup: ```typescript import { defineConfig, UserConfig } from 'vite'; import vue from '@vitejs/plugin-vue'; export default defineConfig(({ mode }): UserConfig => { return { esbuild: { // Remove debugger statements in production drop: mode === 'production' ? ['debugger'] : [], }, build: { emptyOutDir: true, target: 'ES2020', rollupOptions: { output: { // Maintain original file names entryFileNames: '[name].js', }, }, // Disable CSS code splitting cssCodeSplit: false, }, plugins: [ vue({ template: { compilerOptions: { // Define custom elements starting with 'app-element' isCustomElement: (tag) => tag.startsWith('app-element'), }, }, customElement: true, }), { // Hot reload fix for Vue components name: 'force-reload', handleHotUpdate({ file, server }) { if (file.endsWith('.vue')) { server.ws.send({ type: 'full-reload' }); return []; } }, }, ], }; }); ``` **Features:** - Custom element support for tags starting with 'app-element'. - Disabled CSS code splitting for better web component compatibility. - Hot reload improvements for Vue components. - Rollup output configuration to maintain file names. </details> ## Web Component Without Shadow DOM To create a web component without Shadow DOM, set the `disableShadowDOM` option to `true` in the `createWebComponent` function: ```javascript createWebComponent({ // ...other options disableShadowDOM: true, }); ``` This feature uses a patch to the Vue source code, which may lead to issues with future versions of Vue. Please report any issues in the repository. ### Demo Without Shadow DOM [Demo Link](https://stackblitz.com/~/github.com/EranGrin/web-component-no-shadow-dom-demo) ## SFC as Custom Element Enhance the functionality of Single File Components (SFC) as Custom Elements using `defineCustomElement` with two new features: 1. **Nested Components**: Use nested components with styles, sharing base components between multiple custom elements. 2. **Shadow DOM Option**: Disable Shadow DOM for the SFC custom element. ### Usage ```javascript // main.js import { defineCustomElementSFC } from 'vue-web-component-wrapper'; const MyComponentElement = defineCustomElementSFC(MyComponent, { shadowRoot: false }); customElements.define('my-component', MyComponentElement); ``` ### Demo SFC Custom Element [Demo Link](https://stackblitz.com/edit/vue-web-component-wrapper?file=README.md&startScript=SFC-demo) ## Tips - **Testing Production Build**: To test your production build, run a local server in the `dist` folder. You can use [Valet](https://laravel.com/docs/10.x/valet) or any local server. ## Future Plans 1. **TypeScript Support**: Adding proper strict types. ## Contributing Contributions are welcome! To contribute: - **Fork** the repository. - **Create a new branch** for your feature or bug fix. - **Make your changes** and commit them with a clear message. - **Push your changes** to your fork. - **Submit a pull request** to the main repository. Please follow the code style and conventions used in the project. If you find a bug or have a feature request, please [open an issue](https://github.com/EranGrin/vue-web-component-wrapper/issues). ## License This project is licensed under the MIT License.