Syncfusion AI Assistant

How can I help you?

Mail merge in EJ2 TypeScript Rich Text Editor Control

20 Dec 202517 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.
const mailMergeEditor: RichTextEditor = new RichTextEditor({
   toolbarSettings: {
      items: [
         'Bold', 'Italic', 'Underline', '|',
         'Formats', 'Alignments', '|',
         { tooltipText: 'Merge Data', template: '#merge_data', command: 'Custom' },
         { tooltipText: 'Insert Field', template: '#insertField', command: 'Custom' },
         'SourceCode', '|', 'Undo', 'Redo'
      ],
   },
});
mailMergeEditor.appendTo('#mailMergeEditor');

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.

let insertField: DropDownButton = new DropDownButton({
   items: [
      { 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' },
   ],
   content: `<span style="display:inline-flex;"><span class="e-rte-dropdown-btn-text">Insert Field</span></span>`,
   select: onItemSelect,
});
insertField.appendTo('#insertField');

function onItemSelect(args: MenuEventArgs): void {
   const value = textToValueMap[args.item.text];
   mailMergeEditor.executeCommand(
      'insertHTML',
      `<span contenteditable="false" class="e-mention-chip"><span>{{${value}}}</span></span>&nbsp;`,
      { undo: true }
   );
}

Populating merge fields using Mention

The Mention control 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.

const mentionObj: Mention = new Mention({
   dataSource: mergeData,
   target: '#mailMergeEditor',
   mentionChar: `{{`,
   fields: { text: 'text' },
   allowSpaces: true,
   popupWidth: '250px',
   popupHeight: '200px',
   displayTemplate: '<span> {{${value}}} </span>',
});
mentionObj.appendTo('#mentionField');

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.

document.getElementById('merge_data')?.addEventListener('click', onClickHandler);

function onClickHandler(): void {
   let editorContent = mailMergeEditor.value;
   let mergedContent = replacePlaceholders(editorContent, placeholderData);
   mailMergeEditor.value = mergedContent;
}

function replacePlaceholders(template: string, data: { [key: string]: string }): string {
   return template.replace(/{{\s*(\w+)\s*}}/g, (match, key) => data[key.trim()] || match);
}
import {RichTextEditor,Toolbar,Link,Image,HtmlEditor,QuickToolbar,Table,PasteCleanup, ActionCompleteEventArgs, ActionBeginEventArgs} from '@syncfusion/ej2-richtexteditor';
import { DropDownButton, MenuEventArgs } from '@syncfusion/ej2-splitbuttons';
import { Mention } from '@syncfusion/ej2-dropdowns';

RichTextEditor.Inject(Toolbar,Link,Image,HtmlEditor,QuickToolbar,Table,PasteCleanup);

const rteValue: 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 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',
};

const dropdownContent: string = ` <span style="display:inline-flex;">
<span class="e-rte-dropdown-btn-text">Insert Field</span>
</span>`;

const mergeData: any = [
   { 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 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.',
};

function onActionBegin(args: ActionBeginEventArgs) {
   if (
      args.requestType === 'EnterAction' &&
      mentionObj.element.classList.contains('e-popup-open')
   ) {
      args.cancel = true;
   }
}

function actionCompleteHandler(e: ActionCompleteEventArgs): void {
   if (e.requestType === 'SourceCode') {
      mailMergeEditor.getToolbar().querySelector('#merge_data').parentElement.classList.add('e-overlay');
      mailMergeEditor.getToolbar().querySelector('#insertField').parentElement.classList.add('e-overlay');
   } else if (e.requestType === 'Preview') {
      mailMergeEditor.getToolbar().querySelector('#merge_data').parentElement.classList.remove('e-overlay');
      mailMergeEditor.getToolbar().querySelector('#insertField').parentElement.classList.remove('e-overlay');
   }
}

function onDropDownClose() {
   if (mailMergeEditor) {
      mailMergeEditor.focusIn();
   }
}

function onItemSelect(args: MenuEventArgs): void {
   if (args.item.text != null) {
      const value = textToValueMap[args.item.text];
      const trimmedValue = value.trim();
      mailMergeEditor.formatter.editorManager.nodeSelection.restore();
      mailMergeEditor.executeCommand(
            'insertHTML',
            `<span contenteditable="false" class="e-mention-chip"><span>{{${trimmedValue}}}</span></span>&nbsp;`,
            { undo: true }
      );
   }
}

function onClickHandler(args: any): void {
   if (mailMergeEditor) {
      let editorContent = mailMergeEditor.value;
      let mergedContent = replacePlaceholders(editorContent, placeholderData);
      if ((mailMergeEditor as any).formatter.getUndoRedoStack().length === 0) {
            (mailMergeEditor as any).formatter.saveData();
      }
      mailMergeEditor.value = mergedContent;
      (mailMergeEditor as any).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()];
      const result = value !== undefined ? value : match;
      return result;
   });
}

const mailMergeEditor: RichTextEditor = new RichTextEditor({
   value: rteValue,
   toolbarSettings: {
      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',
      ],
   },
   actionBegin: onActionBegin,
   actionComplete: actionCompleteHandler,
   saveInterval: 1,
});
mailMergeEditor.appendTo('#mailMergeEditor');

let insertField: DropDownButton = new DropDownButton({
   items: [
      { 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' },
   ],
   content: dropdownContent,
   select: onItemSelect,
   close: onDropDownClose
});
insertField.appendTo('#insertField');
document.getElementById('merge_data')?.addEventListener('click', onClickHandler);

const mentionObj: Mention = new Mention({
   dataSource: mergeData,
   target: '#mailMergeEditor',
   mentionChar: '{{',
   fields: { text: 'text' },
   allowSpaces: true,
   popupWidth: '250px',
   popupHeight: '200px',
   displayTemplate: '<span> {{${value}}} </span>',
});
mentionObj.appendTo('#mentionField');
<!DOCTYPE html>
<html lang="en">

<head>
    <title>Essential JS 2 Rich Text Editor</title>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta name="description" content="Typescript UI Controls" />
    <meta name="author" content="Syncfusion" />
    <link href="index.css" rel="stylesheet" />
    <link href="https://cdn.syncfusion.com/ej2/33.1.44/ej2-base/styles/tailwind3.css" rel="stylesheet" />
    <link href="https://cdn.syncfusion.com/ej2/33.1.44/ej2-richtexteditor/styles/tailwind3.css" rel="stylesheet" />
    <link href="https://cdn.syncfusion.com/ej2/33.1.44/ej2-inputs/styles/tailwind3.css" rel="stylesheet" />
    <link href="https://cdn.syncfusion.com/ej2/33.1.44/ej2-lists/styles/tailwind3.css" rel="stylesheet" />
    <link href="https://cdn.syncfusion.com/ej2/33.1.44/ej2-navigations/styles/tailwind3.css" rel="stylesheet" />
    <link href="https://cdn.syncfusion.com/ej2/33.1.44/ej2-popups/styles/tailwind3.css" rel="stylesheet" />
     <link href="https://cdn.syncfusion.com/ej2/33.1.44/ej2-buttons/styles/tailwind3.css" rel="stylesheet" />
    <link href="https://cdn.syncfusion.com/ej2/33.1.44/ej2-splitbuttons/styles/tailwind3.css" rel="stylesheet" />
    <link href="https://cdn.syncfusion.com/ej2/33.1.44/ej2-dropdowns/styles/tailwind3.css" rel="stylesheet" />
    <script src="https://cdnjs.cloudflare.com/ajax/libs/systemjs/0.19.38/system.js"></script>
    <script src="systemjs.config.js"></script>
</head>

<body>
    <div id='loader'>Loading....</div>
    <div id="container">
      <div id="mailMergeEditor"></div>
      <button
        class="e-control e-lib e-btn e-formats-tbar-btn e-rte-elements e-tbar-btn"
        tabindex="-1"
        id="merge_data"
        style="width: 100%"
      >
        <span style="display: inline-flex"
          ><span class="e-tbar-btn-text">Merge Data</span></span
        >
      </button>
      <button
        class="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"
        id="insertField"
      ></button>
      <div id="mentionField"></div>
    </div>
</body>

</html>