View | Details | Raw Unified | Return to bug 37911
Collapse All | Expand All

(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/intranet-main.tt (-3 / +4 lines)
Lines 13-19 Link Here
13
    [% END %]</title
13
    [% END %]</title
14
>
14
>
15
[% Asset.css("css/mainpage.css") | $raw %]
15
[% Asset.css("css/mainpage.css") | $raw %]
16
[% Asset.js("js/vue/dist/islands.js") | $raw %]
16
[% Asset.js("js/vue/dist/islands.js", "defer" => 1, async => "1") | $raw %]
17
[% INCLUDE 'doc-head-close.inc' %]
17
[% INCLUDE 'doc-head-close.inc' %]
18
</head>
18
</head>
19
19
Lines 335-341 Link Here
335
<div class="row w-25 m-auto p-4 border border-4 border-warning br-4 rounded text-center">
335
<div class="row w-25 m-auto p-4 border border-4 border-warning br-4 rounded text-center">
336
    <h1>🚧 Static Page with Vue Components 🚧</h1>
336
    <h1>🚧 Static Page with Vue Components 🚧</h1>
337
337
338
    <div id="hello-islands" data-component="HelloIslands" data-props='{"message": "Hello from props!"}'></div>
338
    <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>
339
</div>
339
</div>
340
<!-- /.row -->
340
<!-- /.row -->
341
341
Lines 348-354 Link Here
348
            });
348
            });
349
349
350
            setTimeout(() => {
350
            setTimeout(() => {
351
                document.getElementById("hello-islands").dataset.props = JSON.stringify({ message: "This is a delayed message! Changes to data-props currently unfortunately reset the internal state." });
351
                const [firstHelloIslandsElement] = document.getElementsByTagName("hello-islands");
352
                firstHelloIslandsElement.message = "This is a delayed update to the message attribute of the web commponent.";
352
            }, 2000);
353
            }, 2000);
353
        });
354
        });
354
    </script>
355
    </script>
(-)a/koha-tmpl/intranet-tmpl/prog/js/vue/components/HelloIslands.vue (-2 / +6 lines)
Lines 1-5 Link Here
1
<template>
1
<template>
2
    <h2>Hello from Islands!</h2>
2
    <h2 :style="{ color }">Hello from Islands!</h2>
3
    <p>This component is rendered as an island in a static HTML page.</p>
3
    <p>This component is rendered as an island in a static HTML page.</p>
4
4
5
    <!-- Display message prop -->
5
    <!-- Display message prop -->
Lines 20-26 export default { Link Here
20
    props: {
20
    props: {
21
        message: {
21
        message: {
22
            type: String,
22
            type: String,
23
            default: "",
23
            default: "No content",
24
        },
25
        color: {
26
            type: String,
27
            default: "crimson",
24
        },
28
        },
25
    },
29
    },
26
    data() {
30
    data() {
(-)a/koha-tmpl/intranet-tmpl/prog/js/vue/modules/islands.ts (-88 / +15 lines)
Lines 1-93 Link Here
1
import { createApp, Component, App } from "vue";
1
import { Component, defineCustomElement } from "vue";
2
import HelloIslands from "../components/HelloIslands.vue";
2
3
3
/**
4
/**
4
 * A registry for Vue components.
5
 * A registry for Vue components.
5
 * @type {Map<string, Component>}
6
 * @type {Map<string, string>}
6
 */
7
export const componentRegistry: Map<string, Component> = new Map<
8
    string,
9
    Component
10
>();
11
12
/**
13
 * Registers a Vue component with a name.
14
 * @param {string} name - The name of the component.
15
 * @param {Component} component - The Vue component to register.
16
 * @returns {void}
17
 */
18
export function registerComponent(name: string, component: Component): void {
19
    componentRegistry.set(name, component);
20
}
21
22
/**
23
 * Mounts Vue components to DOM elements based on the `data-component` attribute.
24
 * Components are created with props parsed from the `data-props` attribute.
25
 * Watches for changes in props and updates the component accordingly.
26
 * @returns {void}
27
 */
28
export function mountComponents(): void {
29
    console.log("Mounting components");
30
31
    const elements: NodeListOf<Element> =
32
        document.querySelectorAll("[data-component]");
33
    elements.forEach((element: Element) => {
34
        const componentName: string | null =
35
            element.getAttribute("data-component");
36
        if (!componentName) {
37
            console.warn("No data-component attribute found.");
38
            return;
39
        }
40
41
        const component: Component | undefined =
42
            componentRegistry.get(componentName);
43
        if (!component) {
44
            console.warn(`Component ${componentName} not found.`);
45
            return;
46
        }
47
48
        const props: string | null = element.getAttribute("data-props");
49
        const parsedProps: Record<string, any> = props ? JSON.parse(props) : {};
50
51
        // Create and mount the Vue component
52
        const app: App = createApp(component, parsedProps);
53
        app.mount(element);
54
55
        // Watch for updates to props
56
        watchProps(element, app, component);
57
    });
58
}
59
60
/**
61
 * Watches for changes in props and updates the component accordingly.
62
 * @param {Element} element - The DOM element where the component is mounted.
63
 * @param {App} app - The Vue application instance.
64
 * @param {Component} component - The Vue component.
65
 * @returns {void}
66
 */
7
 */
67
function watchProps(element: Element, app: App, component: Component): void {
8
export const componentRegistry: Map<string, () => Promise<Component>> = new Map(
68
    const propsAttr: string | null = element.getAttribute("data-props");
9
    [["hello-islands", HelloIslands]]
69
    let prevProps: Record<string, any> = propsAttr ? JSON.parse(propsAttr) : {};
10
);
70
11
71
    const observer = new MutationObserver(() => {
12
// Register and define custom elements
72
        const newPropsAttr: string | null = element.getAttribute("data-props");
13
window.requestIdleCallback(() => {
73
        if (newPropsAttr) {
14
    componentRegistry.forEach((component, name) => {
74
            const newProps: Record<string, any> = JSON.parse(newPropsAttr);
15
        customElements.define(
75
            if (JSON.stringify(newProps) !== JSON.stringify(prevProps)) {
16
            name,
76
                prevProps = newProps;
17
            defineCustomElement(component as any, { shadowRoot: false })
77
                app.unmount(); // Unmount existing component
18
        );
78
                createApp(component, newProps).mount(element); // Mount with new props
79
            }
80
        }
81
    });
19
    });
82
20
});
83
    observer.observe(element, {
84
        attributes: true,
85
        attributeFilter: ["data-props"],
86
    });
87
}
88
89
import HelloIslands from "../components/HelloIslands.vue";
90
registerComponent("HelloIslands", HelloIslands);
91
92
// Automatically mount components when the DOM is fully loaded
93
document.addEventListener("DOMContentLoaded", mountComponents);
(-)a/package.json (-4 / +2 lines)
Lines 38-44 Link Here
38
    "pinia": "^2.0.13",
38
    "pinia": "^2.0.13",
39
    "sass": "^1.58.1",
39
    "sass": "^1.58.1",
40
    "style-loader": "^3.3.1",
40
    "style-loader": "^3.3.1",
41
    "vue": "^3.2.31",
41
    "vue": "^3.5.4",
42
    "vue-flatpickr-component": "^9",
42
    "vue-flatpickr-component": "^9",
43
    "vue-router": "^4.0.14",
43
    "vue-router": "^4.0.14",
44
    "vue-select": "4.0.0-beta.3",
44
    "vue-select": "4.0.0-beta.3",
Lines 69-76 Link Here
69
  "author": "",
69
  "author": "",
70
  "license": "GPL-3.0",
70
  "license": "GPL-3.0",
71
  "devDependencies": {
71
  "devDependencies": {
72
    "@babel/core": "^7.26.8",
73
    "@koha-community/prettier-plugin-template-toolkit": "^2.0.2",
74
    "@rspack/cli": "^1.0.1",
72
    "@rspack/cli": "^1.0.1",
75
    "@rspack/core": "^1.0.1",
73
    "@rspack/core": "^1.0.1",
76
    "@vue/compiler-sfc": "^3.2.31",
74
    "@vue/compiler-sfc": "^3.2.31",
Lines 100-103 Link Here
100
    "webpack-cli": "^4.9.2",
98
    "webpack-cli": "^4.9.2",
101
    "webpack-dev-server": "^4.7.4"
99
    "webpack-dev-server": "^4.7.4"
102
  }
100
  }
103
}
101
}
(-)a/rspack.config.js (-1 / +1 lines)
Lines 80-85 module.exports = { Link Here
80
        new rspack.DefinePlugin({
80
        new rspack.DefinePlugin({
81
            __VUE_OPTIONS_API__: true,
81
            __VUE_OPTIONS_API__: true,
82
            __VUE_PROD_DEVTOOLS__: false,
82
            __VUE_PROD_DEVTOOLS__: false,
83
            __VUE_PROD_HYDRATION_MISMATCH_DETAILS__: false,
83
        }),
84
        }),
84
    ],
85
    ],
85
    externals: {
86
    externals: {
86
- 

Return to bug 37911