Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

sahil1711's avatar

How to create an vue embeddable widget ?

I was stuck with creating an embeddable widget using Vue, Laravel , vite , tailwind.

0 likes
6 replies
sahil1711's avatar

@martinbean Hey martin, my stack is vue laravel and vite and inertia. For creating the web component , here is what I have done. I created a new ts file other than app.ts for the component and also updated vite config ts for exporting that bundle. Here is my vite config.

import vue from "@vitejs/plugin-vue";
import tsconfigPaths from "vite-tsconfig-paths";
import path from 'path';
import { fileURLToPath, URL } from 'node:url'
import laravel from "laravel-vite-plugin";

const __dirname = fileURLToPath(new URL('.', import.meta.url));

export default defineConfig({
    server: {
        hmr: {
            host: "localhost",
        },
    },
    plugins: [
        tsconfigPaths(),
        laravel({
            input: "resources/js/app.ts",
            refresh: true,
        }),
        vue({
            template: {
                transformAssetUrls: {
                    base: null,
                    includeAbsolute: false,
                },
                compilerOptions: {
                    isCustomElement: tag => tag.startsWith('sf-')
                },
            customElement:true
            },
        }),
    ],
    build: {
        lib: {
            entry: path.resolve(__dirname, 'src/app.ts'),
            name: 'BoardView',
            formats: ['es', 'umd'],
            fileName: (format) => `board-view.${format}.js`,
            target:"esnext",
            minify: false
        },
        rollupOptions: {
            // Externalize deps that shouldn't be bundled into the library.
            external: ['vue'],
            output: {
                // Provide global variables to use in the UMD build for externalized deps.
                globals: {
                    vue: 'Vue'
                }
            }
        }
    },
    optimizeDeps: {
        include: ['vue']
    },
});

I am getting styles etc in the component since I have scoped it , however as mentioned , my current site is giving me different errors. Here's is the link : https://kanbancast.com/. It's an app and I wanted to export this for my users as a functionality to embed in their site.

sahil1711's avatar

Currently i am getting these error. Uncaught TypeError: Cannot read properties of undefined (reading 'ref') at board-view.umd.js:81:24867 at board-view.umd.js:1:181 at board-view.umd.js:1:193 kanbancast.com/:1 Uncaught TypeError: Failed to resolve module specifier "vue". Relative references must start with either "/", "./", or "../".

gych's avatar

@sahil1711 Did you import ref from vue ?

import { ref } from 'vue';

Can you also share your app.ts file

1 like
sahil1711's avatar

@gych Here's the app.ts

import "./bootstrap";
import "../css/app.css";

import {type Component, DefineComponent, createApp, h, ref} from "vue";
import {createInertiaApp, Link, router} from "@inertiajs/vue3";
import { resolvePageComponent } from "laravel-vite-plugin/inertia-helpers";
import { ZiggyVue } from "../../vendor/tightenco/ziggy/dist";
import EmptyComponent from "@/Jetstream/EmptyComponent.vue";

import { bootstrapVueApp } from './bootstrap';

import axios from 'axios';
window.axios = axios;

window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
const appName = import.meta.env.VITE_APP_NAME || "KanbanCast";

const initialProps = {
    projectId: "1",
    statuses: [
        {
            "id": 1,
            "title": "Pending",
            "slug": "pending",
            "order": 3,
            "tasks": []
        },

        {
            "id": 2,
            "title": "To Do",
            "slug": "to-do",
            "order": 1,
            "tasks": [
                {
                    "id": 1,
                    "title": "Task 1",
                    "description": "Description of Task 1",
                    "order": 1,
                    "votes":1,
                    "status_id": 1
                },
                {
                    "id": 2,
                    "title": "Task 2",
                    "description": "Description of Task 2",
                    "order": 2,
                    "votes":1,
                    "status_id": 1
                }
            ]
        },
    ]
};

const customLink: Component = {
    name: 'CustomLink',
    props: ['item'],
    render() {
        return h(this.item.href ? Link : EmptyComponent, {
            class: window.location.href.startsWith(this.item.href) && 'vsm--link_active',
        }, this.$slots.default);
    },
    mounted: function () {
        this.onRouteChange();

        router.on('start', () => {
            this.onRouteChange();
        });
    },
    inject: ['onRouteChange']
};


createInertiaApp({
    title: (title) => `${title} - ${appName}`,
    resolve: (name) =>
        resolvePageComponent(
            `./Pages/${name}.vue`,
            import.meta.glob<DefineComponent>("./Pages/**/*.vue")
        ),
    setup({ el, App, props, plugin }) {
        const app = createApp({ render: () => h(App, props) });
        app.component("CustomLink", customLink).use(plugin)
            .use(ZiggyVue, (window as Window).Ziggy)
            .mount(el);
        return app;
    },
    progress: {
        color: "#4B5563",
        showSpinner: true,
        includeCSS: true,
    },
}).then();

document.addEventListener('DOMContentLoaded', () => {
    bootstrapVueApp('board-view', initialProps);
});

The bootstrap function comes from bootstrap.ts

import BoardView from '@/components/SFBoardView.ce.vue';

export function bootstrapVueApp(elementId: string, props: any) {
    // Define the custom element from the Vue component
    const BoardViewElement = defineCustomElement(BoardView);

    // Register the custom element if it's not already registered
    if (!customElements.get(elementId)) {
        customElements.define(elementId, BoardViewElement);
    }
    // Create an instance of the custom element
    const elementInstance = new BoardViewElement(props);
    // Append the new custom element instance to the document body or another container
    document.body.appendChild(elementInstance);
}
LaryAI's avatar
Level 58

Creating an embeddable Vue widget that can be integrated into various websites involves several steps, particularly when using a stack that includes Vue, Laravel, Vite, and Tailwind CSS. Below, I'll outline a basic approach to building such a widget.

Step 1: Set Up Your Project

First, ensure you have Node.js, npm, and Composer installed on your machine. Then, create a new Laravel project and set up Vue and Vite within this project.

composer create-project laravel/laravel vue-widget
cd vue-widget
npm install
npm install vue@next vue-loader@next vite tailwindcss --save-dev

Set up Tailwind CSS by following the official documentation, which typically involves creating a tailwind.config.js and adding Tailwind to your CSS.

Step 2: Configure Vite

Create a vite.config.js file in your project root if it doesn't already exist, and configure it to work with Vue:

import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';

export default defineConfig({
  plugins: [vue()],
  build: {
    outDir: 'path/to/public/widget',
    lib: {
      entry: 'resources/js/widget.js',
      name: 'MyWidget',
      formats: ['umd'],
      fileName: (format) => `my-widget.${format}.js`
    }
  }
});

Step 3: Create Your Vue Component

Create a new Vue component that will act as your widget. For example, create resources/js/Widget.vue:

<template>
  <div class="widget">
    <h1 class="text-2xl font-bold">Hello from My Widget!</h1>
  </div>
</template>

<script>
export default {
  name: 'Widget'
}
</script>

<style scoped>
.widget {
  padding: 1rem;
  background-color: #f0f0f0;
  border: 1px solid #ccc;
}
</style>

Step 4: Create an Entry Point

Create an entry point for your widget in resources/js/widget.js:

import { createApp } from 'vue';
import Widget from './Widget.vue';

const mountWidget = (selector) => {
  const app = createApp(Widget);
  app.mount(selector);
};

window.mountWidget = mountWidget;

Step 5: Build the Widget

Run Vite to build your widget:

npm run build

Step 6: Embedding the Widget

To embed the widget into any HTML page, include the script and initialize the widget:

<div id="my-widget"></div>
<script src="http://your-laravel-app-url/path/to/public/widget/my-widget.umd.js"></script>
<script>
  mountWidget('#my-widget');
</script>

Step 7: Serve and Test

Make sure your Laravel application is accessible from the URL you used in the script tag, and test the widget in different environments.

This basic example sets up a simple Vue widget that can be embedded into any site. You can expand upon this by adding more complex interactions, styles, and functionalities according to your needs.

Please or to participate in this conversation.