Docs
Module-25-Web-Components
25.1 Custom Elements
Overview
Custom Elements are the foundation of Web Components, allowing you to define new HTML elements with custom behavior. They're native to the browser and work without any framework.
Learning Objectives
- •Understand the Custom Elements API
- •Create autonomous custom elements
- •Extend built-in HTML elements
- •Master lifecycle callbacks
- •Work with attributes and properties
- •Handle custom element registration
What Are Custom Elements?
Custom Elements let you define your own HTML tags:
<!-- Standard HTML element -->
<div class="card">...</div>
<!-- Custom Element -->
<user-card name="John" role="admin"></user-card>
Defining Custom Elements
Basic Custom Element
class UserCard extends HTMLElement {
constructor() {
super(); // Always call super() first!
// Initial setup
this.innerHTML = `
<div class="user-card">
<h2>User Card</h2>
</div>
`;
}
}
// Register the element
customElements.define('user-card', UserCard);
Naming Rules
Custom element names must:
- •Contain a hyphen (
-) - •Start with a lowercase letter
- •Not use reserved names
// Valid names
customElements.define('my-element', MyElement);
customElements.define('user-profile', UserProfile);
customElements.define('x-button', XButton);
// Invalid names
// customElements.define('myElement', ...); // No hyphen
// customElements.define('1-element', ...); // Starts with number
// customElements.define('font-face', ...); // Reserved
Lifecycle Callbacks
class LifecycleElement extends HTMLElement {
constructor() {
super();
console.log('1. Constructor - Element created');
// Don't access attributes or children here
}
connectedCallback() {
console.log('2. Connected - Added to DOM');
// Safe to access attributes and render
this.render();
}
disconnectedCallback() {
console.log('3. Disconnected - Removed from DOM');
// Cleanup: remove listeners, cancel timers
}
adoptedCallback() {
console.log('4. Adopted - Moved to new document');
}
attributeChangedCallback(name, oldValue, newValue) {
console.log(`5. Attribute "${name}" changed`);
console.log(` Old: ${oldValue}, New: ${newValue}`);
}
static get observedAttributes() {
return ['name', 'role', 'active'];
}
}
Attributes and Properties
Observing Attributes
class ConfigElement extends HTMLElement {
static get observedAttributes() {
return ['theme', 'size', 'disabled'];
}
attributeChangedCallback(name, oldVal, newVal) {
switch (name) {
case 'theme':
this.updateTheme(newVal);
break;
case 'size':
this.updateSize(newVal);
break;
case 'disabled':
this.updateDisabled(newVal !== null);
break;
}
}
}
Attribute-Property Reflection
class ToggleButton extends HTMLElement {
static get observedAttributes() {
return ['pressed'];
}
// Property getter
get pressed() {
return this.hasAttribute('pressed');
}
// Property setter - reflects to attribute
set pressed(value) {
if (value) {
this.setAttribute('pressed', '');
} else {
this.removeAttribute('pressed');
}
}
attributeChangedCallback(name, oldVal, newVal) {
if (name === 'pressed') {
this.render();
}
}
connectedCallback() {
this.render();
this.addEventListener('click', () => {
this.pressed = !this.pressed;
});
}
render() {
this.textContent = this.pressed ? '✓ ON' : '○ OFF';
this.style.background = this.pressed ? 'green' : 'gray';
}
}
Extending Built-in Elements
Customized Built-in Elements
// Extend an existing HTML element
class FancyButton extends HTMLButtonElement {
constructor() {
super();
this.addEventListener('click', this.handleClick.bind(this));
}
connectedCallback() {
this.classList.add('fancy-button');
this.style.cssText = `
background: linear-gradient(45deg, #ff6b6b, #feca57);
border: none;
padding: 10px 20px;
color: white;
border-radius: 25px;
cursor: pointer;
font-weight: bold;
`;
}
handleClick() {
this.animate(
[
{ transform: 'scale(1)' },
{ transform: 'scale(0.95)' },
{ transform: 'scale(1)' },
],
{
duration: 200,
easing: 'ease-out',
}
);
}
}
// Register with extends option
customElements.define('fancy-button', FancyButton, { extends: 'button' });
<!-- Usage requires is="" attribute -->
<button is="fancy-button">Click Me</button>
Common Extended Elements
// Extended anchor with confirmation
class ConfirmLink extends HTMLAnchorElement {
connectedCallback() {
this.addEventListener('click', (e) => {
if (!confirm('Are you sure you want to leave?')) {
e.preventDefault();
}
});
}
}
customElements.define('confirm-link', ConfirmLink, { extends: 'a' });
// Extended input with formatting
class PhoneInput extends HTMLInputElement {
connectedCallback() {
this.addEventListener('input', () => {
this.value = this.formatPhone(this.value);
});
}
formatPhone(value) {
const digits = value.replace(/\D/g, '');
if (digits.length <= 3) return digits;
if (digits.length <= 6) return `(${digits.slice(0, 3)}) ${digits.slice(3)}`;
return `(${digits.slice(0, 3)}) ${digits.slice(3, 6)}-${digits.slice(
6,
10
)}`;
}
}
customElements.define('phone-input', PhoneInput, { extends: 'input' });
Registration and Upgrade
Checking Definition Status
// Check if element is defined
const isDefined = customElements.get('my-element') !== undefined;
// Wait for element to be defined
customElements.whenDefined('my-element').then(() => {
console.log('my-element is now defined');
});
// Get constructor for element
const MyElementClass = customElements.get('my-element');
Upgrade Process
// Elements in HTML before definition are "undefined"
// They get "upgraded" when defined
// Force upgrade of specific element
const element = document.querySelector('my-element');
customElements.upgrade(element);
Best Practices
Constructor Rules
class GoodElement extends HTMLElement {
constructor() {
super();
// ✅ DO: Set up internal state
this._data = {};
// ✅ DO: Attach shadow root
this.attachShadow({ mode: 'open' });
// ❌ DON'T: Access attributes (not ready yet)
// this.getAttribute('name');
// ❌ DON'T: Add children
// this.innerHTML = '...';
// ❌ DON'T: Access parent/siblings
// this.parentElement;
}
connectedCallback() {
// ✅ Safe to do everything here
const name = this.getAttribute('name');
this.shadowRoot.innerHTML = `<h1>Hello, ${name}</h1>`;
}
}
Error Handling
class SafeElement extends HTMLElement {
connectedCallback() {
try {
this.render();
} catch (error) {
console.error('Render error:', error);
this.innerHTML = '<p class="error">Failed to load component</p>';
}
}
render() {
// Rendering logic
}
}
Browser Support Check
if ('customElements' in window) {
// Custom Elements supported
customElements.define('my-element', MyElement);
} else {
// Load polyfill
console.log('Custom Elements not supported');
}
Summary
| Feature | Description |
|---|---|
customElements.define() | Register a custom element |
constructor() | Initialize, attach shadow root |
connectedCallback() | Element added to DOM |
disconnectedCallback() | Element removed from DOM |
attributeChangedCallback() | Observed attribute changed |
observedAttributes | Static getter for watched attributes |
{ extends: 'button' } | Extend built-in element |