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

FeatureDescription
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
observedAttributesStatic getter for watched attributes
{ extends: 'button' }Extend built-in element

Resources

Module 25 Web Components - JavaScript Tutorial | DeepML