Live mjml-react editor

224 views Asked by At

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).

0

There are 0 answers