From 39bae7201d7e22128ea12420d6088194993987e8 Mon Sep 17 00:00:00 2001 From: Paul Derscheid Date: Thu, 12 Sep 2024 19:12:31 +0200 Subject: [PATCH] Bug 37911: Prototype vue islands within static pages This is pretty bare bones, but might still be interesting to take a look at. Signed-off-by: Matt Blenkinsop --- .../prog/en/modules/intranet-main.tt | 10 ++ .../prog/js/vue/components/HelloIslands.vue | 37 ++++++++ .../prog/js/vue/modules/islands.ts | 93 +++++++++++++++++++ rspack.config.js | 18 ++++ 4 files changed, 158 insertions(+) create mode 100644 koha-tmpl/intranet-tmpl/prog/js/vue/components/HelloIslands.vue create mode 100644 koha-tmpl/intranet-tmpl/prog/js/vue/modules/islands.ts diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/intranet-main.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/intranet-main.tt index 65d793f3190..0ef4b492172 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/modules/intranet-main.tt +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/intranet-main.tt @@ -11,6 +11,7 @@ [% t("Koha staff interface") | html %] [% END %] [% Asset.css("css/mainpage.css") | $raw %] +[% Asset.js("js/vue/dist/islands.js") | $raw %] [% INCLUDE 'doc-head-close.inc' %] @@ -301,6 +302,11 @@ +
+

🚧 Static Page with Vue Components 🚧

+ +
+
[% MACRO jsinclude BLOCK %] [% END %] diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/components/HelloIslands.vue b/koha-tmpl/intranet-tmpl/prog/js/vue/components/HelloIslands.vue new file mode 100644 index 00000000000..782296d5e3b --- /dev/null +++ b/koha-tmpl/intranet-tmpl/prog/js/vue/components/HelloIslands.vue @@ -0,0 +1,37 @@ + + + diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/modules/islands.ts b/koha-tmpl/intranet-tmpl/prog/js/vue/modules/islands.ts new file mode 100644 index 00000000000..379c1c954a7 --- /dev/null +++ b/koha-tmpl/intranet-tmpl/prog/js/vue/modules/islands.ts @@ -0,0 +1,93 @@ +import { createApp, Component, App } from "vue"; + +/** + * A registry for Vue components. + * @type {Map} + */ +export const componentRegistry: Map = new Map< + string, + Component +>(); + +/** + * Registers a Vue component with a name. + * @param {string} name - The name of the component. + * @param {Component} component - The Vue component to register. + * @returns {void} + */ +export function registerComponent(name: string, component: Component): void { + componentRegistry.set(name, component); +} + +/** + * Mounts Vue components to DOM elements based on the `data-component` attribute. + * Components are created with props parsed from the `data-props` attribute. + * Watches for changes in props and updates the component accordingly. + * @returns {void} + */ +export function mountComponents(): void { + console.log("Mounting components"); + + const elements: NodeListOf = + document.querySelectorAll("[data-component]"); + elements.forEach((element: Element) => { + const componentName: string | null = + element.getAttribute("data-component"); + if (!componentName) { + console.warn("No data-component attribute found."); + return; + } + + const component: Component | undefined = + componentRegistry.get(componentName); + if (!component) { + console.warn(`Component ${componentName} not found.`); + return; + } + + const props: string | null = element.getAttribute("data-props"); + const parsedProps: Record = props ? JSON.parse(props) : {}; + + // Create and mount the Vue component + const app: App = createApp(component, parsedProps); + app.mount(element); + + // Watch for updates to props + watchProps(element, app, component); + }); +} + +/** + * Watches for changes in props and updates the component accordingly. + * @param {Element} element - The DOM element where the component is mounted. + * @param {App} app - The Vue application instance. + * @param {Component} component - The Vue component. + * @returns {void} + */ +function watchProps(element: Element, app: App, component: Component): void { + const propsAttr: string | null = element.getAttribute("data-props"); + let prevProps: Record = propsAttr ? JSON.parse(propsAttr) : {}; + + const observer = new MutationObserver(() => { + const newPropsAttr: string | null = element.getAttribute("data-props"); + if (newPropsAttr) { + const newProps: Record = JSON.parse(newPropsAttr); + if (JSON.stringify(newProps) !== JSON.stringify(prevProps)) { + prevProps = newProps; + app.unmount(); // Unmount existing component + createApp(component, newProps).mount(element); // Mount with new props + } + } + }); + + observer.observe(element, { + attributes: true, + attributeFilter: ["data-props"], + }); +} + +import HelloIslands from "../components/HelloIslands.vue"; +registerComponent("HelloIslands", HelloIslands); + +// Automatically mount components when the DOM is fully loaded +document.addEventListener("DOMContentLoaded", mountComponents); diff --git a/rspack.config.js b/rspack.config.js index 6a8e40c4f6e..12b621b3fea 100644 --- a/rspack.config.js +++ b/rspack.config.js @@ -13,6 +13,7 @@ module.exports = { "./koha-tmpl/intranet-tmpl/prog/js/vue/modules/preservation.ts", "admin/record_sources": "./koha-tmpl/intranet-tmpl/prog/js/vue/modules/admin/record_sources.ts", + islands: "./koha-tmpl/intranet-tmpl/prog/js/vue/modules/islands.ts", }, output: { filename: "[name].js", @@ -57,6 +58,23 @@ module.exports = { }, ], }, + /** + optimization: { + splitChunks: { + chunks: "all", + cacheGroups: { + default: false, // Disable default cache groups to avoid affecting existing bundles + vendors: false, // Disable vendor caching + vue: { + test: /[\\/]node_modules[\\/]vue[\\/]/, + name: "vue", + chunks: "all", + enforce: true, + }, + }, + }, + }, + */ plugins: [ new VueLoaderPlugin(), new rspack.DefinePlugin({ -- 2.39.3 (Apple Git-146)