How can I help you?
Mail merge in React Rich Text Editor component
20 Dec 202524 minutes to read
The Rich Text Editor can be customized to implement Mail Merge functionality by inserting placeholders into the editor using custom toolbar items. These placeholders are later replaced with actual data to generate personalized content such as letters, invoices, and reports.
This feature simplifies the creation of dynamic documents by allowing users to insert merge fields that are automatically populated with real data during content generation.
Adding custom toolbar items for inserting merge fields
To enable mail merge functionality, the Rich Text Editor toolbar is extended with two custom buttons: Insert Field and Merge Data. These buttons are added using the template property in toolbarSettings.items, which points to custom HTML elements (#insertField and #merge_data).
-
Insert Field: Opens a dropdown list of merge fields for inserting placeholders like
{{FirstName}}into the editor. - Merge Data: Replaces all placeholders in the editor with actual values from a predefined data source.
function App() {
const items: (string | IToolbarItems)[] = ['Bold', 'Italic', 'Underline', '|', 'Formats', 'Alignments', 'OrderedList', 'UnorderedList', '|',
'CreateLink', 'Image', 'CreateTable', '|',
{ tooltipText: 'Merge Data', template: '#merge_data', command: 'Custom' },
{ tooltipText: 'Insert Field', template: '#insertField', command: 'Custom' },
'SourceCode', '|', 'Undo', 'Redo'
]
//Rich Text Editor ToolbarSettings
const toolbarSettings: ToolbarSettingsModel = {
items: items
};
return (
<RichTextEditorComponent toolbarSettings={toolbarSettings}></RichTextEditorComponent>
);
}
export default App;Using DropDownButton for selecting placeholders
The DropDownButton component displays a list of merge fields such as First Name, Last Name, and Company Name. When a user selects an item, the corresponding placeholder (e.g., ) is inserted at the current cursor position using the insertHTML command.
function App() {
let itemsName: { text: string }[] = [
{ text: 'First Name' },
{ text: 'Last Name' },
{ text: 'Support Email' },
{ text: 'Company Name' },
{ text: 'Promo Code' },
{ text: 'Support Phone Number' },
{ text: 'Customer ID' },
{ text: 'Expiration Date' },
{ text: 'Subscription Plan' },
];
function onItemSelect(args: { item: { text: string } }) {
if (args.item.text != null) {
const value = textToValueMap[args.item.text];
const trimmedValue = value.trim();
rteObj.formatter.editorManager.nodeSelection.restore();
rteObj.executeCommand(
'insertHTML',
`<span contenteditable="false" class="e-mention-chip"><span>{{${trimmedValue}}}</span></span> `,
{ undo: true }
);
}
}
return ( <DropDownButtonComponent
className="e-rte-dropdown-btn e-control e-dropdown-btn e-lib e-btn e-rte-dropdown-popup e-rte-dropdown-items e-formats-tbar-btn e-rte-elements e-rte-dropdown-menu"
items={itemsName}
content='<span style="display: inline-flex;"><span class="e-rte-dropdown-btn-text">Insert Field</span></span>'
select={onItemSelect}
id="insertField"
></DropDownButtonComponent>
);
}
export default App;Populating merge fields using Mention
The Mention component provides an alternative way to insert placeholders by typing the {{ character inside the editor. A popup list of merge fields appears, allowing quick selection without using the toolbar.
function App() {
const fieldsData: { text: string; value: string } = { text: 'text', value: 'value' };
const data: { text: string; value: string }[] = [
{ text: 'First Name', value: 'FirstName' },
{ text: 'Last Name', value: 'LastName' },
{ text: 'Support Email', value: 'SupportEmail' },
{ text: 'Company Name', value: 'CompanyName' },
{ text: 'Promo Code', value: 'PromoCode' },
{ text: 'Support Phone Number', value: 'SupportPhoneNumber' },
{ text: 'Customer ID', value: 'CustomerID' },
{ text: 'Expiration Date', value: 'ExpirationDate' },
{ text: 'Subscription Plan', value: 'SubscriptionPlan' },
];
function displayTemplate(data: { value: string }) {
return (<React.Fragment>
{data.value}}}
</React.Fragment>);
}
return (
<MentionComponent id="mentionEditor" target="#mailMergeEditor" mentionChar="{{" showMentionChar={true} allowSpaces={true} dataSource={data} fields={fieldsData} popupWidth="250px" popupHeight="200px" displayTemplate={displayTemplate}></MentionComponent>
);
}
export default App;Replacing placeholders with actual data dynamically
When the Merge Data button is clicked, the editor content is processed to replace all placeholders with actual values from the placeholderData object. This is done using a regular expression in the replacePlaceholders() function.
function App() {
function onClickHandler(args: any): void {
if (rteObj) {
let editorContent: string = rteObj.value;
let mergedContent: string = replacePlaceholders(editorContent, placeholderData);
if (rteObj.formatter.getUndoRedoStack().length === 0) {
rteObj.formatter.saveData();
}
rteObj.value = mergedContent;
rteObj.formatter.saveData();
} else {
console.log('MailMergeEditor is not initialized.');
}
};
function replacePlaceholders(template: string, data: { [key: string]: string }): string {
return template.replace(/{{\s*(\w+)\s*}}/g, (match, key) => {
const value = data[key.trim()];
return value !== undefined ? value : match;
});
};
return (
<button className="e-control e-lib e-btn e-formats-tbar-btn e-rte-elements e-tbar-btn" tabIndex={-1}id="merge_data" style={{ width: '100%' }} onClick={onClickHandler}>
<span style={{ display: 'inline-flex' }}>
<span className="e-tbar-btn-text">Merge Data</span>
</span>
</button>
);
}[Class-component]
import React from 'react';
import {RichTextEditorComponent, HtmlEditor, Toolbar, Image, Link, QuickToolbar, Table, PasteCleanup, Inject} from '@syncfusion/ej2-react-richtexteditor';
import { DropDownButtonComponent } from '@syncfusion/ej2-react-splitbuttons';
import { MentionComponent } from '@syncfusion/ej2-react-dropdowns';
import { NodeSelection } from '@syncfusion/ej2-richtexteditor';
class App extends React.Component {
constructor(props) {
super(props);
this.rteObj = null;
this.mentionObj = null;
this.range = new Range();
this.selection = new NodeSelection();
this.saveSelection = null;
this.value = `<p>Dear <span contenteditable="false" class="e-mention-chip"><span>{{FirstName}}</span></span> <span contenteditable="false" class="e-mention-chip"><span>{{LastName}}</span></span>,</p>
<p>We are thrilled to have you with us! Your unique promotional code for this month is: <span contenteditable="false" class="e-mention-chip"><span>{{PromoCode}}</span></span>.</p>
<p>Your current subscription plan is: <span contenteditable="false" class="e-mention-chip"><span>{{SubscriptionPlan}}</span></span>.</p>
<p>Your customer ID is: <span contenteditable="false" class="e-mention-chip"><span>{{CustomerID}}</span></span>.</p>
<p>Your promotional code expires on: <span contenteditable="false" class="e-mention-chip"><span>{{ExpirationDate}}</span></span>.</p>
<p>Feel free to browse our latest offerings and updates. If you need any assistance, don't hesitate to contact us at <a href="mailto:{{SupportEmail}}"><span contenteditable="false" class="e-mention-chip"><span>{{SupportEmail}}</span></span></a> or call us at <span contenteditable="false" class="e-mention-chip"><span>{{SupportPhoneNumber}}</span></span>.</p>
<p>Best regards,<br>The <span contenteditable="false" class="e-mention-chip"><span>{{CompanyName}}</span></span> Team</p>`;
this.items = [
'Bold',
'Italic',
'Underline',
'|',
'Formats',
'Alignments',
'OrderedList',
'UnorderedList',
'|',
'CreateLink',
'Image',
'CreateTable',
'|',
{ tooltipText: 'Merge Data', template: '#merge_data', command: 'Custom' },
{
tooltipText: 'Insert Field',
template: '#insertField',
command: 'Custom',
},
'SourceCode',
'|',
'Undo',
'Redo',
];
this.toolbarSettings = {
items: this.items,
};
this.mentionChar = '{{';
this.itemsName = [
{ text: 'First Name' },
{ text: 'Last Name' },
{ text: 'Support Email' },
{ text: 'Company Name' },
{ text: 'Promo Code' },
{ text: 'Support Phone Number' },
{ text: 'Customer ID' },
{ text: 'Expiration Date' },
{ text: 'Subscription Plan' },
];
this.placeholderData = {
FirstName: 'John',
LastName: 'Doe',
PromoCode: 'ABC123',
SubscriptionPlan: 'Premium',
CustomerID: '123456',
ExpirationDate: '2024-12-31',
SupportEmail: '[email protected]',
SupportPhoneNumber: '+1-800-555-5555',
CompanyName: 'Example Inc.',
};
this.textToValueMap = {
'First Name': 'FirstName',
'Last Name': 'LastName',
'Support Email': 'SupportEmail',
'Company Name': 'CompanyName',
'Promo Code': 'PromoCode',
'Support Phone Number': 'SupportPhoneNumber',
'Customer ID': 'CustomerID',
'Expiration Date': 'ExpirationDate',
'Subscription Plan': 'SubscriptionPlan',
};
this.data = this.itemsName.map((item) => ({
text: item.text,
value: this.textToValueMap[item.text],
}));
this.fieldsData = { text: 'text', value: 'value' };
}
actionBegin = (args) => {
if (
args.requestType === 'EnterAction' &&
this.mentionObj?.element.classList.contains('e-popup-open')
) {
args.cancel = true;
}
};
actionComplete = (e) => {
const toolbar = this.rteObj?.getToolbar();
if (!toolbar) return;
const mergeBtn = toolbar.querySelector('#merge_data')?.parentElement;
const insertBtn = toolbar.querySelector('#insertField')?.parentElement;
if (e.requestType === 'SourceCode') {
mergeBtn?.classList.add('e-overlay');
insertBtn?.classList.add('e-overlay');
} else if (e.requestType === 'Preview') {
mergeBtn?.classList.remove('e-overlay');
insertBtn?.classList.remove('e-overlay');
}
};
blur = () => {
this.range = this.selection.getRange(document);
this.saveSelection = this.selection.save(this.range, document);
};
onDropDownClose = () => {
if (this.rteObj) {
this.rteObj.focusIn();
}
};
onItemSelect = (args) => {
if (args.item.text) {
const value = this.textToValueMap[args.item.text];
const trimmedValue = value.trim();
this.rteObj.formatter.editorManager.nodeSelection.restore();
this.rteObj.executeCommand(
'insertHTML',
`<span contenteditable="false" class="e-mention-chip"><span>{{${trimmedValue}}}</span></span> `,
{ undo: true }
);
}
};
displayTemplate = (data) => {
return <>{`${data.value}}}`}</>;
};
onClickHandler = () => {
if (this.rteObj) {
const editorContent = this.rteObj.value;
const mergedContent = this.replacePlaceholders(
editorContent,
this.placeholderData
);
if (this.rteObj.formatter.getUndoRedoStack().length === 0) {
this.rteObj.formatter.saveData();
}
this.rteObj.value = mergedContent;
this.rteObj.formatter.saveData();
} else {
console.log('MailMergeEditor is not initialized.');
}
};
replacePlaceholders = (template, data) => {
return template.replace(/{{\s*(\w+)\s*}}/g, (match, key) => {
const value = data[key.trim()];
return value !== undefined ? value : match;
});
};
render() {
return (
<div>
<RichTextEditorComponent
ref={(richtexteditor) => {
this.rteObj = richtexteditor;
}}
value={this.value}
id="mailMergeEditor"
toolbarSettings={this.toolbarSettings}
placeholder="Type @ and tag the name"
blur={this.blur}
actionComplete={this.actionComplete}
actionBegin={this.actionBegin}
saveInterval={1}
>
<Inject
services={[
HtmlEditor,
Toolbar,
Image,
Link,
QuickToolbar,
Table,
PasteCleanup,
]}
/>
</RichTextEditorComponent>
<button
className="e-control e-lib e-btn e-formats-tbar-btn e-rte-elements e-tbar-btn"
tabIndex={-1}
id="merge_data"
style={{ width: '100%' }}
onClick={this.onClickHandler}
>
<span style={{ display: 'inline-flex' }}>
<span className="e-tbar-btn-text">Merge Data</span>
</span>
</button>
<DropDownButtonComponent
className="e-rte-dropdown-btn e-rte-dropdown-popup e-rte-dropdown-items e-rte-elements e-rte-dropdown-menu"
items={this.itemsName}
content={
<span style={{ display: 'inline-flex' }}>
<span className="e-rte-dropdown-btn-text">Insert Field</span>
</span>
}
select={this.onItemSelect}
close={this.onDropDownClose}
id="insertField"
/>
<MentionComponent
ref={(scope) => {
this.mentionObj = scope;
}}
id="mentionEditor"
target="#mailMergeEditor"
mentionChar={this.mentionChar}
showMentionChar={true}
allowSpaces={true}
dataSource={this.data}
fields={this.fieldsData}
popupWidth="250px"
popupHeight="200px"
displayTemplate={this.displayTemplate}
/>
</div>
);
}
}
export default App;import { RichTextEditorComponent, Inject, IToolbarItems, HtmlEditor, Toolbar, ToolbarSettingsModel, Image, Link, NodeSelection, QuickToolbar, ActionCompleteEventArgs, ActionBeginEventArgs, Table, PasteCleanup } from '@syncfusion/ej2-react-richtexteditor';
import { DropDownButtonComponent } from '@syncfusion/ej2-react-splitbuttons';
import { MentionComponent, FieldsModel } from '@syncfusion/ej2-react-dropdowns';
import * as React from 'react';
class App extends React.Component<{}, {}> {
private rteObj: RichTextEditorComponent | null = null;
private mentionObj: MentionComponent | null = null;
private range: Range = new Range();
private selection: NodeSelection = new NodeSelection();
private saveSelection: any = null;
private value: string = `<p>Dear <span contenteditable="false" class="e-mention-chip"><span>{{FirstName}}</span></span> <span contenteditable="false" class="e-mention-chip"><span>{{LastName}}</span></span>,</p>
<p>We are thrilled to have you with us! Your unique promotional code for this month is: <span contenteditable="false" class="e-mention-chip"><span>{{PromoCode}}</span></span>.</p>
<p>Your current subscription plan is: <span contenteditable="false" class="e-mention-chip"><span>{{SubscriptionPlan}}</span></span>.</p>
<p>Your customer ID is: <span contenteditable="false" class="e-mention-chip"><span>{{CustomerID}}</span></span>.</p>
<p>Your promotional code expires on: <span contenteditable="false" class="e-mention-chip"><span>{{ExpirationDate}}</span></span>.</p>
<p>Feel free to browse our latest offerings and updates. If you need any assistance, don't hesitate to contact us at <a href="mailto:{{SupportEmail}}"><span contenteditable="false" class="e-mention-chip"><span>{{SupportEmail}}</span></span></a> or call us at <span contenteditable="false" class="e-mention-chip"><span>{{SupportPhoneNumber}}</span></span>.</p>
<p>Best regards,<br>The <span contenteditable="false" class="e-mention-chip"><span>{{CompanyName}}</span></span> Team</p>`;
private items: (string | IToolbarItems)[] = [
'Bold', 'Italic', 'Underline', '|', 'Formats', 'Alignments',
'OrderedList', 'UnorderedList', '|', 'CreateLink', 'Image', 'CreateTable', '|',
{ tooltipText: 'Merge Data', template: '#merge_data', command: 'Custom' },
{ tooltipText: 'Insert Field', template: '#insertField', command: 'Custom' },
'SourceCode', '|', 'Undo', 'Redo'
];
private toolbarSettings: ToolbarSettingsModel = { items: this.items };
private mentionChar: string = "{{";
private placeholder: string = "Type {{ and tag a field";
private itemsName: { text: string }[] = [
{ text: 'First Name' }, { text: 'Last Name' }, { text: 'Support Email' },
{ text: 'Company Name' }, { text: 'Promo Code' }, { text: 'Support Phone Number' },
{ text: 'Customer ID' }, { text: 'Expiration Date' }, { text: 'Subscription Plan' }
];
private placeholderData = {
FirstName: 'John', LastName: 'Doe', PromoCode: 'ABC123',
SubscriptionPlan: 'Premium', CustomerID: '123456',
ExpirationDate: '2024-12-31', SupportEmail: '[email protected]',
SupportPhoneNumber: '+1-800-555-5555', CompanyName: 'Example Inc.'
};
private textToValueMap: { [key: string]: string } = {
'First Name': 'FirstName', 'Last Name': 'LastName', 'Support Email': 'SupportEmail',
'Company Name': 'CompanyName', 'Promo Code': 'PromoCode',
'Support Phone Number': 'SupportPhoneNumber', 'Customer ID': 'CustomerID',
'Expiration Date': 'ExpirationDate', 'Subscription Plan': 'SubscriptionPlan'
};
private data: { text: string; value: string }[] = [
{ text: 'First Name', value: 'FirstName' },
{ text: 'Last Name', value: 'LastName' },
{ text: 'Support Email', value: 'SupportEmail' },
{ text: 'Company Name', value: 'CompanyName' },
{ text: 'Promo Code', value: 'PromoCode' },
{ text: 'Support Phone Number', value: 'SupportPhoneNumber' },
{ text: 'Customer ID', value: 'CustomerID' },
{ text: 'Expiration Date', value: 'ExpirationDate' },
{ text: 'Subscription Plan', value: 'SubscriptionPlan' }
];
private fieldsData: FieldsModel = { text: 'text', value: 'value' };
private actionBegin = (args: ActionBeginEventArgs) => {
if (args.requestType === 'EnterAction' && this.mentionObj?.element.classList.contains('e-popup-open')) {
args.cancel = true;
}
};
private actionComplete = (e: ActionCompleteEventArgs) => {
const tb = this.rteObj?.getToolbar();
const merge = tb?.querySelector('#merge_data')?.parentElement;
const insert = tb?.querySelector('#insertField')?.parentElement;
if (e.requestType === 'SourceCode') {
merge?.classList.add('e-overlay');
insert?.classList.add('e-overlay');
} else if (e.requestType === 'Preview') {
merge?.classList.remove('e-overlay');
insert?.classList.remove('e-overlay');
}
};
private blur = () => {
this.range = this.selection.getRange(document);
this.saveSelection = this.selection.save(this.range, document);
};
private onDropDownClose = () => this.rteObj?.focusIn();
private onItemSelect = (args: { item: { text: string } }) => {
if (!args.item.text) return;
const val = this.textToValueMap[args.item.text].trim();
this.rteObj?.formatter.editorManager.nodeSelection.restore();
this.rteObj?.executeCommand(
'insertHTML',
`<span contenteditable="false" class="e-mention-chip"><span>{{${val}}}</span></span> `,
{ undo: true }
);
};
private displayTemplate = (data: { value: string }) => (
<span>{`{{${data.value}}}`}</span>
);
private onClickHandler = () => {
if (!this.rteObj) return;
const content = this.rteObj.value;
const merged = this.replacePlaceholders(content, this.placeholderData);
if (this.rteObj.formatter.getUndoRedoStack().length === 0) {
this.rteObj.formatter.saveData();
}
this.rteObj.value = merged;
this.rteObj.formatter.saveData();
};
private replacePlaceholders = (template: string, data: { [k: string]: string }) =>
template.replace(/{{\s*(\w+)\s*}}/g, (_, key) => data[key.trim()] ?? _);
public render() {
return (
<div>
<RichTextEditorComponent
ref={r => (this.rteObj = r)}
value={this.value}
id="mailMergeEditor"
toolbarSettings={this.toolbarSettings}
placeholder={this.placeholder}
blur={this.blur}
actionComplete={this.actionComplete}
actionBegin={this.actionBegin}
saveInterval={1}
>
<Inject services={[HtmlEditor, Toolbar, Image, Link, QuickToolbar, Table, PasteCleanup]} />
</RichTextEditorComponent>
<button
className="e-control e-btn e-formats-tbar-btn e-rte-elements e-tbar-btn"
id="merge_data"
style={{ width: '100%' }}
onClick={this.onClickHandler}
>
<span style={{ display: 'inline-flex' }}>
<span className="e-tbar-btn-text">Merge Data</span>
</span>
</button>
<DropDownButtonComponent
className="e-rte-dropdown-btn e-rte-dropdown-popup e-rte-dropdown-items e-rte-elements e-rte-dropdown-menu"
items={this.itemsName}
content='<span style="display:inline-flex"><span class="e-rte-dropdown-btn-text">Insert Field</span></span>'
select={this.onItemSelect}
close={this.onDropDownClose}
id="insertField"
/>
<MentionComponent
ref={m => (this.mentionObj = m)}
id="mentionEditor"
target="#mailMergeEditor"
mentionChar={this.mentionChar}
showMentionChar={true}
allowSpaces={true}
dataSource={this.data}
fields={this.fieldsData}
popupWidth="250px"
popupHeight="200px"
displayTemplate={this.displayTemplate}
/>
</div>
);
}
}
export default App;[Functional-component]
import { HtmlEditor, Image, Inject, IToolbarItems, Link, QuickToolbar, RichTextEditorComponent, ToolbarSettingsModel, Toolbar, NodeSelection, ActionCompleteEventArgs, ActionBeginEventArgs, Table, PasteCleanup } from '@syncfusion/ej2-react-richtexteditor';
import * as React from 'react';
import { DropDownButtonComponent, MenuEventArgs } from '@syncfusion/ej2-react-splitbuttons';
import { MentionComponent } from '@syncfusion/ej2-react-dropdowns';
function App() {
let value = `<p>Dear <span contenteditable="false" class="e-mention-chip"><span>{{FirstName}}</span></span> <span contenteditable="false" class="e-mention-chip"><span>{{LastName}}</span></span>,</p>
<p>We are thrilled to have you with us! Your unique promotional code for this month is: <span contenteditable="false" class="e-mention-chip"><span>{{PromoCode}}</span></span>.</p>
<p>Your current subscription plan is: <span contenteditable="false" class="e-mention-chip"><span>{{SubscriptionPlan}}</span></span>.</p>
<p>Your customer ID is: <span contenteditable="false" class="e-mention-chip"><span>{{CustomerID}}</span></span>.</p>
<p>Your promotional code expires on: <span contenteditable="false" class="e-mention-chip"><span>{{ExpirationDate}}</span></span>.</p>
<p>Feel free to browse our latest offerings and updates. If you need any assistance, don't hesitate to contact us at <a href="mailto:{{SupportEmail}}"><span contenteditable="false" class="e-mention-chip"><span>{{SupportEmail}}</span></span></a> or call us at <span contenteditable="false" class="e-mention-chip"><span>{{SupportPhoneNumber}}</span></span>.</p>
<p>Best regards,<br>The <span contenteditable="false" class="e-mention-chip"><span>{{CompanyName}}</span></span> Team</p>`;
const items = ['Bold', 'Italic', 'Underline', '|', 'Formats', 'Alignments', 'OrderedList', 'UnorderedList', '|',
'CreateLink', 'Image', 'CreateTable', '|',
{ tooltipText: 'Merge Data', template: '#merge_data', command: 'Custom' },
{ tooltipText: 'Insert Field', template: '#insertField', command: 'Custom' },
'SourceCode', '|', 'Undo', 'Redo'
];
//Rich Text Editor ToolbarSettings
const toolbarSettings = {
items: items
};
let mentionChar = "{{";
let rteObj;
let mentionObj;
let range = new Range();
let selection = new NodeSelection();
let saveSelection;
let itemsName = [
{ text: 'First Name' },
{ text: 'Last Name' },
{ text: 'Support Email' },
{ text: 'Company Name' },
{ text: 'Promo Code' },
{ text: 'Support Phone Number' },
{ text: 'Customer ID' },
{ text: 'Expiration Date' },
{ text: 'Subscription Plan' },
];
const placeholderData = {
FirstName: 'John',
LastName: 'Doe',
PromoCode: 'ABC123',
SubscriptionPlan: 'Premium',
CustomerID: '123456',
ExpirationDate: '2024-12-31',
SupportEmail: '[email protected]',
SupportPhoneNumber: '+1-800-555-5555',
CompanyName: 'Example Inc.'
};
let textToValueMap = {
'First Name': 'FirstName',
'Last Name': 'LastName',
'Support Email': 'SupportEmail',
'Company Name': 'CompanyName',
'Promo Code': 'PromoCode',
'Support Phone Number': 'SupportPhoneNumber',
'Customer ID': 'CustomerID',
'Expiration Date': 'ExpirationDate',
'Subscription Plan': 'SubscriptionPlan'
};
let data = [
{ text: 'First Name', value: 'FirstName' },
{ text: 'Last Name', value: 'LastName' },
{ text: 'Support Email', value: 'SupportEmail' },
{ text: 'Company Name', value: 'CompanyName' },
{ text: 'Promo Code', value: 'PromoCode' },
{ text: 'Support Phone Number', value: 'SupportPhoneNumber' },
{ text: 'Customer ID', value: 'CustomerID' },
{ text: 'Expiration Date', value: 'ExpirationDate' },
{ text: 'Subscription Plan', value: 'SubscriptionPlan' },
];
const fieldsData = { text: 'text', value: 'value' };
function displayTemplate(data) {
return (<React.Fragment>
{data.value}}}
</React.Fragment>);
}
function actionBegin(args) {
if (args.requestType === 'EnterAction' &&
mentionObj.element.classList.contains('e-popup-open')) {
args.cancel = true;
}
}
function actionComplete(e) {
if (e.requestType === 'SourceCode') {
rteObj.getToolbar().querySelector('#merge_data').parentElement.classList.add('e-overlay');
rteObj.getToolbar().querySelector('#insertField').parentElement.classList.add('e-overlay');
}
else if (e.requestType === 'Preview') {
rteObj.getToolbar().querySelector('#merge_data').parentElement.classList.remove('e-overlay');
rteObj.getToolbar().querySelector('#insertField').parentElement.classList.remove('e-overlay');
}
}
function blur() {
const range = selection.getRange(document);
const saveSelection = selection.save(range, document);
}
function onDropDownClose() {
if (rteObj) {
rteObj.focusIn();
}
}
function onItemSelect(args) {
if (args.item.text != null) {
const value = textToValueMap[args.item.text];
const trimmedValue = value.trim();
rteObj.formatter.editorManager.nodeSelection.restore();
rteObj.executeCommand('insertHTML', `<span contenteditable="false" class="e-mention-chip"><span>{{${trimmedValue}}}</span></span> `, { undo: true });
}
}
function onClickHandler(args) {
if (rteObj) {
let editorContent = rteObj.value;
let mergedContent = replacePlaceholders(editorContent, placeholderData);
if (rteObj.formatter.getUndoRedoStack().length === 0) {
rteObj.formatter.saveData();
}
rteObj.value = mergedContent;
rteObj.formatter.saveData();
}
else {
console.log('MailMergeEditor is not initialized.');
}
}
;
function replacePlaceholders(template, data) {
return template.replace(/{{\s*(\w+)\s*}}/g, (match, key) => {
const value = data[key.trim()];
return value !== undefined ? value : match;
});
}
return (<div>
<RichTextEditorComponent ref={(richtexteditor) => { rteObj = richtexteditor; }} value={value} id="mailMergeEditor" toolbarSettings={toolbarSettings} placeholder="Type @ and tag the name" blur={blur} actionBegin={actionBegin} actionComplete={actionComplete} saveInterval={1}>
<Inject services={[HtmlEditor, Toolbar, Image, Link, QuickToolbar, Table, PasteCleanup]}/>
</RichTextEditorComponent>
<button className="e-control e-lib e-btn e-formats-tbar-btn e-rte-elements e-tbar-btn" tabIndex={-1} id="merge_data" style={{ width: '100%' }} onClick={onClickHandler}>
<span style={{ display: 'inline-flex' }}>
<span className="e-tbar-btn-text">Merge Data</span>
</span>
</button>
<DropDownButtonComponent className="e-rte-dropdown-btn e-control e-dropdown-btn e-lib e-btn e-rte-dropdown-popup e-rte-dropdown-items e-formats-tbar-btn e-rte-elements e-rte-dropdown-menu" items={itemsName} content='<span style="display: inline-flex;"><span class="e-rte-dropdown-btn-text">Insert Field</span></span>' select={onItemSelect} close={onDropDownClose} id="insertField"></DropDownButtonComponent>
<MentionComponent ref={(scope) => { mentionObj = scope; }} id="mentionEditor" target="#mailMergeEditor" mentionChar={mentionChar} showMentionChar={true} allowSpaces={true} dataSource={data} fields={fieldsData} popupWidth="250px" popupHeight="200px" displayTemplate={displayTemplate}></MentionComponent>
</div>);
}
export default App;import { HtmlEditor, Image, Inject, IToolbarItems, Link, QuickToolbar, RichTextEditorComponent, ToolbarSettingsModel, Toolbar, NodeSelection, ActionCompleteEventArgs, ActionBeginEventArgs, Table, PasteCleanup } from '@syncfusion/ej2-react-richtexteditor';
import * as React from 'react';
import { DropDownButtonComponent, MenuEventArgs } from '@syncfusion/ej2-react-splitbuttons';
import { MentionComponent } from '@syncfusion/ej2-react-dropdowns';
function App() {
let value: string = `<p>Dear <span contenteditable="false" class="e-mention-chip"><span>{{FirstName}}</span></span> <span contenteditable="false" class="e-mention-chip"><span>{{LastName}}</span></span>,</p>
<p>We are thrilled to have you with us! Your unique promotional code for this month is: <span contenteditable="false" class="e-mention-chip"><span>{{PromoCode}}</span></span>.</p>
<p>Your current subscription plan is: <span contenteditable="false" class="e-mention-chip"><span>{{SubscriptionPlan}}</span></span>.</p>
<p>Your customer ID is: <span contenteditable="false" class="e-mention-chip"><span>{{CustomerID}}</span></span>.</p>
<p>Your promotional code expires on: <span contenteditable="false" class="e-mention-chip"><span>{{ExpirationDate}}</span></span>.</p>
<p>Feel free to browse our latest offerings and updates. If you need any assistance, don't hesitate to contact us at <a href="mailto:{{SupportEmail}}"><span contenteditable="false" class="e-mention-chip"><span>{{SupportEmail}}</span></span></a> or call us at <span contenteditable="false" class="e-mention-chip"><span>{{SupportPhoneNumber}}</span></span>.</p>
<p>Best regards,<br>The <span contenteditable="false" class="e-mention-chip"><span>{{CompanyName}}</span></span> Team</p>`;
const items: (string | IToolbarItems)[] = ['Bold', 'Italic', 'Underline', '|', 'Formats', 'Alignments', 'OrderedList', 'UnorderedList', '|',
'CreateLink', 'Image', 'CreateTable', '|',
{ tooltipText: 'Merge Data', template: '#merge_data', command: 'Custom' },
{ tooltipText: 'Insert Field', template: '#insertField', command: 'Custom' },
'SourceCode', '|', 'Undo', 'Redo'
]
//Rich Text Editor ToolbarSettings
const toolbarSettings: ToolbarSettingsModel = {
items: items
};
let mentionChar: string = "{{";
let rteObj: RichTextEditorComponent | undefined;
let mentionObj: MentionComponent | undefined;
let range: Range = new Range();
let selection: NodeSelection = new NodeSelection();
let saveSelection: any;
let itemsName: { text: string }[] = [
{ text: 'First Name' },
{ text: 'Last Name' },
{ text: 'Support Email' },
{ text: 'Company Name' },
{ text: 'Promo Code' },
{ text: 'Support Phone Number' },
{ text: 'Customer ID' },
{ text: 'Expiration Date' },
{ text: 'Subscription Plan' },
];
const placeholderData: { [key: string]: string } = {
FirstName: 'John',
LastName: 'Doe',
PromoCode: 'ABC123',
SubscriptionPlan: 'Premium',
CustomerID: '123456',
ExpirationDate: '2024-12-31',
SupportEmail: '[email protected]',
SupportPhoneNumber: '+1-800-555-5555',
CompanyName: 'Example Inc.'
};
let textToValueMap: { [key: string]: string } = {
'First Name': 'FirstName',
'Last Name': 'LastName',
'Support Email': 'SupportEmail',
'Company Name': 'CompanyName',
'Promo Code': 'PromoCode',
'Support Phone Number': 'SupportPhoneNumber',
'Customer ID': 'CustomerID',
'Expiration Date': 'ExpirationDate',
'Subscription Plan': 'SubscriptionPlan'
};
let data: { text: string; value: string }[] = [
{ text: 'First Name', value: 'FirstName' },
{ text: 'Last Name', value: 'LastName' },
{ text: 'Support Email', value: 'SupportEmail' },
{ text: 'Company Name', value: 'CompanyName' },
{ text: 'Promo Code', value: 'PromoCode' },
{ text: 'Support Phone Number', value: 'SupportPhoneNumber' },
{ text: 'Customer ID', value: 'CustomerID' },
{ text: 'Expiration Date', value: 'ExpirationDate' },
{ text: 'Subscription Plan', value: 'SubscriptionPlan' },
];
const fieldsData: { text: string; value: string } = { text: 'text', value: 'value' };
function displayTemplate(data: { value: string }) {
return (<React.Fragment>
{data.value}}}
</React.Fragment>);
}
function actionBegin(args: ActionBeginEventArgs) {
if (
args.requestType === 'EnterAction' &&
mentionObj.element.classList.contains('e-popup-open')
) {
args.cancel = true;
}
}
function actionComplete(e: ActionCompleteEventArgs) {
if (e.requestType === 'SourceCode') {
rteObj.getToolbar().querySelector('#merge_data').parentElement.classList.add('e-overlay');
rteObj.getToolbar().querySelector('#insertField').parentElement.classList.add('e-overlay');
} else if (e.requestType === 'Preview') {
rteObj.getToolbar().querySelector('#merge_data').parentElement.classList.remove('e-overlay');
rteObj.getToolbar().querySelector('#insertField').parentElement.classList.remove('e-overlay');
}
}
function blur() {
const range = selection.getRange(document);
const saveSelection = selection.save(range, document);
}
function onDropDownClose() {
if (rteObj) {
rteObj.focusIn();
}
}
function onItemSelect(args: { item: { text: string } }) {
if (args.item.text != null) {
const value = textToValueMap[args.item.text];
const trimmedValue = value.trim();
rteObj.formatter.editorManager.nodeSelection.restore();
rteObj.executeCommand(
'insertHTML',
`<span contenteditable="false" class="e-mention-chip"><span>{{${trimmedValue}}}</span></span> `,
{ undo: true }
);
}
}
function onClickHandler(args: any): void {
if (rteObj) {
let editorContent: string = rteObj.value;
let mergedContent: string = replacePlaceholders(editorContent, placeholderData);
if (rteObj.formatter.getUndoRedoStack().length === 0) {
rteObj.formatter.saveData();
}
rteObj.value = mergedContent;
rteObj.formatter.saveData();
} else {
console.log('MailMergeEditor is not initialized.');
}
};
function replacePlaceholders(template: string, data: { [key: string]: string }): string {
return template.replace(/{{\s*(\w+)\s*}}/g, (match, key) => {
const value = data[key.trim()];
return value !== undefined ? value : match;
});
};
return (
<div>
<RichTextEditorComponent ref={(richtexteditor) => { rteObj = richtexteditor; }} value={value} id="mailMergeEditor" toolbarSettings={toolbarSettings} placeholder="Type @ and tag the name" blur={blur} actionBegin={actionBegin} actionComplete={actionComplete} saveInterval={1}>
<Inject services={[HtmlEditor, Toolbar, Image, Link, QuickToolbar, Table, PasteCleanup]} />
</RichTextEditorComponent>
<button
className="e-control e-lib e-btn e-formats-tbar-btn e-rte-elements e-tbar-btn"
tabIndex={-1}
id="merge_data"
style={{ width: '100%' }}
onClick={onClickHandler}
>
<span style={{ display: 'inline-flex' }}>
<span className="e-tbar-btn-text">Merge Data</span>
</span>
</button>
<DropDownButtonComponent
className="e-rte-dropdown-btn e-control e-dropdown-btn e-lib e-btn e-rte-dropdown-popup e-rte-dropdown-items e-formats-tbar-btn e-rte-elements e-rte-dropdown-menu"
items={itemsName}
content='<span style="display: inline-flex;"><span class="e-rte-dropdown-btn-text">Insert Field</span></span>'
select={onItemSelect} close={onDropDownClose}
id="insertField"
></DropDownButtonComponent>
<MentionComponent ref={(scope) => { mentionObj = scope; }} id="mentionEditor" target="#mailMergeEditor" mentionChar={mentionChar} showMentionChar={true} allowSpaces={true} dataSource={data} fields={fieldsData} popupWidth="250px" popupHeight="200px" displayTemplate={displayTemplate}></MentionComponent>
</div>
);
}
export default App;