Skip to content

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 BlockInput Fields
Where it opensMerge tags control of the text blockMerge tag icons of links, image paths, text inputs (e.g., button text), the email Subject, and the Hidden Preheader
SDK APIUIElement + UIElementTagRegistryExternalMergeTagsLibrary (v3.11.0+)
Returned value{value, label} object via this.api.triggerValueChange()Merge tag value string via onSelectCallback()
Available contextSelected 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.js

Step 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 element
    • selectedMergetag: Merge tag value to highlight
    • selectCallback: Callback invoked when a merge tag is selected
    • cancelCallback: Callback invoked when the modal is closed without selection
    • activeCategory: Currently active filter category
    • isModule: 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 ​

PropertyTypeDescription
categorystringFilter category (personal, contact, company, date, custom, links)
valuestringThe actual merge tag value used in templates
labelstringHuman-readable display name
previewstringSample value shown to users
descriptionstringHelpful 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.

The modal consists of three main sections:

  1. Header: Title, category filter buttons, and close button
  2. Content: Scrollable grid of merge tag cards with labels, values, descriptions, and previews
  3. 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 ​

  1. Responsive Grid: Utilizes CSS Grid with auto-fill to create a responsive layout that adapts to different screen sizes
  2. Merge Tag Cards: Each card displays the merge tag label, value (in monospace), description, and sample preview
  3. Hover Effects: Inline event handlers provide smooth transition animations with conditional logic to preserve selected state
  4. Data Attributes: Merge tag metadata is stored in data attributes for efficient retrieval on selection
  5. Visual Feedback: Selected cards have distinct styling to indicate current selection
  6. 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.

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 ​

MethodPurpose
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:

  • isModuleNode property: Boolean flag that indicates if the current node is within a module
  • blockNode attribute: When updated, the code checks if the node has a closest module ID using value.getClosestModuleId()
  • Module Badge: The modal displays a "Module" badge when isModuleNode is 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:

  1. UIElementType.MERGETAGS: The merge tags control of the text block
  2. EXTERNAL_MERGE_TAGS_UI_ELEMENT_ID: Your custom element's unique ID
  3. 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 ​

ParameterTypeDescription
currentValuestringCurrent value of the whole field (e.g., the text of an input or the URL of a link)
onSelectCallbackFunctionInserts the passed merge tag value into the field
onCancelCallbackFunctionNotifies the editor that the selection was cancelled
contextExternalMergeTagsContextfieldType 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
  • currentValue contains 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:

  1. .addUiElement(MergeTagsUiElement): Registers your custom UI element with the extension system
  2. .withUiElementTagRegistry(ExtensionTagRegistry): Registers your tag registry to replace the merge tags control of the text block
  3. .withExternalMergeTagsLibrary(MyMergeTagsLibrary): Registers your library for the merge tag icons of input fields
  4. .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:

  1. First, register the UI element using addUiElement()
  2. Then, register the tag registry using withUiElementTagRegistry()
  3. The tag registry maps the UIElementType.MERGETAGS to your custom element's ID
  4. 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 ​

OptionPurpose
ignoreClickOutsideSelectorsPrevents editor from closing your modal when clicking inside it
extensionsArray 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 dev

This command will:

  1. Start the Vite development server on http://localhost:3000
  2. Automatically open your default browser
  3. Load the Stripo Editor with your merge tags extension integrated

Test Both Entry Points ​

  1. 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.
  2. 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.
  3. 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.