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