Bug 37911: (follow-up) Allow for initialisation of islands on demand

- Split the rspack config into a umd and an esm section.
- Don't hydrate by default.
    - Instead check the "init" attribute for the currentScript when using the umd.
    - Allow for manual import and call when using the esm.
    - Add examples for both in intranet-main.tt (to be removed).
- Add a DialogIsland wrapper for the shared Dialog component (to map props to stored refs).
- Move DialogIsland as well as Demo components into an Islands directory under components.
- Add i18n initialisation to hydrate's idle callback.

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-18 17:00:13 +00:00 committed by Katrin Fischer
parent aa5283dd62
commit bd15b1829e
No known key found for this signature in database
GPG Key ID: 0EF6E2C03357A834
6 changed files with 288 additions and 87 deletions

View File

@ -347,8 +347,23 @@
</div>
<!-- /.row -->
<div class="row w-25 m-auto p-4 border border-4 border-warning br-5 rounded text-center mt-4">
<dialog-island id="surprise"></dialog-island>
<button id="surprise-button" class="btn btn-primary">Click me for a surprise!</button>
</div>
[% MACRO jsinclude BLOCK %]
[% Asset.js("js/vue/dist/islands.js", "type" => "module") | $raw %]
[% INCLUDE 'calendar.inc' %]
[% Asset.js("js/vue/dist/islands.js", "init" => "1") | $raw %]
<!--
[% SET islands = Asset.js("js/vue/dist/islands.esm.js").match('(src="([^"]+)")').1 %]
<script src="[% islands %]" type="module">
</script>
<script type="module">
import { hydrate } from "[% islands %]";
hydrate();
</script>
-->
<script>
var MSG_CONFIRM_DELETE = _("Are you sure you want to delete this news item? This cannot be undone.");
$(document).ready(function () {
@ -360,6 +375,10 @@
const [firstHelloIslandsElement] = document.getElementsByTagName("hello-islands");
firstHelloIslandsElement.message = "This is a delayed update to the message attribute of the web commponent.";
}, 2000);
$("#surprise-button").on("click", () => {
$("#surprise").attr("warning", `<span>Yes, this is <span class="font-monospace">Dialog.vue</span> (in a wrapper component)!</span><span class="d-none">${Math.random()}</span>`);
});
});
</script>
[% END %]

View File

@ -0,0 +1,107 @@
<template>
<Dialog />
</template>
<script>
import { inject, watch } from "vue";
import Dialog from "../Dialog.vue";
export default {
props: {
message: {
type: String,
default: null,
},
error: {
type: String,
default: null,
},
warning: {
type: String,
default: null,
},
confirmation: {
type: Object,
default: () => null,
},
isSubmitting: {
type: Boolean,
default: false,
},
isLoading: {
type: Boolean,
default: false,
},
acceptCallback: {
type: Function,
default: null,
},
},
setup(props) {
const mainStore = inject("mainStore");
const {
setMessage,
setError,
setWarning,
setConfirmation,
setSubmitting,
setLoading,
setAcceptCallback,
} = mainStore;
watch(
() => props.message,
newVal => {
setMessage(newVal);
}
);
watch(
() => props.error,
newVal => {
setError(newVal);
}
);
watch(
() => props.warning,
newVal => {
setWarning(newVal);
}
);
watch(
() => props.confirmation,
newVal => {
setConfirmation(newVal);
}
);
watch(
() => props.isSubmitting,
newVal => {
setSubmitting(newVal);
}
);
watch(
() => props.isLoading,
newVal => {
setLoading(newVal);
}
);
watch(
() => props.acceptCallback,
newVal => {
setAcceptCallback(newVal);
}
);
return {};
},
components: {
Dialog,
},
};
</script>

View File

@ -1,5 +1,6 @@
import { Component, defineCustomElement } from "vue";
import { createPinia } from "pinia";
import { $__ } from "../i18n";
import { useMainStore } from "../stores/main";
import { useNavigationStore } from "../stores/navigation";
@ -27,7 +28,7 @@ export const componentRegistry: Map<string, WebComponentDynamicImport> =
importFn: async () => {
const module = await import(
/* webpackChunkName: "hello-islands" */
"../components/HelloIslands.vue"
"../components/Islands/HelloIslands.vue"
);
return module.default;
},
@ -42,12 +43,27 @@ export const componentRegistry: Map<string, WebComponentDynamicImport> =
importFn: async () => {
const module = await import(
/* webpackChunkName: "hello-world" */
"../components/HelloWorld.vue"
"../components/Islands/HelloWorld.vue"
);
return module.default;
},
},
],
[
"dialog-island",
{
importFn: async () => {
const module = await import(
/* webpackChunkName: "vue-dialog" */
"../components/Islands/DialogIsland.vue"
);
return module.default;
},
config: {
stores: ["mainStore"],
},
},
],
]);
/**
@ -88,6 +104,7 @@ export function hydrate(): void {
app.provide(store, storesMatrix[store]);
});
}
app.config.globalProperties.$__ = $__;
// Further config options can be added here as we expand this further
},
}),
@ -97,4 +114,6 @@ export function hydrate(): void {
});
}
hydrate();
if (parseInt(document?.currentScript?.getAttribute("init") ?? "0", 10)) {
hydrate();
}

View File

@ -3,8 +3,9 @@ const { VueLoaderPlugin } = require("vue-loader");
const path = require("path");
const rspack = require("@rspack/core");
module.exports = {
resolve: {
module.exports = [
{
resolve: {
alias: {
"@fetch": path.resolve(
__dirname,
@ -13,90 +14,145 @@ module.exports = {
},
},
experiments: {
css: true,
},
entry: {
erm: "./koha-tmpl/intranet-tmpl/prog/js/vue/modules/erm.ts",
preservation:
"./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",
path: path.resolve(
__dirname,
"koha-tmpl/intranet-tmpl/prog/js/vue/dist/"
),
chunkFilename: "[name].[contenthash].js",
globalObject: "window",
},
module: {
rules: [
{
test: /\.vue$/,
loader: "vue-loader",
options: {
experimentalInlineMatchResource: true,
},
exclude: [path.resolve(__dirname, "t/cypress/")],
},
{
test: /\.ts$/,
loader: "builtin:swc-loader",
options: {
jsc: {
parser: {
syntax: "typescript",
},
css: true,
},
entry: {
erm: "./koha-tmpl/intranet-tmpl/prog/js/vue/modules/erm.ts",
preservation:
"./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",
path: path.resolve(
__dirname,
"koha-tmpl/intranet-tmpl/prog/js/vue/dist/"
),
chunkFilename: "[name].[contenthash].js",
globalObject: "window",
},
module: {
rules: [
{
test: /\.vue$/,
loader: "vue-loader",
options: {
experimentalInlineMatchResource: true,
},
appendTsSuffixTo: [/\.vue$/],
exclude: [path.resolve(__dirname, "t/cypress/")],
},
exclude: [
/node_modules/,
path.resolve(__dirname, "t/cypress/"),
],
type: "javascript/auto",
},
{
test: /\.css$/i,
type: "javascript/auto",
use: ["style-loader", "css-loader"],
},
{
test: /\.ts$/,
loader: "builtin:swc-loader",
options: {
jsc: {
parser: {
syntax: "typescript",
},
},
appendTsSuffixTo: [/\.vue$/],
},
exclude: [
/node_modules/,
path.resolve(__dirname, "t/cypress/"),
],
type: "javascript/auto",
},
{
test: /\.css$/i,
type: "javascript/auto",
use: ["style-loader", "css-loader"],
},
],
},
plugins: [
new VueLoaderPlugin(),
new rspack.DefinePlugin({
__VUE_OPTIONS_API__: true,
__VUE_PROD_DEVTOOLS__: false,
__VUE_PROD_HYDRATION_MISMATCH_DETAILS__: false,
}),
],
},
/**
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,
},
},
externals: {
jquery: "jQuery",
"datatables.net": "DataTable",
"datatables.net-buttons": "DataTable",
"datatables.net-buttons/js/buttons.html5": "DataTable",
"datatables.net-buttons/js/buttons.print": "DataTable",
"datatables.net-buttons/js/buttons.colVis": "DataTable",
},
},
*/
plugins: [
new VueLoaderPlugin(),
new rspack.DefinePlugin({
__VUE_OPTIONS_API__: true,
__VUE_PROD_DEVTOOLS__: false,
__VUE_PROD_HYDRATION_MISMATCH_DETAILS__: false,
}),
],
externals: {
jquery: "jQuery",
"datatables.net": "DataTable",
"datatables.net-buttons": "DataTable",
"datatables.net-buttons/js/buttons.html5": "DataTable",
"datatables.net-buttons/js/buttons.print": "DataTable",
"datatables.net-buttons/js/buttons.colVis": "DataTable",
{
experiments: {
css: true,
outputModule: true,
},
entry: {
islands: "./koha-tmpl/intranet-tmpl/prog/js/vue/modules/islands.ts",
},
output: {
filename: "[name].esm.js",
path: path.resolve(
__dirname,
"koha-tmpl/intranet-tmpl/prog/js/vue/dist/"
),
chunkFilename: "[name].[contenthash].esm.js",
globalObject: "window",
library: {
type: "module",
},
},
module: {
rules: [
{
test: /\.vue$/,
loader: "vue-loader",
options: {
experimentalInlineMatchResource: true,
},
exclude: [path.resolve(__dirname, "t/cypress/")],
},
{
test: /\.ts$/,
loader: "builtin:swc-loader",
options: {
jsc: {
parser: {
syntax: "typescript",
},
},
appendTsSuffixTo: [/\.vue$/],
},
exclude: [
/node_modules/,
path.resolve(__dirname, "t/cypress/"),
],
type: "javascript/auto",
},
{
test: /\.css$/i,
type: "javascript/auto",
use: ["style-loader", "css-loader"],
},
],
},
plugins: [
new VueLoaderPlugin(),
new rspack.DefinePlugin({
__VUE_OPTIONS_API__: true,
__VUE_PROD_DEVTOOLS__: false,
__VUE_PROD_HYDRATION_MISMATCH_DETAILS__: false,
}),
],
externals: {
jquery: "jQuery",
"datatables.net": "DataTable",
"datatables.net-buttons": "DataTable",
"datatables.net-buttons/js/buttons.html5": "DataTable",
"datatables.net-buttons/js/buttons.print": "DataTable",
"datatables.net-buttons/js/buttons.colVis": "DataTable",
},
},
};
];