Skip to main content

Custom Block

What is a custom block? Custom block is composed of one or more basic blocks. Since version 1.25.0, Easy Email Pro supports direct MJML syntax and HTML code writing.

Features

  1. Different UI rendering in edit mode and production mode
  2. Support for dynamic data (mergetags) and dynamic rendering
  3. Direct MJML and HTML code writing
  4. In-place text editing via SlateNodePlaceholder

Implementation Guide

1. Define Block Types

First, define your custom block type:

custom-types.ts
import { BasicElement } from "easy-email-pro-core";
import { CustomBlockType } from "./custom";

type CustomType = typeof CustomBlockType;

export interface DemoLogoBlockNode extends BasicElement {
type: CustomType["LOGO"];
data: {
buttonText: string;
};
attributes: {
src: string;
"button-color"?: string;
};
}

declare module "easy-email-pro-core" {
export interface CustomTypes {
Element: DemoLogoBlockNode;
}
}

2. Create Custom Block

Define your block component:

custom/index.tsx
import { createCustomBlock, t, mergeBlock } from "easy-email-pro-core";
import React from "react";
import { ElementCategory } from "easy-email-pro-core";
import { DemoLogoBlockNode } from "src/custom-types";

export const CustomBlockType = {
LOGO: "custom_logo" as const,
};

const defaultData = {
attributes: {
src: "",
},
data: {
buttonText: "Save",
},
};

export const CustomLogo = createCustomBlock<DemoLogoBlockNode>({
get name() {
return t("Custom Logo");
},
defaultData: defaultData,
type: CustomBlockType.LOGO,
void: true,
create: (payload) => {
const data: DemoLogoBlockNode = {
type: CustomBlockType.LOGO,
data: {
...defaultData.data,
},
attributes: {
...defaultData.attributes,
},
children: [],
};
return mergeBlock(data, payload);
},
category: ElementCategory.SECTION,
render(params) {
const { node } = params;
const { data, attributes } = node;

return (
<mj-section {...node.attributes}>
<mj-column>
{data.showLogo ? (
<mj-image
padding="0px 0px 0px 0px"
width="100px"
src={attributes.src}
/>
) : null}
{data.showBtn ? (
<mj-button background-color={attributes["button-color"]} href="#">
{data.buttonText}
</mj-button>
) : null}
</mj-column>
</mj-section>
);
},
});

3. Create Configuration Panel

Create a panel for block configuration:

custom/Panel.tsx
import { Collapse } from "@arco-design/web-react";
import { t } from "easy-email-pro-core";
import { useSelectedNode, ActiveTabKeys } from "easy-email-pro-editor";
import React from "react";
import { Path } from "slate";
import {
ResponsiveTabs,
AttributesPanelWrapper,
CollapseWrapper,
ResponsiveField,
AttributeField,
} from "easy-email-pro-theme";

export const CustomBlockPanel = ({ nodePath }: { nodePath: Path }) => {
return (
<AttributesPanelWrapper>
<ResponsiveTabs
desktop={
<AttributesContainer
mode={ActiveTabKeys.DESKTOP}
nodePath={nodePath}
/>
}
mobile={
<AttributesContainer
mode={ActiveTabKeys.MOBILE}
nodePath={nodePath}
/>
}
/>
</AttributesPanelWrapper>
);
};

function AttributesContainer({
nodePath,
mode,
}: {
mode: ActiveTabKeys;
nodePath: Path;
}) {
const { selectedNode } = useSelectedNode();

if (!selectedNode) return null;

return (
<CollapseWrapper defaultActiveKey={["0", "1"]}>
<Collapse.Item name="0" header={t("Image")}>
<ResponsiveField
component={AttributeField.ImageUrl}
path={nodePath}
name="src"
/>
</Collapse.Item>
<Collapse.Item name="1" header={t("Button")}>
<AttributeField.TextField
label={t("Button Text")}
name="data.buttonText"
path={nodePath}
/>
<ResponsiveField
label={t("Button color")}
component={AttributeField.ColorPickerField}
path={nodePath}
name="button-color"
/>
</Collapse.Item>
</CollapseWrapper>
);
}

4. Register and Use

Register your custom block and add it to the editor:

import { BlockManager, ElementType, t } from "easy-email-pro-core";
import React from "react";
import { EmailEditorProvider } from "easy-email-pro-editor";
import { Retro } from "easy-email-pro-theme";

// Register blocks
BlockManager.registerBlocks([CustomLogo]);
ConfigPanelsMap[CustomLogo.type] = CustomBlockPanel;

const defaultCategories: ThemeConfigProps["categories"] = [
{
get label() {
return t("Content");
},
active: true,
displayType: "grid",
blocks: [
// ... other blocks
{
type: CustomBlockType.LOGO,
icon: (
<IconFont
className={"block-list-grid-item-icon"}
iconName="icon-bag"
/>
),
},
],
},
// ... other categories
];

export default function MyEditor() {
const config = Retro.createConfig({
categories: defaultCategories,
// ... other config
});

return (
<EmailEditorProvider {...config}>
<Retro.Layout />
</EmailEditorProvider>
);
}

Dynamic Content

Logic Components

Easy Email Pro provides built-in logic components:

// ForEach example
<ForEach dataSource={dataSourceKey} itemName="item">
<mj-text>{{item.title}}</mj-text>
</ForEach>

// Show example
<Show
showTruthyInTesting
expression={"order.number == 'Shopify#1001'"}
fallback={<mj-text>false</mj-text>}
>
<mj-text>True</mj-text>
</Show>

Testing vs Production Mode

You can display different content in testing and production modes:

render(params) {
const { node, mode, mergetagsData } = params;

if (mode === 'testing') {
return (
<mj-section {...node.attributes}>
<mj-column>
<mj-text>{mergetagsData.customer.name}</mj-text>
</mj-column>
</mj-section>
);
}

return (
<mj-section {...node.attributes}>
<mj-column>
<mj-text>{{customer.name}}</mj-text>
</mj-column>
</mj-section>
);
}

The reason for using different syntax in testing and production: How it work

HTML Elements

You can use HTML elements directly within mj-text or mj-button:

// Simple HTML
<mj-text>
<p>Hello</p>
</mj-text>

// Iframe example
<mj-text>
<iframe
width={"100%"}
src="https://www.easyemail.pro"
frameBorder="0"
></iframe>
</mj-text>

Important Notes

The void property is crucial for Slate.js rendering:

  • Use void: false for blocks with Element children (e.g., section, column)
  • Use void: true for other cases (recommended)