You can simply initialize the editor by passing a config object to the editEmail
function. The dropdownButtons
, blockLibraries
, and hooks
properties are detailed later in this section.
const editorConfig = {
document: {}, // a JSON object that represents the email document
user: { // or false to turn off the user avatar
id: "5c6bfaba1f848b135dc46c41", // Id of the current user
name: "John Doe", // the display name of your user
avatar: "https://example.com/avatar.png" // the avatar of your user
},
settings: {
staticAssetsBaseUrl: "https://yourdomain.com/path/to/static/assets/", // there are a few image assets that should be hosted on your side
videoElementBaseUrl: "https://yourdomain.com/path/to/video/backend/", // the video element requires a backend that should be hosted on your side
buttons: {
header: [], // an array of objects describing the top right corner buttons of the plugin
textInsert: [] // an array of objects describing the buttons on the inline text editor
},
elements: { // or false, to turn off the elements tab (so only the blocks will be visible)
content: { // or false, to hide the whole element group
text: true,
image: true,
button: true,
divider: true,
social: false,
code: false,
video: false,
},
structure: { // or false, to hide the whole element group
fullWidth: false,
box: true,
multiColumn: true
},
advanced: { // or false, to hide the whole element group
loop: true,
conditional: true,
dynamicImage: true
}
},
blockLibraries: [], // an array of block library descriptors
fontFiles: { // an object, keys are the names of the font families, the values are the URLs to the font files.
"Poppins": "https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,400;0,700;1,400;1,700&display=swap",
"Zen Tokyo Zoo": "https://fonts.googleapis.com/css2?family=Zen+Tokyo+Zoo&display=swap"
},
fontStacks: [ // an array of arrays, the inner array contains the font families as strings in the font stack
["Poppins", "Helvetica Neue", "Helvetica", "Arial", "sans-serif"]
],
hideDefaultFonts: true, // if true, the default built-in font stacks are hidden, if false, the custom font stacks are added to the default ones.
addons: {
blockLock: { // or false to fully turn off (hide) the addon
enabled: false // enable / disable the addon
},
variableSystem: { // or false to fully turn off (hide) the addon
enabled: false // enable / disable the addon
}
},
actionMenu: { // control of different action menus
block: { // if false the Block Menu is turned off (floating menu right side each block)
drag: true, // you can turn off each button individually
save: true, // the save option automatically hidden when no blockLibraries added to the editor instance
duplicate: true,
delete: true,
}
},
toolboxes: { // each element's toolboxes can be hidden
body: true, // if false the element is not customizable in the Editor instance
fullWidth: true,
text: true,
button: true,
box: true,
multiColumn: true,
image: true,
divider: true,
code: true,
social: true,
column: true,
loop: true,
conditional: true,
dynamicImage: true,
video: true,
blockLevelConditional :true,
blockLevelLoop: true,
},
dropzones: {
block: true, // when false dropzones above and under blockes are turned off. No new element can be created by drag and drop, but inside each block
},
},
hooks: {} // an object of available hooks (for example onSave)
};
const editorInstance = await chamaileonPlugins.editEmail(editorConfig);
You can call the editorInstance
functions at any time, but most likely you will use them in event hooks.
This function will resolve the current state of the document as a JSON object. You can save it and reload the editor (or the preview) with this JSON later on. You can also invoke our generator with this JSON object from your backend.
const emailJson = await editorInstance.getDocument();
You can generate the email HTML version of the document with this function. You can do it any time, but I suggest you not to do it on each and every change, because you would hit our rate limits pretty soon.
const emailHtml = await editorInstance.getEmailHtml();
console.log(emailHtml);
This function will replace the current document in the editor with a new one. You can for example update the document on a hook that you configured.
await editorInstance.setDocument({ document });
This function will replace the current settings object in the editor with a new one. The format of the settings object is the same that you pass when you open the editor. It means that you can reconfigure on the fly what settings you want to use.
You can update the settings when your user clicks one of the previously configured buttons.
await editorInstance.updateSettings(settings);
Closes the editor.
await editorInstance.close();
As you have seen previously, some of the configuration values are pretty self-explanatory, some of them needs somewhat more clarification. The config object you pass to the editEmail
function, has the following properties:
Property | Type | Description |
---|---|---|
document | object | The document descriptor object. You might want to save it as a JSON object. |
user | object or null | You can define how the user will be displayed in the editor. The name property of this object will be the name displayed when you hover on the user's avatar. The avatar property is the URL or the user's profile picture. If the value is null then the avatar won't be displayed. |
settings | object | The settings of the editor instance. |
hooks | object | You can register callbacks on multiple events coming from the editor. For more, please check out the editorConfig.hooks section. |
The settings object has the following properties:
Property | Type | Description |
---|---|---|
staticAssetsBaseUrl | string | This parameter sets the base URL of the default image and the social icon images. You can find those images in the following repository and should host on your domain: https://github.com/chamaileon-sdk/static-content If you are not hosting these images on your domain, that can negatively impact the deliverability of your emails. |
videoElementBaseUrl | string | This parameter sets the base URL of the video element. This should point to the hosted video backend. If you are not hosting this backend on your domain, then the video element will not work. |
autoSaveInterval | number | This optional parameter sets the frequency of autosaving in milliseconds. The onAutoSave hook will be called when autosave happens. If the value is not given, the autosave hook will not be triggered. |
buttons | object | The configuration of the header buttons and textInsert buttons. |
blockLibraries | array | You can provide multiple block libraries for your users, with different access-level. See the editorConfig.blockLibraries section for more. |
fontFiles | object | An object, containing the custom font resources. Keys are the font names, values are the URLs. |
fontStacks | array | An array of array, containing the font stacks. |
hideDefaultFonts | boolean | If it's true, the default built-in font stacks are hidden, if it's false, the custom font stacks are added to the default ones. |
addons | object | Addon configurations |
You can configure the buttons in the top right corner of the editor. You can set up their icons, labels and ids. If a user clicks on them, then the onHeaderButtonClicked
hook will be called with the id
of the button. With this option, you can add custom functionality to the editor. For example, you can create custom dialogs. You can also configure dropdowns.
Example:
editorConfig.settings.buttons.header = [
{
id: 'preview',
type: 'button',
icon: 'eye',
label: 'Preview',
color: '#aaaaaa',
style: 'text' // filled, depressed (no shadow filled), outlined, text, plain
},
{
type: 'dropdown',
icon: 'dog',
label: 'Dropdown',
color: "secondary",
style: 'outlined',
items: [ // if any button has a items field will generate a dropdown button, and only the items get callbacks
{
id: "share-email",
label: "Get shareable link",
icon: "share",
},
{
id: "send-test-email",
label: "Send test email",
icon: "email",
},
{
id: "request-review",
label: "Request a review",
icon: "comment-eye",
},
]
}
];
You can also use the onClick
property of buttons. There is only one function within the editor that you can trigger this way. It is called openVariableModal
which will open the modal on which you can add, remove or edit variables. (You need to have the variable system addon enabled on your API key.)
You can add clickable buttons into the CKEditor, to insert your custom text. The onTextInsertPluginButtonClicked
hook will be called, when you click on one of the buttons. This is a great way to provide merge tags to your users.
The textInsertPluginButtons
array consists of objects with the following properties:
Property | Type | Description |
---|---|---|
id | string | The id of the button. The onTextInsertPluginButtonClicked hook will get this value as a parameter. |
label | string | This will be displayed in the CKEditor header. |
icon | string (optional) | You can set the CKEditor icon with an image src. If blank or undefined a text button will be created with the given label. |
Example:
editorConfig.buttons.textInsert = [
{
id: "insert-tag",
label: "Custom tags",
icon: "https://raw.githubusercontent.com/ckeditor/ckeditor4/major/skins/kama/icons/paste.png",
}
]
This is an object that let's you to turn off some (or all) of the elements draggable from the left-hand-side.
Example:
editorConfig.settings.elements = { // or false, then the elements tab will not be visible
content: { // or false, then the content elements section will not be visible
text: true, // true: visible, false: hidden
image: true,
button: true,
social: true,
divider: true,
code: true
},
structure: { // or false, then the structure elements section will not be visible
box: true,
multiColumn: true
},
advanced: { // or false, then the advanced personalization elements section will not be visible
loop: true,
conditional: true,
dynamicImage: true
}
}
You can also configure block libraries, with the blockLibraries
array. They will appear in two parts of the editor:
This way, you can provide multiple block libraries to your users and can allow them to save their blocks into different libraries.
When a user selects a block library on the left hand side, the onLoadBlocks
hook will be called with the id
of the library. This way, you can provide as many block libraries to your clients as you want.
The blockLibraries
array consists of objects with the following properties:
Property | Type | Description |
---|---|---|
id | string | The id of the block library. The onLoadBlocks hook will get this value as a parameter. |
label | string | This will be displayed in the forementioned dropdowns. |
canDeleteBlock | boolean | If true, the actual user will be able to delete from this block library. If false, the delete button is not shown. |
canRenameBlock | boolean | If true, the actual user will be able to rename blocks in this block library. If false, the rename functionality is disabled. |
canSaveBlock | boolean | If true, the actual user will be able to save blocks to this this block library. If false, then this block library will not be shown to the user when they try to save a block. |
Example:
editorConfig.blockLibraries = [
{
id: "email-blocks",
label: "Email's blocks",
canDeleteBlock: false,
canRenameBlock: false,
canSaveBlock: false,
},
{
id: "john-doe-favs",
label: "John Doe's Favorite Blocks",
canDeleteBlock: true,
canRenameBlock: true,
canSaveBlock: true,
}
]
To display a new font in the email editor and your emails (depending on email clients), you need to provide a font URL under the font URL settings.
Every font URL should include the following font styles:
As an example, the URL for Poppins would look like this:
https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,400;0,700;1,400;1,700&display=swap
Keys are the names of the font families, the values are the URLs to the font files.
Example:
editorConfig.settings.fontFiles = {
"Poppins": "https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,400;0,700;1,400;1,700&display=swap"
};
Once you added the required Font URL, you need to define the Font stack to ensure that email clients (like Gmail) that don't support custom fonts will display your text in a similar web-safe font.
This property is an array of arrays, the inner array contains the font families as strings in the font stack. The font stacks are displayed in the order as they are in the outer array.
Example:
editorConfig.settings.fontStacks: [
["Poppins", "Helvetica Neue", "Helvetica", "Arial", "sans-serif"]
];
In the HTML code, it will look like this:
"Poppins, Helvetica Neue, Helvetica, Arial, sans-serif"
This is a boolean property. If the value is true, the default built-in font stacks are hidden, if it's falsy, the custom font stacks are added to the default ones.
Property | Type | Description |
---|---|---|
blockLock | object or false | Enabling or disabling the block lock addon. |
variableSystem | object or false | Enabling or disabling the variable system addon. |
Each of the addons can be an object or false. If you set it as false, the addon will not be visible. If you use an object, then there should be an enabled
property on the addon. If it is true, then the addon is enabled, if its value is false, the addon will be visible, but disabled.
Example:
editorConfig.settings.addons = {
blockLock: { // or false, then the addon will not be visible
enabled: false // if it is false, then the addon will be disabled
},
variableSystem: { // or false, then the addon will not be visible
enabled: true // if it is false, then the addon will be disabled
}
}
The block lock addon allows users to lock the design or design & content of a block. If the block is locked, you won't be able to edit the layout or even the content of that block. You can find the toggle button for this feature in the "Block toolbox".
The variable system addons enables your users to define variables, and refer those variables in the email. For example, you can define a primaryColor variable, that you can use at multiple places. Whenever you update the value of the primaryColor variable, it will be automatically updated everywhere in the email where you refer its value.
Property | Type | Description |
---|---|---|
block | object or false | Enabling or disabling blocks' actions. |
Action menu fields can be objects or false. False will hide the whole menu. In the object each action can be disabled with a false value. When all actions are disabled, the menu will be hidden. All actionMenu items default value is true.
Example:
editorConfig.settings.actionMenu.block = {
drag: true,
save: true,
duplicate: true,
delete: true,
}
The block actions are designed to handle block level editing easily. You can duplicate, delete or drag blocks by pressing the action button. The save action only available if the blockLibraries array has at least one element defined.
Property | Type | Description |
---|---|---|
*element | true or false | Enabling or disabling toolboxes for *element |
Toolboxes' fields can be a Boolean value. False hides all toolboxes for the element type. Default value is true for all elements.
Example:
editorConfig.settings.actionMenu.toolboxes: {
body: true,
fullWidth: true,
text: true,
button: true,
box: true,
multiColumn: true,
image: true,
divider: true,
code: true,
social: true,
column: true,
loop: true,
conditional: true,
dynamicImage: true,
video: true,
blockLevelConditional :true,
blockLevelLoop: true,
},
Toolboxes are located in the right side drawer in the editor. Each element can have multiple toolboxes, this options hide them all, make them not costumizable.
Property | Type | Description |
---|---|---|
block | true or false | Enabling or disabling blok level dropzone |
When false, dropzones above and under blocks are turned off. No new element can be created by drag and drop, but inside each block.
Example:
editorConfig.settings.dropzone: {
block: true,
},
Each and every hook should be an asynchronous process, so all of the hook handler functions has to return Promise
s. The SDK resolves these promises and sends back the result to the plugin.
For example, when a user clicks on the save button, a load indicator will start spinning, and the onSave
hook is called. Until the promise is not resolved, the loading indicator will continue spinning.
In most of the cases you just have to resolve the promise when the async operation is done without any params, but in some cases you will have to resolve certain objects with properties that the editor plugin can use. Similarly to the parameters, we always expect an object to be resolved, even if it only has one property. (This way it will be easier to add new properties later on if needed.)
If any errors occurred on your side, you can reject the promise with an instance of Error
. In this case, the error message will be shown in a snackbar in the editor.
function handler(params) {
return new Promise((resolve, reject) => {
// You can put the logic here.
// Resolve the promise when everything is okay
// Reject the promise on error
if (!error) {
resolve(dataToResolve) // In some cases, you don't have to resolve any data. You can resolve the promise without a parameter.
} else {
reject(new Error("Your error message"))
}
})
}
Note that, with the async syntax, the unexpected errors will be also displayed:
async function handler(params) {
// Unexpected errors will also cause promise rejections in this case
// For example, if you get a timeout error, that will also be displayed in a snackbar in the editor.
// Any exception will be catched by the SDK and the message property of the error object will be shown in a snackbar.
return dataToResolve
}
You can see below the hooks you can use. Read more about them in the following sections.
editorConfig.hooks = {
onSave,
onAutoSave,
onChange,
onBeforeClose,
onAfterClose,
onEditTitle,
onEditImage,
onEditBackgroundImage,
onBlockSave,
onLoadBlocks,
onBlockRename,
onBlockDelete,
onHeaderButtonClicked,
onTextInsertPluginButtonClicked,
onExpressionEditClicked,
onUserEvent
};
This function is called when the user clicks on the save button. The "save in progress" indicator will be spinning, until the returned promise is not resolved or rejected.
/*
Params:
- emailJson: The object representation of the document.
Has to resolve: nothing.
*/
editorConfig.hooks.onSave = ({ emailJson }) => {
return new Promise(resolve => {
// you can put here the logic that saves the emailJson object
// and when it's done, you can resolve the promise
resolve();
});
};
This hook is very similar to the previous one. It is triggered when autosave happened, and it gets the same params as the previous one. The progress indicator will also be spinning until the promise is not resolved.
/*
Params:
- emailJson: The object representation of the document.
Has to resolve: nothing.
*/
editorConfig.hooks.onAutoSave = ({ emailJson }) => {
return new Promise(resolve => {
// you can put the logic here that saves the emailJson object
// and when it's done, you can resolve the promise
resolve();
});
};
This hook is invoked whenever something happened in the editor. It might be useful if you want to know if there were any changes since the last time you saved the document. For example, you can set a variable that shows that there were changes, and you can check it in the onBeforeClose
hook.
/*
Params: nothing.
Has to resolve: nothing.
*/
editorConfig.hooks.onChange = ({ mutation }) => {
return new Promise(resolve => {
// you can put the logic here that saves the email mutation
resolve();
});
};
This hook is called right before the editor is closed. The editor will not be closed until you resolve the promise. You can use this hook if you want to save the current state of the document before closing (using editorInstance.getEmailJson
).
Since this prevents the editor from closing, you will have to make sure to show a progress indicator if your saving process takes a long time.
/*
Params: nothing.
Has to resolve: nothing.
*/
editorConfig.hooks.onBeforeClose = () => {
return new Promise(resolve => {
resolve();
});
};
This hook is called when the editor is already closed. From this point in time, you will not be able to call the editor instance functions (getEmailJson
and getEmailHtml
), because the editor instance is destroyed.
/*
Params: nothing.
Has to resolve: nothing.
*/
editorConfig.hooks.onAfterClose = () => {
return new Promise(resolve => {
resolve();
});
};
Invoked when a user changes the title of the document. The progress indicator will be spinning until the promise is resolved.
/*
Params:
- title: The title of the document, you previously set up in the editorConfig object
Has to resolve: nothing.
*/
editorConfig.hooks.onEditTitle = ({ title }) => {
return new Promise(resolve => {
resolve();
});
};
This function is called when the user wants to edit an image. You can use this hook to pop up your gallery. This function has to be resolved with an object that has an src property. That will be the new src of the image.
This function has two optional parameters:
The first is the originalImage
. If you get a string value in this parameter, that means that the user wants to edit that image, so you should initialize an image editor and resolve the promise with the modified image. If this value is undefined, that means that the user wants to change the image, so you might want to pop up an image selector.
The second is the lockDimensions
. When defined, it means that the user wants to change an image in a block, which has a locked design. In this case, you will have to resolve an image with the exact same aspect ratio as defined by the width
and height
property of the lockDimensions
object. (Also, the dimensions has to be at least as big as these values.) Usually it means, that you will have to use a crop tool if you don't have images with the proper aspect ratio.
When you resolve the promise with an src
property on an object, the editor will set up that value as the new src
of the image. This might be a long-running promise, since you will probably resolve it, when your user selects one of their images from your image library.
/*
Params:
- originalImage: optional, string, shows that a user wants to edit an image
- lockDimensions: optional, object, you have to crop the image to this aspect ratio
- width
- height
Has to resolve:
- src
*/
editorConfig.hooks.onEditImage = ({ originalImage, lockDimensions: { width, height } }) => {
return new Promise(resolve => {
// Eventually, you will have to resolve an object with an src prop
resolve({ src });
});
};
This hook is invoked when a user wants to change the background image of an element.
/*
Params: nothing.
Has to resolve:
- src
*/
editorConfig.hooks.onEditBackgroundImage = () => {
return new Promise(resolve => {
// Eventually, you will have to resolve an object with an src prop
resolve({ src });
});
};
This will be called when the user selects a block library and right after the editor is loaded. The editor will call this hook with one of the preconfigured block library ids. This way, you may provide multiple sets of blocks for your clients.
This hook is also called right after the editor is loaded, because we pre-load one of the block libraries.
The resolved blocks
array has to contain the block objects. You can get those block objects in the onSaveBlock
hook.
/*
Params:
- libId: string id of the library from the editorConfig.blockLibraries array
Has to resolve:
- blocks: array of block objects
*/
editorConfig.hooks.onLoadBlocks = ({ libId }) => {
return new Promise(resolve => {
// Eventually, you will have to resolve an object with an src prop
const blocks = [];
// Just put the block objects into the array
resolve({ blocks });
});
};
Called when a user saves a new block. After you saved it to your backend, you have to resolve the promise with a block object on it. The block object has to have an _id
property, that is the id of your database entry.
/*
Params:
- libId: string id of the library from the editorConfig.blockLibraries array
- block: the object representing a block
Has to resolve:
- block: the final block object with an _id field
*/
editorConfig.hooks.onBlockSave = ({ libId, block }) => {
return new Promise(resolve => {
// Eventually, you will have to resolve an object with an src prop
block._id = "something"; // You have to set up the _id of the block
resolve({ block });
});
};
When the user renames a block, this hook will be invoked. You can save the changes in your db.
/*
Params:
- libId: string id of the library from the editorConfig.blockLibraries array
- block: the object representing a block
- _id: the id of the block, you can update your db entry based on this
- title: the new value of the block's title
Has to resolve: nothing.
*/
editorConfig.hooks.onBlockRename = ({ libId, block: { _id, title } }) => {
return new Promise(resolve => {
// Here, you can save the new title of the block -> resolve when done.
resolve();
});
};
Invoked when a user deletes a block from a block library.
/*
Params:
- libId: string id of the library from the editorConfig.blockLibraries array
- block: the object representing a block
- _id: the id of the block, you can delete your db entry based on this
Has to resolve: nothing.
*/
editorConfig.hooks.onBlockDelete = ({ libId, block: { _id } }) => {
return new Promise(resolve => {
// Here, you can delete your entry and resolve the promise
resolve();
});
};
If you have set up header buttons in the config, you can use this hook to implement anything you like. (Most likely you will pop up a dialog.)
/*
Params:
- buttonId: string id of the button from the editorConfig.dropdownButtons array
Has to resolve: nothing.
*/
editorConfig.hooks.onHeaderButtonClicked = ({ buttonId }) => {
return new Promise(resolve => {
// Here, you can implement your custom dialog
resolve();
});
};
When a user clicks on any of the configured buttons in the CKEditor header, you will be able to handle the actions with this hook, and insert your own custom text into the editor surface. This is a very handy feature for implementing merge tags.
/*
Params:
- buttonId: string id of the button from the editorConfig.textInsertPluginButtons array
Has to resolve:
- value: The string you want to insert.
Can resolve:
- isHTML: true if the value contains a HTML snippet. Defaults to false.
*/
editorConfig.hooks.onTextInsertPluginButtonClicked = ({ buttonId }) => {
return new Promise(resolve => {
// Here, you can implement your custom dialog
resolve({ value: "Your inserted text.", isHTML: false });
});
};
Loop, conditional and dynamic image elements have an expression option. If you have a list of expressions, you can pop up a dialog, and resolve the selected expression, and it will be added to the editor's expression input field.
/*
Params:
- expression: The current selected expression in the input field.
Has to resolve:
- expression: Your expression to insert into the editor's toolbox input field.
*/
editorConfig.hooks.onExpressionEditClicked = ({ expression }) => {
return new Promise(resolve => {
// Here, you can implement your custom dialog with a list of expressions
resolve({ expression: "<Your inserted expression>" });
});
};
You can get various information about user interaction on this hook. For example if the zoom was changed or a new element was dropped onto the canvas.
/*
Params:
- userEvent: The current event information
Has to resolve: nothing.
*/
editorConfig.hooks.onUserEvent = ({ userEvent }) => {
return new Promise(resolve => {
// Here, you can save the event information
resolve();
});
};
We put together a few demos and you can check out the code here.
You can also check out the email variable editor plugin on the Chamaileon SDK Playground.