The easiest way to edit MJML in React/Vue.
Smart MJML is a library that provides TypeScript intelligence hints and checks, making editing MJML simpler.
-
Syntax Consistency: Smart MJML offers an almost consistent coding style with MJML. You can write your code just like you're writing MJML, while enjoying the intelligence hints and compile-time checks provided by TypeScript.
-
Platform Compatibility: Smart MJML can run on both the browser and Node.js. No matter which platform you're developing on, Smart MJML can provide you with a consistent experience.
-
Template Engine Compatibility: You can easily integrate Smart MJML with other template engines, without worrying about layout confusion.
-
Framework Support: Smart MJML already supports React and Vue, and in theory, it can be easily integrated with other JSX frameworks.
Open CodeSandbox, edit template.tsx, and you will see TypeScript hints.
npm install smart-mjml
Usage:
import mjml2html from "mjml-browser";
// When using vue, use vueJsxToString
import { reactJsxToString } from "smart-mjml";
const mjmlString = reactJsxToString(
<mjml>
<mj-head>
<mj-style inline="inline">
{`.hide-desktop-block,.hide-desktop-inline-block,.hide-block{display:none!important;mso-hide:all!important}`}
</mj-style>
<mj-style>
{`.mjbody a{color:inherit}.overflow-hidden{overflow:hidden}@media (max-width: 480px){.hide-mobile-block{max-height:0px;overflow:hidden;display:none!important}.hide-desktop-block{display:block!important}.hide-mobile-inline-block{max-height:0px;overflow:hidden;display:none!important}.hide-desktop-inline-block{display:inline-block!important}}`}
</mj-style>
<mj-style>
{`.mjbody a, .mjbody a:hover, .mjbody a:active, .mjbody a[href], .mjbody a[href]:hover, .mjbody a[href]:active {color: inherit;text-decoration: underline}`}
</mj-style>
<mj-breakpoint width="480px"></mj-breakpoint>
</mj-head>
<mj-body width="600px" background-color="#8C9A80" css-class="mjbody">
<mj-section padding-bottom="0px" background-color="#263D29">
<mj-column width="100%">
<mj-text
align="center"
color="#FFFFFF"
font-family="Arial, sans-serif"
font-size="16px"
line-height="130%"
padding-top="3px"
padding-bottom="3px"
font-weight="normal"
>
JOIN US FOR A FEAST ON
</mj-text>
</mj-column>
</mj-section>
<mj-section
padding-top="0px"
padding-bottom="0px"
background-color="#263D29"
>
<mj-column width="100%">
<mj-image
padding="0px 0px 0px 0px"
padding-top="0px"
padding-right="0px"
padding-bottom="0px"
padding-left="0px"
src="http://res.cloudinary.com/djnkpbshx/image/upload/v1706006437/easy-email-pro-test/u7tvjqdbjtxqkykaazra.jpg"
></mj-image>
</mj-column>
</mj-section>
</mj-body>
</mjml>
);
const data = mjml2html(mjmlString);
function App() {
return (
<div style={{ display: "flex", height: "100vh" }}>
<div
style={{
width: "50%",
whiteSpace: "pre",
backgroundColor: "#1f2227",
color: "#ffffff",
height: "100%",
overflow: "auto",
}}
>
{mjmlString}
</div>
<iframe
style={{ width: "50%", minWidth: 600, height: "100%" }}
frameBorder={"none"}
srcDoc={data.html}
/>
<style>{`body {margin:0}`}</style>
</div>
);
}
Due to the syntax restrictions of JSX, comments cannot be used directly and need to be wrapped as a string.
For example, <!-- Your comment here -->
needs to be written as { '<!-- Your comment here -->' }
.
and <mj-style>.css{color: red;}</mj-style>
needs to be written as <mj-style>{ '.css{color: red;}' }</mj-style>
.
Let's first talk about the common way of integrating with template engines. Here, we take liquidjs as an example.
...
<mj-wrapper background-color="#ffffff" padding-right="10px" padding-left="10px">
<mj-section background-color="#FFFFFF" padding="0px">
<mj-column padding="0px" border="none">
<mj-text
align="center"
font-family="Arial, sans-serif"
font-size="24px"
line-height="130%"
font-weight="600"
padding-top="3px"
padding-bottom="3px"
>
You might also like
</mj-text>
</mj-column>
</mj-section>
<mj-section background-color="#FFFFFF" padding="0px">
<mj-group>
<mj-raw>
{`<!-- htmlmin:ignore -->{% for item in products %}<!-- htmlmin:ignore -->`}
</mj-raw>
<mj-column width="50%">
<mj-image
border-radius="5px"
padding="10px"
src="{{item.image}}"
></mj-image>
<mj-text
align="center"
font-family="Arial, sans-serif"
font-size="16px"
padding="10px 0px 0px 0px"
>{`{{item.title}}`}</mj-text>
<mj-text
align="center"
font-family="Arial, sans-serif"
padding="10px 0px 0px 0px"
>{`{{item.price}}`}</mj-text>
<mj-text
align="center"
color="#737373"
font-family="Arial, sans-serif"
text-decoration="line-through"
padding="10px 0px 0px 0px"
>{`{{item.comparePrice}}`}</mj-text>
<mj-button
background-color="#000000"
font-family="Arial, sans-serif"
href="#"
>
Buy now
</mj-button>
</mj-column>
<mj-raw>
{`<!-- htmlmin:ignore -->{% endfor %}<!-- htmlmin:ignore -->`}
</mj-raw>
</mj-group>
</mj-section>
</mj-wrapper>
...
import { Liquid } from "liquidjs";
const engine = new Liquid();
...
<iframe
style={{ width: "50%", minWidth: 600, height: "100%", border: "none" }}
srcDoc={engine.parseAndRenderSync(data.html, dynamicData)}
/>
...
It works, but it's still not pretty. We can directly define a custom helper to solve this problem.
- Define the interface, create a new jsx.d.ts
declare namespace JSX {
interface IntrinsicElements {
"mj-for": {
item: string;
list: string;
children: any;
};
}
}
- Define the helper
const helper = (
type: string,
attributes: Record<string, string>,
children: string
): string | null => {
if (type === "mj-for") {
const itemName = attributes.item;
const listName = attributes.list;
return `
<mj-raw>
<!-- htmlmin:ignore -->{% for ${itemName} in ${listName} %}<!-- htmlmin:ignore -->
</mj-raw>
${children}
<mj-raw>
<!-- htmlmin:ignore -->{% endfor %}<!-- htmlmin:ignore -->
</mj-raw>
`;
}
return null;
};
That's it, we can now use it.
<mj-wrapper background-color="#ffffff" padding-right="10px" padding-left="10px">
<mj-section background-color="#FFFFFF" padding="0px">
<mj-column padding="0px" border="none">
<mj-text
align="center"
font-family="Arial, sans-serif"
font-size="24px"
line-height="130%"
font-weight="600"
padding-top="3px"
padding-bottom="3px"
>
You might also like
</mj-text>
</mj-column>
</mj-section>
<mj-section background-color="#FFFFFF" padding="0px">
<mj-group>
<mj-for item="item" list="products">
<mj-column width="50%">
<mj-image
border-radius="5px"
padding="10px"
src="{{item.image}}"
></mj-image>
<mj-text
align="center"
font-family="Arial, sans-serif"
font-size="16px"
padding="10px 0px 0px 0px"
>{`{{item.title}}`}</mj-text>
<mj-text
align="center"
font-family="Arial, sans-serif"
padding="10px 0px 0px 0px"
>{`{{item.price}}`}</mj-text>
<mj-text
align="center"
color="#737373"
font-family="Arial, sans-serif"
text-decoration="line-through"
padding="10px 0px 0px 0px"
>{`{{item.comparePrice}}`}</mj-text>
<mj-button
background-color="#000000"
font-family="Arial, sans-serif"
href="#"
>
Buy now
</mj-button>
</mj-column>
</mj-for>
</mj-group>
</mj-section>
</mj-wrapper>
npm run dev