Bug 37911: (follow-up) Update to vue ^3.5.4, use vue's web component implementation instead
- This would require us to update vue to a new minor version. - We could then use the defineCustomElement function with the shadowRoot option set to false (this is important). - The current implementation in islands.ts is naive and would result in an intolerable bundle size. - The right way to do code splitting here is to use dynamic imports. 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
b2459bb9fc
commit
4c40cf8f9f
|
|
@ -13,7 +13,7 @@
|
|||
[% END %]</title
|
||||
>
|
||||
[% Asset.css("css/mainpage.css") | $raw %]
|
||||
[% Asset.js("js/vue/dist/islands.js") | $raw %]
|
||||
[% Asset.js("js/vue/dist/islands.js", "defer" => 1, async => "1") | $raw %]
|
||||
[% INCLUDE 'doc-head-close.inc' %]
|
||||
</head>
|
||||
|
||||
|
|
@ -335,7 +335,7 @@
|
|||
<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>
|
||||
<hello-islands message="This is an attribute on the web component, that's bound to a prop in the Vue component. Cool, right?" color="dodgerblue"></hello-islands>
|
||||
</div>
|
||||
<!-- /.row -->
|
||||
|
||||
|
|
@ -348,7 +348,8 @@
|
|||
});
|
||||
|
||||
setTimeout(() => {
|
||||
document.getElementById("hello-islands").dataset.props = JSON.stringify({ message: "This is a delayed message! Changes to data-props currently unfortunately reset the internal state." });
|
||||
const [firstHelloIslandsElement] = document.getElementsByTagName("hello-islands");
|
||||
firstHelloIslandsElement.message = "This is a delayed update to the message attribute of the web commponent.";
|
||||
}, 2000);
|
||||
});
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<h2>Hello from Islands!</h2>
|
||||
<h2 :style="{ color }">Hello from Islands!</h2>
|
||||
<p>This component is rendered as an island in a static HTML page.</p>
|
||||
|
||||
<!-- Display message prop -->
|
||||
|
|
@ -20,7 +20,11 @@ export default {
|
|||
props: {
|
||||
message: {
|
||||
type: String,
|
||||
default: "",
|
||||
default: "No content",
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: "crimson",
|
||||
},
|
||||
},
|
||||
data() {
|
||||
|
|
|
|||
|
|
@ -1,93 +1,20 @@
|
|||
import { createApp, Component, App } from "vue";
|
||||
import { Component, defineCustomElement } from "vue";
|
||||
import HelloIslands from "../components/HelloIslands.vue";
|
||||
|
||||
/**
|
||||
* A registry for Vue components.
|
||||
* @type {Map<string, Component>}
|
||||
* @type {Map<string, string>}
|
||||
*/
|
||||
export const componentRegistry: Map<string, Component> = new Map<
|
||||
string,
|
||||
Component
|
||||
>();
|
||||
export const componentRegistry: Map<string, () => Promise<Component>> = new Map(
|
||||
[["hello-islands", HelloIslands]]
|
||||
);
|
||||
|
||||
/**
|
||||
* 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);
|
||||
// Register and define custom elements
|
||||
window.requestIdleCallback(() => {
|
||||
componentRegistry.forEach((component, name) => {
|
||||
customElements.define(
|
||||
name,
|
||||
defineCustomElement(component as any, { shadowRoot: false })
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@
|
|||
"pinia": "^2.0.13",
|
||||
"sass": "^1.58.1",
|
||||
"style-loader": "^3.3.1",
|
||||
"vue": "^3.2.31",
|
||||
"vue": "^3.5.4",
|
||||
"vue-flatpickr-component": "^9",
|
||||
"vue-router": "^4.0.14",
|
||||
"vue-select": "4.0.0-beta.3",
|
||||
|
|
@ -69,8 +69,6 @@
|
|||
"author": "",
|
||||
"license": "GPL-3.0",
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.26.8",
|
||||
"@koha-community/prettier-plugin-template-toolkit": "^2.0.2",
|
||||
"@rspack/cli": "^1.0.1",
|
||||
"@rspack/core": "^1.0.1",
|
||||
"@vue/compiler-sfc": "^3.2.31",
|
||||
|
|
@ -101,4 +99,4 @@
|
|||
"webpack-cli": "^4.9.2",
|
||||
"webpack-dev-server": "^4.7.4"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -88,6 +88,7 @@ module.exports = {
|
|||
new rspack.DefinePlugin({
|
||||
__VUE_OPTIONS_API__: true,
|
||||
__VUE_PROD_DEVTOOLS__: false,
|
||||
__VUE_PROD_HYDRATION_MISMATCH_DETAILS__: false,
|
||||
}),
|
||||
],
|
||||
externals: {
|
||||
|
|
|
|||
Loading…
Reference in New Issue