Skip to main content

Component Structure

A Myop HTML component consists of three main parts that work together to create a dynamic, data-driven UI.

1. HTML Structure​

The main container for your component UI:

<div id="app-root">
<!-- Your component UI goes here -->
<header class="component-header">
<h1>My Component</h1>
</header>

<main class="component-content">
<div class="item-list">
<!-- Dynamic content rendered by JavaScript -->
</div>
</main>

<footer class="component-footer">
<button class="action-btn">Take Action</button>
</footer>
</div>

Key Points​

  • Use a root container (#app-root) to wrap all component UI
  • Keep the initial HTML minimal - dynamic content is rendered by JavaScript
  • Use semantic HTML for accessibility
  • Apply opacity: 0 by default (shown after data loads)

2. Loader UI​

The loader is displayed while waiting for data from the host application:

<div id="loader-container">
<div class="loader-spinner"></div>
<div class="loader-text">Loading...</div>
</div>

Loader Styles​

#loader-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
gap: 16px;
}

.loader-spinner {
width: 40px;
height: 40px;
border: 4px solid #f3f3f3;
border-top: 4px solid #3498db;
border-radius: 50%;
animation: spin 1s linear infinite;
}

@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}

.loader-text {
color: #666;
font-size: 14px;
}

Initial State CSS​

/* Loader visible by default */
#loader-container {
display: flex;
}

/* App hidden by default */
#app-root {
opacity: 0;
transition: opacity 0.3s ease-in-out;
}

/* App visible state */
#app-root.visible {
opacity: 1;
}

3. JavaScript Logic (IIFE Pattern)​

All JavaScript is encapsulated in an Immediately Invoked Function Expression (IIFE):

(function() {
// ===== PRIVATE SCOPE =====
// All implementation details stay private

// Private state
let isInitialized = false;
let componentData = [];

// DOM references (cached for performance)
const appRoot = document.getElementById('app-root');
const loaderContainer = document.getElementById('loader-container');
const itemList = appRoot.querySelector('.item-list');

// Private functions
function hideLoader() {
loaderContainer.style.display = 'none';
appRoot.classList.add('visible');
}

function renderItems(items) {
itemList.innerHTML = items.map(item => `
<div class="item" data-id="${item.id}">
<h3>${item.name}</h3>
<p>${item.description}</p>
</div>
`).join('');
}

function attachEventListeners() {
// Use event delegation for dynamic content
itemList.addEventListener('click', (e) => {
const item = e.target.closest('.item');
if (item) {
const itemId = item.dataset.id;
window.myop_cta_handler('item_clicked', { itemId });
}
});
}

function initializeComponent(data) {
// 1. Store data
componentData = data.items || [];

// 2. Render UI
renderItems(componentData);

// 3. Attach event listeners (only once)
if (!isInitialized) {
attachEventListeners();
isInitialized = true;
}

// 4. Show the component
hideLoader();
}

// ===== PUBLIC API =====
// Only these two functions are exposed globally

window.myop_init_interface = function(data) {
if (data) {
initializeComponent(data);
}
// When called without arguments, return current state
return { items: componentData };
};

window.myop_cta_handler = function(action_id, payload) {
// Default handler - will be overwritten by host
console.log('CTA:', action_id, payload);
};
})();

Why IIFE Pattern?​

The IIFE pattern provides several benefits:

BenefitDescription
EncapsulationAll variables and functions are private to the component
No Global PollutionOnly myop_init_interface and myop_cta_handler are exposed
Clean NamespaceMultiple Myop components can coexist without conflicts
MaintainabilityClear separation between public API and implementation

Complete Example​

Here's a full component template:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My Myop Component</title>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}

body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #fff;
}

/* Loader styles */
#loader-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
gap: 16px;
}

.loader-spinner {
width: 40px;
height: 40px;
border: 4px solid #f3f3f3;
border-top: 4px solid #3498db;
border-radius: 50%;
animation: spin 1s linear infinite;
}

@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}

/* App styles */
#app-root {
opacity: 0;
transition: opacity 0.3s ease-in-out;
}

#app-root.visible {
opacity: 1;
}

.item {
padding: 16px;
border-bottom: 1px solid #eee;
cursor: pointer;
}

.item:hover {
background: #f9f9f9;
}

.item h3 {
font-size: 16px;
margin-bottom: 4px;
}

.item p {
font-size: 14px;
color: #666;
}
</style>
</head>
<body>
<!-- Loader -->
<div id="loader-container">
<div class="loader-spinner"></div>
<div class="loader-text">Loading...</div>
</div>

<!-- App -->
<div id="app-root">
<div class="item-list"></div>
</div>

<!-- Component Logic -->
<script>
(function() {
let isInitialized = false;
let componentData = [];

const appRoot = document.getElementById('app-root');
const loaderContainer = document.getElementById('loader-container');
const itemList = appRoot.querySelector('.item-list');

function hideLoader() {
loaderContainer.style.display = 'none';
appRoot.classList.add('visible');
}

function renderItems(items) {
itemList.innerHTML = items.map(item => `
<div class="item" data-id="${item.id}">
<h3>${item.name}</h3>
<p>${item.description}</p>
</div>
`).join('');
}

function attachEventListeners() {
itemList.addEventListener('click', (e) => {
const item = e.target.closest('.item');
if (item) {
window.myop_cta_handler('item_clicked', {
itemId: item.dataset.id
});
}
});
}

function initializeComponent(data) {
componentData = data.items || [];
renderItems(componentData);

if (!isInitialized) {
attachEventListeners();
isInitialized = true;
}

hideLoader();
}

window.myop_init_interface = function(data) {
if (data) initializeComponent(data);
return { items: componentData };
};

window.myop_cta_handler = function(action_id, payload) {
console.log('CTA:', action_id, payload);
};
})();
</script>

<!-- Preview Mock Data -->
<script id="myop_preview">
setTimeout(() => {
window.myop_init_interface({
items: [
{ id: '1', name: 'First Item', description: 'This is the first item' },
{ id: '2', name: 'Second Item', description: 'This is the second item' },
{ id: '3', name: 'Third Item', description: 'This is the third item' }
]
});
});
</script>
</body>
</html>

Next Steps​