I'm trying to create a client-side email editor. The client will write JSX (using mjml-react) on the left, and see a preview of the output on the right. This is for internal use so I'm OK with large bundles and use of eval if I have to.
Here is my code so far:
import * as React from "react";
import {
MonacoJsxSyntaxHighlight,
getWorker,
} from "monaco-jsx-syntax-highlight";
import Editor from "@monaco-editor/react";
import { useCallback, useEffect, useState } from "react";
import { renderToMjml } from "@faire/mjml-react/utils/renderToMjml";
import mjml2html from "mjml-browser";
import "shared/tailwind.css";
import {
Mjml,
MjmlBody,
MjmlButton,
MjmlColumn,
MjmlHead,
MjmlImage,
MjmlPreview,
MjmlSection,
MjmlTitle,
} from "@faire/mjml-react";
const defaultCode = (
<Mjml>
<MjmlHead>
<MjmlTitle>Last Minute Offer</MjmlTitle>
<MjmlPreview>Last Minute Offer...</MjmlPreview>
</MjmlHead>
<MjmlBody width={500}>
<MjmlSection fullWidth backgroundColor="#efefef">
<MjmlColumn>
<MjmlImage src="https://static.wixstatic.com/media/5cb24728abef45dabebe7edc1d97ddd2.jpg" />
</MjmlColumn>
</MjmlSection>
<MjmlSection>
<MjmlColumn>
<MjmlButton
padding="20px"
backgroundColor="#346DB7"
href="https://www.wix.com/"
>
I like it!
</MjmlButton>
</MjmlColumn>
</MjmlSection>
</MjmlBody>
</Mjml>
);
export default function () {
const [email, setEmail] = useState<string>();
const [html, setHtml] = useState<string>("");
useEffect(() => {
const mjml = renderToMjml(defaultCode);
const rendered = mjml2html(mjml);
setHtml(rendered.html);
}, []);
const handleEditorDidMount = useCallback((editor: any, monaco: any) => {
monaco.languages.typescript.typescriptDefaults.setCompilerOptions({
jsx: monaco.languages.typescript.JsxEmit.Preserve,
target: monaco.languages.typescript.ScriptTarget.ES2020,
esModuleInterop: true,
});
const monacoJsxSyntaxHighlight = new MonacoJsxSyntaxHighlight(
getWorker(),
monaco
);
// editor is the result of monaco.editor.create
const { highlighter, dispose } =
monacoJsxSyntaxHighlight.highlighterBuilder({
editor: editor,
});
// init highlight
highlighter();
editor.onDidChangeModelContent(() => {
// content change, highlight
highlighter();
});
return dispose;
}, []);
return (
<div className={"m-auto max-w-7xl grid grid-cols-2 h-[100vh]"}>
<Editor
className={"editor"}
height={"80%"}
onMount={handleEditorDidMount}
onChange={(e) => setEmail(e)}
theme={"vs-dark"}
path={"file:///index.tsx"}
defaultLanguage="typescript"
options={{
fontSize: 16,
lineHeight: 28,
automaticLayout: true,
}}
/>
<div
className={"row-span-2"}
dangerouslySetInnerHTML={{ __html: html }}
></div>
</div>
);
}
The above works, but there is no interactivity - inputting JSX into the editor does nothing, and the rendered HTML is from separate pre-written JSX.
I have a significant problem: I can get output from my Monaco editor (the JSX that I'm writing) as a string, but I need to turn it into a ReactElement to pass to renderToMjml. I think I'm going to need to pass my editor output to a compiler (esbuild-wasm? @babel/standalone?) I'm not sure how to turn that output into a ReactElement however. Is this possible or straight crazy?
(Ideally I'd love to have a separate editor tab with variables inserted into the JSX for rendering. But that's down the line).