javascript

examples

examples.js⚔
/**
 * 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">&times;</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,
  };
}
Examples - JavaScript Tutorial | DeepML