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 { computed } from 'vue';
import type { EditorInit, MergeTagItem } from '@erag/text-editor-vue';

const mergeTagItems = computed<MergeTagItem[]>(() => [
    { value: '{{amit}}' },
    { value: '{{client.name}}', group: 'Client' },
    { value: '{{client.salutation}}', group: 'Client' },
    { value: '{{submission.date}}', group: 'Proposal' },
    { value: '{{proposal.number}}', group: 'Proposal' },
]);

const editorConfig = computed<EditorInit>(() => ({
    mergeTags: {
        enabled: true,
        limit: 10,
        items: mergeTagItems.value,
    },
}));
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.
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.value,
    },
};

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 both 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 emits merge-tag-remove.

Merge Tag Events

Listen to tag selection and removal events:

vue
<script setup lang="ts">
import { shallowRef } from 'vue';
import {
    Editor,
    type MergeTagRemoveEvent,
    type MergeTagSelectEvent,
} from '@erag/text-editor-vue';

const content = shallowRef('');

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

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

<template>
    <Editor
        v-model="content"
        :init="editorConfig"
        @merge-tag-select="handleTagSelect"
        @merge-tag-remove="handleTagRemove"
    />
</template>

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