Appearance
Integrate an External Display Conditions Library
Overview
The External Display Conditions Library integration enables users to create conditional display rules for email content directly within the Stripo Email Editor. This powerful feature allows you to show or hide specific content blocks based on user attributes, creating personalized email experiences for different audience segments.
What You'll Build
In this tutorial, you'll create a fully functional display conditions modal that:
- Provides a user-friendly interface for creating conditional logic rules
- Supports multiple condition fields (Email Address, Phone Number)
- Offers various operations (Equals, Contains) for flexible matching
- Allows combining multiple conditions with AND/OR logic
- Validates user input to ensure proper condition structure
- Returns properly formatted condition scripts for the Stripo editor
Use Cases
- Personalization: Show different content to users based on email domain (e.g., @gmail.com vs @company.com)
- Segmentation: Display specialized offers for specific user groups
- A/B Testing: Create conditional content variations for testing
- Localization: Show region-specific content based on user attributes
- Dynamic Pricing: Display different pricing for different customer segments
- VIP Content: Reveal exclusive content for premium users
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 the Interface
The ExternalDisplayConditionsLibrary class must implement a single method:
typescript
openExternalDisplayConditionsDialog(
currentCondition: DisplayCondition | null,
onSelectCallback: (condition: DisplayCondition | null) => void,
onCancelCallback: () => void
): void
Parameters
Parameter | Type | Description |
---|---|---|
currentCondition | DisplayCondition | null | Currently applied display condition (if any) |
onSelectCallback | Function | Callback function invoked when conditions are applied or removed |
onCancelCallback | Function | Callback function invoked when the modal is cancelled |
DisplayCondition Object
When a user applies conditions, your library implementation must return an object with the following structure:
typescript
{
id: string; // ID of the condition
name: string; // Display name
description: string; // User-friendly description
beforeScript: string; // Opening condition script
afterScript: string; // Closing condition script
extraData: string; // Extra custom data, can be set by user
}
To remove all conditions, pass null
to the onSelectCallback
.
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-display-conditions/
├── index.html
├── src/
│ ├── creds.js
│ ├── index.js
│ └── extension.js
├── package.json
└── vite.config.js
Step 2: Create the Display Conditions Library Class
Create a new file src/MyExternalDisplayConditions.js
with the following basic class structure:
javascript
import {ExternalDisplayConditionsLibrary} from '@stripoinc/ui-editor-extensions';
/**
* External Display Conditions Library Implementation
* Provides UI for creating conditional display rules for email content
*/
export class MyExternalDisplayConditions extends ExternalDisplayConditionsLibrary {
// Instance properties
selectConditionsCallback = null;
conditionsPopupElement = null;
onCancelCallback = null;
/**
* Required method called by the Stripo editor
* Opens the display conditions modal dialog
* @param {DisplayCondition|null} currentCondition - Currently applied condition
* @param {Function} onSelectCallback - Callback invoked when conditions are applied/removed
* @param {Function} onCancelCallback - Callback invoked when the modal is cancelled
*/
openExternalDisplayConditionsDialog(currentCondition, onSelectCallback, onCancelCallback) {
// Store callbacks
this.selectConditionsCallback = onSelectCallback;
this.onCancelCallback = onCancelCallback;
// Show modal
this.activateConditionsPopup(currentCondition);
}
/**
* Gets the category name displayed in the editor UI
* @returns {string} The category name
*/
getCategoryName() {
return 'External display conditions';
}
/**
* Determines if the context action should be enabled in the editor
* @returns {boolean} true if enabled
*/
getIsContextActionEnabled() {
return true;
}
/**
* Gets the index position for the context action in the context menu
* @returns {number} The index position (1-based)
*/
getContextActionIndex() {
return 1;
}
}
Key Components Explained
Instance Properties:
selectConditionsCallback
: Stores the success callback from the Stripo editorconditionsPopupElement
: Reference to the modal DOM elementonCancelCallback
: Stores the cancel callback from the Stripo editor
openExternalDisplayConditionsDialog: Required method that opens the modal and manages callbacks
getCategoryName: Returns the category name shown in the editor's UI
Context Action Methods: Control how the display conditions action appears in the editor's context menu
Step 3: Define Configuration Constants and Styles
Add configuration constants and UI styles. Continue editing src/MyExternalDisplayConditions.js
:
javascript
export class MyExternalDisplayConditions extends ExternalDisplayConditionsLibrary {
// ... existing properties ...
// Configuration constants
static AVAILABLE_CONDITION_NAMES = [
{label: 'Email Address', value: '$EMAIL'},
{label: 'Phone number', value: '$PHONE'},
];
static AVAILABLE_CONDITION_OPERATIONS = [
{label: 'Equals (Is)', value: 'equals'},
{label: 'Contains', value: 'in_array'},
];
static AVAILABLE_CONDITION_CONCATENATIONS = [
{label: 'all', value: '&&'},
{label: 'any', value: '||'}
];
static DEFAULT_CONDITION = {
name: '$EMAIL',
operation: 'equals',
value: ''
};
// CSS class names
static CSS_CLASSES = {
DROPDOWN_NAME: 'dropdownConditionField',
DROPDOWN_OPERATION: 'dropdownConditionOperation',
DROPDOWN_CONCATENATION: 'dropdownConcatenation',
CONDITION_ROW: 'condition-row',
CONDITION_VALUE: 'condition-value',
CONDITIONS_TABLE: 'conditionsTable',
VALIDATION_ERROR: 'validation-error',
DELETE_ACTION_PREFIX: 'condition-delete-action-'
};
// Validation messages
static MESSAGES = {
VALIDATION_ERROR: 'Please enter a value for at least one condition.',
CONDITION_NAME: 'Conditions applied',
CONDITION_DESCRIPTION: 'Only users that fit conditions will see this part of the email.'
};
// 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: '700px',
width: '90%',
maxHeight: '90vh',
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 styles
content: {
padding: '32px',
overflowY: 'auto',
flex: '1'
},
// Form control styles
select: {
width: '100%',
padding: '8px 12px',
border: '1px solid #e5e7eb',
borderRadius: '6px',
fontSize: '14px',
backgroundColor: 'white',
cursor: 'pointer',
transition: 'border-color 0.2s',
outline: 'none'
},
input: {
width: '100%',
padding: '8px 12px',
border: '1px solid #e5e7eb',
borderRadius: '6px',
fontSize: '14px',
transition: 'border-color 0.2s',
outline: 'none'
},
// Button styles
buttonPrimary: {
padding: '8px 20px',
borderRadius: '6px',
border: 'none',
backgroundColor: '#34c759',
color: 'white',
fontSize: '14px',
fontWeight: '500',
cursor: 'pointer',
transition: 'background-color 0.2s'
},
buttonSecondary: {
padding: '8px 20px',
borderRadius: '6px',
border: '1px solid #e5e7eb',
backgroundColor: 'white',
color: '#6b7280',
fontSize: '14px',
fontWeight: '500',
cursor: 'pointer',
transition: 'all 0.2s'
},
buttonAdd: {
padding: '6px 16px',
borderRadius: '6px',
border: '1px solid #3b82f6',
backgroundColor: 'white',
color: '#3b82f6',
fontSize: '14px',
fontWeight: '500',
cursor: 'pointer',
transition: 'all 0.2s',
display: 'inline-flex',
alignItems: 'center',
gap: '6px'
}
};
// ... rest of the class ...
}
Configuration Explained
- AVAILABLE_CONDITION_NAMES: Defines the available user attributes (fields) that can be used in conditions
- AVAILABLE_CONDITION_OPERATIONS: Defines the comparison operations (equals, contains)
- AVAILABLE_CONDITION_CONCATENATIONS: Defines how multiple conditions are combined (AND/OR)
- DEFAULT_CONDITION: Default values when adding a new condition row
- CSS_CLASSES: Centralized CSS class names for consistent element selection
- MESSAGES: User-facing messages for validation and display
- STYLES: Complete styling configuration for the modal UI
Customization
You can easily extend the available conditions by adding more field types to AVAILABLE_CONDITION_NAMES
or operations to AVAILABLE_CONDITION_OPERATIONS
. For example, you could add fields for $LOCATION
, $AGE_GROUP
, or operations like starts_with
, ends_with
.
Step 4: Build Utility Methods
Implement helper methods for style conversion and dropdown management:
javascript
/**
* Converts style object to 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('; ');
}
/**
* Gets the value of a dropdown element
* @param {HTMLElement} baseElement - The base element to search within
* @param {string} identifierClass - The CSS class of the dropdown
* @returns {string|null} The selected value or null if not found
*/
getDropdownValue(baseElement, identifierClass) {
if (!baseElement) {
baseElement = this.conditionsPopupElement;
}
const selectElement = baseElement.querySelector('select.' + identifierClass);
return selectElement ? selectElement.value : null;
}
/**
* Sets the value of a dropdown element
* @param {HTMLElement} baseElement - The base element to search within
* @param {string} identifierClass - The CSS class of the dropdown
* @param {string} value - The value to set
*/
setDropdownValue(baseElement, identifierClass, value) {
if (!baseElement) {
baseElement = this.conditionsPopupElement;
}
const selectElement = baseElement.querySelector('select.' + identifierClass);
if (selectElement) {
selectElement.value = value;
}
}
/**
* Sets dropdown options and attaches event listeners
* @param {HTMLElement} baseElement - The base element to search within
* @param {string} identifierClass - The CSS class of the dropdown
* @param {Array} options - Array of {label, value} option objects
*/
setDropdownOptions(baseElement, identifierClass, options) {
if (!baseElement) {
baseElement = this.conditionsPopupElement;
}
const selectElement = baseElement.querySelector('select.' + identifierClass);
if (!selectElement) return;
// Clear existing options
selectElement.innerHTML = '';
// Add new options
options.forEach(option => {
const optionElement = document.createElement('option');
optionElement.value = option.value;
optionElement.innerHTML = option.label;
selectElement.appendChild(optionElement);
});
// Remove existing event listeners to avoid duplicates
const newSelectElement = selectElement.cloneNode(true);
selectElement.parentNode.replaceChild(newSelectElement, selectElement);
// Add focus/hover styles
newSelectElement.addEventListener('focus', function() {
this.style.borderColor = '#3b82f6';
this.style.boxShadow = '0 0 0 3px rgba(59, 130, 246, 0.1)';
});
newSelectElement.addEventListener('blur', function() {
this.style.borderColor = '#e5e7eb';
this.style.boxShadow = 'none';
});
// Add change event to clear validation error
newSelectElement.addEventListener('change', () => {
this.hideValidationError();
});
}
/**
* Creates dropdown markup
* @param {string} className - CSS class for the dropdown
* @returns {string} HTML for the dropdown
*/
getDropdownMarkup(className) {
return `<select style="${this.styleObjToString(MyExternalDisplayConditions.STYLES.select)}" class="${className}"></select>`;
}
Important
The styleObjToString()
method is essential for converting JavaScript style objects into inline CSS strings. This approach ensures consistent styling across all modal elements.
Step 5: Create the Modal Structure
Implement the modal creation method that generates the complete UI:
javascript
/**
* Creates the modal HTML structure and appends it to the document body
*/
createConditionsPopup() {
const div = document.createElement('div');
div.innerHTML = `
<div id="externalDisplayConditionsPopup"
style="${this.styleObjToString(MyExternalDisplayConditions.STYLES.overlay)}; visibility: hidden;"
class="esdev-app">
<div style="${this.styleObjToString(MyExternalDisplayConditions.STYLES.modal)}">
${this.generateHeaderHTML()}
${this.generateContentHTML()}
${this.generateFooterHTML()}
</div>
</div>`;
document.body.appendChild(div);
this.conditionsPopupElement = document.getElementById('externalDisplayConditionsPopup');
// Attach event listeners
this.attachModalEventListeners();
}
/**
* Generates the modal header section HTML
* @returns {string} HTML string for the header section
*/
generateHeaderHTML() {
return `
<div style="${this.styleObjToString(MyExternalDisplayConditions.STYLES.header)}">
<h2 style="margin: 0; font-size: 24px; font-weight: 600; color: #111827; letter-spacing: -0.025em;">
Display Conditions
</h2>
<button id="closePopupButton" 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>
</div>
`;
}
/**
* Generates the modal content section HTML
* @returns {string} HTML string for the content section
*/
generateContentHTML() {
const CSS = MyExternalDisplayConditions.CSS_CLASSES;
return `
<div style="${this.styleObjToString(MyExternalDisplayConditions.STYLES.content)}">
<!-- Conditions table -->
<div style="margin-bottom: 24px;">
<h3 style="font-size: 16px; font-weight: 600; color: #374151; margin: 0 0 16px 0;">
Condition Rules
</h3>
<table class="${CSS.CONDITIONS_TABLE}" style="width: 100%; border-collapse: collapse;"></table>
<button id="addNewCondition"
style="${this.styleObjToString(MyExternalDisplayConditions.STYLES.buttonAdd)}"
onmouseover="this.style.backgroundColor='#3b82f6'; this.style.color='white';"
onmouseout="this.style.backgroundColor='white'; this.style.color='#3b82f6';">
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4z"/>
</svg>
Add Condition
</button>
</div>
<!-- Concatenation setting -->
<div style="background-color: #f9fafb; padding: 16px; border-radius: 8px; margin-bottom: 24px;">
<label style="display: flex; align-items: center; gap: 8px; font-size: 14px; color: #374151;">
Show this content if
<span style="display: inline-block; min-width: 80px;">
${this.getDropdownMarkup(CSS.DROPDOWN_CONCATENATION)}
</span>
conditions are met
</label>
</div>
<!-- Footer actions -->
<div style="display: flex; align-items: center; justify-content: space-between;
padding-top: 24px; border-top: 1px solid #e5e7eb;">
<a id="removeConditionsPopup"
style="color: #ef4444; text-decoration: none; font-size: 14px; cursor: pointer;
transition: color 0.2s;"
onmouseover="this.style.color='#dc2626';"
onmouseout="this.style.color='#ef4444';">
Remove all conditions
</a>
<div style="display: flex; gap: 12px;">
<button id="closeConditionsPopup"
style="${this.styleObjToString(MyExternalDisplayConditions.STYLES.buttonSecondary)}"
onmouseover="this.style.backgroundColor='#f9fafb';"
onmouseout="this.style.backgroundColor='white';">
Cancel
</button>
<button id="applyConditionsAction"
style="${this.styleObjToString(MyExternalDisplayConditions.STYLES.buttonPrimary)}"
onmouseover="this.style.backgroundColor='#22c55e';"
onmouseout="this.style.backgroundColor='#34c759';">
Apply Conditions
</button>
</div>
</div>
</div>
`;
}
/**
* Generates the modal footer section HTML with disclaimer notice
* @returns {string} HTML string for the footer section
*/
generateFooterHTML() {
return `
<div style="padding: 16px 32px; border-top: 1px solid #e5e7eb; background-color: #fef3c7;
border-radius: 0 0 12px 12px; text-align: center;">
<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>
`;
}
/**
* Attaches event listeners to modal elements after creation
*/
attachModalEventListeners() {
this.conditionsPopupElement.querySelector('#closePopupButton')
.addEventListener('click', this.closePopup.bind(this));
this.conditionsPopupElement.querySelector('#closeConditionsPopup')
.addEventListener('click', this.cancelConditions.bind(this));
this.conditionsPopupElement.querySelector('#applyConditionsAction')
.addEventListener('click', this.applyConditions.bind(this));
this.conditionsPopupElement.querySelector('#addNewCondition')
.addEventListener('click', this.addConditionRow.bind(this));
this.conditionsPopupElement.querySelector('#removeConditionsPopup')
.addEventListener('click', this.removeConditions.bind(this));
// Set up concatenation dropdown
const CSS = MyExternalDisplayConditions.CSS_CLASSES;
this.setDropdownOptions(
this.conditionsPopupElement,
CSS.DROPDOWN_CONCATENATION,
MyExternalDisplayConditions.AVAILABLE_CONDITION_CONCATENATIONS
);
this.setDropdownValue(
this.conditionsPopupElement,
CSS.DROPDOWN_CONCATENATION,
MyExternalDisplayConditions.AVAILABLE_CONDITION_CONCATENATIONS[0].value
);
}
Modal Structure Overview
The modal consists of three main sections:
- Header: Title and close button
- Content:
- Conditions table (dynamically populated)
- Add Condition button
- Concatenation selector (all/any)
- Action buttons (Remove All, Cancel, Apply)
- Footer: Informational disclaimer
Step 6: Implement Condition Row Management
Add methods for creating and managing individual condition rows:
javascript
/**
* Creates HTML for a condition row
* @param {string} deleteActionClass - Unique class for the delete button
* @returns {string} HTML string for the condition row
*/
createConditionRowHTML(deleteActionClass) {
const CSS = MyExternalDisplayConditions.CSS_CLASSES;
return `
<td style="padding: 0 8px 16px 0;">
${this.getDropdownMarkup(CSS.DROPDOWN_NAME)}
</td>
<td style="padding: 0 8px 16px 0;">
${this.getDropdownMarkup(CSS.DROPDOWN_OPERATION)}
</td>
<td style="padding: 0 8px 16px 0;">
<input type="text"
class="${CSS.CONDITION_VALUE}"
style="${this.styleObjToString(MyExternalDisplayConditions.STYLES.input)}"
placeholder="Enter value">
</td>
<td style="width: 40px; padding-bottom: 16px;">
<button class="${deleteActionClass}"
type="button"
data-action="delete"
style="background: transparent; border: none; color: #ef4444;
cursor: pointer; padding: 8px; border-radius: 6px;
transition: all 0.2s; width: 32px; height: 32px;
display: flex; align-items: center; justify-content: center;">
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z"/>
<path fill-rule="evenodd" d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1zM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118zM2.5 3V2h11v1h-11z"/>
</svg>
</button>
</td>`;
}
/**
* Sets up event listeners for a condition row
* @param {HTMLElement} row - The row element
* @param {string} deleteActionClass - Class for the delete button
*/
setupConditionRowListeners(row, deleteActionClass) {
const CSS = MyExternalDisplayConditions.CSS_CLASSES;
const inputElement = row.querySelector(`.${CSS.CONDITION_VALUE}`);
// Add focus/hover styles to input
inputElement.addEventListener('focus', function() {
this.style.borderColor = '#3b82f6';
this.style.boxShadow = '0 0 0 3px rgba(59, 130, 246, 0.1)';
});
inputElement.addEventListener('blur', function() {
this.style.borderColor = '#e5e7eb';
this.style.boxShadow = 'none';
});
// Clear validation error when user starts typing
inputElement.addEventListener('input', () => {
this.hideValidationError();
});
// Add delete button listener
const deleteButton = row.querySelector('.' + deleteActionClass);
if (deleteButton) {
deleteButton.addEventListener('click', this.deleteConditionRow);
// Add hover effects
deleteButton.addEventListener('mouseenter', function() {
this.style.backgroundColor = '#fee2e2';
});
deleteButton.addEventListener('mouseleave', function() {
this.style.backgroundColor = 'transparent';
});
}
}
/**
* Adds a new condition row to the table
* @param {Event} e - The event object (can be null)
* @param {Object} conditionValue - The condition values to populate
*/
addConditionRow(e, conditionValue) {
const CSS = MyExternalDisplayConditions.CSS_CLASSES;
if (!conditionValue) {
conditionValue = MyExternalDisplayConditions.DEFAULT_CONDITION;
}
const deleteActionClass = CSS.DELETE_ACTION_PREFIX + Math.random().toString().replace('.', 'd');
const tr = document.createElement('tr');
tr.classList.add(CSS.CONDITION_ROW);
tr.innerHTML = this.createConditionRowHTML(deleteActionClass);
this.conditionsPopupElement.querySelector(`.${CSS.CONDITIONS_TABLE}`).appendChild(tr);
// Set dropdown options and values
this.setDropdownOptions(tr, CSS.DROPDOWN_NAME, MyExternalDisplayConditions.AVAILABLE_CONDITION_NAMES);
this.setDropdownValue(tr, CSS.DROPDOWN_NAME, conditionValue.name);
this.setDropdownOptions(tr, CSS.DROPDOWN_OPERATION, MyExternalDisplayConditions.AVAILABLE_CONDITION_OPERATIONS);
this.setDropdownValue(tr, CSS.DROPDOWN_OPERATION, conditionValue.operation);
// Set input value
const inputElement = tr.querySelector(`.${CSS.CONDITION_VALUE}`);
inputElement.value = conditionValue.value;
// Setup event listeners
this.setupConditionRowListeners(tr, deleteActionClass);
this.updateDeleteActionVisibility();
}
/**
* Deletes a condition row
* @param {Event} e - The click event
*/
deleteConditionRow = (e) => {
const CSS = MyExternalDisplayConditions.CSS_CLASSES;
const row = e.target.closest(`.${CSS.CONDITION_ROW}`);
if (row) {
row.remove();
this.updateDeleteActionVisibility();
}
}
/**
* Updates visibility of delete buttons based on row count
*/
updateDeleteActionVisibility() {
const CSS = MyExternalDisplayConditions.CSS_CLASSES;
const rows = this.conditionsPopupElement.querySelectorAll(`.${CSS.CONDITIONS_TABLE} .${CSS.CONDITION_ROW}`);
if (rows.length > 0) {
const firstDeleteButton = rows[0].querySelector('button[class*="condition-delete-action"]');
if (firstDeleteButton) {
// Hide delete button for first row if it's the only row
firstDeleteButton.style.display = rows.length > 1 ? 'flex' : 'none';
}
}
}
Row Management Features
- Dynamic Row Creation: Each condition row contains three dropdowns (field, operation) and one text input (value)
- Unique Delete Buttons: Each row gets a uniquely identified delete button
- Visual Feedback: Hover effects and focus states provide clear user feedback
- Smart Delete Visibility: The first row's delete button is hidden when it's the only row
- Validation Integration: Input changes clear any displayed validation errors
Step 7: Implement Validation
Add validation methods to ensure user input is complete:
javascript
/**
* Shows a validation error message
* @param {string} message - The error message to display
*/
showValidationError(message) {
// Remove any existing error message
this.hideValidationError();
// Create error element
const errorDiv = document.createElement('div');
errorDiv.className = 'validation-error';
errorDiv.style.cssText = `
background-color: #fef2f2;
border: 1px solid #fecaca;
color: #dc2626;
padding: 12px 16px;
border-radius: 6px;
margin-bottom: 16px;
font-size: 14px;
display: flex;
align-items: center;
gap: 8px;
`;
errorDiv.innerHTML = `
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" style="flex-shrink: 0;">
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/>
<path d="M7.002 11a1 1 0 1 1 2 0 1 1 0 0 1-2 0zM7.1 4.995a.905.905 0 1 1 1.8 0l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 4.995z"/>
</svg>
<span>${message}</span>
`;
// Insert at the beginning of the content area
const contentDiv = this.conditionsPopupElement.querySelector('[style*="padding: 32px"]');
if (contentDiv) {
contentDiv.insertBefore(errorDiv, contentDiv.firstChild);
}
}
/**
* Hides the validation error message
*/
hideValidationError() {
const existingError = this.conditionsPopupElement.querySelector('.validation-error');
if (existingError) {
existingError.remove();
}
}
Step 8: Implement Condition Application Logic
Add the logic to collect, validate, and format conditions:
javascript
/**
* Applies the conditions and closes the modal
*/
applyConditions() {
const CSS = MyExternalDisplayConditions.CSS_CLASSES;
const MSG = MyExternalDisplayConditions.MESSAGES;
const conditions = [];
const rows = this.conditionsPopupElement.querySelectorAll(`.${CSS.CONDITIONS_TABLE} .${CSS.CONDITION_ROW}`);
// Collect conditions with non-empty values
for (let i = 0; i < rows.length; i++) {
const row = rows[i];
const value = row.querySelector(`.${CSS.CONDITION_VALUE}`).value;
if (value.length) {
conditions.push({
name: this.getDropdownValue(row, CSS.DROPDOWN_NAME),
operation: this.getDropdownValue(row, CSS.DROPDOWN_OPERATION),
value
});
}
}
// Validation: at least one condition must have a value
if (conditions.length === 0) {
this.showValidationError(MSG.VALIDATION_ERROR);
return;
}
// Build the final condition script
const concatenation = this.getDropdownValue(this.conditionsPopupElement, CSS.DROPDOWN_CONCATENATION);
const finalCondition = conditions.map(condition => {
return condition.operation + '(\'' + condition.value + '\', ' + condition.name + ')';
}).join(' ' + concatenation + ' ');
// Call the success callback
this.selectConditionsCallback({
name: MSG.CONDITION_NAME,
description: MSG.CONDITION_DESCRIPTION,
beforeScript: '%IF ' + finalCondition + '%',
afterScript: '%/IF%'
});
this.closePopup();
}
/**
* Removes all conditions
*/
removeConditions() {
this.selectConditionsCallback(null);
this.closePopup();
}
/**
* Cancels the modal without applying changes
*/
cancelConditions() {
this.onCancelCallback();
this.closePopup();
}
/**
* Closes the modal
*/
closePopup() {
this.conditionsPopupElement.style.visibility = 'hidden';
this.hideValidationError();
}
Condition Script Format
The applyConditions
method generates a script in this format:
Single condition:
%IF equals('test@gmail.com', $EMAIL)%
Multiple conditions with AND:
%IF equals('test@gmail.com', $EMAIL) && in_array('premium', $PHONE)%
Multiple conditions with OR:
%IF equals('test@gmail.com', $EMAIL) || equals('test@yahoo.com', $EMAIL)%
Step 9: Implement Condition Parsing
Add methods to parse existing conditions when editing:
javascript
/**
* Activates the conditions popup
* @param {DisplayCondition} appliedCondition - Currently applied condition
*/
activateConditionsPopup(appliedCondition) {
if (!this.conditionsPopupElement) {
this.createConditionsPopup();
}
this.initConditions(appliedCondition);
this.conditionsPopupElement.style.visibility = 'visible';
}
/**
* Initializes conditions from applied condition data
* @param {DisplayCondition} appliedCondition - The applied condition object
*/
initConditions(appliedCondition) {
const CSS = MyExternalDisplayConditions.CSS_CLASSES;
// Clear existing conditions
const table = this.conditionsPopupElement.querySelector(`.${CSS.CONDITIONS_TABLE}`);
if (table) {
table.innerHTML = '';
}
// Parse and add conditions
const initialConditions = this.parseAppliedCondition(appliedCondition.beforeScript);
initialConditions.conditions.forEach(condition => {
this.addConditionRow(null, condition);
});
// Set concatenation value
this.setDropdownValue(this.conditionsPopupElement, CSS.DROPDOWN_CONCATENATION, initialConditions.concatenation);
}
/**
* Parses an applied condition string into its components
* @param {string} appliedCondition - The condition string
* @returns {Object} Parsed condition object with conditions array and concatenation
*/
parseAppliedCondition(appliedCondition) {
// Remove wrapper tags
const str = appliedCondition
.trim()
.replace('%IF ', '')
.replace('%/IF%', '');
// Find concatenation operator
const concatenation = this.findConditionOptionValue(
str,
MyExternalDisplayConditions.AVAILABLE_CONDITION_CONCATENATIONS
);
// Split by concatenation and parse individual conditions
const conditions = str
.split(concatenation)
.map((conditionStr) => {
// Extract value between quotes
const valueMatch = conditionStr.match(/'([^']+)'/);
const value = valueMatch ? valueMatch[1] : '';
return {
name: this.findConditionOptionValue(
conditionStr,
MyExternalDisplayConditions.AVAILABLE_CONDITION_NAMES
),
operation: this.findConditionOptionValue(
conditionStr,
MyExternalDisplayConditions.AVAILABLE_CONDITION_OPERATIONS
),
value
};
});
return {
conditions,
concatenation
};
}
/**
* Finds the value of an option that exists in the given string
* @param {string} str - The string to search in
* @param {Array} options - Array of option objects with value property
* @returns {string} The found option value or first option's value as default
*/
findConditionOptionValue(str, options) {
const foundOption = options.find(option => str.includes(option.value));
return foundOption ? foundOption.value : options[0].value;
}
Parsing Logic
The parsing logic handles:
- Script Cleanup: Removes
%IF
and%/IF%
wrapper tags - Concatenation Detection: Identifies
&&
(AND) or||
(OR) operators - Condition Splitting: Splits multiple conditions based on the concatenation operator
- Value Extraction: Uses regex to extract values between single quotes
- Field Detection: Matches field names (
$EMAIL
,$PHONE
) from the condition string - Operation Detection: Identifies the operation (
equals
,in_array
) used
Step 10: Register the Extension
Create src/extension.js
to register your display conditions library with the Stripo extension system:
javascript
import {ExtensionBuilder} from '@stripoinc/ui-editor-extensions';
import {MyExternalDisplayConditions} from './MyExternalDisplayConditions';
const extension = new ExtensionBuilder()
.withExternalDisplayCondition(MyExternalDisplayConditions)
.build();
export default extension;
Extension Registration Explained
The ExtensionBuilder
class provides a fluent API for registering integrations:
withExternalDisplayCondition()
: Registers your custom display conditions implementationbuild()
: Constructs and returns the final extension object for the editor
Multiple Integrations
Multiple .with*()
methods can be chained to register different integrations within a single extension:
javascript
new ExtensionBuilder()
.withExternalDisplayCondition(MyExternalDisplayConditions)
.withExternalImageLibrary(MyExternalImageLibrary)
.withExternalVideosLibrary(MyExternalVideosLibrary)
.build();
Step 11: Enable Conditions in Editor Configuration
Update your src/index.js
to enable display conditions:
javascript
import extension from './extension.js';
import {PLUGIN_ID, SECRET_KEY, EDITOR_URL, EMAIL_ID, USER_ID} from './creds';
function _runEditor(template, extension) {
window.UIEditor.initEditor(
document.querySelector('#stripoEditorContainer'),
{
html: template.html,
css: template.css,
metadata: {
emailId: EMAIL_ID
},
locale: 'en',
conditionsEnabled: true, // IMPORTANT: Enable display conditions
extensions: [
extension
],
// ... other configuration
}
);
}
Important Configuration
You must set conditionsEnabled: true
in the editor configuration for display conditions to work. Without this flag, the display conditions UI will not appear in the editor.
Step 12: Run the Development Server
Your implementation is now ready for testing.
Start the Development Server
bash
npm run dev
This command will:
- Start the Vite development server on
http://localhost:3000
- Automatically open your default browser
- Load the Stripo Editor with your display conditions extension integrated
Complete Example
For a full working example, check out the complete implementation in our GitHub repository.