javascript
examples
examples.jsā”javascript
/**
* Component Patterns - Examples
*
* Advanced patterns for building robust Web Components
*/
// ============================================
// EXAMPLE 1: Event Bus for Component Communication
// ============================================
class EventBus {
static _events = new Map();
static _onceEvents = new Set();
/**
* Subscribe to an event
*/
static on(event, handler) {
if (!this._events.has(event)) {
this._events.set(event, new Set());
}
this._events.get(event).add(handler);
return () => this.off(event, handler);
}
/**
* Subscribe to an event (only once)
*/
static once(event, handler) {
const wrapper = (...args) => {
this.off(event, wrapper);
handler(...args);
};
this._onceEvents.add(wrapper);
return this.on(event, wrapper);
}
/**
* Unsubscribe from an event
*/
static off(event, handler) {
const handlers = this._events.get(event);
if (handlers) {
handlers.delete(handler);
}
}
/**
* Emit an event
*/
static emit(event, data) {
const handlers = this._events.get(event);
if (handlers) {
handlers.forEach((handler) => {
try {
handler(data);
} catch (error) {
console.error(`Error in event handler for ${event}:`, error);
}
});
}
}
/**
* Clear all handlers for an event
*/
static clear(event) {
this._events.delete(event);
}
/**
* Clear all events
*/
static clearAll() {
this._events.clear();
}
}
// Components using EventBus
class SenderComponent extends HTMLElement {
connectedCallback() {
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = `
<button>Send Message</button>
`;
this.shadowRoot.querySelector('button').addEventListener('click', () => {
EventBus.emit('message', {
from: 'SenderComponent',
text: 'Hello from sender!',
timestamp: Date.now(),
});
});
}
}
class ReceiverComponent extends HTMLElement {
connectedCallback() {
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = `
<div class="messages"></div>
`;
this._unsubscribe = EventBus.on('message', (data) => {
const messages = this.shadowRoot.querySelector('.messages');
messages.innerHTML += `<p>${data.text}</p>`;
});
}
disconnectedCallback() {
if (this._unsubscribe) {
this._unsubscribe();
}
}
}
customElements.define('sender-component', SenderComponent);
customElements.define('receiver-component', ReceiverComponent);
// ============================================
// EXAMPLE 2: Reactive State with Proxy
// ============================================
function createReactiveState(initialState, onChange) {
const handlers = {
set(target, property, value, receiver) {
const oldValue = target[property];
const result = Reflect.set(target, property, value, receiver);
if (oldValue !== value) {
onChange({
property,
oldValue,
newValue: value,
state: target,
});
}
return result;
},
deleteProperty(target, property) {
const oldValue = target[property];
const result = Reflect.deleteProperty(target, property);
onChange({
property,
oldValue,
newValue: undefined,
state: target,
deleted: true,
});
return result;
},
};
return new Proxy({ ...initialState }, handlers);
}
class ReactiveCounter extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.state = createReactiveState({ count: 0 }, () => this.render());
}
connectedCallback() {
this.render();
}
increment() {
this.state.count++;
}
decrement() {
this.state.count--;
}
render() {
this.shadowRoot.innerHTML = `
<style>
:host {
display: inline-flex;
align-items: center;
gap: 12px;
font-family: system-ui, sans-serif;
}
button {
width: 36px;
height: 36px;
border: none;
border-radius: 50%;
background: #3b82f6;
color: white;
font-size: 18px;
cursor: pointer;
}
button:hover { background: #2563eb; }
.count {
min-width: 40px;
text-align: center;
font-size: 24px;
font-weight: bold;
}
</style>
<button class="decrement">ā</button>
<span class="count">${this.state.count}</span>
<button class="increment">+</button>
`;
this.shadowRoot.querySelector('.increment').onclick = () =>
this.increment();
this.shadowRoot.querySelector('.decrement').onclick = () =>
this.decrement();
}
}
customElements.define('reactive-counter', ReactiveCounter);
// ============================================
// EXAMPLE 3: Global Store Pattern
// ============================================
class Store {
constructor(initialState = {}) {
this._state = initialState;
this._subscribers = new Map();
this._nextId = 0;
}
getState() {
return { ...this._state };
}
setState(newState) {
const prevState = this._state;
this._state = { ...this._state, ...newState };
this._notify(prevState);
}
subscribe(selector, callback) {
const id = this._nextId++;
this._subscribers.set(id, { selector, callback });
// Return unsubscribe function
return () => this._subscribers.delete(id);
}
_notify(prevState) {
this._subscribers.forEach(({ selector, callback }) => {
const prev = selector ? selector(prevState) : prevState;
const curr = selector ? selector(this._state) : this._state;
if (prev !== curr) {
callback(curr, prev);
}
});
}
}
// Create global store instance
const globalStore = new Store({
user: null,
theme: 'light',
notifications: [],
});
// Component connected to store
class StoreConnected extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this._unsubscribers = [];
}
connectedCallback() {
// Subscribe to specific state slice
const unsubTheme = globalStore.subscribe(
(state) => state.theme,
(theme) => this.onThemeChange(theme)
);
this._unsubscribers.push(unsubTheme);
this.render();
}
disconnectedCallback() {
this._unsubscribers.forEach((unsub) => unsub());
}
onThemeChange(theme) {
this.setAttribute('data-theme', theme);
this.render();
}
toggleTheme() {
const current = globalStore.getState().theme;
globalStore.setState({ theme: current === 'light' ? 'dark' : 'light' });
}
render() {
const { theme } = globalStore.getState();
this.shadowRoot.innerHTML = `
<style>
:host([data-theme="dark"]) .container {
background: #1f2937;
color: white;
}
.container {
padding: 20px;
border-radius: 8px;
background: #f3f4f6;
transition: all 0.3s;
}
</style>
<div class="container">
<p>Current theme: ${theme}</p>
<button>Toggle Theme</button>
</div>
`;
this.shadowRoot.querySelector('button').onclick = () => this.toggleTheme();
}
}
customElements.define('store-connected', StoreConnected);
// ============================================
// EXAMPLE 4: Mixin Pattern
// ============================================
// Logger Mixin
const LoggerMixin = (Base) =>
class extends Base {
log(level, message) {
const timestamp = new Date().toISOString();
console[level](`[${timestamp}] [${this.tagName}] ${message}`);
}
logInfo(message) {
this.log('info', message);
}
logWarn(message) {
this.log('warn', message);
}
logError(message) {
this.log('error', message);
}
};
// Event Emitter Mixin
const EventEmitterMixin = (Base) =>
class extends Base {
emit(eventName, detail = {}, options = {}) {
const event = new CustomEvent(eventName, {
detail,
bubbles: options.bubbles ?? true,
composed: options.composed ?? true,
cancelable: options.cancelable ?? false,
});
return this.dispatchEvent(event);
}
};
// State Mixin
const StateMixin = (Base) =>
class extends Base {
_state = {};
get state() {
return { ...this._state };
}
setState(newState) {
const prevState = this._state;
this._state = { ...this._state, ...newState };
if (typeof this.stateChanged === 'function') {
this.stateChanged(prevState, this._state);
}
if (typeof this.render === 'function') {
this.scheduleRender();
}
}
scheduleRender() {
if (!this._renderScheduled) {
this._renderScheduled = true;
requestAnimationFrame(() => {
this._renderScheduled = false;
this.render();
});
}
}
};
// Cleanup Mixin
const CleanupMixin = (Base) =>
class extends Base {
_cleanupFunctions = [];
addCleanup(fn) {
this._cleanupFunctions.push(fn);
}
cleanup() {
this._cleanupFunctions.forEach((fn) => fn());
this._cleanupFunctions = [];
}
disconnectedCallback() {
this.cleanup();
if (super.disconnectedCallback) {
super.disconnectedCallback();
}
}
};
// Combined component using mixins
class MixedComponent extends CleanupMixin(
StateMixin(EventEmitterMixin(LoggerMixin(HTMLElement)))
) {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.setState({ clicks: 0 });
}
connectedCallback() {
this.logInfo('Connected');
this.render();
// Event listener with cleanup
const handler = (e) => this.logInfo('Document clicked');
document.addEventListener('click', handler);
this.addCleanup(() => document.removeEventListener('click', handler));
}
handleClick() {
this.setState({ clicks: this.state.clicks + 1 });
this.emit('click-count', { count: this.state.clicks });
}
render() {
this.shadowRoot.innerHTML = `
<style>
button {
padding: 10px 20px;
border: none;
border-radius: 6px;
background: #8b5cf6;
color: white;
cursor: pointer;
}
</style>
<button>Clicks: ${this.state.clicks}</button>
`;
this.shadowRoot.querySelector('button').onclick = () => this.handleClick();
}
}
customElements.define('mixed-component', MixedComponent);
// ============================================
// EXAMPLE 5: Form-Associated Component
// ============================================
class CustomInput extends HTMLElement {
static formAssociated = true;
constructor() {
super();
this._internals = this.attachInternals();
this.attachShadow({ mode: 'open' });
this._value = '';
}
static get observedAttributes() {
return ['value', 'placeholder', 'required', 'disabled', 'pattern'];
}
get value() {
return this._value;
}
set value(v) {
this._value = v;
this._internals.setFormValue(v);
this.validate();
}
get validity() {
return this._internals.validity;
}
get validationMessage() {
return this._internals.validationMessage;
}
connectedCallback() {
this.render();
}
attributeChangedCallback(name, oldVal, newVal) {
if (name === 'value') {
this._value = newVal;
}
this.render();
}
validate() {
const input = this.shadowRoot?.querySelector('input');
if (!input) return true;
if (this.hasAttribute('required') && !this._value) {
this._internals.setValidity(
{ valueMissing: true },
'This field is required',
input
);
return false;
}
const pattern = this.getAttribute('pattern');
if (pattern && !new RegExp(pattern).test(this._value)) {
this._internals.setValidity(
{ patternMismatch: true },
'Please match the requested format',
input
);
return false;
}
this._internals.setValidity({});
return true;
}
formResetCallback() {
this.value = '';
this.render();
}
formStateRestoreCallback(state) {
this.value = state;
}
formDisabledCallback(disabled) {
this.toggleAttribute('disabled', disabled);
this.render();
}
render() {
const disabled = this.hasAttribute('disabled');
const placeholder = this.getAttribute('placeholder') || '';
this.shadowRoot.innerHTML = `
<style>
:host {
display: block;
}
input {
width: 100%;
padding: 10px 12px;
border: 2px solid #d1d5db;
border-radius: 6px;
font-size: 14px;
box-sizing: border-box;
transition: border-color 0.2s;
}
input:focus {
outline: none;
border-color: #3b82f6;
}
input:invalid {
border-color: #ef4444;
}
input:disabled {
background: #f3f4f6;
cursor: not-allowed;
}
</style>
<input
type="text"
.value="${this._value}"
placeholder="${placeholder}"
${disabled ? 'disabled' : ''}
/>
`;
const input = this.shadowRoot.querySelector('input');
input.value = this._value;
input.addEventListener('input', (e) => {
this.value = e.target.value;
this.dispatchEvent(new Event('input', { bubbles: true }));
});
input.addEventListener('change', (e) => {
this.dispatchEvent(new Event('change', { bubbles: true }));
});
}
}
customElements.define('custom-input', CustomInput);
// ============================================
// EXAMPLE 6: Accessible Component
// ============================================
class AccessibleTabs extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this._selectedIndex = 0;
}
connectedCallback() {
this.render();
this.setupAccessibility();
}
get selectedIndex() {
return this._selectedIndex;
}
set selectedIndex(value) {
this._selectedIndex = value;
this.updateSelection();
}
setupAccessibility() {
const tablist = this.shadowRoot.querySelector('[role="tablist"]');
tablist.addEventListener('keydown', (e) => {
const tabs = this.shadowRoot.querySelectorAll('[role="tab"]');
const currentIndex = this._selectedIndex;
switch (e.key) {
case 'ArrowRight':
case 'ArrowDown':
e.preventDefault();
this.selectedIndex = (currentIndex + 1) % tabs.length;
tabs[this.selectedIndex].focus();
break;
case 'ArrowLeft':
case 'ArrowUp':
e.preventDefault();
this.selectedIndex = (currentIndex - 1 + tabs.length) % tabs.length;
tabs[this.selectedIndex].focus();
break;
case 'Home':
e.preventDefault();
this.selectedIndex = 0;
tabs[0].focus();
break;
case 'End':
e.preventDefault();
this.selectedIndex = tabs.length - 1;
tabs[tabs.length - 1].focus();
break;
}
});
}
updateSelection() {
const tabs = this.shadowRoot.querySelectorAll('[role="tab"]');
const panels = this.shadowRoot.querySelectorAll('[role="tabpanel"]');
tabs.forEach((tab, i) => {
const selected = i === this._selectedIndex;
tab.setAttribute('aria-selected', selected);
tab.setAttribute('tabindex', selected ? '0' : '-1');
});
panels.forEach((panel, i) => {
panel.hidden = i !== this._selectedIndex;
});
this.dispatchEvent(
new CustomEvent('tab-change', {
detail: { index: this._selectedIndex },
})
);
}
render() {
const panels = Array.from(this.children);
const tabs = panels.map((panel, i) => ({
label: panel.getAttribute('data-label') || `Tab ${i + 1}`,
id: `tab-${i}`,
}));
this.shadowRoot.innerHTML = `
<style>
:host {
display: block;
font-family: system-ui, sans-serif;
}
[role="tablist"] {
display: flex;
gap: 4px;
border-bottom: 2px solid #e5e7eb;
}
[role="tab"] {
padding: 12px 20px;
border: none;
background: none;
cursor: pointer;
font-size: 14px;
color: #64748b;
border-bottom: 2px solid transparent;
margin-bottom: -2px;
transition: all 0.2s;
}
[role="tab"]:hover {
color: #3b82f6;
}
[role="tab"][aria-selected="true"] {
color: #3b82f6;
border-bottom-color: #3b82f6;
}
[role="tab"]:focus {
outline: 2px solid #3b82f6;
outline-offset: -2px;
}
[role="tabpanel"] {
padding: 20px;
}
[role="tabpanel"][hidden] {
display: none;
}
</style>
<div role="tablist" aria-label="Tabs">
${tabs
.map(
(tab, i) => `
<button
role="tab"
id="${tab.id}"
aria-selected="${i === 0}"
aria-controls="panel-${i}"
tabindex="${i === 0 ? 0 : -1}"
>${tab.label}</button>
`
)
.join('')}
</div>
${panels
.map(
(_, i) => `
<div
role="tabpanel"
id="panel-${i}"
aria-labelledby="tab-${i}"
${i !== 0 ? 'hidden' : ''}
>
<slot name="panel-${i}"></slot>
</div>
`
)
.join('')}
`;
// Move children to named slots
panels.forEach((panel, i) => {
panel.setAttribute('slot', `panel-${i}`);
});
// Add click handlers
this.shadowRoot.querySelectorAll('[role="tab"]').forEach((tab, i) => {
tab.addEventListener('click', () => {
this.selectedIndex = i;
});
});
}
}
customElements.define('accessible-tabs', AccessibleTabs);
// ============================================
// EXAMPLE 7: Lazy Loading Component
// ============================================
class LazyImage extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this._loaded = false;
this._observer = null;
}
static get observedAttributes() {
return ['src', 'alt', 'width', 'height'];
}
connectedCallback() {
this.render();
this.setupObserver();
}
disconnectedCallback() {
if (this._observer) {
this._observer.disconnect();
}
}
setupObserver() {
this._observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting && !this._loaded) {
this.loadImage();
}
});
},
{ rootMargin: '50px' }
);
this._observer.observe(this);
}
loadImage() {
const src = this.getAttribute('src');
if (!src) return;
const img = this.shadowRoot.querySelector('img');
const placeholder = this.shadowRoot.querySelector('.placeholder');
img.onload = () => {
this._loaded = true;
img.classList.add('loaded');
placeholder?.remove();
this._observer?.disconnect();
};
img.onerror = () => {
placeholder.textContent = 'Failed to load';
};
img.src = src;
}
render() {
const width = this.getAttribute('width') || 'auto';
const height = this.getAttribute('height') || 'auto';
const alt = this.getAttribute('alt') || '';
this.shadowRoot.innerHTML = `
<style>
:host {
display: inline-block;
position: relative;
overflow: hidden;
}
.placeholder {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
background: #f3f4f6;
color: #9ca3af;
}
img {
display: block;
width: ${width};
height: ${height};
opacity: 0;
transition: opacity 0.3s ease;
}
img.loaded {
opacity: 1;
}
</style>
<div class="placeholder">Loading...</div>
<img alt="${alt}" />
`;
}
}
customElements.define('lazy-image', LazyImage);
// ============================================
// EXAMPLE 8: Component with Async Data
// ============================================
class AsyncDataComponent extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this._state = 'idle';
this._data = null;
this._error = null;
}
static get observedAttributes() {
return ['url', 'auto-load'];
}
connectedCallback() {
this.render();
if (this.hasAttribute('auto-load')) {
this.load();
}
}
attributeChangedCallback(name, oldVal, newVal) {
if (name === 'url' && oldVal !== newVal && this.hasAttribute('auto-load')) {
this.load();
}
}
async load() {
const url = this.getAttribute('url');
if (!url) return;
this._state = 'loading';
this._error = null;
this.render();
try {
const response = await fetch(url);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
this._data = await response.json();
this._state = 'success';
} catch (error) {
this._error = error.message;
this._state = 'error';
}
this.render();
this.dispatchEvent(
new CustomEvent('load-complete', {
detail: { state: this._state, data: this._data, error: this._error },
})
);
}
retry() {
this.load();
}
render() {
const templates = {
idle: `<p>Click load to fetch data</p><button class="load">Load</button>`,
loading: `<p>Loading...</p>`,
error: `
<p style="color: #ef4444;">Error: ${this._error}</p>
<button class="retry">Retry</button>
`,
success: `
<pre style="background: #f3f4f6; padding: 12px; border-radius: 6px; overflow: auto;">
${JSON.stringify(this._data, null, 2)}
</pre>
`,
};
this.shadowRoot.innerHTML = `
<style>
:host {
display: block;
font-family: system-ui, sans-serif;
}
button {
padding: 8px 16px;
border: none;
border-radius: 4px;
background: #3b82f6;
color: white;
cursor: pointer;
}
</style>
${templates[this._state]}
`;
this.shadowRoot
.querySelector('.load')
?.addEventListener('click', () => this.load());
this.shadowRoot
.querySelector('.retry')
?.addEventListener('click', () => this.retry());
}
}
customElements.define('async-data', AsyncDataComponent);
// ============================================
// EXAMPLE 9: Debounced Render Component
// ============================================
class DebouncedRender extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this._renderScheduled = false;
this._updates = 0;
this._renders = 0;
}
connectedCallback() {
this.render();
// Simulate rapid updates
const button = document.createElement('button');
button.textContent = 'Trigger 100 updates';
button.onclick = () => this.simulateRapidUpdates();
this.shadowRoot.appendChild(button);
}
scheduleRender() {
this._updates++;
if (!this._renderScheduled) {
this._renderScheduled = true;
requestAnimationFrame(() => {
this._renderScheduled = false;
this._renders++;
this.render();
});
}
}
simulateRapidUpdates() {
this._updates = 0;
this._renders = 0;
for (let i = 0; i < 100; i++) {
this.scheduleRender();
}
// Show results after render
requestAnimationFrame(() => {
requestAnimationFrame(() => {
alert(`Updates: ${this._updates}, Actual Renders: ${this._renders}`);
});
});
}
render() {
this.shadowRoot.innerHTML = `
<style>
:host { display: block; font-family: system-ui; }
button { margin-top: 12px; }
</style>
<p>Updates: ${this._updates}, Renders: ${this._renders}</p>
`;
}
}
customElements.define('debounced-render', DebouncedRender);
// ============================================
// EXAMPLE 10: Component Testing Utilities
// ============================================
class ComponentTestUtils {
/**
* Wait for component to be defined
*/
static async whenDefined(tagName) {
return customElements.whenDefined(tagName);
}
/**
* Create and attach component to DOM
*/
static async create(tagName, props = {}, container = document.body) {
await this.whenDefined(tagName);
const element = document.createElement(tagName);
// Set attributes
Object.entries(props).forEach(([key, value]) => {
if (typeof value === 'boolean') {
if (value) element.setAttribute(key, '');
} else {
element.setAttribute(key, value);
}
});
container.appendChild(element);
// Wait for next frame for rendering
await new Promise((resolve) => requestAnimationFrame(resolve));
return element;
}
/**
* Query shadow DOM
*/
static shadowQuery(element, selector) {
return element.shadowRoot?.querySelector(selector);
}
/**
* Simulate click
*/
static click(element) {
element.dispatchEvent(new MouseEvent('click', { bubbles: true }));
}
/**
* Simulate input
*/
static input(element, value) {
element.value = value;
element.dispatchEvent(new Event('input', { bubbles: true }));
}
/**
* Wait for event
*/
static waitForEvent(element, eventName, timeout = 1000) {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
reject(new Error(`Timeout waiting for ${eventName}`));
}, timeout);
element.addEventListener(
eventName,
(e) => {
clearTimeout(timer);
resolve(e);
},
{ once: true }
);
});
}
/**
* Cleanup
*/
static cleanup(element) {
element?.parentNode?.removeChild(element);
}
}
// ============================================
// USAGE DEMONSTRATION
// ============================================
function demonstratePatterns() {
console.log('=== Component Patterns Demonstration ===\n');
console.log('Patterns demonstrated:');
console.log('1. EventBus - Cross-component communication');
console.log('2. Reactive State - Proxy-based reactivity');
console.log('3. Global Store - Centralized state management');
console.log('4. Mixins - Composable functionality');
console.log('5. Form-Associated - Native form integration');
console.log('6. Accessible Tabs - ARIA support');
console.log('7. Lazy Loading - Performance optimization');
console.log('8. Async Data - Data fetching pattern');
console.log('9. Debounced Render - Batched updates');
console.log('10. Test Utilities - Testing helpers');
console.log('\nComponents:');
const components = [
'sender-component',
'receiver-component',
'reactive-counter',
'store-connected',
'mixed-component',
'custom-input',
'accessible-tabs',
'lazy-image',
'async-data',
'debounced-render',
];
components.forEach((name) => console.log(` <${name}>`));
}
// Export for module usage
if (typeof module !== 'undefined' && module.exports) {
module.exports = {
EventBus,
Store,
globalStore,
createReactiveState,
ComponentTestUtils,
demonstratePatterns,
};
}