Install easy-email-core
masterYou can install easy-email-core using npm or yarn.
$ npm install --save easy-email-core
# or
$ yarn add easy-email-corerepository·master·Indexed 25 days ago
https://github.com/zalify/easy-email-editorA developer-friendly drag-and-drop email editor built on top of MJML. It consists of several packages: easy-email-core for JSON to MJML transformation and custom block creation, easy-email-editor for the render and preview container, and easy-email-extensions for structured layouts like StandardLayout and SimpleLayout.
You can install easy-email-core using npm or yarn.
$ npm install --save easy-email-core
# or
$ yarn add easy-email-coreYou can install the easy-email-extensions package using npm or yarn.
$ npm install --save easy-email-extensions$ yarn add easy-email-extensionsInstall the easy-email-editor package using npm or yarn to use the email render and preview container in your project.
$ npm install --save easy-email-editor$ yarn add easy-email-editorTo get started with Easy Email, install the core packages, the editor, extensions, and react-final-form using npm:
$ npm install --save easy-email-core easy-email-editor easy-email-extensions react-final-formTo use the drag-and-drop editor, wrap your application in the EmailEditorProvider and use the SimpleLayout from easy-email-extensions. You must provide initialValues containing a subject, subTitle, and a content object initialized via BlockManager.
Don't forget to import the required CSS files for the editor, extensions, and the Arco theme.
Required CSS imports:
easy-email-editor/lib/style.csseasy-email-extensions/lib/style.css@arco-themes/react-easy-email-theme/css/arco.cssimport React from 'react';
import { BlockManager, BasicType, AdvancedType } from 'easy-email-core';
import { EmailEditor, EmailEditorProvider } from 'easy-email-editor';
import { ExtensionProps, SimpleLayout } from 'easy-email-extensions';
import 'easy-email-editor/lib/style.css';
import 'easy-email-extensions/lib/style.css';
import '@arco-themes/react-easy-email-theme/css/arco.css';
const initialValues = {
subject: 'Welcome to Easy-email',
subTitle: 'Nice to meet you!',
content: BlockManager.getBlockByType(BasicType.PAGE)!.create({}),
};
export default function App() {
return (
<EmailEditorProvider
data={initialValues}
height={'calc(100vh - 72px)'}
autoComplete
dashed={false}
>
{({ values }) => {
return (
<SimpleLayout>
<EmailEditor />
</SimpleLayout>
);
}}
</EmailEditorProvider>
);
}Easy Email offers two primary paths for developers:
An open-source React email editor foundation used for:
A self-hosted commercial SDK designed for SaaS teams to embed a polished editor into their own products. Key features include:
To use the email editor, wrap your application (or the editor section) with EmailEditorProvider. You must provide an initialValues object containing the email structure and specify a height. The EmailEditor component is then rendered within the provider's render function to access the current editor state.
import React from 'react';
import { BlockManager } from 'easy-email-core';
import { EmailEditor, EmailEditorProvider } from 'easy-email-editor';
import 'easy-email-editor/lib/style.css';
const initialValues = {
subject: 'Welcome to Easy-email',
subTitle: 'Nice to meet you!',
content: BlockManager.getBlockByType(BasicType.PAGE).create({}),
};
export function App() {
return (
<EmailEditorProvider
data={initialValues}
height={'calc(100vh - 72px)'}
>
{({ values }) => {
return <EmailEditor />;
}}
</EmailEditorProvider>
);
}To use the extensions in your email editor, import StandardLayout and ExtensionProps from easy-email-extensions. You must also import the corresponding CSS files for both the editor and the extensions to ensure correct styling.
StandardLayout is used to wrap the EmailEditor and provides a structured interface for categories and blocks (such as Content and Layout) that users can drag into the editor.
import React from 'react';
import { BlockManager, BasicType, AdvancedType } from 'easy-email-core';
import { EmailEditor, EmailEditorProvider } from 'easy-email-editor';
import { ExtensionProps, StandardLayout } from 'easy-email-extensions';
import 'easy-email-editor/lib/style.css';
import 'easy-email-extensions/lib/style.css';
const categories: ExtensionProps['categories'] = [
{
label: 'Content',
active: true,
blocks: [
{ type: AdvancedType.TEXT },
{ type: AdvancedType.IMAGE, payload: { attributes: { padding: '0px 0px 0px 0px' } } },
{ type: AdvancedType.BUTTON },
{ type: AdvancedType.SOCIAL },
{ type: AdvancedType.DIVIDER },
{ type: AdvancedType.SPACER },
{ type: AdvancedType.HERO },
{ type: AdvancedType.WRAPPER },
],
},
{
label: 'Layout',
active: true,
displayType: 'column',
blocks: [
{
title: '2 columns',
payload: [
['50%', '50%'],
['33%', '67%'],
['67%', '33%'],
['25%', '75%'],
['75%', '25%'],
],
},
{
title: '3 columns',
payload: [
['33.33%', '33.33%', '33.33%'],
['25%', '25%', '50%'],
['50%', '25%', '25%'],
],
},
{
title: '4 columns',
payload: [['25%', '25%', '25%', '25%']],
},
],
},
];
const initialValues = {
subject: 'Welcome to Easy-email',
subTitle: 'Nice to meet you!',
content: BlockManager.getBlockByType(BasicType.PAGE)!.create({}),
};
export default function App() {
return (
<EmailEditorProvider
data={initialValues}
height={'calc(100vh - 72px)'}
autoComplete
dashed={false}
>
{({ values }) => {
return (
<StandardLayout
categories={categories}
showSourceCode={true}
>
<EmailEditor />
</StandardLayout>
);
}}
</EmailEditorProvider>
);
}Use the JsonToMjml function to convert your email JSON data into MJML format. This is useful for generating the actual email markup from the editor's state.
Options:
data: The JSON object representing the email structure.context: Contextual data (can be null).mode: The rendering mode, e.g., 'production' or 'testing'.import { JsonToMjml } from 'easy-email-core';
const xml = JsonToMjml({
data: json,
context: null,
mode: 'production',
});
console.log(xml);You can extend the editor by creating custom blocks using createCustomBlock.
Workflow:
IBlockData.createCustomBlock to define the name, type, create (for default values), validParentType (to restrict where it can be placed), and render (to define the JSX/component structure).BlockManager.registerBlocks.BlockManager.getBlockByType to retrieve and manipulate the block.import { merge } from 'lodash';
import {
createCustomBlock,
IBlockData,
components,
BasicType,
JsonToMjml,
BlockManager,
} from 'easy-email-core';
const { Section, Column, Image, Button } = components;
type IMyFirstBlock = IBlockData<
{
'background-color': string;
'text-color': string;
},
{
buttonText: string;
imageUrl: string;
}
>;
const myFirstBlock = createCustomBlock({
name: 'My first block',
type: 'MY_FIRST_BLOCK',
create(payload) {
const defaultData: IMyFirstBlock = {
type: 'MY_FIRST_BLOCK',
data: {
value: {
buttonText: 'Got it',
imageUrl:
'http://res.cloudinary.com/dwkp0e1yo/image/upload/v1665841616/pn7npfspxaqfzxiensue.png',
},
},
attributes: {
'background-color': '#4A90E2',
'text-color': '#ffffff',
},
children: [],
};
return merge(defaultData, payload);
},
validParentType: [BasicType.PAGE, BasicType.WRAPPER],
render(
data: IMyFirstBlock,
idx: string | null,
mode: 'testing' | 'production',
context?: IPage,
dataSource?: { [key: string]: any },
) {
const { imageUrl, buttonText } = data.data.value;
const attributes = data.attributes;
const instance = (
<Section padding='20px'>
<Column>
<Image
padding='0px 0px 0px 0px'
width='100px'
src={imageUrl}
/>
<Button
background-color={attributes['background-color']}
color={attributes['text-color']}
href='#'
>
{buttonText}
</Button>
</Column>
</Section>
);
return instance;
},
});
BlockManager.registerBlocks({ myFirstBlock });
const pageBlock = BlockManager.getBlockByType(BasicType.PAGE);
console.log(
JsonToMjml({
data: pageBlock.create({
children: [myFirstBlock.create()],
}),
mode: 'production',
}),
);When building the easy-email-demo project for production, the Vite configuration uses specific aliases and Rollup chunking strategies to optimize the bundle.
The configuration maps several internal packages and dependencies to specific paths to ensure correct resolution during the build process:
@demo: ./srcreact: ./node_modules/reactreact-final-form: ./node_modules/react-final-formeasy-email-localization: ../packages/easy-email-localizationeasy-email-core: ../packages/easy-email-coreeasy-email-editor: ../packages/easy-email-editoreasy-email-extensions: ../packages/easy-email-extensionsminify: true).es2015.html2canvaslodashmjml-browsermjml-browser, html2canvas, and browser-image-compression, the hash is omitted from the filename (e.g., [name].js) to maintain stability. Other chunks use [name]-[hash].js.dashes convention for locals.javascriptEnabled is set to true to support Less features used by components.@arco-design/web-react and @arco-design/web-react/icon components.buildTime meta tag into the HTML.import { defineConfig } from 'vite';
import styleImport from 'vite-plugin-style-import';
import path from 'path';
import { injectHtml } from 'vite-plugin-html';
export default defineConfig({
resolve: {
alias: {
'@demo': path.resolve(__dirname, './src'),
react: path.resolve(__dirname, './node_modules/react'),
'react-final-form': path.resolve(__dirname, './node_modules/react-final-form'),
'easy-email-localization': path.resolve(
__dirname,
'../packages/easy-email-localization',
),
'easy-email-core': path.resolve(__dirname, '../packages/easy-email-core'),
'easy-email-editor': path.resolve(__dirname, '../packages/easy-email-editor'),
'easy-email-extensions': path.resolve(
__dirname,
'../packages/easy-email-extensions',
),
},
},
optimizeDeps: {},
define: {},
build: {
minify: true,
manifest: true,
sourcemap: false,
target: 'es2015',
rollupOptions: {
output: {
manualChunks(id) {
if (//node_modules\/html2canvas\/.*/.test(id)) {
return 'html2canvas';
}
if (//node_modules\/lodash\/.*/.test(id)) {
return 'lodash';
}
if (//node_modules\/mjml-browser\/.*/.test(id)) {
return 'mjml-browser';
}
},
chunkFileNames(info) {
if (
['mjml-browser', 'html2canvas', 'browser-image-compression'].some(name =>
info.name?.includes(name),
),
) {
return '[name].js';
}
return '[name]-[hash].js';
},
},
},
},
css: {
modules: {
localsConvention: 'dashes',
},
preprocessorOptions: {
scss: {},
less: {
javascriptEnabled: true,
},
},
},
plugins: [
styleImport({
libs: [
{
libraryName: '@arco-design/web-react',
libraryNameChangeCase: 'pascalCase',
esModule: true,
resolveStyle: name => `@arco-design/web-react/es/${name}/style/index`,
},
{
libraryName: '@arco-design/web-react/icon',
libraryNameChangeCase: 'pascalCase',
resolveStyle: name => `@arco-design/web-react/icon/react-icon/${name}`,
resolveComponent: name => `@arco-design/web-react/icon/react-icon/${name}`,
},
],
}),
injectHtml({
data: {
buildTime: `<meta name="updated-time" content="${new Date().toUTCString()}" />`,
},
}),
].filter(Boolean),
});The demo application uses a pre-configured axiosInstance with a base URL of https://www.maocanhua.cn. It includes a request interceptor that automatically attaches an authorization token retrieved from UserStorage.getToken() to the authorization header of every request. It also includes a response interceptor that flattens the response to return the data directly and enhances error objects by extracting the message from error.response.data.message if available.
import axios, { AxiosResponse, AxiosRequestConfig } from 'axios';
import { UserStorage } from '@demo/utils/user-storage';
export const axiosInstance = axios.create({
baseURL: 'https://www.maocanhua.cn',
});
axiosInstance.interceptors.request.use(async function (config) {
try {
const token = await UserStorage.getToken();
if (!config.headers) {
config.headers = {};
}
config.headers.authorization = token;
} catch (error) {
// window.location.assign(LOGIN_ADDRESS);
} finally {
return config;
}
});
axiosInstance.interceptors.response.use(
function <T>(res: AxiosResponse<T>) {
return new Promise((resolve, reject) => {
return resolve(res);
});
},
(error) => {
throw {
...error,
message: error?.response?.data?.message || error?.message || error,
};
}
});