I am currently working on utilizing the GrapesJS editor for creating email templates, and I've encountered a specific challenge that I could use some assistance with.
My situation involves certain constraints regarding how templates should be passed to the server. Specifically, I plan to utilize placeholders actively within the editor, such as @firstname, to personalize the email content.
Additionally, I've implemented a custom media library that allows for adding images to the template with a placeholder attribute in the src field, following this format: @media-item-url(${itemID}).
My goal is to have the actual image displayed on the canvas while still using the placeholder within the src attribute. Essentially, I aim to modify only the representation of the component without altering its underlying model.
Below, I've attached some code snippets for reference.
I would greatly appreciate any insights or guidance on how to achieve this functionality within the GrapesJS editor. Thank you in advance for your time and assistance.
import { useEffect, useRef, useState } from "react";
import { useParams } from "react-router-dom";
import grapesjs, { Editor } from "grapesjs";
import grapesjsMJML from "grapesjs-mjml";
import "grapesjs/dist/css/grapes.min.css";
import "../../css/grapesJsEditor.css";
import MediaLibraryExperimental from "../Template/MediaLibrary/MediaLibraryExperimental";
// Ensure that the id doesn't start with a number, as it's not valid HTML format.
// If it does, prepend "editor-" to it; otherwise, keep it as is.
const getId = (id: string) => {
return /^[0-9]/.test(id) ? `editor-${id}` : `${id}`;
};
interface Props {
currentContent: string;
onContentChange: (field: string, value: string | number) => void;
}
const GrapesJSEditor = ({ currentContent, onContentChange }: Props) => {
const { id } = useParams();
const [showMediaLibrary, setShowMediaLibrary] = useState(false);
const [mediaLibrary, setMediaLibrary] = useState<any[]>([]);
const assetManager = useRef<any>(null);
const initializeEditor = () => {
if (!id) return;
const selector = `#${getId(id)}`;
const editor = grapesjs.init({
container: selector,
storageManager: false,
assetManager: {
assets: [...mediaLibrary],
custom: {
open(props) {
setShowMediaLibrary(true);
// `props` are the same used in `asset:custom` event
// Init and open your external Asset Manager
assetManager.current = props;
},
close(props) {
setShowMediaLibrary(false);
},
},
},
fromElement: true,
avoidInlineStyle: false,
plugins: [grapesjsMJML],
pluginsOpts: {
// @ts-ignore
[grapesjsMJML]: {
blocks: [
"mj-1-column",
"mj-2-columns",
"mj-3-columns",
"mj-text",
"mj-button",
"mj-divider",
"mj-spacer",
"mj-navbar",
"mj-navbar-link",
],
},
},
height: "70vh",
});
editor.setComponents(currentContent);
assetsHandler(editor);
componentUpdateHandler(editor);
addBlocks(editor);
closeDefaultCategories(editor);
return () => editor.destroy();
};
const addBlocks = (editor: any) => {
editor.Blocks.add("add-media-item", {
label: "Media Item",
content: `<mj-image src="#"/>`,
attributes: { class: "fa fa-file-image-o", src: "#" },
category: "Media Library",
activate: true,
});
const placeholders = [
{
name: "salutation",
icon: "fa-venus-mars",
content: "salutation(Sehr geehrte Frau;Sehr geehrter Herr)",
},
{ name: "first name", icon: "fa-user-o" },
{ name: "last name", icon: "fa-user-o" },
{ name: "company", icon: "fa-building-o" },
{ name: "email", icon: "fa-envelope-o" },
{ name: "phone", icon: "fa-phone" },
];
placeholders.forEach((placeholder) => {
editor.Blocks.add(`${placeholder.name}-button`, {
// first name -> First name
label:
placeholder.name.charAt(0).toUpperCase() + placeholder.name.slice(1),
content: `<mj-text>@${
placeholder.name === "salutation"
? placeholder.content
: placeholder.name
}</mj-text>`,
attributes: { class: `fa ${placeholder.icon}` },
category: "Placeholders",
activate: false,
});
});
};
// Define the action when the custom button is clicked
const assetsHandler = (editor: Editor) => {
editor.on("asset:custom", (props) => {
// Define the select function for the asset manager
props.select = (asset: string, isSelected: boolean) => {
// Check if the asset is selected
if (asset) {
// If selected, set the URL of the image component to the provided imgUrl
const imgComponent = editor.getSelected();
const srcPlaceHolder = asset;
if (imgComponent) {
imgComponent.setAttributes({
src: srcPlaceHolder,
alt: asset,
});
}
} else {
console.log("Asset deselected");
}
};
componentsRenderHandler(editor);
});
};
const componentUpdateHandler = (editor: any) => {
editor.on("update", () => {
const mjmlContent = editor.getHtml();
onContentChange("email_content", mjmlContent);
});
};
editor.DomComponents.addType("add-media-item", {
view: {
init() {
console.log("Local hook: view.init");
},
onRender({ el }: { el: any }) {
console.log("render in view");
const src = el.getAttribute("src");
const id = src.match(/@media-item-url\(([^)]+)\)/); // between @media-item-url()
const newSrc = `images/${id}.jpg`;
el.setAttribute("src", newSrc);
},
},
});
};
useEffect(initializeEditor, []);
// Handle the selection of an item from the media library
const handleItemSelect = (itemID: any) => {
assetManager.current.select(`@media-item-url(${itemID})`, true);
assetManager.current.close();
};
return (
<div>
{id ? <div id={getId(id)} /> : null}
<MediaLibraryExperimental
onItemSelectHandler={handleItemSelect}
isVisible={showMediaLibrary}
setIsVisible={setShowMediaLibrary}
setMediaLibrary={setMediaLibrary}
></MediaLibraryExperimental>
</div>
);
};
export default GrapesJSEditor;