javascript

examples

examples.js⚔
/**
 * Custom Elements - Examples
 *
 * Comprehensive examples of creating and using Custom Elements
 */

// ============================================
// EXAMPLE 1: Basic Custom Element
// ============================================

class HelloWorld extends HTMLElement {
  constructor() {
    super();
  }

  connectedCallback() {
    this.innerHTML = `
            <div style="
                padding: 20px;
                background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
                color: white;
                border-radius: 8px;
                text-align: center;
            ">
                <h2>Hello, World!</h2>
                <p>This is my first custom element</p>
            </div>
        `;
  }
}

customElements.define('hello-world', HelloWorld);

// Usage: <hello-world></hello-world>

// ============================================
// EXAMPLE 2: Element with Attributes
// ============================================

class GreetingCard extends HTMLElement {
  static get observedAttributes() {
    return ['name', 'greeting', 'theme'];
  }

  constructor() {
    super();
    this._name = 'Friend';
    this._greeting = 'Hello';
    this._theme = 'light';
  }

  // Attribute getters and setters with reflection
  get name() {
    return this.getAttribute('name') || this._name;
  }

  set name(value) {
    this.setAttribute('name', value);
  }

  get greeting() {
    return this.getAttribute('greeting') || this._greeting;
  }

  set greeting(value) {
    this.setAttribute('greeting', value);
  }

  get theme() {
    return this.getAttribute('theme') || this._theme;
  }

  set theme(value) {
    this.setAttribute('theme', value);
  }

  connectedCallback() {
    this.render();
  }

  attributeChangedCallback(name, oldValue, newValue) {
    if (oldValue !== newValue && this.isConnected) {
      this.render();
    }
  }

  render() {
    const isDark = this.theme === 'dark';
    const bgColor = isDark ? '#2d3748' : '#f7fafc';
    const textColor = isDark ? '#e2e8f0' : '#2d3748';

    this.innerHTML = `
            <div style="
                padding: 24px;
                background: ${bgColor};
                color: ${textColor};
                border-radius: 12px;
                box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
            ">
                <h2 style="margin: 0 0 8px 0;">
                    ${this.greeting}, ${this.name}!
                </h2>
                <p style="margin: 0; opacity: 0.7;">
                    Welcome to Custom Elements
                </p>
            </div>
        `;
  }
}

customElements.define('greeting-card', GreetingCard);

// Usage: <greeting-card name="John" greeting="Welcome" theme="dark"></greeting-card>

// ============================================
// EXAMPLE 3: Complete Lifecycle Demo
// ============================================

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

  constructor() {
    super();
    this.log('constructor', 'Element instance created');
    this._count = 0;
    this._intervalId = null;
  }

  connectedCallback() {
    this.log('connectedCallback', 'Added to DOM');
    this.render();

    // Start interval when connected
    this._intervalId = setInterval(() => {
      this._count++;
      this.updateCount();
    }, 1000);
  }

  disconnectedCallback() {
    this.log('disconnectedCallback', 'Removed from DOM');

    // Cleanup interval when disconnected
    if (this._intervalId) {
      clearInterval(this._intervalId);
      this._intervalId = null;
    }
  }

  adoptedCallback() {
    this.log('adoptedCallback', 'Moved to new document');
  }

  attributeChangedCallback(name, oldValue, newValue) {
    this.log(
      'attributeChangedCallback',
      `Attribute '${name}' changed from '${oldValue}' to '${newValue}'`
    );

    if (this.isConnected) {
      this.render();
    }
  }

  log(callback, message) {
    const timestamp = new Date().toISOString().split('T')[1].slice(0, 12);
    console.log(`[${timestamp}] ${callback}: ${message}`);
  }

  updateCount() {
    const countEl = this.querySelector('.count');
    if (countEl) {
      countEl.textContent = this._count;
    }
  }

  render() {
    const message = this.getAttribute('message') || 'Lifecycle Demo';

    this.innerHTML = `
            <div style="
                padding: 16px;
                border: 2px solid #4a5568;
                border-radius: 8px;
                font-family: monospace;
            ">
                <h3 style="margin: 0 0 12px 0;">${message}</h3>
                <p>Seconds connected: <span class="count">${this._count}</span></p>
                <p style="font-size: 12px; color: #718096;">
                    Check console for lifecycle logs
                </p>
            </div>
        `;
  }
}

customElements.define('lifecycle-logger', LifecycleLogger);

// ============================================
// EXAMPLE 4: Interactive Counter Element
// ============================================

class CounterElement extends HTMLElement {
  static get observedAttributes() {
    return ['value', 'min', 'max', 'step'];
  }

  constructor() {
    super();
    this._value = 0;
  }

  get value() {
    return this._value;
  }

  set value(val) {
    const num = parseInt(val, 10);
    const min = parseInt(this.getAttribute('min')) || -Infinity;
    const max = parseInt(this.getAttribute('max')) || Infinity;

    this._value = Math.max(min, Math.min(max, num));
    this.setAttribute('value', this._value);
    this.dispatchEvent(
      new CustomEvent('change', {
        detail: { value: this._value },
        bubbles: true,
      })
    );
  }

  connectedCallback() {
    // Initialize from attribute
    if (this.hasAttribute('value')) {
      this._value = parseInt(this.getAttribute('value'), 10) || 0;
    }

    this.render();
    this.attachEventListeners();
  }

  attributeChangedCallback(name, oldVal, newVal) {
    if (name === 'value' && oldVal !== newVal) {
      this._value = parseInt(newVal, 10) || 0;
      this.updateDisplay();
    }
  }

  increment() {
    const step = parseInt(this.getAttribute('step')) || 1;
    this.value = this._value + step;
  }

  decrement() {
    const step = parseInt(this.getAttribute('step')) || 1;
    this.value = this._value - step;
  }

  reset() {
    this.value = 0;
  }

  attachEventListeners() {
    this.querySelector('.btn-dec').addEventListener('click', () =>
      this.decrement()
    );
    this.querySelector('.btn-inc').addEventListener('click', () =>
      this.increment()
    );
    this.querySelector('.btn-reset')?.addEventListener('click', () =>
      this.reset()
    );
  }

  updateDisplay() {
    const display = this.querySelector('.value-display');
    if (display) {
      display.textContent = this._value;
    }
  }

  render() {
    const min = this.getAttribute('min');
    const max = this.getAttribute('max');

    this.innerHTML = `
            <style>
                .counter-container {
                    display: inline-flex;
                    align-items: center;
                    gap: 8px;
                    padding: 8px;
                    background: #f0f0f0;
                    border-radius: 8px;
                }
                .counter-btn {
                    width: 36px;
                    height: 36px;
                    border: none;
                    border-radius: 50%;
                    font-size: 18px;
                    cursor: pointer;
                    transition: background 0.2s;
                }
                .counter-btn:hover {
                    background: #ddd;
                }
                .btn-dec { background: #fee2e2; }
                .btn-inc { background: #dcfce7; }
                .btn-reset { 
                    background: #e0e7ff;
                    border-radius: 4px;
                    width: auto;
                    padding: 0 12px;
                }
                .value-display {
                    min-width: 60px;
                    text-align: center;
                    font-size: 24px;
                    font-weight: bold;
                }
            </style>
            <div class="counter-container">
                <button class="counter-btn btn-dec" ${
                  this._value <= min ? 'disabled' : ''
                }>āˆ’</button>
                <span class="value-display">${this._value}</span>
                <button class="counter-btn btn-inc" ${
                  this._value >= max ? 'disabled' : ''
                }>+</button>
                <button class="counter-btn btn-reset">Reset</button>
            </div>
        `;
  }
}

customElements.define('counter-element', CounterElement);

// ============================================
// EXAMPLE 5: Extending Built-in Elements
// ============================================

// Extend Button
class RippleButton extends HTMLButtonElement {
  constructor() {
    super();
    this.style.cssText = `
            position: relative;
            overflow: hidden;
            padding: 12px 24px;
            border: none;
            border-radius: 4px;
            background: #3b82f6;
            color: white;
            font-size: 14px;
            cursor: pointer;
            transition: background 0.3s;
        `;
  }

  connectedCallback() {
    this.addEventListener('click', this.createRipple.bind(this));
  }

  createRipple(event) {
    const ripple = document.createElement('span');
    const rect = this.getBoundingClientRect();
    const size = Math.max(rect.width, rect.height);
    const x = event.clientX - rect.left - size / 2;
    const y = event.clientY - rect.top - size / 2;

    ripple.style.cssText = `
            position: absolute;
            width: ${size}px;
            height: ${size}px;
            left: ${x}px;
            top: ${y}px;
            background: rgba(255, 255, 255, 0.5);
            border-radius: 50%;
            transform: scale(0);
            animation: ripple 0.6s ease-out;
            pointer-events: none;
        `;

    // Add keyframes if not exists
    if (!document.querySelector('#ripple-keyframes')) {
      const style = document.createElement('style');
      style.id = 'ripple-keyframes';
      style.textContent = `
                @keyframes ripple {
                    to {
                        transform: scale(2);
                        opacity: 0;
                    }
                }
            `;
      document.head.appendChild(style);
    }

    this.appendChild(ripple);
    ripple.addEventListener('animationend', () => ripple.remove());
  }
}

customElements.define('ripple-button', RippleButton, { extends: 'button' });

// Extend Input with validation
class EmailInput extends HTMLInputElement {
  constructor() {
    super();
    this.type = 'email';
  }

  connectedCallback() {
    this.placeholder = this.placeholder || 'Enter email';
    this.style.cssText = `
            padding: 10px;
            border: 2px solid #ddd;
            border-radius: 4px;
            font-size: 14px;
            transition: border-color 0.2s;
        `;

    this.addEventListener('input', this.validate.bind(this));
    this.addEventListener('blur', this.validate.bind(this));
  }

  validate() {
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    const isValid = emailRegex.test(this.value);

    if (this.value === '') {
      this.style.borderColor = '#ddd';
    } else if (isValid) {
      this.style.borderColor = '#22c55e';
    } else {
      this.style.borderColor = '#ef4444';
    }

    return isValid;
  }
}

customElements.define('email-input', EmailInput, { extends: 'input' });

// Extend Anchor with external link handling
class ExternalLink extends HTMLAnchorElement {
  connectedCallback() {
    // Always open in new tab
    this.target = '_blank';
    this.rel = 'noopener noreferrer';

    // Add external icon
    if (!this.querySelector('.external-icon')) {
      const icon = document.createElement('span');
      icon.className = 'external-icon';
      icon.innerHTML = ' ↗';
      icon.style.fontSize = '0.8em';
      this.appendChild(icon);
    }

    this.style.cssText = `
            color: #3b82f6;
            text-decoration: none;
        `;

    this.addEventListener('mouseenter', () => {
      this.style.textDecoration = 'underline';
    });

    this.addEventListener('mouseleave', () => {
      this.style.textDecoration = 'none';
    });
  }
}

customElements.define('external-link', ExternalLink, { extends: 'a' });

// ============================================
// EXAMPLE 6: Alert/Notification Element
// ============================================

class AlertBox extends HTMLElement {
  static get observedAttributes() {
    return ['type', 'dismissible', 'auto-dismiss'];
  }

  constructor() {
    super();
    this._types = {
      info: { bg: '#dbeafe', border: '#3b82f6', icon: 'ā„¹ļø' },
      success: { bg: '#dcfce7', border: '#22c55e', icon: 'āœ“' },
      warning: { bg: '#fef3c7', border: '#f59e0b', icon: 'āš ļø' },
      error: { bg: '#fee2e2', border: '#ef4444', icon: 'āœ•' },
    };
  }

  get type() {
    return this.getAttribute('type') || 'info';
  }

  set type(value) {
    this.setAttribute('type', value);
  }

  get dismissible() {
    return this.hasAttribute('dismissible');
  }

  connectedCallback() {
    this.render();

    // Auto-dismiss if specified
    const autoDismiss = this.getAttribute('auto-dismiss');
    if (autoDismiss) {
      setTimeout(() => this.dismiss(), parseInt(autoDismiss, 10));
    }
  }

  attributeChangedCallback(name, oldVal, newVal) {
    if (this.isConnected && oldVal !== newVal) {
      this.render();
    }
  }

  dismiss() {
    this.style.animation = 'fadeOut 0.3s ease-out';
    this.addEventListener('animationend', () => {
      this.remove();
      this.dispatchEvent(new CustomEvent('dismissed', { bubbles: true }));
    });
  }

  render() {
    const config = this._types[this.type] || this._types.info;

    // Add fadeOut keyframes
    if (!document.querySelector('#alert-keyframes')) {
      const style = document.createElement('style');
      style.id = 'alert-keyframes';
      style.textContent = `
                @keyframes fadeOut {
                    to { opacity: 0; transform: translateX(20px); }
                }
            `;
      document.head.appendChild(style);
    }

    this.innerHTML = `
            <div style="
                display: flex;
                align-items: flex-start;
                gap: 12px;
                padding: 16px;
                background: ${config.bg};
                border-left: 4px solid ${config.border};
                border-radius: 4px;
            ">
                <span style="font-size: 20px;">${config.icon}</span>
                <div style="flex: 1;">
                    <slot></slot>
                </div>
                ${
                  this.dismissible
                    ? `
                    <button class="dismiss-btn" style="
                        background: none;
                        border: none;
                        font-size: 18px;
                        cursor: pointer;
                        opacity: 0.5;
                    ">Ɨ</button>
                `
                    : ''
                }
            </div>
        `;

    if (this.dismissible) {
      this.querySelector('.dismiss-btn').addEventListener('click', () =>
        this.dismiss()
      );
    }
  }
}

customElements.define('alert-box', AlertBox);

// ============================================
// EXAMPLE 7: Loading Spinner Element
// ============================================

class LoadingSpinner extends HTMLElement {
  static get observedAttributes() {
    return ['size', 'color', 'text'];
  }

  connectedCallback() {
    this.render();
  }

  attributeChangedCallback() {
    if (this.isConnected) {
      this.render();
    }
  }

  render() {
    const size = this.getAttribute('size') || '40';
    const color = this.getAttribute('color') || '#3b82f6';
    const text = this.getAttribute('text') || '';

    this.innerHTML = `
            <style>
                @keyframes spin {
                    to { transform: rotate(360deg); }
                }
            </style>
            <div style="
                display: flex;
                flex-direction: column;
                align-items: center;
                gap: 12px;
            ">
                <div style="
                    width: ${size}px;
                    height: ${size}px;
                    border: 3px solid #e5e7eb;
                    border-top-color: ${color};
                    border-radius: 50%;
                    animation: spin 0.8s linear infinite;
                "></div>
                ${text ? `<span style="color: #6b7280;">${text}</span>` : ''}
            </div>
        `;
  }
}

customElements.define('loading-spinner', LoadingSpinner);

// ============================================
// EXAMPLE 8: User Profile Card
// ============================================

class ProfileCard extends HTMLElement {
  static get observedAttributes() {
    return ['name', 'title', 'avatar', 'status'];
  }

  constructor() {
    super();
    this._statuses = {
      online: '#22c55e',
      offline: '#9ca3af',
      away: '#f59e0b',
      busy: '#ef4444',
    };
  }

  connectedCallback() {
    this.render();
  }

  attributeChangedCallback(name, oldVal, newVal) {
    if (this.isConnected && oldVal !== newVal) {
      this.render();
    }
  }

  render() {
    const name = this.getAttribute('name') || 'Unknown User';
    const title = this.getAttribute('title') || '';
    const avatar = this.getAttribute('avatar') || '';
    const status = this.getAttribute('status') || 'offline';
    const statusColor = this._statuses[status] || this._statuses.offline;

    // Generate initials if no avatar
    const initials = name
      .split(' ')
      .map((n) => n[0])
      .join('')
      .toUpperCase()
      .slice(0, 2);

    this.innerHTML = `
            <div style="
                display: flex;
                align-items: center;
                gap: 16px;
                padding: 16px;
                background: white;
                border-radius: 12px;
                box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
            ">
                <div style="position: relative;">
                    ${
                      avatar
                        ? `
                        <img src="${avatar}" alt="${name}" style="
                            width: 56px;
                            height: 56px;
                            border-radius: 50%;
                            object-fit: cover;
                        ">
                    `
                        : `
                        <div style="
                            width: 56px;
                            height: 56px;
                            border-radius: 50%;
                            background: #e0e7ff;
                            display: flex;
                            align-items: center;
                            justify-content: center;
                            font-weight: bold;
                            color: #4338ca;
                        ">${initials}</div>
                    `
                    }
                    <div style="
                        position: absolute;
                        bottom: 2px;
                        right: 2px;
                        width: 14px;
                        height: 14px;
                        background: ${statusColor};
                        border: 2px solid white;
                        border-radius: 50%;
                    "></div>
                </div>
                <div>
                    <h3 style="margin: 0; font-size: 16px;">${name}</h3>
                    ${
                      title
                        ? `<p style="margin: 4px 0 0; color: #6b7280; font-size: 14px;">${title}</p>`
                        : ''
                    }
                </div>
            </div>
        `;
  }
}

customElements.define('profile-card', ProfileCard);

// ============================================
// EXAMPLE 9: Custom Element Registry Helper
// ============================================

const ComponentRegistry = {
  components: new Map(),

  define(name, constructor, options = {}) {
    if (customElements.get(name)) {
      console.warn(`Element <${name}> already defined`);
      return false;
    }

    try {
      customElements.define(name, constructor, options);
      this.components.set(name, {
        constructor,
        options,
        definedAt: new Date(),
      });
      console.log(`āœ“ Registered <${name}>`);
      return true;
    } catch (error) {
      console.error(`āœ— Failed to register <${name}>:`, error);
      return false;
    }
  },

  get(name) {
    return customElements.get(name);
  },

  isDefined(name) {
    return customElements.get(name) !== undefined;
  },

  async whenDefined(name) {
    return customElements.whenDefined(name);
  },

  list() {
    return Array.from(this.components.keys());
  },
};

// ============================================
// EXAMPLE 10: Data Binding Element
// ============================================

class DataBind extends HTMLElement {
  static get observedAttributes() {
    return ['path'];
  }

  constructor() {
    super();
    this._store = null;
    this._unsubscribe = null;
  }

  set store(value) {
    // Unsubscribe from previous store
    if (this._unsubscribe) {
      this._unsubscribe();
    }

    this._store = value;

    if (value && typeof value.subscribe === 'function') {
      this._unsubscribe = value.subscribe(() => this.update());
    }

    this.update();
  }

  get store() {
    return this._store;
  }

  connectedCallback() {
    this.update();
  }

  disconnectedCallback() {
    if (this._unsubscribe) {
      this._unsubscribe();
    }
  }

  update() {
    if (!this._store) return;

    const path = this.getAttribute('path');
    if (!path) return;

    const value = path
      .split('.')
      .reduce((obj, key) => obj?.[key], this._store.getState());
    this.textContent = value !== undefined ? String(value) : '';
  }
}

customElements.define('data-bind', DataBind);

// ============================================
// USAGE DEMONSTRATION
// ============================================

function demonstrateCustomElements() {
  console.log('=== Custom Elements Demonstration ===\n');

  // List all defined elements
  console.log('Defined elements:');
  const elements = [
    'hello-world',
    'greeting-card',
    'lifecycle-logger',
    'counter-element',
    'alert-box',
    'loading-spinner',
    'profile-card',
    'data-bind',
  ];

  elements.forEach((name) => {
    const defined = customElements.get(name);
    console.log(`  <${name}>: ${defined ? 'āœ“ defined' : 'āœ— not defined'}`);
  });

  // Extended elements
  console.log('\nExtended built-in elements:');
  console.log('  <button is="ripple-button">');
  console.log('  <input is="email-input">');
  console.log('  <a is="external-link">');

  // Wait for all elements
  Promise.all(elements.map((n) => customElements.whenDefined(n))).then(() =>
    console.log('\nAll custom elements ready!')
  );
}

// Export for module usage
if (typeof module !== 'undefined' && module.exports) {
  module.exports = {
    HelloWorld,
    GreetingCard,
    LifecycleLogger,
    CounterElement,
    RippleButton,
    EmailInput,
    ExternalLink,
    AlertBox,
    LoadingSpinner,
    ProfileCard,
    ComponentRegistry,
    DataBind,
    demonstrateCustomElements,
  };
}
Examples - JavaScript Tutorial | DeepML