javascript
examples
examples.jsā”javascript
/**
* Custom Elements - Examples
*
* Comprehensive examples of creating and using Custom Elements
*/
// ============================================
// EXAMPLE 1: Basic Custom Element
// ============================================
class HelloWorld extends HTMLElement {
constructor() {
super();
}
connectedCallback() {
this.innerHTML = `
<div style="
padding: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: 8px;
text-align: center;
">
<h2>Hello, World!</h2>
<p>This is my first custom element</p>
</div>
`;
}
}
customElements.define('hello-world', HelloWorld);
// Usage: <hello-world></hello-world>
// ============================================
// EXAMPLE 2: Element with Attributes
// ============================================
class GreetingCard extends HTMLElement {
static get observedAttributes() {
return ['name', 'greeting', 'theme'];
}
constructor() {
super();
this._name = 'Friend';
this._greeting = 'Hello';
this._theme = 'light';
}
// Attribute getters and setters with reflection
get name() {
return this.getAttribute('name') || this._name;
}
set name(value) {
this.setAttribute('name', value);
}
get greeting() {
return this.getAttribute('greeting') || this._greeting;
}
set greeting(value) {
this.setAttribute('greeting', value);
}
get theme() {
return this.getAttribute('theme') || this._theme;
}
set theme(value) {
this.setAttribute('theme', value);
}
connectedCallback() {
this.render();
}
attributeChangedCallback(name, oldValue, newValue) {
if (oldValue !== newValue && this.isConnected) {
this.render();
}
}
render() {
const isDark = this.theme === 'dark';
const bgColor = isDark ? '#2d3748' : '#f7fafc';
const textColor = isDark ? '#e2e8f0' : '#2d3748';
this.innerHTML = `
<div style="
padding: 24px;
background: ${bgColor};
color: ${textColor};
border-radius: 12px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
">
<h2 style="margin: 0 0 8px 0;">
${this.greeting}, ${this.name}!
</h2>
<p style="margin: 0; opacity: 0.7;">
Welcome to Custom Elements
</p>
</div>
`;
}
}
customElements.define('greeting-card', GreetingCard);
// Usage: <greeting-card name="John" greeting="Welcome" theme="dark"></greeting-card>
// ============================================
// EXAMPLE 3: Complete Lifecycle Demo
// ============================================
class LifecycleLogger extends HTMLElement {
static get observedAttributes() {
return ['message', 'count'];
}
constructor() {
super();
this.log('constructor', 'Element instance created');
this._count = 0;
this._intervalId = null;
}
connectedCallback() {
this.log('connectedCallback', 'Added to DOM');
this.render();
// Start interval when connected
this._intervalId = setInterval(() => {
this._count++;
this.updateCount();
}, 1000);
}
disconnectedCallback() {
this.log('disconnectedCallback', 'Removed from DOM');
// Cleanup interval when disconnected
if (this._intervalId) {
clearInterval(this._intervalId);
this._intervalId = null;
}
}
adoptedCallback() {
this.log('adoptedCallback', 'Moved to new document');
}
attributeChangedCallback(name, oldValue, newValue) {
this.log(
'attributeChangedCallback',
`Attribute '${name}' changed from '${oldValue}' to '${newValue}'`
);
if (this.isConnected) {
this.render();
}
}
log(callback, message) {
const timestamp = new Date().toISOString().split('T')[1].slice(0, 12);
console.log(`[${timestamp}] ${callback}: ${message}`);
}
updateCount() {
const countEl = this.querySelector('.count');
if (countEl) {
countEl.textContent = this._count;
}
}
render() {
const message = this.getAttribute('message') || 'Lifecycle Demo';
this.innerHTML = `
<div style="
padding: 16px;
border: 2px solid #4a5568;
border-radius: 8px;
font-family: monospace;
">
<h3 style="margin: 0 0 12px 0;">${message}</h3>
<p>Seconds connected: <span class="count">${this._count}</span></p>
<p style="font-size: 12px; color: #718096;">
Check console for lifecycle logs
</p>
</div>
`;
}
}
customElements.define('lifecycle-logger', LifecycleLogger);
// ============================================
// EXAMPLE 4: Interactive Counter Element
// ============================================
class CounterElement extends HTMLElement {
static get observedAttributes() {
return ['value', 'min', 'max', 'step'];
}
constructor() {
super();
this._value = 0;
}
get value() {
return this._value;
}
set value(val) {
const num = parseInt(val, 10);
const min = parseInt(this.getAttribute('min')) || -Infinity;
const max = parseInt(this.getAttribute('max')) || Infinity;
this._value = Math.max(min, Math.min(max, num));
this.setAttribute('value', this._value);
this.dispatchEvent(
new CustomEvent('change', {
detail: { value: this._value },
bubbles: true,
})
);
}
connectedCallback() {
// Initialize from attribute
if (this.hasAttribute('value')) {
this._value = parseInt(this.getAttribute('value'), 10) || 0;
}
this.render();
this.attachEventListeners();
}
attributeChangedCallback(name, oldVal, newVal) {
if (name === 'value' && oldVal !== newVal) {
this._value = parseInt(newVal, 10) || 0;
this.updateDisplay();
}
}
increment() {
const step = parseInt(this.getAttribute('step')) || 1;
this.value = this._value + step;
}
decrement() {
const step = parseInt(this.getAttribute('step')) || 1;
this.value = this._value - step;
}
reset() {
this.value = 0;
}
attachEventListeners() {
this.querySelector('.btn-dec').addEventListener('click', () =>
this.decrement()
);
this.querySelector('.btn-inc').addEventListener('click', () =>
this.increment()
);
this.querySelector('.btn-reset')?.addEventListener('click', () =>
this.reset()
);
}
updateDisplay() {
const display = this.querySelector('.value-display');
if (display) {
display.textContent = this._value;
}
}
render() {
const min = this.getAttribute('min');
const max = this.getAttribute('max');
this.innerHTML = `
<style>
.counter-container {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 8px;
background: #f0f0f0;
border-radius: 8px;
}
.counter-btn {
width: 36px;
height: 36px;
border: none;
border-radius: 50%;
font-size: 18px;
cursor: pointer;
transition: background 0.2s;
}
.counter-btn:hover {
background: #ddd;
}
.btn-dec { background: #fee2e2; }
.btn-inc { background: #dcfce7; }
.btn-reset {
background: #e0e7ff;
border-radius: 4px;
width: auto;
padding: 0 12px;
}
.value-display {
min-width: 60px;
text-align: center;
font-size: 24px;
font-weight: bold;
}
</style>
<div class="counter-container">
<button class="counter-btn btn-dec" ${
this._value <= min ? 'disabled' : ''
}>ā</button>
<span class="value-display">${this._value}</span>
<button class="counter-btn btn-inc" ${
this._value >= max ? 'disabled' : ''
}>+</button>
<button class="counter-btn btn-reset">Reset</button>
</div>
`;
}
}
customElements.define('counter-element', CounterElement);
// ============================================
// EXAMPLE 5: Extending Built-in Elements
// ============================================
// Extend Button
class RippleButton extends HTMLButtonElement {
constructor() {
super();
this.style.cssText = `
position: relative;
overflow: hidden;
padding: 12px 24px;
border: none;
border-radius: 4px;
background: #3b82f6;
color: white;
font-size: 14px;
cursor: pointer;
transition: background 0.3s;
`;
}
connectedCallback() {
this.addEventListener('click', this.createRipple.bind(this));
}
createRipple(event) {
const ripple = document.createElement('span');
const rect = this.getBoundingClientRect();
const size = Math.max(rect.width, rect.height);
const x = event.clientX - rect.left - size / 2;
const y = event.clientY - rect.top - size / 2;
ripple.style.cssText = `
position: absolute;
width: ${size}px;
height: ${size}px;
left: ${x}px;
top: ${y}px;
background: rgba(255, 255, 255, 0.5);
border-radius: 50%;
transform: scale(0);
animation: ripple 0.6s ease-out;
pointer-events: none;
`;
// Add keyframes if not exists
if (!document.querySelector('#ripple-keyframes')) {
const style = document.createElement('style');
style.id = 'ripple-keyframes';
style.textContent = `
@keyframes ripple {
to {
transform: scale(2);
opacity: 0;
}
}
`;
document.head.appendChild(style);
}
this.appendChild(ripple);
ripple.addEventListener('animationend', () => ripple.remove());
}
}
customElements.define('ripple-button', RippleButton, { extends: 'button' });
// Extend Input with validation
class EmailInput extends HTMLInputElement {
constructor() {
super();
this.type = 'email';
}
connectedCallback() {
this.placeholder = this.placeholder || 'Enter email';
this.style.cssText = `
padding: 10px;
border: 2px solid #ddd;
border-radius: 4px;
font-size: 14px;
transition: border-color 0.2s;
`;
this.addEventListener('input', this.validate.bind(this));
this.addEventListener('blur', this.validate.bind(this));
}
validate() {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
const isValid = emailRegex.test(this.value);
if (this.value === '') {
this.style.borderColor = '#ddd';
} else if (isValid) {
this.style.borderColor = '#22c55e';
} else {
this.style.borderColor = '#ef4444';
}
return isValid;
}
}
customElements.define('email-input', EmailInput, { extends: 'input' });
// Extend Anchor with external link handling
class ExternalLink extends HTMLAnchorElement {
connectedCallback() {
// Always open in new tab
this.target = '_blank';
this.rel = 'noopener noreferrer';
// Add external icon
if (!this.querySelector('.external-icon')) {
const icon = document.createElement('span');
icon.className = 'external-icon';
icon.innerHTML = ' ā';
icon.style.fontSize = '0.8em';
this.appendChild(icon);
}
this.style.cssText = `
color: #3b82f6;
text-decoration: none;
`;
this.addEventListener('mouseenter', () => {
this.style.textDecoration = 'underline';
});
this.addEventListener('mouseleave', () => {
this.style.textDecoration = 'none';
});
}
}
customElements.define('external-link', ExternalLink, { extends: 'a' });
// ============================================
// EXAMPLE 6: Alert/Notification Element
// ============================================
class AlertBox extends HTMLElement {
static get observedAttributes() {
return ['type', 'dismissible', 'auto-dismiss'];
}
constructor() {
super();
this._types = {
info: { bg: '#dbeafe', border: '#3b82f6', icon: 'ā¹ļø' },
success: { bg: '#dcfce7', border: '#22c55e', icon: 'ā' },
warning: { bg: '#fef3c7', border: '#f59e0b', icon: 'ā ļø' },
error: { bg: '#fee2e2', border: '#ef4444', icon: 'ā' },
};
}
get type() {
return this.getAttribute('type') || 'info';
}
set type(value) {
this.setAttribute('type', value);
}
get dismissible() {
return this.hasAttribute('dismissible');
}
connectedCallback() {
this.render();
// Auto-dismiss if specified
const autoDismiss = this.getAttribute('auto-dismiss');
if (autoDismiss) {
setTimeout(() => this.dismiss(), parseInt(autoDismiss, 10));
}
}
attributeChangedCallback(name, oldVal, newVal) {
if (this.isConnected && oldVal !== newVal) {
this.render();
}
}
dismiss() {
this.style.animation = 'fadeOut 0.3s ease-out';
this.addEventListener('animationend', () => {
this.remove();
this.dispatchEvent(new CustomEvent('dismissed', { bubbles: true }));
});
}
render() {
const config = this._types[this.type] || this._types.info;
// Add fadeOut keyframes
if (!document.querySelector('#alert-keyframes')) {
const style = document.createElement('style');
style.id = 'alert-keyframes';
style.textContent = `
@keyframes fadeOut {
to { opacity: 0; transform: translateX(20px); }
}
`;
document.head.appendChild(style);
}
this.innerHTML = `
<div style="
display: flex;
align-items: flex-start;
gap: 12px;
padding: 16px;
background: ${config.bg};
border-left: 4px solid ${config.border};
border-radius: 4px;
">
<span style="font-size: 20px;">${config.icon}</span>
<div style="flex: 1;">
<slot></slot>
</div>
${
this.dismissible
? `
<button class="dismiss-btn" style="
background: none;
border: none;
font-size: 18px;
cursor: pointer;
opacity: 0.5;
">Ć</button>
`
: ''
}
</div>
`;
if (this.dismissible) {
this.querySelector('.dismiss-btn').addEventListener('click', () =>
this.dismiss()
);
}
}
}
customElements.define('alert-box', AlertBox);
// ============================================
// EXAMPLE 7: Loading Spinner Element
// ============================================
class LoadingSpinner extends HTMLElement {
static get observedAttributes() {
return ['size', 'color', 'text'];
}
connectedCallback() {
this.render();
}
attributeChangedCallback() {
if (this.isConnected) {
this.render();
}
}
render() {
const size = this.getAttribute('size') || '40';
const color = this.getAttribute('color') || '#3b82f6';
const text = this.getAttribute('text') || '';
this.innerHTML = `
<style>
@keyframes spin {
to { transform: rotate(360deg); }
}
</style>
<div style="
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
">
<div style="
width: ${size}px;
height: ${size}px;
border: 3px solid #e5e7eb;
border-top-color: ${color};
border-radius: 50%;
animation: spin 0.8s linear infinite;
"></div>
${text ? `<span style="color: #6b7280;">${text}</span>` : ''}
</div>
`;
}
}
customElements.define('loading-spinner', LoadingSpinner);
// ============================================
// EXAMPLE 8: User Profile Card
// ============================================
class ProfileCard extends HTMLElement {
static get observedAttributes() {
return ['name', 'title', 'avatar', 'status'];
}
constructor() {
super();
this._statuses = {
online: '#22c55e',
offline: '#9ca3af',
away: '#f59e0b',
busy: '#ef4444',
};
}
connectedCallback() {
this.render();
}
attributeChangedCallback(name, oldVal, newVal) {
if (this.isConnected && oldVal !== newVal) {
this.render();
}
}
render() {
const name = this.getAttribute('name') || 'Unknown User';
const title = this.getAttribute('title') || '';
const avatar = this.getAttribute('avatar') || '';
const status = this.getAttribute('status') || 'offline';
const statusColor = this._statuses[status] || this._statuses.offline;
// Generate initials if no avatar
const initials = name
.split(' ')
.map((n) => n[0])
.join('')
.toUpperCase()
.slice(0, 2);
this.innerHTML = `
<div style="
display: flex;
align-items: center;
gap: 16px;
padding: 16px;
background: white;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
">
<div style="position: relative;">
${
avatar
? `
<img src="${avatar}" alt="${name}" style="
width: 56px;
height: 56px;
border-radius: 50%;
object-fit: cover;
">
`
: `
<div style="
width: 56px;
height: 56px;
border-radius: 50%;
background: #e0e7ff;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
color: #4338ca;
">${initials}</div>
`
}
<div style="
position: absolute;
bottom: 2px;
right: 2px;
width: 14px;
height: 14px;
background: ${statusColor};
border: 2px solid white;
border-radius: 50%;
"></div>
</div>
<div>
<h3 style="margin: 0; font-size: 16px;">${name}</h3>
${
title
? `<p style="margin: 4px 0 0; color: #6b7280; font-size: 14px;">${title}</p>`
: ''
}
</div>
</div>
`;
}
}
customElements.define('profile-card', ProfileCard);
// ============================================
// EXAMPLE 9: Custom Element Registry Helper
// ============================================
const ComponentRegistry = {
components: new Map(),
define(name, constructor, options = {}) {
if (customElements.get(name)) {
console.warn(`Element <${name}> already defined`);
return false;
}
try {
customElements.define(name, constructor, options);
this.components.set(name, {
constructor,
options,
definedAt: new Date(),
});
console.log(`ā Registered <${name}>`);
return true;
} catch (error) {
console.error(`ā Failed to register <${name}>:`, error);
return false;
}
},
get(name) {
return customElements.get(name);
},
isDefined(name) {
return customElements.get(name) !== undefined;
},
async whenDefined(name) {
return customElements.whenDefined(name);
},
list() {
return Array.from(this.components.keys());
},
};
// ============================================
// EXAMPLE 10: Data Binding Element
// ============================================
class DataBind extends HTMLElement {
static get observedAttributes() {
return ['path'];
}
constructor() {
super();
this._store = null;
this._unsubscribe = null;
}
set store(value) {
// Unsubscribe from previous store
if (this._unsubscribe) {
this._unsubscribe();
}
this._store = value;
if (value && typeof value.subscribe === 'function') {
this._unsubscribe = value.subscribe(() => this.update());
}
this.update();
}
get store() {
return this._store;
}
connectedCallback() {
this.update();
}
disconnectedCallback() {
if (this._unsubscribe) {
this._unsubscribe();
}
}
update() {
if (!this._store) return;
const path = this.getAttribute('path');
if (!path) return;
const value = path
.split('.')
.reduce((obj, key) => obj?.[key], this._store.getState());
this.textContent = value !== undefined ? String(value) : '';
}
}
customElements.define('data-bind', DataBind);
// ============================================
// USAGE DEMONSTRATION
// ============================================
function demonstrateCustomElements() {
console.log('=== Custom Elements Demonstration ===\n');
// List all defined elements
console.log('Defined elements:');
const elements = [
'hello-world',
'greeting-card',
'lifecycle-logger',
'counter-element',
'alert-box',
'loading-spinner',
'profile-card',
'data-bind',
];
elements.forEach((name) => {
const defined = customElements.get(name);
console.log(` <${name}>: ${defined ? 'ā defined' : 'ā not defined'}`);
});
// Extended elements
console.log('\nExtended built-in elements:');
console.log(' <button is="ripple-button">');
console.log(' <input is="email-input">');
console.log(' <a is="external-link">');
// Wait for all elements
Promise.all(elements.map((n) => customElements.whenDefined(n))).then(() =>
console.log('\nAll custom elements ready!')
);
}
// Export for module usage
if (typeof module !== 'undefined' && module.exports) {
module.exports = {
HelloWorld,
GreetingCard,
LifecycleLogger,
CounterElement,
RippleButton,
EmailInput,
ExternalLink,
AlertBox,
LoadingSpinner,
ProfileCard,
ComponentRegistry,
DataBind,
demonstrateCustomElements,
};
}