Skip to content

Merge Tags ​

Merge tags provide dynamic, interactive placeholders (such as client.name or invoice.amount) for document generation and email templates. Typing {{ opens an autocomplete dropdown at the cursor caret, and enabling the feature unlocks a slide-out Merge Tag sidebar panel in the menubar.

Merge tags are disabled by default. The trigger is fixed as {{; item values are normalized to the same double-braced form before display, insertion, and event emission.


Key Highlights ​

Fixed Trigger

Typing {{ instantly opens a filtered popup menu at the cursor position.

Grouped Slide-out Drawer

A right slide-out panel categorizes merge tags by custom group names (e.g. Client, Proposal, Billing).

Atomic Token Chips

Inserted tags render as non-editable inline tokens that delete cleanly as a single unit when pressing Backspace or Delete.

Safe HTML Storage

Content is saved as structured HTML data attributes (data-erag-merge-tag-value="client.name").


Configuration & Item Interface ​

Define merge tag items in init.mergeTags:

ts
import type { EditorInit, MergeTagItem } from '@erag/text-editor-react';

const mergeTagItems: MergeTagItem[] = [
    { value: '{{amit}}', name: 'Consultant name' },
    { value: '{{client.name}}', name: 'Client name', group: 'Client' },
    {
        value: '{{client.salutation}}',
        name: 'Client salutation',
        group: 'Client',
    },
    {
        value: '{{submission.date}}',
        name: 'Submission date',
        group: 'Proposal',
    },
    {
        value: '{{proposal.number}}',
        name: 'Proposal number',
        group: 'Proposal',
    },
];

const editorConfig: EditorInit = {
    mergeTags: {
        enabled: true,
        limit: 10,
        items: mergeTagItems,
    },
};

If the items come from component state or props, build the config with useMemo(() => ({ mergeTags: { enabled: true, items } }), [items]) (or pass an inline object — the editor detects configuration changes structurally).

OptionTypeDefaultDescription
enabledbooleantrueEnables the feature for an object configuration.
limitnumber10Maximum autocomplete results.
itemsMergeTagItem[][]Consumer-provided tags displayed by the UI.

The package does not invent merge tags. Supply at least one item for the menubar entry, sidebar, and suggestions to have content.

Merge Tag Item Interface (MergeTagItem) ​

PropertyTypeRequiredDescription
valuestringYesVisible and stored token value, normally supplied as {{client.name}}. A bare or single-braced value is normalized to double braces.
namestringOptionalFriendly display name shown in autocomplete and the sidebar. The inserted token remains the normalized value.
groupstringOptionalGroup name used to categorize tags in the right slide-out drawer (e.g. Client, Proposal).

When mergeTags.enabled is true, a Merge tag menu option is added to the menubar. Clicking it opens a right slide-out sidebar drawer organized by category groups.

If you specify an explicit menubar array in configuration, include 'merge-tags':

ts
const editorConfig: EditorInit = {
    menubar: ['file', 'edit', 'insert', 'format', 'merge-tags'],
    mergeTags: {
        enabled: true,
        items: mergeTagItems,
    },
};

The sidebar keeps ungrouped items in a common list and renders named groups as separate sections. Clicking an item restores the saved editor selection and inserts the tag at that caret position.

Autocomplete controls ​

After typing {{, suggestions are filtered against friendly names, normalized values, and group names. Starts-with matches appear before contains matches, and limit controls the maximum visible results.

  • ArrowDown / ArrowUp moves through results and wraps.
  • Home / End jumps to the first or last result.
  • Enter / Tab inserts the active result.
  • Escape closes the dropdown.
  • Backspace after a token or Delete before it removes the complete token and calls onMergeTagRemove.

Merge Tag Events ​

Listen to tag selection and removal with the onMergeTagSelect and onMergeTagRemove callback props:

tsx
import { useState } from 'react';
import {
    Editor,
    type MergeTagRemoveEvent,
    type MergeTagSelectEvent,
} from '@erag/text-editor-react';

export default function App() {
    const [content, setContent] = useState('');

    function handleTagSelect(event: MergeTagSelectEvent) {
        console.log('Inserted merge tag:', event.item.value);
    }

    function handleTagRemove(event: MergeTagRemoveEvent) {
        console.log('Removed merge tag:', event.item.value);
    }

    return (
        <Editor
            value={content}
            onChange={setContent}
            init={editorConfig}
            onMergeTagSelect={handleTagSelect}
            onMergeTagRemove={handleTagRemove}
        />
    );
}
  • onMergeTagSelect: Called with { item, query } when a tag is inserted from autocomplete or the sidebar (query is an empty string for sidebar insertions).
  • onMergeTagRemove: Called with { item } when Backspace or Delete removes an inserted token.

HTML Output & Backend Replacement ​

Inserted merge tags render as atomic non-editable token chips in exported HTML:

html
<span
    class="erag-merge-tag"
    data-erag-merge-tag="true"
    data-erag-merge-tag-value="{{client.name}}"
    contenteditable="false"
    >&#123;&#123;client.name&#125;&#125;</span
>

Backend Placeholder Resolution Example (Laravel / PHP) ​

Before sending an email or rendering a PDF report, parse the stored HTML and replace only recognized merge-tag elements. Avoid a global string replacement because it also changes the token's data attribute and leaves stale chip markup behind.

php
$document = new DOMDocument();
$document->loadHTML(
    '<meta charset="utf-8"><body>'.$template->body.'</body>',
    LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD,
);

$replacements = [
    '{{client.name}}' => $client->name,
    '{{client.salutation}}' => $client->salutation,
    '{{consultant.name}}' => $consultant->name,
];

$xpath = new DOMXPath($document);

foreach ($xpath->query('//span[@data-erag-merge-tag="true"]') as $node) {
    $token = $node->attributes?->getNamedItem('data-erag-merge-tag-value')?->nodeValue;

    if ($token !== null && array_key_exists($token, $replacements)) {
        $node->parentNode?->replaceChild(
            $document->createTextNode($replacements[$token]),
            $node,
        );
    }
}

Serialize the body children after replacement and pass the result through your server-side HTML sanitizer. Only replace values from an application allowlist. Do not treat a stored tag as executable code or as an arbitrary object path.

Released under the MIT License. Copyright © Er Amit Gupta