Best Practices
Guidelines for building maintainable, performant, and accessible Myop components.
Code Organization​
1. Use IIFE Encapsulation​
Keep all implementation private. Only expose the public API:
(function() {
// ===== PRIVATE =====
let state = {};
const cache = {};
function privateHelper() { /* ... */ }
function render() { /* ... */ }
// ===== PUBLIC API =====
window.myop_init_interface = function(data) { /* ... */ };
window.myop_cta_handler = function(action, payload) { /* ... */ };
})();
2. Cache DOM References​
Query the DOM once during initialization:
// Good - cached references
const elements = {
appRoot: document.getElementById('app-root'),
loader: document.getElementById('loader-container'),
list: document.querySelector('.item-list'),
searchInput: document.querySelector('.search-input'),
filterPanel: document.querySelector('.filter-panel')
};
function render() {
elements.list.innerHTML = renderItems();
}
// Bad - repeated queries
function render() {
document.querySelector('.item-list').innerHTML = renderItems();
}
3. Single Initialization Flag​
Attach event listeners only once:
let isInitialized = false;
function initializeComponent(data) {
// Process data...
render();
// Attach listeners only on first init
if (!isInitialized) {
attachEventListeners();
isInitialized = true;
}
hideLoader();
}
Event Handling​
1. Use Event Delegation​
For dynamic content, attach listeners to stable parent elements:
// Good - event delegation
elements.list.addEventListener('click', (e) => {
const item = e.target.closest('.item');
if (item) {
handleItemClick(item.dataset.id);
}
const deleteBtn = e.target.closest('.delete-btn');
if (deleteBtn) {
handleDelete(deleteBtn.dataset.id);
}
});
// Bad - attaching to each element
items.forEach(item => {
item.addEventListener('click', () => handleItemClick(item.id));
});
2. Debounce Expensive Operations​
function debounce(fn, delay) {
let timeoutId;
return function(...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => fn.apply(this, args), delay);
};
}
const debouncedSearch = debounce((query) => {
filterComponents(query);
render();
}, 300);
elements.searchInput.addEventListener('input', (e) => {
debouncedSearch(e.target.value);
});
Data Handling​
1. Parse Dates Once​
Transform date strings during initialization, not during render:
// Good - parse once
function initializeComponent(data) {
state.components = data.components.map(c => ({
...c,
lastEditedDate: new Date(c.lastEditedDate)
}));
}
function formatDate(date) {
return date.toLocaleDateString(); // Already a Date object
}
// Bad - parse on every render
function renderItem(item) {
const date = new Date(item.lastEditedDate); // Parsed every time
return `<div>${date.toLocaleDateString()}</div>`;
}
2. Provide Fallback Values​
Handle missing or malformed data:
function normalizeComponent(raw) {
return {
id: raw.id || `temp-${Date.now()}`,
name: raw.name || 'Unnamed Component',
description: raw.description || '',
tags: Array.isArray(raw.tags) ? raw.tags : [],
environments: Array.isArray(raw.environments) ? raw.environments : [],
developerName: raw.developerName || 'Unknown',
lastEditedDate: raw.lastEditedDate
? new Date(raw.lastEditedDate)
: new Date()
};
}
3. Immutable Updates​
Don't mutate the original state:
// Good - immutable update
function updateComponent(updated) {
state.components = state.components.map(c =>
c.id === updated.id ? { ...c, ...updated } : c
);
render();
}
// Bad - mutation
function updateComponent(updated) {
const index = state.components.findIndex(c => c.id === updated.id);
state.components[index] = { ...state.components[index], ...updated }; // Mutates array
render();
}
Performance​
1. Minimize DOM Operations​
Batch DOM updates:
// Good - single innerHTML update
function render() {
elements.list.innerHTML = state.components
.map(renderItem)
.join('');
}
// Bad - multiple appendChild calls
function render() {
elements.list.innerHTML = '';
state.components.forEach(comp => {
const div = document.createElement('div');
div.innerHTML = renderItem(comp);
elements.list.appendChild(div); // Triggers reflow each time
});
}
2. Use Document Fragments for Large Lists​
function render() {
const fragment = document.createDocumentFragment();
state.components.forEach(comp => {
const div = document.createElement('div');
div.className = 'item';
div.innerHTML = renderItemContent(comp);
fragment.appendChild(div);
});
elements.list.innerHTML = '';
elements.list.appendChild(fragment);
}
3. Virtual Scrolling for Large Datasets​
For lists with 100+ items, consider virtual scrolling:
const ITEM_HEIGHT = 60;
const VISIBLE_ITEMS = 20;
const BUFFER = 5;
function renderVisibleItems(scrollTop) {
const startIndex = Math.max(0, Math.floor(scrollTop / ITEM_HEIGHT) - BUFFER);
const endIndex = startIndex + VISIBLE_ITEMS + (BUFFER * 2);
const visibleItems = state.components.slice(startIndex, endIndex);
elements.list.style.paddingTop = `${startIndex * ITEM_HEIGHT}px`;
elements.list.style.height = `${state.components.length * ITEM_HEIGHT}px`;
elements.list.innerHTML = visibleItems.map(renderItem).join('');
}
elements.scrollContainer.addEventListener('scroll', (e) => {
requestAnimationFrame(() => {
renderVisibleItems(e.target.scrollTop);
});
});
Security​
1. Escape User Content​
Always escape HTML in user-provided content:
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function renderItem(item) {
return `
<div class="item">
<h3>${escapeHtml(item.name)}</h3>
<p>${escapeHtml(item.description)}</p>
</div>
`;
}
2. Validate Data Attributes​
Use data attributes safely:
// Good - validate before use
const itemId = e.target.dataset.id;
if (itemId && /^[a-zA-Z0-9-]+$/.test(itemId)) {
handleItemClick(itemId);
}
// Bad - trusting user input
const itemId = e.target.dataset.id;
document.querySelector(`[data-id="${itemId}"]`); // XSS risk
3. Sanitize URLs​
function createLink(url, text) {
// Only allow http/https URLs
if (!/^https?:\/\//i.test(url)) {
return escapeHtml(text);
}
return `<a href="${escapeHtml(url)}" rel="noopener noreferrer">${escapeHtml(text)}</a>`;
}
Accessibility​
1. Use Semantic HTML​
<!-- Good -->
<nav aria-label="Component filters">
<ul role="listbox">
<li role="option" aria-selected="true">All</li>
<li role="option">Frontend</li>
</ul>
</nav>
<main>
<ul class="component-list" role="list">
<li class="item" role="listitem">...</li>
</ul>
</main>
<!-- Bad -->
<div class="nav">
<div class="filter">All</div>
<div class="filter">Frontend</div>
</div>
<div class="list">
<div class="item">...</div>
</div>
2. Add ARIA Labels​
function renderItem(item) {
return `
<article
class="item"
aria-label="${escapeHtml(item.name)}"
tabindex="0"
>
<h3>${escapeHtml(item.name)}</h3>
<button
class="delete-btn"
aria-label="Delete ${escapeHtml(item.name)}"
data-id="${item.id}"
>
Delete
</button>
</article>
`;
}
3. Support Keyboard Navigation​
elements.list.addEventListener('keydown', (e) => {
const item = e.target.closest('.item');
if (!item) return;
switch (e.key) {
case 'Enter':
case ' ':
e.preventDefault();
handleItemClick(item.dataset.id);
break;
case 'Delete':
case 'Backspace':
e.preventDefault();
handleDelete(item.dataset.id);
break;
case 'ArrowDown':
e.preventDefault();
item.nextElementSibling?.focus();
break;
case 'ArrowUp':
e.preventDefault();
item.previousElementSibling?.focus();
break;
}
});
Error Handling​
1. Graceful Degradation​
function initializeComponent(data) {
try {
if (!data || !Array.isArray(data.components)) {
throw new Error('Invalid data format');
}
state.components = data.components.map(normalizeComponent);
render();
hideLoader();
} catch (error) {
console.error('Failed to initialize:', error);
showErrorState(error.message);
}
}
function showErrorState(message) {
elements.loader.style.display = 'none';
elements.appRoot.innerHTML = `
<div class="error-state" role="alert">
<h2>Unable to load components</h2>
<p>${escapeHtml(message)}</p>
<button onclick="window.myop_cta_handler('retry')">
Try Again
</button>
</div>
`;
elements.appRoot.style.opacity = '1';
}
2. Safe CTA Handler​
window.myop_cta_handler = function(action_id, payload) {
try {
// Validate action
if (typeof action_id !== 'string') {
console.warn('Invalid action_id type');
return;
}
console.log('CTA:', action_id, payload);
} catch (error) {
console.error('CTA handler error:', error);
}
};
Summary Checklist​
| Category | Practice |
|---|---|
| Structure | Use IIFE pattern |
| Structure | Cache DOM references |
| Structure | Initialize once flag |
| Events | Event delegation |
| Events | Debounce expensive operations |
| Data | Parse dates once |
| Data | Provide fallbacks |
| Data | Immutable updates |
| Performance | Batch DOM updates |
| Performance | Use fragments for large lists |
| Security | Escape HTML |
| Security | Validate data attributes |
| Security | Sanitize URLs |
| Accessibility | Semantic HTML |
| Accessibility | ARIA labels |
| Accessibility | Keyboard navigation |
| Errors | Graceful degradation |
| Errors | Safe handlers |
Next Steps​
- Review Component Structure for the complete pattern
- Learn about Data Loading lifecycle
- See Host Integration examples