READMEJavaScript

README

Module 25 Web Components / .4 Component Patterns

Concept Lesson
Advanced
4 min

Learning Objective

Understand Module 25 Web Components well enough to explain it, recognize it in JavaScript, and apply it in a small task.

Why It Matters

This concept is part of the foundation that later lessons and projects assume you already understand.

WebComponentsLearning ObjectivesComponent Communication PatternsParent To Child Props/Attributes
Private notes
0/8000

Notes stay private to your browser until account sync is configured.

README
1 min read18 headings

25.4 Component Patterns

Overview

This section covers advanced patterns and best practices for building Web Components. Learn how to create maintainable, reusable, and performant component libraries.

Learning Objectives

  • Implement common component design patterns
  • Build component communication systems
  • Create state management for components
  • Handle component lifecycle effectively
  • Apply accessibility best practices
  • Test and debug Web Components

Component Communication Patterns

Parent to Child (Props/Attributes)

class ParentComponent extends HTMLElement {
  connectedCallback() {
    const child = this.querySelector('child-component');

    // Via attribute
    child.setAttribute('message', 'Hello');

    // Via property
    child.data = { name: 'John', age: 30 };
  }
}

class ChildComponent extends HTMLElement {
  static get observedAttributes() {
    return ['message'];
  }

  set data(value) {
    this._data = value;
    this.render();
  }
}

Child to Parent (Events)

class ChildComponent extends HTMLElement {
  handleClick() {
    this.dispatchEvent(
      new CustomEvent('item-selected', {
        detail: { id: this.itemId },
        bubbles: true,
        composed: true,
      })
    );
  }
}

class ParentComponent extends HTMLElement {
  connectedCallback() {
    this.addEventListener('item-selected', (e) => {
      console.log('Selected:', e.detail.id);
    });
  }
}

Sibling Communication (Event Bus)

class EventBus {
  static _events = new Map();

  static emit(event, data) {
    const handlers = this._events.get(event) || [];
    handlers.forEach((handler) => handler(data));
  }

  static on(event, handler) {
    if (!this._events.has(event)) {
      this._events.set(event, []);
    }
    this._events.get(event).push(handler);

    // Return unsubscribe function
    return () => {
      const handlers = this._events.get(event);
      const index = handlers.indexOf(handler);
      if (index > -1) handlers.splice(index, 1);
    };
  }
}

State Management Patterns

Local State

class StatefulComponent extends HTMLElement {
  constructor() {
    super();
    this._state = {};
  }

  get state() {
    return this._state;
  }

  setState(newState) {
    const oldState = { ...this._state };
    this._state = { ...this._state, ...newState };
    this.stateChanged(oldState, this._state);
    this.render();
  }

  stateChanged(oldState, newState) {
    // Override for side effects
  }
}

Reactive State with Proxy

function createReactiveState(initialState, onChange) {
  return new Proxy(initialState, {
    set(target, property, value) {
      const oldValue = target[property];
      target[property] = value;
      if (oldValue !== value) {
        onChange(property, value, oldValue);
      }
      return true;
    },
  });
}

class ReactiveComponent extends HTMLElement {
  constructor() {
    super();
    this.state = createReactiveState({}, (prop, value) => {
      this.render();
    });
  }
}

Global State Store

class Store {
  static _state = {};
  static _subscribers = [];

  static getState() {
    return this._state;
  }

  static setState(newState) {
    this._state = { ...this._state, ...newState };
    this._notify();
  }

  static subscribe(callback) {
    this._subscribers.push(callback);
    return () => {
      const index = this._subscribers.indexOf(callback);
      if (index > -1) this._subscribers.splice(index, 1);
    };
  }

  static _notify() {
    this._subscribers.forEach((cb) => cb(this._state));
  }
}

Lifecycle Patterns

Lazy Initialization

class LazyComponent extends HTMLElement {
  constructor() {
    super();
    this._initialized = false;
  }

  connectedCallback() {
    if (!this._initialized) {
      this._initialize();
      this._initialized = true;
    }
    this._activate();
  }

  disconnectedCallback() {
    this._deactivate();
  }

  _initialize() {
    // One-time setup (attach shadow, create templates)
  }

  _activate() {
    // Activate (add listeners, start timers)
  }

  _deactivate() {
    // Deactivate (remove listeners, stop timers)
  }
}

Cleanup Pattern

class CleanupComponent extends HTMLElement {
  constructor() {
    super();
    this._cleanupFunctions = [];
  }

  addCleanup(fn) {
    this._cleanupFunctions.push(fn);
  }

  connectedCallback() {
    // Add event listener with cleanup
    const handler = () => console.log('clicked');
    document.addEventListener('click', handler);
    this.addCleanup(() => document.removeEventListener('click', handler));

    // Add interval with cleanup
    const interval = setInterval(() => {}, 1000);
    this.addCleanup(() => clearInterval(interval));
  }

  disconnectedCallback() {
    this._cleanupFunctions.forEach((fn) => fn());
    this._cleanupFunctions = [];
  }
}

Composition Patterns

Mixin Pattern

const LoggerMixin = (Base) =>
  class extends Base {
    log(message) {
      console.log(`[${this.tagName}] ${message}`);
    }
  };

const EventMixin = (Base) =>
  class extends Base {
    emit(eventName, detail = {}) {
      this.dispatchEvent(
        new CustomEvent(eventName, {
          detail,
          bubbles: true,
          composed: true,
        })
      );
    }
  };

class MyComponent extends EventMixin(LoggerMixin(HTMLElement)) {
  connectedCallback() {
    this.log('Connected');
    this.emit('connected');
  }
}

Render Props / Function as Child

class DataProvider extends HTMLElement {
  async connectedCallback() {
    const data = await this.fetchData();

    // Call render function from attribute
    const renderFn = this.getAttribute('render');
    if (renderFn && window[renderFn]) {
      this.innerHTML = window[renderFn](data);
    }
  }

  async fetchData() {
    const url = this.getAttribute('url');
    const response = await fetch(url);
    return response.json();
  }
}

Form Integration Patterns

Form-Associated Component

class FormInput extends HTMLElement {
  static formAssociated = true;

  constructor() {
    super();
    this._internals = this.attachInternals();
    this.attachShadow({ mode: 'open' });
  }

  get value() {
    return this._value;
  }

  set value(v) {
    this._value = v;
    this._internals.setFormValue(v);
  }

  get validity() {
    return this._internals.validity;
  }

  formResetCallback() {
    this.value = '';
  }

  formStateRestoreCallback(state) {
    this.value = state;
  }
}

Accessibility Patterns

ARIA Support

class AccessibleButton extends HTMLElement {
  static get observedAttributes() {
    return ['disabled', 'pressed'];
  }

  connectedCallback() {
    this.setAttribute('role', 'button');
    this.setAttribute('tabindex', '0');

    this.addEventListener('click', this.handleClick);
    this.addEventListener('keydown', this.handleKeydown);
  }

  attributeChangedCallback(name, oldVal, newVal) {
    if (name === 'disabled') {
      this.setAttribute('aria-disabled', newVal !== null);
      this.setAttribute('tabindex', newVal !== null ? '-1' : '0');
    }
    if (name === 'pressed') {
      this.setAttribute('aria-pressed', newVal !== null);
    }
  }

  handleKeydown = (e) => {
    if (e.key === 'Enter' || e.key === ' ') {
      e.preventDefault();
      this.click();
    }
  };
}

Performance Patterns

Debounced Rendering

class DebouncedComponent extends HTMLElement {
  constructor() {
    super();
    this._renderTimeout = null;
  }

  scheduleRender() {
    if (this._renderTimeout) {
      cancelAnimationFrame(this._renderTimeout);
    }
    this._renderTimeout = requestAnimationFrame(() => {
      this.render();
    });
  }
}

Virtual Scrolling

class VirtualList extends HTMLElement {
  // Render only visible items
  // Use IntersectionObserver for visibility
  // Recycle DOM nodes
}

Testing Patterns

// Component testing utilities
async function waitForComponent(tagName) {
  await customElements.whenDefined(tagName);
}

function createComponent(tagName, attributes = {}) {
  const element = document.createElement(tagName);
  Object.entries(attributes).forEach(([key, value]) => {
    element.setAttribute(key, value);
  });
  document.body.appendChild(element);
  return element;
}

function cleanupComponent(element) {
  if (element.parentNode) {
    element.parentNode.removeChild(element);
  }
}

Summary

PatternUse Case
Event BusCross-component communication
Reactive StateAutomatic UI updates
MixinsShared functionality
Form AssociatedNative form integration
CleanupResource management
ARIA SupportAccessibility
Debounced RenderPerformance

Resources

Skill Check

Test this lesson

Answer 4 quick questions to lock in the lesson and feed your adaptive practice queue.

--
Score
0/4
Answered
Not attempted
Status
1

Which module does this lesson belong to?

2

Which section is covered in this lesson content?

3

Which term is most central to this lesson?

4

What is the best way to use this lesson for real learning?

Your answers save locally first, then sync when account storage is available.
Practice queue