easy-email

repository·master·Indexed 25 days ago

https://github.com/zalify/easy-email-editor

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

Tokens
44.1K
Snippets
64
Records
315
Agent score
84%

What's inside easy-email

  1. Implement the EmailEditor with EmailEditorProvider

    master

    To 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.css
    • easy-email-extensions/lib/style.css
    • @arco-themes/react-easy-email-theme/css/arco.css
    import 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>
      );
    }
  2. Overview of Easy Email Open Source vs Pro

    master

    Easy Email offers two primary paths for developers:

    Easy Email OSS (Open Source)

    An open-source React email editor foundation used for:

    • Inspecting block data.
    • Editing templates.
    • Generating MJML output.
    • Drag-and-drop email editing.

    Easy Email Pro

    A self-hosted commercial SDK designed for SaaS teams to embed a polished editor into their own products. Key features include:

    • Self-hosted SDK: Embed the editor in your product while keeping template data in your own infrastructure.
    • Custom Blocks (CB): Ship reusable product-specific blocks (e.g., for orders, catalogs, or campaigns).
    • AI Assistant (AI): Rewrite copy, generate assets, and refine layouts.
    • Block Studio (BS): Visually compose reusable widget elements and expose specific settings.
    • Responsive Previews (RP): Review desktop and mobile layouts.
    • File Manager (FM): Centralize brand assets, uploads, and media.
  3. Implement the EmailEditor component

    master

    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>
      );
    }
  4. Use easy-email-extensions with StandardLayout

    master

    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>
      );
    }
  5. Transform JSON to MJML using JsonToMjml

    master

    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);
  6. Create a custom block with createCustomBlock

    master

    You can extend the editor by creating custom blocks using createCustomBlock.

    Workflow:

    1. Define your block's data structure using IBlockData.
    2. Use 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).
    3. Register the block using BlockManager.registerBlocks.
    4. Use 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',
      }),
    );
  7. Configure Vite for production builds in easy-email-demo

    master

    When building the easy-email-demo project for production, the Vite configuration uses specific aliases and Rollup chunking strategies to optimize the bundle.

    Module Aliases

    The configuration maps several internal packages and dependencies to specific paths to ensure correct resolution during the build process:

    • @demo: ./src
    • react: ./node_modules/react
    • react-final-form: ./node_modules/react-final-form
    • easy-email-localization: ../packages/easy-email-localization
    • easy-email-core: ../packages/easy-email-core
    • easy-email-editor: ../packages/easy-email-editor
    • easy-email-extensions: ../packages/easy-email-extensions

    Build Optimization

    • Minification: Enabled (minify: true).
    • Target: es2015.
    • Manual Chunking: To improve loading performance, specific heavy dependencies are split into their own chunks:
      • html2canvas
      • lodash
      • mjml-browser
    • Chunk Naming: For the chunks mjml-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.

    CSS Configuration

    • CSS Modules: Uses dashes convention for locals.
    • Less: javascriptEnabled is set to true to support Less features used by components.

    Plugins

    • vite-plugin-style-import: Configured to automatically import styles for @arco-design/web-react and @arco-design/web-react/icon components.
    • vite-plugin-html: Injects a 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),
    });
  8. Configure the Axios request instance

    master

    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,
        };
      }
    });