Skip to main content

Custom block

What is a custom block?Custom block is composed of one or more basic blocks.

In the 1.25.0 version, we have already supported the syntax of MJML, and you can use the mj-x format directly. At the same time, it also supports direct HTML code writing.

Please note, the previous components were imported from easy-email-pro-core, but mj-x doesn't need to be imported, it's just syntactic sugar.

Supported features

  1. Different UI can be used in testing (Edit mode) and production modes.For instance,
  2. It can receive mergetagsData data, and also dynamic rendering can be done in Edit mode, such as rendering product lists.
  3. Direct MJML codes can be written, for example, <mj-text><p style={{ color: "red", fontSize: 40 }}>Text here.</p>
  4. In custom block, users can edit text directly via SlateNodePlaceholder.

Write a custom block

We define its type here

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

Customize the display of components in emails

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

Here is the block element 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>
);
}

After completing the above, we need to register the custom block and display the custom block in the blocks panel on the left

import { BlockManager, ElementType, t } from "easy-email-pro-core";
import React, { useCallback, useMemo } from "react";
import {
EmailEditorProvider,
EmailEditorProps,
EmailTemplate,
} from "easy-email-pro-editor";
import { Retro } from "easy-email-pro-theme";
import 'easy-email-pro-theme/lib/style.css';
import '@arco-themes/react-easy-email-pro-retro/css/arco.css';

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

const defaultCategories: ThemeConfigProps["categories"] = [
{
get label() {
return t("Content");
},
active: true,
displayType: "grid",
blocks: [
{
type: ElementType.STANDARD_PARAGRAPH,
icon: (
<IconFont
className={"block-list-grid-item-icon"}
iconName="icon-text"
/>
),
},
{
type: ElementType.STANDARD_IMAGE,
payload: { attributes: { padding: "0px 0px 0px 0px" } },
icon: (
<IconFont
className={"block-list-grid-item-icon"}
iconName="icon-img"
/>
),
},
{
type: ElementType.STANDARD_BUTTON,
icon: (
<IconFont
className={"block-list-grid-item-icon"}
iconName="icon-button"
/>
),
},
{
type: ElementType.STANDARD_DIVIDER,
icon: (
<IconFont
className={"block-list-grid-item-icon"}
iconName="icon-divider"
/>
),
},
{
type: ElementType.STANDARD_SPACER,
icon: (
<IconFont
className={"block-list-grid-item-icon"}
iconName="icon-spacing"
/>
),
},
{
type: ElementType.STANDARD_NAVBAR,
icon: (
<IconFont
className={"block-list-grid-item-icon"}
iconName="icon-navbar"
/>
),
},
{
type: ElementType.STANDARD_SOCIAL,
icon: (
<IconFont
className={"block-list-grid-item-icon"}
iconName="icon-social"
/>
),
},
{
type: ElementType.STANDARD_HERO,
icon: (
<IconFont
className={"block-list-grid-item-icon"}
iconName="icon-hero"
/>
),
},
{
type: ElementType.MARKETING_SHOPWINDOW,
icon: (
<IconFont
className={"block-list-grid-item-icon"}
iconName="icon-bag"
/>
),
},
{
type: CustomBlockType.LOGO,
icon: (
<IconFont
className={"block-list-grid-item-icon"}
iconName="icon-bag"
/>
),
},
],
},
{
get label() {
return t("Layout");
},
active: true,
displayType: "column",
blocks: [
{
get title() {
return t("1 column");
},
payload: [["100%"]],
},
{
get title() {
return t("2 column");
},
payload: [
["50%", "50%"],
["33%", "67%"],
["67%", "33%"],
["25%", "75%"],
["75%", "25%"],
],
},
{
get title() {
return t("3 column");
},
payload: [
["33.33%", "33.33%", "33.33%"],
["25%", "50%", "25%"],
["25%", "25%", "50%"],
["50%", "25%", "25%"],
],
},
{
get title() {
return t("4 column");
},
payload: [["25%", "25%", "25%", "25%"]],
},
],
},
];

export default function MyEditor() {

const config = Retro.createConfig({
...
categories: defaultCategories,
})

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

Logic

When defining business components, we usually use logic such as if-else, foreach, etc. But a lot of the time, this logic code is compiled into templates for the backend to render. To make things convenient, we have defined the ForEach and Show components. The usage format is as follows.


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


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

testing/production mode

We can display different content in testing(editing) mode and production(preview) mode

For example, we want the customer's name to be linked with dynamic data during the editing process.

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

const { data, attributes } = node;

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


Why use mergetagsData.customer.name in testing mode and {{customer.name}} in production mode?

Let's take a look at the process from JSON to an email.

How it work

Because in this render function, if it is directly combined with mergetagsData, the generated HTML cannot be cached. The same JSON can be converted to HTML, which can be cached (since this step is a bit slower). Then it can be combined with different dynamic data (this step is very fast)

Using HTML elements

With mj-text or mj-button, you can directly embed HTML elements.

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

MORE

The custom block has an important attribute, void, which is used to control the rendering of slatejs. Generally, blocks with Element children are void = false, such as section and column, etc. Otherwise, it is recommended to be true in other cases.