Skip to content

Instantly share code, notes, and snippets.

@imp-dance
Created April 21, 2022 17:08
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save imp-dance/f78106eaa44d6ce63234f74cdea08c9b to your computer and use it in GitHub Desktop.
Save imp-dance/f78106eaa44d6ce63234f74cdea08c9b to your computer and use it in GitHub Desktop.
A fine way to preview user-provided code in an iFrame
import React, { useState, useRef, useEffect } from "react";
import { transform } from "@babel/standalone";
import { TransformOptions } from "@babel/core";
import { useDebounce } from "use-debounce";
import { v4 } from "uuid";
type UseCodePreviewArgs = {
js: string | undefined;
html: string | undefined;
css: string | undefined;
log?: (type: string, ...args: any[]) => void;
onError?: (error: string) => void;
scripts?: string[];
disableBabel?: boolean;
babelOpts?: TransformOptions;
debounceTime?: number;
};
export function useCodePreview({
js,
html,
css,
log,
onError,
scripts,
disableBabel,
babelOpts,
debounceTime,
}: UseCodePreviewArgs): [
string,
React.RefObject<HTMLIFrameElement>,
() => void
] {
const [frameKey, setFrameKey] = useState("1");
const frameRef = useRef<HTMLIFrameElement>(null);
const [debouncedJs] = useDebounce(js, debounceTime ?? 500);
const [debouncedHTML] = useDebounce(html, debounceTime ?? 500);
const [debouncedCSS] = useDebounce(css, debounceTime ?? 500);
const getContext = (): [Document, Window] => [
frameRef?.current?.contentWindow?.document as Document,
frameRef?.current?.contentWindow as Window,
];
const refresh = () => setFrameKey(v4());
const loadScript = (
scriptURL: string,
callback: () => void
) => {
const [frameDoc] = getContext();
const scriptTag = frameDoc.createElement("script");
scriptTag.onload = () => {
callback();
};
scriptTag.src = scriptURL;
scriptTag.crossOrigin = "crossorigin";
frameDoc.body.appendChild(scriptTag);
};
const injectScriptsAndLog = (callback: () => void) => {
const [frameDoc, frameWindow] = getContext();
if (frameDoc && frameWindow) {
const onScriptLoaded = (index: number) => {
if (
(index === 0 && scripts?.length === 0) ||
scripts === undefined ||
index === scripts?.length - 1
) {
// done
callback();
} else {
// que next
loadScript(scripts[index + 1], () =>
onScriptLoaded(index + 1)
);
}
};
if (scripts?.length) {
loadScript(scripts[0], () => onScriptLoaded(0));
} else {
onScriptLoaded(0);
}
if (log) {
(frameWindow as any).console.log = (...args: any[]) =>
log("log", ...args);
(frameWindow as any).console.warn = (...args: any[]) =>
log("warn", ...args);
(frameWindow as any).console.info = (...args: any[]) =>
log("info", ...args);
}
}
};
const removeScriptTagsFromDoc = () => {
const [frameDoc] = getContext();
if (frameRef?.current && frameDoc) {
const existingTags =
frameDoc.querySelectorAll(".___generated");
existingTags.forEach((tag) => {
tag.parentElement?.removeChild(tag);
});
}
};
const clearCanvas = () => {
const [frameDoc] = getContext();
if (frameRef?.current && frameDoc) {
frameDoc.body.innerHTML = "";
}
};
const injectUserCode = () => {
const [frameDoc] = getContext();
if (frameRef?.current) {
if (html) {
const htmlRoot = frameDoc.createElement("div");
htmlRoot.id = `koding-no-root`;
htmlRoot.innerHTML = html ?? "";
frameDoc.body.appendChild(htmlRoot);
}
if (js) {
const scriptTag = frameDoc.createElement("script");
scriptTag.className = "___generated";
try {
scriptTag.innerHTML =
transform !== undefined && !disableBabel
? transform(
js,
babelOpts ?? {
presets: ["es2015", "react"],
}
).code ?? ""
: js;
onError?.("");
} catch (err) {
onError?.(JSON.stringify(err, null, 2));
}
frameDoc.body.appendChild(scriptTag);
}
if (css) {
const styleTag = frameDoc.createElement("style");
styleTag.className = "___generated";
styleTag.innerHTML = css;
frameDoc.body.appendChild(styleTag);
}
}
};
useEffect(() => {
if (frameRef?.current && js) {
clearCanvas();
removeScriptTagsFromDoc();
injectScriptsAndLog(() => {
injectUserCode();
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [frameKey]);
useEffect(() => {
refresh();
}, [debouncedJs]);
useEffect(() => {
refresh();
}, [debouncedCSS]);
useEffect(() => {
refresh();
}, [debouncedHTML]);
return [frameKey, frameRef, refresh];
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment