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:
parent
03dd9efb72
commit
cdd5194183
|
|
@ -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 %]
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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);
|
||||
|
|
@ -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({
|
||||
|
|
|
|||
Loading…
Reference in New Issue