Skip to main content

Interfaces

Easy Email Pro is built on top of Slate.js, a framework for building rich text editors. As a result, many concepts and data structures are similar to Slate. The core principle is that Easy Email Pro works with pure JSON objects that conform to specific TypeScript interfaces.

Understanding these interfaces is crucial for:

  • Creating custom blocks
  • Manipulating templates programmatically
  • Extending editor functionality
  • Understanding the data structure

Core Concept​

All email templates in Easy Email Pro are represented as JSON trees, similar to how Slate represents documents. Each node in the tree is either:

  • A TextNode (leaf node containing text)
  • An Element (container node containing other nodes)

For example:

TextNode​

A TextNode represents a leaf node containing text content. Text nodes can have formatting applied to them (bold, italic, colors, links, etc.).

type TextNode = {
text: string;
bold?: boolean;
italic?: boolean;
underline?: boolean;
strikethrough?: boolean;
color?: string;
bgColor?: string;
link?: {
href?: string;
blank?: boolean;
} | null;
};

Example:

{
"text": "Hello World",
"bold": true,
"color": "#000000",
"link": {
"href": "https://example.com",
"blank": true
}
}

Key Points:

  • Text nodes are always leaves (they cannot have children)
  • All formatting is optional
  • Links can be applied to text spans

Element​

An Element represents a container node that can hold other elements or text nodes. Elements have:

  • A type that identifies what kind of element it is (e.g., "standard-section", "standard-text")
  • attributes for styling and configuration (CSS properties, MJML attributes)
  • data for custom data storage
  • children array containing child elements or text nodes
  • Optional responsive attributes for mobile/desktop differences
  • Optional logic for conditional rendering and iteration
interface Element<
K extends { [key: string]: any } = any,
T extends { [key: string]: any } = any,
Type extends string = string,
> {
uid?: string; // Unique identifier (optional)
title?: string; // Display title (optional)
type: Type; // Element type (required)
data: T; // Custom data storage
logic?: {
// Dynamic rendering logic
condition?: LogicCondition;
iteration?: LogicIteration;
};
visible?: "desktop" | "mobile"; // Visibility control
mobileAttributes?: Omit<K, "css-class" | "mj-class"> &
Partial<{
"css-class": string;
"mj-class": string;
}>;
attributes: Omit<K, "css-class" | "mj-class"> &
Partial<{
"css-class": string;
"mj-class": string;
}>;
children: Array<Element | TextNode>;
}

Example Element:

{
"type": "standard-section",
"data": {},
"attributes": {
"background-color": "#ffffff",
"padding": "20px"
},
"children": [
{
"type": "standard-column",
"data": {},
"attributes": {},
"children": [
{
"type": "standard-paragraph",
"data": {},
"attributes": {
"font-size": "16px",
"color": "#333333"
},
"children": [
{
"text": "Hello, World!"
}
]
}
]
}
]
}

Important Notes:

  • All elements must have a children array, even if empty
  • Empty elements should have children: [{ text: "" }] to be consistent with Slate behavior
  • The children array cannot be null or undefined

Each Element is an instance created by the ElementDefinition, which needs to be registered to the BlockManager before it knows how to render. By default, all the base block definitions are already registered to BlockManager, and you only need BlockManager.getBlockByType(type) to get its definition.

type ElementDefinition<T extends Element = Element> = {
name: string;
type: Element["type"];
create: (payload?: RecursivePartial<T>) => T;
void?: boolean;
inlineElement?: boolean;
category: ElementCategoryType;
defaultData: Omit<T, "children" | "type">;
render: (node: {
node: T;
mode: "testing" | "production";
context: { content: PageElement };
children?: React.ReactNode;
idx?: string | null;
keepEmptyAttributes: boolean;
mergetagsData?: Record<string, any>;
}) => React.ReactNode;
};

void​

Corresponding to the void element of the slate, please note than when defining the custom block. When void=true, children will not and cannot be rendered.

inlineElement​

Corresponding to the inline element of the slate, the MergeTag element is an inline element

category​

type ElementCategoryType =
| "DIVIDER"
| "PAGE"
| "RAW"
| "HERO"
| "WRAPPER"
| "SECTION"
| "COLUMN"
| "GROUP"
| "TEXT"
| "BUTTON"
| "IMAGE"
| "NAVBAR"
| "SOCIAL"
| "SPACER"

// extra
| "TEXT_LIST"
| "TEXT_LIST_ITEM"
| "INLINE_TEXT"
| "UNSET";

Examples of ElementDefinitions, such as the definition of StandColumn

const defaultData: ElementDefinition<ColumnElement>["defaultData"] = {
attributes: {
direction: "ltr",
"vertical-align": "top",
width: "100%",
},
data: {},
};

export const StandardColumn = createBlock<StandardColumnElement>({
get name() {
return t("Column");
},
defaultData,
type: StandardType.STANDARD_COLUMN,
create: (payload) => {
const defaultData: StandardColumnElement = {
type: StandardType.STANDARD_COLUMN,
data: {},
attributes: {},
children: [],
};
return mergeBlock(defaultData, payload);
},
category: ElementCategory.COLUMN,
render(params) {
const { node } = params;
return (
<Column
idx={params.idx}
{...node.attributes}
data={node.data}
children={node.children}
/>
);
},
});

Some people may have discovered that, except for Page and Raw, all corresponding MJML elements are standard-xx. Just because we need a one-to-one correspondence between a basic element and mjml components, but many times we need to extend these elements.

For example in StandardButton,

type StandardButtonElement = BasicElement<
Omit<ButtonElement["attributes"], "inner-padding"> & {
"inner-padding-top"?: string;
"inner-padding-bottom"?: string;
"inner-padding-left"?: string;
"inner-padding-right"?: string;
"border-enabled"?: boolean;
"border-width"?: string;
"border-style"?: string;
"border-color"?: string;
},
{}
> & { type: InternalElementType["STANDARD_BUTTON"] };

We added border-enabled to control whether to display the border, and split the border into border-width border-style border-color