Vue Widgets: From a Charting Widget to Embedded Widgets with Shadow DOM
Introduction
At work, I encountered a Vue-based widget seamlessly integrated into a web page without affecting the surrounding site's styles. This piqued my interest in how such a widget could be isolated, yet functional, when embedded into any web page. The following is a guide on how to build a reusable charting widget using Vue and transform it into an embedded widget with Shadow DOM for complete style encapsulation.
What is a Vue Widget?
- Self-contained: A modular component that doesn't interfere with the host page.
- Built with Vue.js: Takes advantage of Vue's reactive system.
- Encapsulated styles: Uses Shadow DOM to avoid style clashes with the host page.
- Reusable: Can be embedded into any website as a custom element.
Setting Up the Development Environment
Start by setting up a Vue environment using Vite for fast builds:
npm install vite -g npm create vite@latest vue-chart-widget -- --template vue cd vue-chart-widget npm install
Test the setup:
npm run dev
Check the widget at http://localhost:5173
.
Step 1: Building a Simple Charting Widget
Here, we'll create a Vue component that displays a chart using Chart.js.
<!-- ChartWidget.vue --> <template> <div class="chart-widget"> <h2>{{ title }}</h2> <canvas ref="chart"></canvas> </div> </template> <script setup> import { ref, onMounted } from 'vue'; import Chart from 'chart.js/auto'; const props = defineProps({ title: String, chartData: Array, }); const chartRef = ref(null); onMounted(() => { const ctx = chartRef.value.getContext('2d'); new Chart(ctx, { type: 'line', data: { labels: chartData.map(d => d.label), datasets: [{ label: 'Data Values', data: chartData.map(d => d.value), borderColor: 'rgb(75, 192, 192)', tension: 0.1, }] } }); }); </script>
This widget renders a chart using Chart.js and can be customized by passing data through the chartData
prop.
Step 2: Transforming the Widget into an Embedded Widget with Shadow DOM
Next, let's transform this Vue component into a custom web element that can be embedded into any webpage. The key here is using the Custom Elements API and Shadow DOM for style encapsulation.
Create a Bootstrap File
We'll create a bootstrap.js
file to mount our Vue app within the custom element's shadow root.
// src/bootstrap.js import { createApp } from 'vue'; import ChartWidget from './ChartWidget.vue'; export function bootstrap(target, attributes) { const app = createApp(ChartWidget, attributes); app.mount(target); }
Define the Custom Element
Now, modify main.js
to define a custom element that attaches a shadow DOM and renders the widget.
// src/main.js import { bootstrap } from './bootstrap.js'; customElements.define('chart-widget', class extends HTMLElement { connectedCallback() { const shadowRoot = this.attachShadow({ mode: 'open' }); const attributes = { title: this.getAttribute('title'), chartData: JSON.parse(this.getAttribute('chartData')) }; bootstrap(shadowRoot, attributes); } });
Configure Vite for Build
To compile your project into distributable JavaScript and CSS files, modify vite.config.js
to include the necessary options for building your widget as a library.
// vite.config.js import { defineConfig } from 'vite'; import vue from '@vitejs/plugin-vue'; export default defineConfig({ plugins: [vue()], build: { lib: { entry: 'src/main.js', name: 'ChartWidget', fileName: 'chart-widget', }, rollupOptions: { output: { inlineDynamicImports: false, entryFileNames: '[name].js', chunkFileNames: '[name].js', assetFileNames: '[name].[ext]' } } } });
Run the build:
npm run build
Step 3: Embedding the Widget into External Sites
Once built, the widget can be embedded in any website by including the compiled JavaScript file and using the custom element tag.
Here’s an example of embedding the widget in a simple HTML page:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Embedded Chart Widget</title> <script src="https://example.com/chart-widget.js" async></script> </head> <body> <chart-widget title="Data Over Time" chartData='[{"label":"Jan","value":30},{"label":"Feb","value":25}]'></chart-widget> </body> </html>
Shadow DOM for Encapsulation
Using Shadow DOM ensures that the widget's styles are encapsulated, preventing interference with the host site. In main.js
, when we call attachShadow({ mode: 'open' })
, it creates an isolated environment for the widget’s content, which shields it from external styles or scripts.
This encapsulation allows your widget to be integrated into any website without needing to worry about conflicts.
Step 4: Optional Tailwind CSS Integration
If you’d like to style your widget with Tailwind CSS, follow these steps to configure Tailwind within the widget:
Install Tailwind CSS:
npm install -D tailwindcss postcss autoprefixer npx tailwindcss init -p
Configure Tailwind in
tailwind.config.js
:module.exports = { content: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'], theme: { extend: {}, }, plugins: [], };
Import Tailwind styles in
main.css
:/* src/assets/styles/main.css */ @import 'tailwindcss/base'; @import 'tailwindcss/components'; @import 'tailwindcss/utilities';
Finally, import the CSS file in
main.js
:import './assets/styles/main.css';
Rebuild the project using:
npm run build
Conclusion
By converting a simple Vue charting widget into an embedded web component with Shadow DOM, you can create reusable, isolated components that integrate seamlessly into any webpage. This guide demonstrates how to encapsulate styles using Shadow DOM and how to compile the widget using Vite for easy distribution.
With these steps, you can expand this approach to create more complex widgets like countdowns or other interactive components, and embed them across different websites without worrying about conflicts or dependencies on the hosting environment.
Happy coding!