New mechanism for dynamically loading css and js (checkpoint)
This commit is contained in:
parent
771d6328af
commit
9e21b5cb93
@ -1,30 +1,17 @@
|
|||||||
import { useReducer, useEffect } from "react";
|
import { useReducer, useEffect } from "react";
|
||||||
import { headInsert } from "keycloakify/tools/headInsert";
|
|
||||||
import { clsx } from "keycloakify/tools/clsx";
|
import { clsx } from "keycloakify/tools/clsx";
|
||||||
import { assert } from "tsafe/assert";
|
import { assert } from "tsafe/assert";
|
||||||
|
import { useInsertScriptTags, type ScriptTag } from "keycloakify/tools/useInsertScriptTags";
|
||||||
|
|
||||||
export function usePrepareTemplate(params: {
|
export function usePrepareTemplate(params: {
|
||||||
styles: string[];
|
styleSheetHrefs: string[];
|
||||||
scripts: {
|
scriptTags: ScriptTag[];
|
||||||
isModule: boolean;
|
|
||||||
source:
|
|
||||||
| {
|
|
||||||
type: "url";
|
|
||||||
src: string;
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: "inline";
|
|
||||||
code: string;
|
|
||||||
};
|
|
||||||
}[];
|
|
||||||
htmlClassName: string | undefined;
|
htmlClassName: string | undefined;
|
||||||
bodyClassName: string | undefined;
|
bodyClassName: string | undefined;
|
||||||
htmlLangProperty: string | undefined;
|
htmlLangProperty: string | undefined;
|
||||||
documentTitle: string | undefined;
|
documentTitle: string | undefined;
|
||||||
}) {
|
}) {
|
||||||
const { styles, scripts, htmlClassName, bodyClassName, htmlLangProperty, documentTitle } = params;
|
const { styleSheetHrefs, scriptTags, htmlClassName, bodyClassName, htmlLangProperty, documentTitle } = params;
|
||||||
|
|
||||||
const [isReady, setReady] = useReducer(() => true, styles.length === 0 && scripts.length === 0);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (htmlLangProperty === undefined) {
|
if (htmlLangProperty === undefined) {
|
||||||
@ -44,58 +31,10 @@ export function usePrepareTemplate(params: {
|
|||||||
document.title = documentTitle;
|
document.title = documentTitle;
|
||||||
}, [documentTitle]);
|
}, [documentTitle]);
|
||||||
|
|
||||||
useEffect(() => {
|
const { areAllStyleSheetsLoaded } = useInsertLinkTags({ "hrefs": styleSheetHrefs });
|
||||||
let isUnmounted = false;
|
|
||||||
|
|
||||||
const removeArray: (() => void)[] = [];
|
// NOTE: We want to load the script after the page have been fully rendered.
|
||||||
|
useInsertScriptTags({ "scriptTags": !areAllStyleSheetsLoaded ? [] : scriptTags });
|
||||||
(async () => {
|
|
||||||
for (const style of [...styles].reverse()) {
|
|
||||||
const { prLoaded, remove } = headInsert({
|
|
||||||
"type": "css",
|
|
||||||
"position": "prepend",
|
|
||||||
"href": style
|
|
||||||
});
|
|
||||||
|
|
||||||
removeArray.push(remove);
|
|
||||||
|
|
||||||
// TODO: Find a way to do that in parallel (without breaking the order)
|
|
||||||
await prLoaded;
|
|
||||||
|
|
||||||
if (isUnmounted) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setReady();
|
|
||||||
})();
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
isUnmounted = true;
|
|
||||||
removeArray.forEach(remove => remove());
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isReady) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const removeArray: (() => void)[] = [];
|
|
||||||
|
|
||||||
scripts.forEach(script => {
|
|
||||||
const { remove } = headInsert({
|
|
||||||
"type": "javascript",
|
|
||||||
...script
|
|
||||||
});
|
|
||||||
|
|
||||||
removeArray.push(remove);
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
removeArray.forEach(remove => remove());
|
|
||||||
};
|
|
||||||
}, [isReady]);
|
|
||||||
|
|
||||||
useSetClassName({
|
useSetClassName({
|
||||||
"target": "html",
|
"target": "html",
|
||||||
@ -107,7 +46,7 @@ export function usePrepareTemplate(params: {
|
|||||||
"className": bodyClassName
|
"className": bodyClassName
|
||||||
});
|
});
|
||||||
|
|
||||||
return { isReady };
|
return { areAllStyleSheetsLoaded };
|
||||||
}
|
}
|
||||||
|
|
||||||
function useSetClassName(params: { target: "html" | "body"; className: string | undefined }) {
|
function useSetClassName(params: { target: "html" | "body"; className: string | undefined }) {
|
||||||
@ -129,3 +68,53 @@ function useSetClassName(params: { target: "html" | "body"; className: string |
|
|||||||
};
|
};
|
||||||
}, [className]);
|
}, [className]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hrefByPrLoaded = new Map<string, Promise<void>>();
|
||||||
|
|
||||||
|
/** NOTE: The hrefs can't changes. There should be only one one call on this. */
|
||||||
|
function useInsertLinkTags(params: { hrefs: string[] }) {
|
||||||
|
const { hrefs } = params;
|
||||||
|
|
||||||
|
const [areAllStyleSheetsLoaded, setAllStyleSheetLoaded] = useReducer(() => true, hrefs.length === 0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let isActive = true;
|
||||||
|
|
||||||
|
let lastMountedHtmlElement: HTMLLinkElement | undefined = undefined;
|
||||||
|
|
||||||
|
for (const href of hrefs) {
|
||||||
|
if (hrefByPrLoaded.has(href)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const htmlElement = document.createElement("link");
|
||||||
|
|
||||||
|
hrefByPrLoaded.set(href, new Promise<void>(resolve => htmlElement.addEventListener("load", () => resolve())));
|
||||||
|
|
||||||
|
htmlElement.rel = "stylesheet";
|
||||||
|
|
||||||
|
htmlElement.href = href;
|
||||||
|
|
||||||
|
if (lastMountedHtmlElement !== undefined) {
|
||||||
|
lastMountedHtmlElement.insertAdjacentElement("afterend", htmlElement);
|
||||||
|
} else {
|
||||||
|
document.head.prepend(htmlElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
lastMountedHtmlElement = htmlElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
Promise.all(Array.from(hrefByPrLoaded.values())).then(() => {
|
||||||
|
if (!isActive) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setAllStyleSheetLoaded();
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
isActive = false;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return { areAllStyleSheetsLoaded };
|
||||||
|
}
|
||||||
|
@ -1,87 +0,0 @@
|
|||||||
import "./HTMLElement.prototype.prepend";
|
|
||||||
import { Deferred } from "evt/tools/Deferred";
|
|
||||||
|
|
||||||
export function headInsert(
|
|
||||||
params:
|
|
||||||
| {
|
|
||||||
type: "css";
|
|
||||||
href: string;
|
|
||||||
position: "append" | "prepend";
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: "javascript";
|
|
||||||
isModule: boolean;
|
|
||||||
source:
|
|
||||||
| {
|
|
||||||
type: "url";
|
|
||||||
src: string;
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: "inline";
|
|
||||||
code: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
): { remove: () => void; prLoaded: Promise<void> } {
|
|
||||||
const htmlElement = document.createElement(
|
|
||||||
(() => {
|
|
||||||
switch (params.type) {
|
|
||||||
case "css":
|
|
||||||
return "link";
|
|
||||||
case "javascript":
|
|
||||||
return "script";
|
|
||||||
}
|
|
||||||
})()
|
|
||||||
);
|
|
||||||
|
|
||||||
const dLoaded = new Deferred<void>();
|
|
||||||
|
|
||||||
htmlElement.addEventListener("load", () => dLoaded.resolve());
|
|
||||||
|
|
||||||
Object.assign(
|
|
||||||
htmlElement,
|
|
||||||
(() => {
|
|
||||||
switch (params.type) {
|
|
||||||
case "css":
|
|
||||||
return {
|
|
||||||
"href": params.href,
|
|
||||||
"rel": "stylesheet"
|
|
||||||
};
|
|
||||||
case "javascript":
|
|
||||||
return {
|
|
||||||
...(() => {
|
|
||||||
switch (params.source.type) {
|
|
||||||
case "inline":
|
|
||||||
return { "textContent": params.source.code };
|
|
||||||
case "url":
|
|
||||||
return { "src": params.source.src };
|
|
||||||
}
|
|
||||||
})(),
|
|
||||||
"type": params.isModule ? "module" : "text/javascript"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
})()
|
|
||||||
);
|
|
||||||
|
|
||||||
document.getElementsByTagName("head")[0][
|
|
||||||
(() => {
|
|
||||||
switch (params.type) {
|
|
||||||
case "javascript":
|
|
||||||
return "appendChild";
|
|
||||||
case "css":
|
|
||||||
return (() => {
|
|
||||||
switch (params.position) {
|
|
||||||
case "append":
|
|
||||||
return "appendChild";
|
|
||||||
case "prepend":
|
|
||||||
return "prepend";
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
}
|
|
||||||
})()
|
|
||||||
](htmlElement);
|
|
||||||
|
|
||||||
return {
|
|
||||||
"prLoaded": dLoaded.pr,
|
|
||||||
"remove": () => htmlElement.remove()
|
|
||||||
};
|
|
||||||
}
|
|
63
src/tools/useInsertScriptTags.ts
Normal file
63
src/tools/useInsertScriptTags.ts
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
export type ScriptTag = ScriptTag.TextContent | ScriptTag.Src;
|
||||||
|
|
||||||
|
export namespace ScriptTag {
|
||||||
|
type Common = {
|
||||||
|
type: "text/javascript" | "module";
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TextContent = Common & {
|
||||||
|
isModule: boolean;
|
||||||
|
sourceType: "textContent";
|
||||||
|
id: string;
|
||||||
|
textContent: string;
|
||||||
|
};
|
||||||
|
export type Src = Common & {
|
||||||
|
isModule: boolean;
|
||||||
|
sourceType: "src";
|
||||||
|
src: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: Loaded scripts cannot be unloaded so we need to keep track of them
|
||||||
|
// to avoid loading them multiple times.
|
||||||
|
const loadedScripts = new Set<string>();
|
||||||
|
|
||||||
|
export function useInsertScriptTags(params: { scriptTags: ScriptTag[] }) {
|
||||||
|
const { scriptTags } = params;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
for (const scriptTag of scriptTags) {
|
||||||
|
const scriptId = (() => {
|
||||||
|
switch (scriptTag.sourceType) {
|
||||||
|
case "src":
|
||||||
|
return scriptTag.src;
|
||||||
|
case "textContent":
|
||||||
|
return scriptTag.textContent;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
if (loadedScripts.has(scriptId)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const htmlElement = document.createElement("script");
|
||||||
|
|
||||||
|
htmlElement.type = scriptTag.type;
|
||||||
|
|
||||||
|
switch (scriptTag.sourceType) {
|
||||||
|
case "src":
|
||||||
|
htmlElement.src = scriptTag.src;
|
||||||
|
break;
|
||||||
|
case "textContent":
|
||||||
|
htmlElement.textContent = scriptTag.textContent;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.head.appendChild(htmlElement);
|
||||||
|
|
||||||
|
loadedScripts.add(scriptId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user