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: 0by 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:
| Benefit | Description |
|---|---|
| Encapsulation | All variables and functions are private to the component |
| No Global Pollution | Only myop_init_interface and myop_cta_handler are exposed |
| Clean Namespace | Multiple Myop components can coexist without conflicts |
| Maintainability | Clear 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​
- Learn about the Data Loading lifecycle
- Understand the Public API in detail
- Set up Mock Data for development