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