javascript
examples
examples.jsā”javascript
/**
* Web Components - Examples
*
* Comprehensive examples of building custom elements with Web Components
*/
// ============================================
// EXAMPLE 1: Basic Custom Element
// ============================================
class HelloWorld extends HTMLElement {
constructor() {
super();
this.innerHTML = '<p>Hello, World!</p>';
}
}
customElements.define('hello-world', HelloWorld);
// Usage: <hello-world></hello-world>
// ============================================
// EXAMPLE 2: Custom Element with Shadow DOM
// ============================================
class ShadowCard extends HTMLElement {
constructor() {
super();
// Create shadow root
const shadow = this.attachShadow({ mode: 'open' });
// Create component structure
shadow.innerHTML = `
<style>
:host {
display: block;
font-family: system-ui, sans-serif;
}
.card {
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
padding: 20px;
margin: 10px 0;
}
.card-title {
font-size: 1.25rem;
font-weight: bold;
margin: 0 0 10px 0;
color: #333;
}
.card-content {
color: #666;
line-height: 1.6;
}
:host([variant="outlined"]) .card {
box-shadow: none;
border: 1px solid #ddd;
}
:host([variant="elevated"]) .card {
box-shadow: 0 4px 16px rgba(0,0,0,0.15);
}
</style>
<div class="card">
<h3 class="card-title">
<slot name="title">Card Title</slot>
</h3>
<div class="card-content">
<slot>Card content goes here</slot>
</div>
</div>
`;
}
}
customElements.define('shadow-card', ShadowCard);
/* Usage:
<shadow-card variant="elevated">
<span slot="title">My Card</span>
<p>This is the card content</p>
</shadow-card>
*/
// ============================================
// EXAMPLE 3: Lifecycle Callbacks Demo
// ============================================
class LifecycleDemo extends HTMLElement {
static get observedAttributes() {
return ['name', 'count'];
}
constructor() {
super();
console.log('1. Constructor called');
this._internals = {};
}
connectedCallback() {
console.log('2. Connected to DOM');
this.render();
// Setup that requires DOM
this._observer = new MutationObserver((mutations) => {
console.log('Child nodes changed:', mutations);
});
this._observer.observe(this, { childList: true });
}
disconnectedCallback() {
console.log('3. Disconnected from DOM');
// Cleanup
if (this._observer) {
this._observer.disconnect();
}
}
adoptedCallback() {
console.log('4. Adopted into new document');
}
attributeChangedCallback(name, oldValue, newValue) {
console.log(
`Attribute changed: ${name} from "${oldValue}" to "${newValue}"`
);
if (this.isConnected) {
this.render();
}
}
render() {
const name = this.getAttribute('name') || 'World';
const count = this.getAttribute('count') || '0';
this.innerHTML = `
<div style="padding: 10px; border: 1px solid #ccc;">
<p>Hello, ${name}! Count: ${count}</p>
</div>
`;
}
}
customElements.define('lifecycle-demo', LifecycleDemo);
// ============================================
// EXAMPLE 4: Reactive Counter Component
// ============================================
class ReactiveCounter extends HTMLElement {
static get observedAttributes() {
return ['value', 'min', 'max', 'step'];
}
constructor() {
super();
this.attachShadow({ mode: 'open' });
this._value = 0;
this._min = -Infinity;
this._max = Infinity;
this._step = 1;
}
// Property getters and setters
get value() {
return this._value;
}
set value(val) {
const num = Number(val);
if (!isNaN(num)) {
this._value = Math.min(Math.max(num, this._min), this._max);
this.render();
this.dispatchEvent(
new CustomEvent('change', {
detail: { value: this._value },
bubbles: true,
})
);
}
}
get min() {
return this._min;
}
set min(val) {
this._min = Number(val) || -Infinity;
}
get max() {
return this._max;
}
set max(val) {
this._max = Number(val) || Infinity;
}
get step() {
return this._step;
}
set step(val) {
this._step = Number(val) || 1;
}
connectedCallback() {
// Initialize from attributes
if (this.hasAttribute('value')) {
this._value = Number(this.getAttribute('value')) || 0;
}
if (this.hasAttribute('min')) {
this._min = Number(this.getAttribute('min'));
}
if (this.hasAttribute('max')) {
this._max = Number(this.getAttribute('max'));
}
if (this.hasAttribute('step')) {
this._step = Number(this.getAttribute('step'));
}
this.render();
this.attachListeners();
}
attributeChangedCallback(name, oldVal, newVal) {
if (oldVal === newVal) return;
switch (name) {
case 'value':
this.value = newVal;
break;
case 'min':
this.min = newVal;
break;
case 'max':
this.max = newVal;
break;
case 'step':
this.step = newVal;
break;
}
}
increment() {
this.value = this._value + this._step;
}
decrement() {
this.value = this._value - this._step;
}
attachListeners() {
this.shadowRoot
.querySelector('.decrement')
.addEventListener('click', () => this.decrement());
this.shadowRoot
.querySelector('.increment')
.addEventListener('click', () => this.increment());
}
render() {
const atMin = this._value <= this._min;
const atMax = this._value >= this._max;
this.shadowRoot.innerHTML = `
<style>
:host {
display: inline-flex;
align-items: center;
font-family: system-ui, sans-serif;
}
button {
width: 36px;
height: 36px;
border: 1px solid #ccc;
background: white;
cursor: pointer;
font-size: 18px;
display: flex;
align-items: center;
justify-content: center;
transition: background 0.2s;
}
button:hover:not(:disabled) {
background: #f0f0f0;
}
button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.decrement {
border-radius: 4px 0 0 4px;
}
.increment {
border-radius: 0 4px 4px 0;
}
.value {
min-width: 50px;
height: 36px;
border-top: 1px solid #ccc;
border-bottom: 1px solid #ccc;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
}
</style>
<button class="decrement" ${atMin ? 'disabled' : ''}>ā</button>
<div class="value">${this._value}</div>
<button class="increment" ${atMax ? 'disabled' : ''}>+</button>
`;
// Reattach listeners after render
if (this.isConnected) {
this.attachListeners();
}
}
}
customElements.define('reactive-counter', ReactiveCounter);
// ============================================
// EXAMPLE 5: Template and Slots
// ============================================
// Define template
const modalTemplate = document.createElement('template');
modalTemplate.innerHTML = `
<style>
:host {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: none;
align-items: center;
justify-content: center;
z-index: 1000;
}
:host([open]) {
display: flex;
}
.backdrop {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.5);
}
.modal {
position: relative;
background: white;
border-radius: 8px;
min-width: 300px;
max-width: 90%;
max-height: 90%;
overflow: auto;
box-shadow: 0 10px 40px rgba(0,0,0,0.3);
}
.header {
padding: 16px 20px;
border-bottom: 1px solid #eee;
display: flex;
justify-content: space-between;
align-items: center;
}
.header h2 {
margin: 0;
font-size: 1.25rem;
}
.close-btn {
background: none;
border: none;
font-size: 24px;
cursor: pointer;
color: #666;
}
.content {
padding: 20px;
}
.footer {
padding: 16px 20px;
border-top: 1px solid #eee;
display: flex;
justify-content: flex-end;
gap: 10px;
}
</style>
<div class="backdrop"></div>
<div class="modal">
<div class="header">
<h2><slot name="title">Modal Title</slot></h2>
<button class="close-btn">×</button>
</div>
<div class="content">
<slot>Modal content</slot>
</div>
<div class="footer">
<slot name="footer">
<button class="cancel-btn">Cancel</button>
<button class="confirm-btn">Confirm</button>
</slot>
</div>
</div>
`;
class ModalDialog extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' });
shadow.appendChild(modalTemplate.content.cloneNode(true));
// Bind methods
this._handleKeydown = this._handleKeydown.bind(this);
}
static get observedAttributes() {
return ['open'];
}
get open() {
return this.hasAttribute('open');
}
set open(val) {
if (val) {
this.setAttribute('open', '');
} else {
this.removeAttribute('open');
}
}
connectedCallback() {
this.shadowRoot
.querySelector('.backdrop')
.addEventListener('click', () => this.close());
this.shadowRoot
.querySelector('.close-btn')
.addEventListener('click', () => this.close());
this.shadowRoot
.querySelector('.cancel-btn')
?.addEventListener('click', () => this.close());
this.shadowRoot
.querySelector('.confirm-btn')
?.addEventListener('click', () => this.confirm());
}
attributeChangedCallback(name, oldVal, newVal) {
if (name === 'open') {
if (newVal !== null) {
document.addEventListener('keydown', this._handleKeydown);
this.dispatchEvent(new CustomEvent('open'));
} else {
document.removeEventListener('keydown', this._handleKeydown);
this.dispatchEvent(new CustomEvent('close'));
}
}
}
_handleKeydown(e) {
if (e.key === 'Escape') {
this.close();
}
}
show() {
this.open = true;
}
close() {
this.open = false;
}
confirm() {
this.dispatchEvent(new CustomEvent('confirm', { bubbles: true }));
this.close();
}
}
customElements.define('modal-dialog', ModalDialog);
// ============================================
// EXAMPLE 6: Extending Built-in Elements
// ============================================
class CollapsibleDetails extends HTMLDetailsElement {
constructor() {
super();
// Add custom styles
this.style.cssText = `
border: 1px solid #ddd;
border-radius: 4px;
padding: 0;
margin: 8px 0;
`;
}
connectedCallback() {
// Style the summary
const summary = this.querySelector('summary');
if (summary) {
summary.style.cssText = `
padding: 12px 16px;
cursor: pointer;
background: #f5f5f5;
font-weight: bold;
outline: none;
`;
}
// Add animation
this.addEventListener('toggle', () => {
const content = [...this.children].filter((c) => c.tagName !== 'SUMMARY');
content.forEach((el) => {
if (this.open) {
el.style.cssText = `
padding: 16px;
animation: slideDown 0.3s ease-out;
`;
}
});
});
}
}
customElements.define('collapsible-details', CollapsibleDetails, {
extends: 'details',
});
// ============================================
// EXAMPLE 7: Form-Associated Custom Element
// ============================================
class StarRating extends HTMLElement {
static formAssociated = true;
static get observedAttributes() {
return ['value', 'max', 'readonly'];
}
constructor() {
super();
this._internals = this.attachInternals();
this._value = 0;
this._max = 5;
this.attachShadow({ mode: 'open' });
}
get value() {
return this._value;
}
set value(val) {
this._value = Math.min(Math.max(0, Number(val) || 0), this._max);
this._internals.setFormValue(this._value.toString());
this.render();
}
get max() {
return this._max;
}
set max(val) {
this._max = Number(val) || 5;
this.render();
}
connectedCallback() {
if (this.hasAttribute('value')) {
this._value = Number(this.getAttribute('value')) || 0;
}
if (this.hasAttribute('max')) {
this._max = Number(this.getAttribute('max')) || 5;
}
this._internals.setFormValue(this._value.toString());
this.render();
}
attributeChangedCallback(name, oldVal, newVal) {
if (oldVal === newVal) return;
if (name === 'value') {
this.value = newVal;
} else if (name === 'max') {
this.max = newVal;
} else if (name === 'readonly') {
this.render();
}
}
// Form callbacks
formResetCallback() {
this.value = 0;
}
formStateRestoreCallback(state) {
this.value = state;
}
_handleClick(rating) {
if (this.hasAttribute('readonly')) return;
this.value = rating;
this.dispatchEvent(
new CustomEvent('change', {
detail: { value: this.value },
bubbles: true,
})
);
}
render() {
const isReadonly = this.hasAttribute('readonly');
this.shadowRoot.innerHTML = `
<style>
:host {
display: inline-flex;
gap: 4px;
}
.star {
font-size: 24px;
cursor: ${isReadonly ? 'default' : 'pointer'};
transition: transform 0.1s;
user-select: none;
}
.star:not([data-readonly]):hover {
transform: scale(1.2);
}
.star.filled {
color: #ffc107;
}
.star.empty {
color: #ddd;
}
</style>
${Array.from(
{ length: this._max },
(_, i) => `
<span
class="star ${i < this._value ? 'filled' : 'empty'}"
data-rating="${i + 1}"
${isReadonly ? 'data-readonly' : ''}
>ā
</span>
`
).join('')}
`;
// Attach click listeners
if (!isReadonly) {
this.shadowRoot.querySelectorAll('.star').forEach((star) => {
star.addEventListener('click', () => {
this._handleClick(Number(star.dataset.rating));
});
});
}
}
}
customElements.define('star-rating', StarRating);
// ============================================
// EXAMPLE 8: Component with Custom Events
// ============================================
class TabPanel extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this._activeTab = 0;
}
connectedCallback() {
this.render();
}
get activeTab() {
return this._activeTab;
}
set activeTab(index) {
const tabs = this.querySelectorAll('[slot^="tab-"]');
if (index >= 0 && index < tabs.length) {
this._activeTab = index;
this.render();
this.dispatchEvent(
new CustomEvent('tab-change', {
detail: { index, tab: tabs[index] },
bubbles: true,
})
);
}
}
render() {
const tabs = this.querySelectorAll('[slot^="tab-"]');
const panels = this.querySelectorAll('[slot^="panel-"]');
this.shadowRoot.innerHTML = `
<style>
:host {
display: block;
font-family: system-ui, sans-serif;
}
.tabs {
display: flex;
border-bottom: 2px solid #eee;
}
.tab {
padding: 12px 24px;
cursor: pointer;
border: none;
background: none;
font-size: 14px;
color: #666;
transition: color 0.2s, border-color 0.2s;
border-bottom: 2px solid transparent;
margin-bottom: -2px;
}
.tab:hover {
color: #333;
}
.tab.active {
color: #0066cc;
border-bottom-color: #0066cc;
}
.panels {
padding: 20px;
}
.panel {
display: none;
}
.panel.active {
display: block;
}
</style>
<div class="tabs">
${Array.from(tabs)
.map(
(tab, i) => `
<button class="tab ${
i === this._activeTab ? 'active' : ''
}"
data-index="${i}">
${tab.textContent}
</button>
`
)
.join('')}
</div>
<div class="panels">
${Array.from(panels)
.map(
(panel, i) => `
<div class="panel ${i === this._activeTab ? 'active' : ''}">
<slot name="panel-${i + 1}"></slot>
</div>
`
)
.join('')}
</div>
`;
// Attach tab click listeners
this.shadowRoot.querySelectorAll('.tab').forEach((tab) => {
tab.addEventListener('click', () => {
this.activeTab = Number(tab.dataset.index);
});
});
}
}
customElements.define('tab-panel', TabPanel);
// ============================================
// EXAMPLE 9: Lazy Loading Component
// ============================================
class LazyImage extends HTMLElement {
static get observedAttributes() {
return ['src', 'alt', 'placeholder'];
}
constructor() {
super();
this.attachShadow({ mode: 'open' });
this._loaded = false;
}
connectedCallback() {
this.render();
this.setupIntersectionObserver();
}
disconnectedCallback() {
if (this._observer) {
this._observer.disconnect();
}
}
setupIntersectionObserver() {
this._observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting && !this._loaded) {
this.loadImage();
}
});
},
{
rootMargin: '100px',
}
);
this._observer.observe(this);
}
loadImage() {
const src = this.getAttribute('src');
if (!src) return;
const img = new Image();
img.onload = () => {
this._loaded = true;
this.render();
this.dispatchEvent(new CustomEvent('loaded'));
};
img.onerror = () => {
this.dispatchEvent(new CustomEvent('error'));
};
img.src = src;
}
render() {
const src = this.getAttribute('src');
const alt = this.getAttribute('alt') || '';
const placeholder =
this.getAttribute('placeholder') ||
'data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"%3E%3Crect fill="%23ddd" width="100" height="100"/%3E%3C/svg%3E';
this.shadowRoot.innerHTML = `
<style>
:host {
display: block;
overflow: hidden;
}
img {
width: 100%;
height: 100%;
object-fit: cover;
transition: opacity 0.3s;
}
.loading {
opacity: 0.5;
filter: blur(5px);
}
</style>
<img
src="${this._loaded ? src : placeholder}"
alt="${alt}"
class="${this._loaded ? '' : 'loading'}"
/>
`;
}
}
customElements.define('lazy-image', LazyImage);
// ============================================
// EXAMPLE 10: Component Library Pattern
// ============================================
const ComponentLibrary = {
// Registry of all components
components: new Map(),
// Register a component
define(name, constructor, options = {}) {
if (this.components.has(name)) {
console.warn(`Component ${name} already registered`);
return;
}
this.components.set(name, { constructor, options });
customElements.define(
name,
constructor,
options.extends ? { extends: options.extends } : undefined
);
console.log(`Component ${name} registered`);
},
// Get component constructor
get(name) {
return this.components.get(name)?.constructor;
},
// Check if component is defined
isDefined(name) {
return customElements.get(name) !== undefined;
},
// Wait for component to be defined
async whenDefined(name) {
return customElements.whenDefined(name);
},
// Create base component class
createBase(options = {}) {
return class BaseComponent extends HTMLElement {
constructor() {
super();
if (options.shadow !== false) {
this.attachShadow({ mode: options.shadowMode || 'open' });
}
this._state = {};
this._mounted = false;
}
// State management
get state() {
return this._state;
}
setState(newState) {
const oldState = { ...this._state };
this._state = { ...this._state, ...newState };
if (this._mounted) {
this.stateChanged(oldState, this._state);
this.render();
}
}
// Lifecycle hooks
connectedCallback() {
this._mounted = true;
this.mount();
this.render();
}
disconnectedCallback() {
this._mounted = false;
this.unmount();
}
// Override these in subclasses
mount() {}
unmount() {}
stateChanged(oldState, newState) {}
render() {}
// Helper to dispatch events
emit(name, detail = {}) {
this.dispatchEvent(
new CustomEvent(name, {
detail,
bubbles: true,
composed: true,
})
);
}
};
},
};
// Example using the library pattern
const BaseComponent = ComponentLibrary.createBase({ shadow: true });
class MyButton extends BaseComponent {
static get observedAttributes() {
return ['variant', 'disabled'];
}
mount() {
this.setState({
variant: this.getAttribute('variant') || 'primary',
disabled: this.hasAttribute('disabled'),
});
this.addEventListener('click', this._handleClick.bind(this));
}
attributeChangedCallback(name, oldVal, newVal) {
if (name === 'variant') {
this.setState({ variant: newVal || 'primary' });
} else if (name === 'disabled') {
this.setState({ disabled: newVal !== null });
}
}
_handleClick(e) {
if (this.state.disabled) {
e.preventDefault();
e.stopPropagation();
return;
}
this.emit('button-click');
}
render() {
const { variant, disabled } = this.state;
this.shadowRoot.innerHTML = `
<style>
:host {
display: inline-block;
}
button {
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: all 0.2s;
}
button.primary {
background: #0066cc;
color: white;
}
button.secondary {
background: #f0f0f0;
color: #333;
}
button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
</style>
<button class="${variant}" ${disabled ? 'disabled' : ''}>
<slot></slot>
</button>
`;
}
}
ComponentLibrary.define('my-button', MyButton);
// ============================================
// USAGE DEMONSTRATION
// ============================================
function demonstrateWebComponents() {
console.log('=== Web Components Demonstration ===\n');
// Check support
const supportsWebComponents =
'customElements' in window && 'attachShadow' in Element.prototype;
console.log('Web Components supported:', supportsWebComponents);
// List all registered components
console.log('\nRegistered components:');
console.log('- hello-world');
console.log('- shadow-card');
console.log('- lifecycle-demo');
console.log('- reactive-counter');
console.log('- modal-dialog');
console.log('- star-rating');
console.log('- tab-panel');
console.log('- lazy-image');
console.log('- my-button');
// Wait for all components to be defined
Promise.all([
customElements.whenDefined('hello-world'),
customElements.whenDefined('reactive-counter'),
customElements.whenDefined('star-rating'),
]).then(() => {
console.log('\nAll components ready!');
});
}
// Export for module usage
if (typeof module !== 'undefined' && module.exports) {
module.exports = {
HelloWorld,
ShadowCard,
LifecycleDemo,
ReactiveCounter,
ModalDialog,
StarRating,
TabPanel,
LazyImage,
ComponentLibrary,
demonstrateWebComponents,
};
}