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
typethat identifies what kind of element it is (e.g., "standard-section", "standard-text") attributesfor styling and configuration (CSS properties, MJML attributes)datafor custom data storagechildrenarray 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
childrenarray, even if empty - Empty elements should have
children: [{ text: "" }]to be consistent with Slate behavior - The
childrenarray cannot benullorundefined
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