feat: Add support for HTML fragments (#845)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
Juro Oravec 2024-12-19 10:03:35 +01:00 committed by GitHub
parent 6681fc0085
commit 4dab940db8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 1225 additions and 246 deletions

View file

@ -1,22 +1,14 @@
/** This file defines the API of the JS code. */
import { createComponentsManager } from './manager';
import { unescapeJs } from './utils';
export type * from './manager';
export const Components = (() => {
const manager = createComponentsManager();
/** Unescape JS that was escaped in Django side with `escape_js` */
const unescapeJs = (escapedJs: string) => {
return new DOMParser().parseFromString(escapedJs, 'text/html').documentElement.textContent;
};
return {
manager,
createComponentsManager,
unescapeJs,
};
})();
export const Components = {
manager: createComponentsManager(),
createComponentsManager,
unescapeJs,
};
// In browser, this is accessed as `Components.manager`, etc
globalThis.Components = Components;

View file

@ -1,5 +1,7 @@
/** The actual code of the JS dependency manager */
import { callWithAsyncErrorHandling } from './errorHandling';
import { observeScriptTag } from './mutationObserver';
import { unescapeJs } from './utils';
type MaybePromise<T> = Promise<T> | T;
@ -233,24 +235,35 @@ export const createComponentsManager = () => {
toLoadCssTags: string[];
toLoadJsTags: string[];
}) => {
const loadedCssUrls = inputs.loadedCssUrls.map((s) => atob(s));
const loadedJsUrls = inputs.loadedJsUrls.map((s) => atob(s));
const toLoadCssTags = inputs.toLoadCssTags.map((s) => atob(s));
const toLoadJsTags = inputs.toLoadJsTags.map((s) => atob(s));
// Mark as loaded the CSS that WAS inlined into the HTML.
inputs.loadedCssUrls.forEach((s) => markScriptLoaded("css", s));
inputs.loadedJsUrls.forEach((s) => markScriptLoaded("js", s));
loadedCssUrls.forEach((s) => markScriptLoaded("css", s));
loadedJsUrls.forEach((s) => markScriptLoaded("js", s));
// Load CSS that was not inlined into the HTML
// NOTE: We don't need to wait for CSS to load
Promise
.all(inputs.toLoadCssTags.map((s) => loadCss(s)))
.all(toLoadCssTags.map((s) => loadCss(s)))
.catch(console.error);
// Load JS that was not inlined into the HTML
const jsScriptsPromise = Promise
// NOTE: Interestingly enough, when we insert scripts into the DOM programmatically,
// the order of execution is the same as the order of insertion.
.all(inputs.toLoadJsTags.map((s) => loadJs(s)))
.all(toLoadJsTags.map((s) => loadJs(s)))
.catch(console.error);
};
// Initialise the MutationObserver that watches for `<script>` tags with `data-djc` attribute
observeScriptTag((script) => {
const data = JSON.parse(script.text);
_loadComponentScripts(data);
});
return {
callComponent,
registerComponent,
@ -258,6 +271,5 @@ export const createComponentsManager = () => {
loadJs,
loadCss,
markScriptLoaded,
_loadComponentScripts,
};
};

View file

@ -0,0 +1,27 @@
/** Set up MutationObserver that watches for `<script>` tags with `data-djc` attribute */
export const observeScriptTag = (onScriptTag: (node: HTMLScriptElement) => void) => {
const observer = new MutationObserver((mutationsList) => {
for (const mutation of mutationsList) {
if (mutation.type === "childList") {
// Check added nodes
mutation.addedNodes.forEach((node) => {
if (
node.nodeName === "SCRIPT" &&
(node as HTMLElement).hasAttribute("data-djc")
) {
onScriptTag(node as HTMLScriptElement);
}
});
2;
}
}
});
// Observe the entire document
observer.observe(document, {
childList: true,
subtree: true, // To detect nodes added anywhere in the DOM
});
return observer;
};

View file

@ -1,10 +1,19 @@
// Helper functions taken from @vue/shared
/** Unescape JS that was escaped in Django side with `escape_js` */
export const unescapeJs = (escapedJs: string) => {
const doc = new DOMParser().parseFromString(escapedJs, "text/html")
return doc.documentElement.textContent as string;
};
// ////////////////////////////////////////////////////////
// Helper functions below were taken from @vue/shared
// See https://github.com/vuejs/core/blob/91112520427ff55941a1c759d7d60a0811ff4a61/packages/shared/src/general.ts#L105
// ////////////////////////////////////////////////////////
export const isArray = Array.isArray;
export const isFunction = (val: unknown): val is Function => typeof val === 'function';
export const isFunction = (val: unknown): val is Function =>
typeof val === "function";
export const isObject = (val: unknown): val is Record<any, any> => {
return val !== null && typeof val === 'object';
return val !== null && typeof val === "object";
};
export const isPromise = <T = any>(val: unknown): val is Promise<T> => {
return (