Skip to main content

Interfaces

Since Easy Email Pro is based on Slate, many aspects are very similar. For example, Easy Email Pro works with pure JSON objects. All it requires is that those JSON objects conform to certain interfaces. For example

TextNode

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

Element

interface Element<
K extends { [key: string]: any } = any,
T extends { [key: string]: any } = any,
Type extends string = string
> {
uid?: string;
title?: string;
type: Type;
data: T;
logic?: {
condition?: LogicCondition;
iteration?: LogicIteration;
};
visible?: "desktop" | "mobile";
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>;
}

Note that all nodes must include children and must not be empty. If it is an empty element, you need to set childrne to [{ text: ""}], in order to be consistent with the slate behavior.

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