Integrate an External Merge Tags Selector
Overview
The External Merge Tags Selector integration enables users to browse and select dynamic merge tags from your customer data platform, CRM, or custom data sources directly within the Stripo Email Editor — both in the text block and in the editor's input fields. Merge tags are placeholders that get replaced with actual customer data when emails are sent, enabling personalized email content at scale.
What You'll Build
In this tutorial, you'll create a fully functional merge tags selector modal that:
- Displays a responsive grid of merge tag cards with labels, values, descriptions, and previews
- Supports category-based filtering (All, Personal, Contact, Company, Date/Time, Custom, Links)
- Opens from the merge tags control of the text block and from the merge tag icons of input fields: links, image paths, text inputs, the email Subject, and the Hidden Preheader
- Handles merge tag selection with proper callback integration
- Shows real-time preview values for each merge tag
- Detects and displays module context with a visual badge indicator
- Returns merge tag data in the format expected by the Stripo editor



Use Cases
- CRM Integration: Connect to Salesforce, HubSpot, or other CRM systems to access contact fields
- Customer Data Platforms: Integrate with Segment, mParticle, or custom CDP solutions
- E-commerce Platforms: Access customer purchase history, cart data, and product recommendations
- Marketing Automation: Connect to Mailchimp, SendGrid, or custom email service providers
- Custom Data Sources: Fetch merge tags from your API or internal systems
- Multi-System Integration: Combine merge tags from multiple data sources in a single interface
Prerequisites
Before starting this tutorial, ensure you have:
- Node.js version 22.x or higher installed
- Basic understanding of JavaScript ES6+ syntax
- Familiarity with the Stripo Extensions SDK
- Understanding of how merge tags work in email marketing
Version Availability
Opening the merge tags modal from input fields requires @stripoinc/ui-editor-extensions v3.11.0 or higher and the Stripo Editor v2.79.0 or higher.
Understanding the Interface
The integration consists of a merge tags modal that you implement yourself and two entry points that open it from the editor.
1. Text Block: Custom UI Element
The merge tags control of the text block is replaced with a custom UI element that extends UIElement:
typescript
class MergeTagsUiElement extends UIElement {
getId(): string; // Unique element ID
getTemplate(): string; // HTML template for the button
onRender(container: HTMLElement): void; // Setup event listeners
onDestroy(): void; // Cleanup
onAttributeUpdated(name: string, value: any): void; // Handle attribute changes
}The custom UI element is registered in place of the default merge tags control with a UI element tag registry:
typescript
class ExtensionTagRegistry extends UIElementTagRegistry {
registerUiElements(uiElementsTagsMap: Record<string, string>): void;
}2. Input Fields: External Merge Tags Library
Version Availability
This entry point is available starting from v3.11.0
The merge tag icons of the editor's input fields open a class that extends ExternalMergeTagsLibrary:
typescript
class MyMergeTagsLibrary extends ExternalMergeTagsLibrary {
openMergeTagsLibrary(
currentValue: string, // Current value of the whole field
onSelectCallback: (value: string) => void, // Inserts the merge tag into the field
onCancelCallback: () => void, // Cancels the selection
context: ExternalMergeTagsContext // Field type ('text' | 'link') and name
): void;
}Entry Points Comparison
| Text Block | Input Fields | |
|---|---|---|
| Where it opens | Merge tags control of the text block | Merge tag icons of links, image paths, text inputs (e.g., button text), the email Subject, and the Hidden Preheader |
| SDK API | UIElement + UIElementTagRegistry | ExternalMergeTagsLibrary (v3.11.0+) |
| Returned value | {value, label} object via this.api.triggerValueChange() | Merge tag value string via onSelectCallback() |
| Available context | Selected merge tag and block node (module detection) | Whole field value, field type, and field name |
Both entry points are independent: you can register only one of them or both, as in this tutorial.
Merge Tag Object Structure
When a user selects a merge tag, the modal returns an object with the following structure:
typescript
{
value: string; // The merge tag value (e.g., "*|FNAME|*")
label: string; // Human-readable label (e.g., "First Name")
}The text block expects this object, while input fields expect only its value.
Merge Tag Formats
Merge tags can follow different formats depending on your email service provider:
- Mailchimp:
*|FIELD|* - Campaign Monitor:
%%field%% - Custom: Any format your system supports :::
Step 1: Project Setup
Create your project structure and install dependencies according to the Getting Started guide.
Your project directory structure should look like this:
bash
external-merge-tags/
├── index.html
├── src/
│ ├── creds.js
│ ├── index.js
│ ├── extension.js
├── package.json
└── vite.config.jsStep 2: Create the Merge Tags Modal Class
Create a new file src/MergeTagsModal.js with the following basic class structure:
javascript
/**
* Merge Tags Modal Implementation
* This class implements a modal merge tags selector with filtering capabilities.
* A single instance is shared by the text block merge tags control and the input fields merge tags library.
*/
export class MergeTagsModal {
// Shared instance
static instance = null;
// Instance properties
externalLibrary;
selectedMergetag = null;
selectCallback = () => {};
cancelCallback = () => {};
activeCategory = 'all';
isModule = false;
/**
* Returns the shared modal instance, creating it on first use
* @returns {MergeTagsModal} The modal instance
*/
static getInstance() {
if (!MergeTagsModal.instance) {
MergeTagsModal.instance = new MergeTagsModal();
}
return MergeTagsModal.instance;
}
constructor() {
this.createModal();
this.attachEventListeners();
this.initializeFilters();
this.addStyles();
}
/**
* Opens the merge tags modal
* @param {Object} options - Modal options
* @param {string} [options.selectedValue] - Merge tag value to highlight (if any)
* @param {boolean} [options.isModule] - Whether the merge tag is being used in a module context
* @param {string} [options.category] - Category to display when the modal opens
* @param {Function} options.onSelect - Callback invoked with the selected merge tag
* @param {Function} [options.onCancel] - Callback invoked when the modal is closed without selection
*/
open({selectedValue, isModule = false, category = 'all', onSelect, onCancel = () => {}}) {
// Store callbacks and selected tag
this.selectedMergetag = selectedValue;
this.isModule = isModule;
this.selectCallback = onSelect;
this.cancelCallback = onCancel;
// Update module badge visibility
const moduleBadge = this.externalLibrary.querySelector('.module-badge');
if (moduleBadge) {
moduleBadge.style.display = this.isModule ? 'inline-block' : 'none';
}
// Update selected state
this.updateSelectedTag();
// Show modal
this.externalLibrary.style.display = 'flex';
// Show the requested category
this.filterTags(category);
const categoryButton = this.externalLibrary.querySelector(`.filter-buttons [data-category="${category}"]`);
if (categoryButton) {
this.updateActiveButton(categoryButton);
}
}
}Key Components Explained
Instance Properties:
externalLibrary: Reference to the modal DOM elementselectedMergetag: Merge tag value to highlightselectCallback: Callback invoked when a merge tag is selectedcancelCallback: Callback invoked when the modal is closed without selectionactiveCategory: Currently active filter categoryisModule: Boolean flag indicating if the merge tag is used in a module context
getInstance: Returns a single shared modal, so both entry points use the same DOM element instead of creating duplicates
Constructor: Initializes the modal UI, event listeners, filters, and custom styles
open: Method called by both entry points that:
- Stores the merge tag to highlight, module context, and callbacks
- Shows/hides the "Module" badge based on context
- Updates visual selection state
- Displays the modal
- Shows the requested category (all tags by default)
Step 3: Define Merge Tags Data and Styles
Add static properties for merge tags data and UI configuration. Continue editing src/MergeTagsModal.js:
javascript
export class MergeTagsModal {
// ... existing properties ...
// UI Style configurations
static STYLES = {
// Modal overlay styles
overlay: {
backgroundColor: 'rgba(0,0,0,.7)',
position: 'fixed',
top: '0',
right: '0',
bottom: '0',
left: '0',
zIndex: '1050',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
backdropFilter: 'blur(4px)',
fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif"
},
// Modal container styles
modal: {
backgroundColor: '#ffffff',
borderRadius: '12px',
boxShadow: '0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)',
maxWidth: '900px',
width: '90%',
display: 'flex',
flexDirection: 'column',
position: 'relative'
},
// Header styles
header: {
padding: '24px 32px',
borderBottom: '1px solid #e5e7eb',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
backgroundColor: '#f9fafb',
borderRadius: '12px 12px 0 0'
},
// Content container styles
content: {
padding: '32px',
height: '315px',
overflowY: 'auto',
overflowX: 'hidden',
boxSizing: 'border-box'
},
// Grid styles
grid: {
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(220px, 1fr))',
gap: '16px'
},
// Button styles
buttonActive: {
padding: '6px 14px',
borderRadius: '6px',
border: 'none',
backgroundColor: '#34c759',
color: 'white',
fontSize: '14px',
fontWeight: '500',
cursor: 'pointer',
transition: 'background-color 0.2s'
},
buttonInactive: {
padding: '6px 14px',
borderRadius: '6px',
border: '1px solid #e5e7eb',
backgroundColor: 'white',
color: '#6b7280',
fontSize: '14px',
fontWeight: '500',
cursor: 'pointer',
transition: 'all 0.2s'
},
// Footer styles
footer: {
padding: '16px 32px',
borderTop: '1px solid #e5e7eb',
backgroundColor: '#fef3c7',
borderRadius: '0 0 12px 12px',
textAlign: 'center'
}
};
// Sample merge tags data
static MERGE_TAGS = [
{
category: 'personal',
value: '*|FNAME|*',
label: 'First Name',
preview: 'John',
description: 'Recipient\'s first name'
},
{
category: 'personal',
value: '*|LNAME|*',
label: 'Last Name',
preview: 'Doe',
description: 'Recipient\'s last name'
},
{
category: 'personal',
value: '*|EMAIL|*',
label: 'Email Address',
preview: 'john.doe@example.com',
description: 'Recipient\'s email address'
},
{
category: 'contact',
value: '%%Phone%%',
label: 'Phone Number',
preview: '+1 (555) 123-4567',
description: 'Recipient\'s phone number'
},
{
category: 'company',
value: '{{company}}',
label: 'Company Name',
preview: 'Acme Corp',
description: 'Recipient\'s company'
},
{
category: 'date',
value: '*|DATE|*',
label: 'Current Date',
preview: new Date().toLocaleDateString(),
description: 'Today\'s date'
},
{
category: 'custom',
value: '*|CUSTOM_FIELD|*',
label: 'Custom Field',
preview: 'Custom Value',
description: 'Custom merge field'
},
{
category: 'links',
value: '*|UNSUB|*',
label: 'Unsubscribe Link',
preview: 'https://example.com/unsubscribe',
description: 'Link to unsubscribe from emails'
},
{
category: 'links',
value: '*|ARCHIVE|*',
label: 'View in Browser Link',
preview: 'https://example.com/archive',
description: 'Link to the web version of the email'
}
];
// ... rest of the class ...
}Merge Tag Object Properties
| Property | Type | Description |
|---|---|---|
category | string | Filter category (personal, contact, company, date, custom, links) |
value | string | The actual merge tag value used in templates |
label | string | Human-readable display name |
preview | string | Sample value shown to users |
description | string | Helpful description of the merge tag's purpose |
The links category contains merge tags that are replaced with URLs. The modal opens this category for link fields (see Step 12).
Production Implementation
In production environments, replace the static MERGE_TAGS array with API calls to dynamically fetch merge tags from your CRM, CDP, or custom data sources.
Step 4: Build the Modal UI Structure
Implement the core modal creation methods. These methods generate the modal HTML structure and inject it into the page.
Create Modal Method
javascript
/**
* Creates the modal HTML structure and appends it to the document body
*/
createModal() {
const modalHtml = this.generateModalHTML();
const container = document.createElement('div');
container.innerHTML = modalHtml;
document.body.appendChild(container);
// Store reference to the modal element
this.externalLibrary = document.getElementById('externalMergeTags');
// Initially hide the modal
this.externalLibrary.style.display = 'none';
}
/**
* Generates the complete modal HTML structure
* @returns {string} Complete HTML string for the modal
*/
generateModalHTML() {
return `
<div id="externalMergeTags" style="${this.styleObjToString(MergeTagsModal.STYLES.overlay)}">
<div style="${this.styleObjToString(MergeTagsModal.STYLES.modal)}">
${this.generateHeaderHTML()}
${this.generateContentHTML()}
${this.generateFooterHTML()}
</div>
</div>
`;
}Style Conversion Helper
Add the following utility method that converts JavaScript style objects to inline CSS strings:
javascript
/**
* Converts a style object to an inline style string
* @param {Object} styleObj - Style object with camelCase properties
* @returns {string} Inline CSS style string with kebab-case properties
*/
styleObjToString(styleObj) {
return Object.entries(styleObj)
.map(([key, value]) => {
// Convert camelCase to kebab-case
const kebabKey = key.replace(/([A-Z])/g, '-$1').toLowerCase();
return `${kebabKey}: ${value}`;
})
.join('; ');
}Add Custom Styles for Selection State
javascript
/**
* Adds custom styles for the selected merge tag state
*/
addStyles() {
const style = document.createElement('style');
style.innerHTML = `
#externalMergeTags .tag-card.selected {
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
`;
document.head.appendChild(style);
}Important
The styleObjToString() method is essential for converting the STYLES object into inline CSS strings. The addStyles() method adds CSS for visual selection feedback.
Modal Structure Overview
The modal consists of three main sections:
- Header: Title, category filter buttons, and close button
- Content: Scrollable grid of merge tag cards with labels, values, descriptions, and previews
- Footer: Informational disclaimer
Step 5: Generate Header with Filters
Create the header section with title, filter buttons, and close button.
Header HTML Generator
javascript
/**
* Generates the modal header section HTML
* @returns {string} HTML string for the header section
*/
generateHeaderHTML() {
return `
<div style="${this.styleObjToString(MergeTagsModal.STYLES.header)}">
<div style="display: flex; align-items: center; gap: 12px;">
<h2 style="margin: 0; font-size: 24px; font-weight: 600; color: #111827; letter-spacing: -0.025em;">
Merge Tags
</h2>
<span class="module-badge" style="display: none; background-color: #3b82f6; color: white; padding: 4px 12px; border-radius: 6px; font-size: 14px; font-weight: 600; letter-spacing: 0.025em;">
Module
</span>
<div class="filter-buttons" style="display: flex; gap: 8px; margin-left: 24px;">
${this.generateFilterButtons()}
</div>
</div>
${this.generateCloseButton()}
</div>
`;
}Module Badge Feature
The header includes a "Module" badge that displays when the merge tag selector is opened from within a module context. This badge is hidden by default and only becomes visible when isModule is true, helping users understand whether they're working with merge tags in a regular template or within a reusable module component. The module context is available for the text block entry point only.
Filter Buttons Generator
javascript
/**
* Generates category filter buttons HTML
* @returns {string} HTML string for all filter buttons
*/
generateFilterButtons() {
const categories = [
{ id: 'all', label: 'All', active: true },
{ id: 'personal', label: 'Personal', active: false },
{ id: 'contact', label: 'Contact', active: false },
{ id: 'company', label: 'Company', active: false },
{ id: 'date', label: 'Date/Time', active: false },
{ id: 'custom', label: 'Custom', active: false },
{ id: 'links', label: 'Links', active: false }
];
return categories.map(cat => `
<button
data-category="${cat.id}"
style="${this.styleObjToString(cat.active ? MergeTagsModal.STYLES.buttonActive : MergeTagsModal.STYLES.buttonInactive)}">
${cat.label}
</button>
`).join('');
}Category Customization
Categories can be added or modified by updating the categories array. Ensure your merge tags data contains matching category values for proper filtering. Common categories include personal info, contact details, company data, transactional data, and custom fields.
Close Button Generator
javascript
/**
* Generates close button HTML with hover effects
* @returns {string} HTML string for the close button
*/
generateCloseButton() {
return `
<button class="close" type="button"
style="cursor: pointer; background: transparent; border: none; font-size: 24px;
color: #6b7280; width: 40px; height: 40px; display: flex; align-items: center;
justify-content: center; border-radius: 8px; transition: all 0.2s;"
onmouseover="this.style.backgroundColor='#f3f4f6'; this.style.color='#111827';"
onmouseout="this.style.backgroundColor='transparent'; this.style.color='#6b7280';">
<span style="line-height: 1;">×</span>
</button>
`;
}Step 6: Generate Content with Merge Tags Grid
Create the content section that displays merge tag cards in a responsive grid.
Content Container Generator
javascript
/**
* Generates the modal content section HTML with merge tags grid
* @returns {string} HTML string for the content section
*/
generateContentHTML() {
return `
<div style="${this.styleObjToString(MergeTagsModal.STYLES.content)}">
<div class="tags-grid" style="${this.styleObjToString(MergeTagsModal.STYLES.grid)}">
${this.generateMergeTagCards()}
</div>
</div>
`;
}Merge Tag Cards Generator
javascript
/**
* Generates merge tag card HTML
* @returns {string} HTML string for all merge tag cards
*/
generateMergeTagCards() {
return MergeTagsModal.MERGE_TAGS.map(tag => `
<div class="tag-card"
data-category="${tag.category}"
data-value="${tag.value}"
data-label="${tag.label}"
style="cursor: pointer; border: 2px solid #e5e7eb; border-radius: 8px;
padding: 16px; background-color: #ffffff; transition: all 0.2s;
display: flex; flex-direction: column; gap: 8px;"
onmouseover="if(!this.classList.contains('selected')) {
this.style.borderColor='#d1d5db';
this.style.transform='translateY(-2px)';
this.style.boxShadow='0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)';
}"
onmouseout="if(!this.classList.contains('selected')) {
this.style.borderColor='#e5e7eb';
this.style.transform='translateY(0)';
this.style.boxShadow='none';
}">
<div style="display: flex; justify-content: space-between; align-items: flex-start;">
<h3 style="margin: 0; font-size: 16px; font-weight: 600; color: #111827;">
${tag.label}
</h3>
<span style="font-family: 'Monaco', 'Consolas', monospace; font-size: 12px;
background-color: #f3f4f6; padding: 2px 6px; border-radius: 4px;
color: #6b7280;">
${tag.value}
</span>
</div>
<p style="margin: 0; font-size: 14px; color: #6b7280;">
${tag.description}
</p>
<div style="background-color: #f9fafb; padding: 8px; border-radius: 4px;
margin-top: 4px;">
<span style="font-size: 12px; color: #9ca3af;">Preview: </span>
<span style="font-size: 12px; color: #4b5563; font-weight: 500;">
${tag.preview}
</span>
</div>
</div>
`).join('');
}Key Features
- Responsive Grid: Utilizes CSS Grid with
auto-fillto create a responsive layout that adapts to different screen sizes - Merge Tag Cards: Each card displays the merge tag label, value (in monospace), description, and sample preview
- Hover Effects: Inline event handlers provide smooth transition animations with conditional logic to preserve selected state
- Data Attributes: Merge tag metadata is stored in data attributes for efficient retrieval on selection
- Visual Feedback: Selected cards have distinct styling to indicate current selection
- Preview Display: Shows sample data to help users understand what the merge tag will display
Design Considerations
The monospace font for merge tag values helps users distinguish the actual merge tag syntax from the display labels. The preview section provides crucial context about what data the merge tag represents.
Step 7: Generate Footer with Disclaimer
Add a footer section to display important notices or disclaimers.
javascript
/**
* Generates the modal footer section HTML with disclaimer notice
* @returns {string} HTML string for the footer section
*/
generateFooterHTML() {
return `
<div style="${this.styleObjToString(MergeTagsModal.STYLES.footer)}">
<p style="margin: 0; font-size: 13px; color: #92400e; font-weight: 500;">
<span style="font-weight: 700; color: #d97706;">⚠️ Notice:</span> This popup window is not part of the plugin. It is intended solely for demonstration purposes and can be implemented independently in any desired way.
</p>
</div>
`;
}Step 8: Implement Event Handlers
Add event listeners to handle user interactions with the modal.
Attach Event Listeners
javascript
/**
* Attaches event listeners to modal elements after creation
*/
attachEventListeners() {
// Close button click handler
this.externalLibrary.querySelector('.close')
.addEventListener('click', this.cancelAndClose.bind(this));
// Tag card click handler (using event delegation)
this.externalLibrary.addEventListener('click', this.onTagClick.bind(this));
}Handle Merge Tag Selection
javascript
/**
* Handles click events on merge tag cards
* @param {Event} e - Click event object
*/
onTagClick(e) {
// Check if clicked on tag card or any of its children
const tagCard = e.target.closest('.tag-card');
if (!tagCard) return;
// Create callback object with tag data
const tagData = {
value: tagCard.getAttribute('data-value'),
label: tagCard.getAttribute('data-label')
};
// Close modal and execute callback
this.close();
this.selectCallback(tagData);
}Data Format
The onSelect callback receives an object with value and label properties. The value is the actual merge tag syntax that will be inserted into the template, while the label provides the human-readable name for display purposes. Each entry point passes this data to the editor in the format it expects (see Step 10 and Step 12).
Handle Selected State Updates
javascript
/**
* Updates the selected state of merge tag cards
*/
updateSelectedTag() {
// Remove selected class from all cards
const selectedElement = this.externalLibrary.querySelector('.tag-card.selected');
if (selectedElement) {
selectedElement.classList.remove('selected');
// Reset styles
selectedElement.style.borderColor = '#e5e7eb';
selectedElement.style.transform = 'translateY(0)';
selectedElement.style.boxShadow = 'none';
}
// Add selected class to current tag
if (this.selectedMergetag) {
const currentTag = this.externalLibrary.querySelector(`[data-value="${CSS.escape(this.selectedMergetag)}"]`);
if (currentTag) {
currentTag.classList.add('selected');
}
}
}CSS.escape() keeps the selector valid for any value, because input fields pass their whole current value to the modal (for example, a text with quotes).
Handle Modal Closure
javascript
/**
* Closes the modal and executes cancel callback
*/
cancelAndClose() {
this.close();
this.cancelCallback();
}
/**
* Closes the modal by hiding it from view
*/
close() {
this.externalLibrary.style.display = 'none';
}Event Delegation Benefits
Using event delegation by listening to the parent container provides several advantages:
- Improved Performance: A single event listener replaces multiple individual listeners for each card
- Simplified Maintenance: Eliminates the need to dynamically attach and detach listeners
- Future-Proof Implementation: Automatically handles dynamically added merge tag elements
- Memory Efficiency: Reduces the memory footprint when managing numerous elements
Step 9: Implement Category Filtering
Add filtering functionality to help users find merge tags by category.
Initialize Filter Buttons
javascript
/**
* Initializes category filter button functionality
*/
initializeFilters() {
const filterButtons = this.externalLibrary.querySelectorAll('.filter-buttons button');
filterButtons.forEach(button => {
button.addEventListener('click', (e) => {
const category = e.target.getAttribute('data-category');
this.filterTags(category);
this.updateActiveButton(e.target);
});
});
}Filter Merge Tags by Category
javascript
/**
* Filters displayed merge tags based on the selected category
* @param {string} category - Category identifier to filter by (or 'all' for all tags)
*/
filterTags(category) {
this.activeCategory = category;
const tagCards = this.externalLibrary.querySelectorAll('.tag-card');
tagCards.forEach(card => {
const shouldShow = category === 'all' ||
card.getAttribute('data-category') === category;
card.style.display = shouldShow ? 'flex' : 'none';
});
}Update Button Visual States
javascript
/**
* Updates the visual state of category filter buttons
* @param {HTMLElement} activeButton - The button element that was clicked and should be marked active
*/
updateActiveButton(activeButton) {
const buttons = this.externalLibrary.querySelectorAll('.filter-buttons button');
buttons.forEach(button => {
const isActive = button === activeButton;
const styles = isActive ?
MergeTagsModal.STYLES.buttonActive :
MergeTagsModal.STYLES.buttonInactive;
// Apply styles
Object.assign(button.style, styles);
});
}Step 10: Create the Merge Tags UI Element
Create src/MergeTagsUiElement.js to define the custom UI element that will replace the merge tags control of the text block:
javascript
import {UIElement, UIElementType} from '@stripoinc/ui-editor-extensions';
import {MergeTagsModal} from './MergeTagsModal';
export const EXTERNAL_MERGE_TAGS_UI_ELEMENT_ID = 'external-merge-tags-ui-element';
export class MergeTagsUiElement extends UIElement {
isModuleNode = false;
/**
* Returns the unique identifier for this UI element
*/
getId() {
return EXTERNAL_MERGE_TAGS_UI_ELEMENT_ID;
}
/**
* Returns the HTML template for the merge tags button
*/
getTemplate() {
return `
<div>
<${UIElementType.BUTTON} id="mergeTagsButton" class="btn btn-primary">Open merge tags</${UIElementType.BUTTON}>
</div>`;
}
/**
* Called when the element is rendered in the editor
* @param {HTMLElement} container - The container element
*/
onRender(container) {
this.listener = this._onClick.bind(this);
this.mergeTagsButton = container.querySelector('#mergeTagsButton');
this.mergeTagsButton.addEventListener('click', this.listener);
}
/**
* Called when the element is destroyed
*/
onDestroy() {
this.mergeTagsButton.removeEventListener('click', this.listener);
}
/**
* Handles button click events
*/
_onClick(event) {
this.openMergeTagsModal();
}
/**
* Opens the merge tags modal
*/
openMergeTagsModal() {
MergeTagsModal.getInstance().open({
selectedValue: this.selectedMergeTag?.value,
isModule: this.isModuleNode,
onSelect: (data) => this.api.triggerValueChange(data)
});
}
/**
* Called when an attribute is updated
* @param {string} name - Attribute name
* @param {any} value - New attribute value
*/
onAttributeUpdated(name, value) {
if (name === 'blockNode') {
this.isModuleNode = !!value.getClosestModuleId();
}
if (name === 'mergeTag') {
this.selectedMergeTag = value;
// If a merge tag is selected, open the modal immediately
this.selectedMergeTag && this.openMergeTagsModal();
}
}
}UI Element Lifecycle Methods
| Method | Purpose |
|---|---|
getId() | Returns unique identifier for the UI element |
getTemplate() | Returns HTML template for the button |
onRender(container) | Sets up event listeners when rendered |
onDestroy() | Cleans up event listeners when destroyed |
onAttributeUpdated(name, value) | Handles attribute changes from the editor |
Module Context Detection
The UI element tracks whether the merge tag is being used within a module context through the blockNode attribute:
isModuleNodeproperty: Boolean flag that indicates if the current node is within a moduleblockNodeattribute: When updated, the code checks if the node has a closest module ID usingvalue.getClosestModuleId()- Module Badge: The modal displays a "Module" badge when
isModuleNodeis true, helping users understand the context
API Integration
The this.api.triggerValueChange(data) method notifies the Stripo editor of the selected merge tag. The editor will then insert or update the merge tag in the template based on the current context. The isModuleNode flag is passed to the modal to enable context-aware UI features like the module badge.
Step 11: Create the UI Element Tag Registry
Create src/ExtensionTagRegistry.js to register your custom UI element in place of the merge tags control of the text block:
javascript
import {UIElementTagRegistry, UIElementType} from '@stripoinc/ui-editor-extensions';
import {EXTERNAL_MERGE_TAGS_UI_ELEMENT_ID} from './MergeTagsUiElement';
export class ExtensionTagRegistry extends UIElementTagRegistry {
/**
* Registers custom UI elements to replace default editor elements
* @param {Object} uiElementsTagsMap - Map of UI element types to custom element IDs
*/
registerUiElements(uiElementsTagsMap) {
uiElementsTagsMap[UIElementType.MERGETAGS] = EXTERNAL_MERGE_TAGS_UI_ELEMENT_ID;
}
}How Tag Registry Works
The UIElementTagRegistry allows you to map standard Stripo UI element types to your custom implementations:
- UIElementType.MERGETAGS: The merge tags control of the text block
- EXTERNAL_MERGE_TAGS_UI_ELEMENT_ID: Your custom element's unique ID
- When the editor renders the merge tags control of the text block, it will use your custom implementation instead
Important
The tag registry is what enables your custom UI element to replace the default merge tags control of the text block. Without this registration, your custom element would exist but not be automatically used by the editor. The merge tag icons of input fields are handled separately — see the next step.
Step 12: Create the Merge Tags Library for Input Fields
Version Availability
This step requires @stripoinc/ui-editor-extensions v3.11.0 or higher
Create src/MyMergeTagsLibrary.js to open the same modal from the merge tag icons of the editor's input fields:
javascript
import {ExternalMergeTagsLibrary} from '@stripoinc/ui-editor-extensions';
import {MergeTagsModal} from './MergeTagsModal';
/**
* External Merge Tags Library Implementation
* Opens the merge tags modal from the merge tag icons of the editor's input fields:
* text inputs, links, image paths, the email Subject and the Hidden Preheader.
*/
export class MyMergeTagsLibrary extends ExternalMergeTagsLibrary {
/**
* Called by the editor when a user clicks the merge tag icon of an input field
* @param {string} currentValue - Current value of the whole field
* @param {Function} onSelectCallback - Inserts the selected merge tag value into the field
* @param {Function} onCancelCallback - Notifies the editor that the selection was cancelled
* @param {Object} context - Field information: fieldType ('text' | 'link') and optional fieldName
*/
openMergeTagsLibrary(currentValue, onSelectCallback, onCancelCallback, context) {
const isLinkField = context.fieldType === 'link';
MergeTagsModal.getInstance().open({
// A merge tag replaces the whole link, so the current value of a link field can match one of the tags
selectedValue: currentValue,
// Link fields expect a URL, so open the Links category for them
category: isLinkField ? 'links' : 'all',
onSelect: (data) => onSelectCallback(data.value),
onCancel: onCancelCallback
});
}
}Where the Editor Opens the Library
When the library is registered, the editor opens it instead of the built-in merge tags picker from:
- The merge tag icon of text inputs in the settings panel (for example, button text or image alt text)
- The merge tag icon of link fields
- The merge tag icon of the image path field
- The Insert merge tag option of the email Subject and Hidden Preheader fields
Method Parameters
| Parameter | Type | Description |
|---|---|---|
currentValue | string | Current value of the whole field (e.g., the text of an input or the URL of a link) |
onSelectCallback | Function | Inserts the passed merge tag value into the field |
onCancelCallback | Function | Notifies the editor that the selection was cancelled |
context | ExternalMergeTagsContext | fieldType is 'link' for link fields and 'text' for all other fields; optional fieldName identifies the field |
How the Selected Value Is Applied
- In text fields, the merge tag is inserted at the cursor position
- In link fields, the merge tag replaces the whole URL — this is why the example opens the Links category for link fields
currentValuecontains the whole field value, not the selected merge tag. The modal highlights a card only when the field value matches a merge tag exactly, which is typical for link fields- The module context is not passed to the library, so the "Module" badge is displayed for the text block only
API Integration
Call onSelectCallback() with the merge tag value string, not with the whole merge tag object, and call onCancelCallback() when the user closes the modal without selection. The editor takes into account only the first call of either callback.
Step 13: Register the Extension
Create src/extension.js to register your merge tags integration with the Stripo extension system:
javascript
import { ExtensionBuilder } from '@stripoinc/ui-editor-extensions';
import {MergeTagsUiElement} from './MergeTagsUiElement';
import {ExtensionTagRegistry} from './ExtensionTagRegistry';
import {MyMergeTagsLibrary} from './MyMergeTagsLibrary';
const extension = new ExtensionBuilder()
// Text block: replace the merge tags control with the custom UI element
.addUiElement(MergeTagsUiElement)
.withUiElementTagRegistry(ExtensionTagRegistry)
// Input fields: open the same modal from the merge tag icons
.withExternalMergeTagsLibrary(MyMergeTagsLibrary)
.build();
export default extension;Extension Registration Explained
The ExtensionBuilder class provides a fluent API for registering integrations:
.addUiElement(MergeTagsUiElement): Registers your custom UI element with the extension system.withUiElementTagRegistry(ExtensionTagRegistry): Registers your tag registry to replace the merge tags control of the text block.withExternalMergeTagsLibrary(MyMergeTagsLibrary): Registers your library for the merge tag icons of input fields.build(): Constructs and returns the final extension object for the editor
Registration Order
The text block entry point requires both the UI element and the tag registry:
- First, register the UI element using
addUiElement() - Then, register the tag registry using
withUiElementTagRegistry() - The tag registry maps the
UIElementType.MERGETAGSto your custom element's ID - When the editor renders the merge tags control of the text block, it will use your custom element
The input fields entry point is independent and requires only withExternalMergeTagsLibrary().
Multiple Integrations
Multiple integration methods can be chained to register different types of extensions within a single extension object:
javascript
new ExtensionBuilder()
.addUiElement(MergeTagsUiElement)
.withUiElementTagRegistry(ExtensionTagRegistry)
.withExternalMergeTagsLibrary(MyMergeTagsLibrary)
.withExternalImageLibrary(MyExternalImageLibrary)
.withExternalVideosLibrary(MyExternalVideosLibrary)
.build();Step 14: Configure the Editor Integration
Update your src/index.js to properly integrate the extension with the Stripo editor:
javascript
import extension from './extension.js';
import {PLUGIN_ID, SECRET_KEY, EDITOR_URL, EMAIL_ID, USER_ID} from './creds';
// Initialize the editor with your extension
function _runEditor(template, extension) {
window.UIEditor.initEditor(
document.querySelector('#stripoEditorContainer'),
{
html: template.html,
css: template.css,
metadata: {
emailId: EMAIL_ID
},
locale: 'en',
onTokenRefreshRequest: function (callback) {
_request('POST', 'https://plugins.stripo.email/api/v1/auth',
JSON.stringify({
pluginId: PLUGIN_ID,
secretKey: SECRET_KEY,
userId: USER_ID,
role: 'user'
}),
function(data) {
callback(JSON.parse(data).token);
}
);
},
// ... other configuration ...
ignoreClickOutsideSelectors: ['#externalMergeTags'],
extensions: [
extension
]
}
);
}Critical Configuration Options
| Option | Purpose |
|---|---|
ignoreClickOutsideSelectors | Prevents editor from closing your modal when clicking inside it |
extensions | Array of extension objects to register with the editor |
Important
The ignoreClickOutsideSelectors: ['#externalMergeTags'] configuration is critical. Without it, clicking inside your modal may trigger the editor's click-outside handlers and cause unexpected behavior. Make sure the selector matches your modal's ID.
Step 15: Run the Development Server
Your implementation is now ready for testing.
Start the Development Server
bash
npm run devThis command will:
- Start the Vite development server on
http://localhost:3000 - Automatically open your default browser
- Load the Stripo Editor with your merge tags extension integrated
Test Both Entry Points
- Text block: Select a text block and click Open merge tags in place of the merge tags control. The selected merge tag is inserted into the text. Inside a module, the modal displays the "Module" badge.
- Link field: Select a button and click the merge tag icon of its link field. The modal opens with the Links category, and the selected merge tag replaces the link.
- Text input: Click the merge tag icon of the button text. The selected merge tag is inserted at the cursor position and displayed as a chip.
Complete Example
For a full working example, check out the complete implementation in our GitHub repository.