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 <matt.blenkinsop@ptfs-europe.com>

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Katrin Fischer <katrin.fischer@bsz-bw.de>
This commit is contained in:
Paul Derscheid 2024-09-12 19:12:31 +02:00 committed by Katrin Fischer
parent 03dd9efb72
commit cdd5194183
No known key found for this signature in database
GPG Key ID: 0EF6E2C03357A834
4 changed files with 160 additions and 0 deletions

View File

@ -13,6 +13,7 @@
[% END %]</title
>
[% Asset.css("css/mainpage.css") | $raw %]
[% Asset.js("js/vue/dist/islands.js") | $raw %]
[% INCLUDE 'doc-head-close.inc' %]
</head>
@ -331,6 +332,13 @@
<!-- /.row -->
</div>
<!-- /#container-main.container-fluid -->
<div class="row w-25 m-auto p-4 border border-4 border-warning br-4 rounded text-center">
<h1>🚧 Static Page with Vue Components 🚧</h1>
<div id="hello-islands" data-component="HelloIslands" data-props='{"message": "Hello from props!"}'></div>
</div>
<!-- /.row -->
[% MACRO jsinclude BLOCK %]
<script>
var MSG_CONFIRM_DELETE = _("Are you sure you want to delete this news item? This cannot be undone.");
@ -338,6 +346,10 @@
$(".news_delete").on("click", function () {
return confirmDelete(MSG_CONFIRM_DELETE);
});
setTimeout(() => {
document.getElementById("hello-islands").dataset.props = JSON.stringify({ message: "This is a delayed message! Props (data-props) reflect." });
}, 2000);
});
</script>
[% END %]

View File

@ -0,0 +1,37 @@
<template>
<h2>Hello from Islands!</h2>
<p>This component is rendered as an island in a static HTML page.</p>
<!-- Display message prop -->
<p v-if="message">{{ message }}</p>
<!-- Reactive counter example -->
<p>Counter: {{ count }}</p>
<!-- Koha's bootstrap works in here! -->
<button @click="incrementCounter" class="btn btn-primary">
Increment Counter
</button>
</template>
<script>
import { ref } from "vue";
export default {
props: {
message: {
type: String,
default: "",
},
},
data() {
return {
count: 0,
};
},
methods: {
incrementCounter() {
this.count++;
},
},
};
</script>

View File

@ -0,0 +1,93 @@
import { createApp, Component, App } from "vue";
/**
* A registry for Vue components.
* @type {Map<string, Component>}
*/
export const componentRegistry: Map<string, Component> = 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<Element> =
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<string, any> = 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<string, any> = propsAttr ? JSON.parse(propsAttr) : {};
const observer = new MutationObserver(() => {
const newPropsAttr: string | null = element.getAttribute("data-props");
if (newPropsAttr) {
const newProps: Record<string, any> = 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);

View File

@ -21,6 +21,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",
@ -65,6 +66,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({