â˜€ī¸Siang
Web Development

Web Components: Komponen Web Native

Tutorial lengkap Web Components — Shadow DOM, Custom Elements, HTML Templates, Slots, lifecycle callbacks, dan membuat UI reusable tanpa framework

Artikel: Javascript Web Components Artikel: Javascript Web Components


1. Pengenalan Web Components

Web Components adalah sekumpulan API web standar yang memungkinkan Anda membuat elemen HTML kustom yang dapat digunakan kembali (reusable), dengan fungsionalitas terenkapsulasi. Berbeda dengan framework seperti React atau Vue, Web Components menggunakan standar web bawaan yang bekerja di semua browser modern tanpa perlu library tambahan.

Tiga teknologi utama yang membentuk Web Components adalah: Custom Elements (mendefinisikan elemen baru), Shadow DOM (enkapsulasi markup dan style), dan HTML Templates (template yang tidak dirender sampai diaktifkan).

Mengapa Menggunakan Web Components?

Keunggulan Penjelasan
Framework-AgnosticBekerja di React, Vue, Angular, Svelte, atau vanilla JS — tanpa lock-in
Standar WebDidukung langsung oleh browser, tidak perlu bundler atau compiler
EnkapsulasiShadow DOM mencegah konflik CSS dan JS antar komponen
ReusabilitasSekali dibuat, bisa dipakai di mana saja — bahkan di halaman HTML statis
LongevityKarena ini standar, kode Anda tidak akan obsolete dengan pergantian framework
InteroperabilitasTim berbeda bisa pakai framework berbeda tapi berbagi komponen yang sama

Web Components vs Framework Components

Aspek Web Components React Components Vue Components
StandarW3C Web StandardsLibrary APIFramework API
EnkapsulasiShadow DOM (native)CSS Modules / CSS-in-JSScoped styles
Bundle Size0 KB (native)~42 KB (React)~33 KB (Vue)
ReactivityManual / Attribute APIVirtual DOMReactive Proxy
EcosystemKecil tapi tumbuhSangat besarBesar
Diagram: Shadow DOM Tree Structure

Projected ke

🔒 Tidak bocor ke luar

📄 Document DOM Tree

🔐 #shadow-root\n(Shadow DOM)

\n(slot default)

\nStruktur internal

Light DOM Children

\nKonten diproyeksikan



4. HTML Templates & Slots

<template> dan <slot> adalah elemen HTML khusus yang sangat berguna untuk Web Components. <template> mendefinisikan fragmen HTML yang tidak dirender sampai diaktifkan oleh JavaScript. <slot> menyediakan placeholder di dalam komponen untuk konten yang di-supply dari luar.

Menggunakan HTML Template

HTML & JavaScript — Templates
<!-- Definisikan template di HTML -->
<template id="alert-template">
  <style>
    .alert {
      padding: 16px 20px;
      border-radius: 8px;
      display: flex;
      align-items: center;
      gap: 12px;
      font-family: system-ui, sans-serif;
    }
    .alert-success { background: #1a3a2a; color: #a6e3a1; border: 1px solid #2a5a3a; }
    .alert-warning { background: #3a3a1a; color: #f9e2af; border: 1px solid #5a5a2a; }
    .alert-error   { background: #3a1a1a; color: #f38ba8; border: 1px solid #5a2a2a; }
    .alert-info    { background: #1a2a3a; color: #89b4fa; border: 1px solid #2a3a5a; }
    .icon { font-size: 20px; }
    .close-btn {
      margin-left: auto;
      background: none;
      border: none;
      color: inherit;
      cursor: pointer;
      opacity: 0.6;
      font-size: 18px;
    }
    .close-btn:hover { opacity: 1; }
  </style>
  <div class="alert">
    <span class="icon"></span>
    <span class="message"></span>
    <button class="close-btn">✕</button>
  </div>
</template>

<script>
// Ambil template dari DOM
const template = document.getElementById('alert-template');

class AlertBox extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });

    // Clone template content ke shadow DOM
    const content = template.content.cloneNode(true);
    this.shadowRoot.appendChild(content);
  }

  connectedCallback() {
    const type = this.getAttribute('type') || 'info';
    const pesan = this.getAttribute('message') || 'Ini pesan alert';
    const dismissible = this.hasAttribute('dismissible');

    const icons = {
      success: '✅',
      warning: 'âš ī¸',
      error: '❌',
      info: 'â„šī¸'
    };

    const alertDiv = this.shadowRoot.querySelector('.alert');
    alertDiv.classList.add(`alert-${type}`);
    this.shadowRoot.querySelector('.icon').textContent = icons[type] || icons.info;
    this.shadowRoot.querySelector('.message').textContent = pesan;

    const closeBtn = this.shadowRoot.querySelector('.close-btn');
    if (!dismissible) {
      closeBtn.style.display = 'none';
    } else {
      closeBtn.addEventListener('click', () => {
        this.dispatchEvent(new CustomEvent('alert-dismissed', {
          bubbles: true,
          composed: true,
        }));
        this.remove();
      });
    }
  }
}

customElements.define('alert-box', AlertBox);
</script>

<!-- Penggunaan -->
<alert-box type="success" message="Data berhasil disimpan!" dismissible></alert-box>
<alert-box type="error" message="Gagal menghubungi server" dismissible></alert-box>
<alert-box type="warning" message="Sesi akan berakhir dalam 5 menit"></alert-box>
<alert-box type="info" message="Versi baru tersedia, silakan refresh"></alert-box>

Named Slots & Default Slot

JavaScript — Slots System
// ====== Modal Component dengan Named Slots ======
class ModalDialog extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
    this.shadowRoot.innerHTML = `
      <style>
        :host { display: none; }
        :host([open]) { display: block; }

        .overlay {
          position: fixed;
          inset: 0;
          background: rgba(0, 0, 0, 0.6);
          display: flex;
          align-items: center;
          justify-content: center;
          z-index: 1000;
          animation: fadeIn 0.2s ease;
        }

        @keyframes fadeIn {
          from { opacity: 0; }
          to { opacity: 1; }
        }

        .modal {
          background: #1e1e2e;
          border: 1px solid #313244;
          border-radius: 16px;
          padding: 0;
          min-width: 400px;
          max-width: 90vw;
          max-height: 80vh;
          overflow: hidden;
          box-shadow: 0 24px 48px rgba(0,0,0,0.4);
          animation: slideUp 0.3s ease;
        }

        @keyframes slideUp {
          from { transform: translateY(20px); opacity: 0; }
          to { transform: translateY(0); opacity: 1; }
        }

        .modal-header {
          display: flex;
          align-items: center;
          justify-content: space-between;
          padding: 20px 24px;
          border-bottom: 1px solid #313244;
        }

        .modal-header h2 {
          margin: 0;
          color: #cdd6f4;
          font-size: 18px;
        }

        .close-btn {
          background: none;
          border: none;
          color: #6c7086;
          font-size: 24px;
          cursor: pointer;
          padding: 4px;
          border-radius: 4px;
        }
        .close-btn:hover { background: #313244; color: #cdd6f4; }

        .modal-body {
          padding: 24px;
          overflow-y: auto;
          max-height: 50vh;
          color: #bac2de;
        }

        .modal-footer {
          padding: 16px 24px;
          border-top: 1px solid #313244;
          display: flex;
          justify-content: flex-end;
          gap: 8px;
        }

        /* Slot styling */
        ::slotted([slot="header"]) {
          margin: 0;
          color: #cdd6f4;
        }

        ::slotted([slot="footer"]) {
          margin: 0;
        }
      </style>

      <div class="overlay">
        <div class="modal">
          <div class="modal-header">
            <!-- Named slot: "header" -->
            <slot name="header"><h2>Modal Title</h2></slot>
            <button class="close-btn">✕</button>
          </div>
          <div class="modal-body">
            <!-- Default slot: konten tanpa nama -->
            <slot>Konten modal di sini.</slot>
          </div>
          <div class="modal-footer">
            <!-- Named slot: "footer" -->
            <slot name="footer"></slot>
          </div>
        </div>
      </div>
    `;

    this.shadowRoot.querySelector('.close-btn').addEventListener('click', () => {
      this.close();
    });

    this.shadowRoot.querySelector('.overlay').addEventListener('click', (e) => {
      if (e.target === e.currentTarget) this.close();
    });
  }

  open() { this.setAttribute('open', ''); }
  close() { this.removeAttribute('open'); }
}

customElements.define('modal-dialog', ModalDialog);

// Penggunaan:
// <modal-dialog id="myModal">
//   <h2 slot="header">Konfirmasi Hapus</h2>
//   <p>Apakah Anda yakin ingin menghapus item ini?</p>
//   <p>Tindakan ini tidak bisa dibatalkan.</p>
//   <div slot="footer">
//     <button onclick="myModal.close()">Batal</button>
//     <button onclick="hapusItem()">Hapus</button>
//   </div>
// </modal-dialog>


5. Lifecycle Callbacks

Custom Elements memiliki serangkaian lifecycle callbacks yang dipanggil secara otomatis oleh browser pada titik-titik penting dalam kehidupan elemen. Memahami lifecycle ini sangat penting untuk membuat komponen yang berperilaku benar.

JavaScript — Lifecycle Callbacks
// ====== Semua Lifecycle Callbacks ======
class LifecycleDemo extends HTMLElement {
  // 1. Constructor — saat elemen dibuat (new LifecycleDemo() atau parsing HTML)
  constructor() {
    super();
    console.log('1ī¸âƒŖ Constructor: Elemen dibuat');

    // ✅ Boleh: setup shadow DOM, bind methods, inisialisasi state
    this.attachShadow({ mode: 'open' });
    this._state = { count: 0 };

    // ❌ Jangan: akses attributes, children, atau parent di sini
    // this.getAttribute('data'); // Kemungkinan null karena belum dipasang
  }

  // 2. connectedCallback — saat elemen ditambahkan ke DOM
  connectedCallback() {
    console.log('2ī¸âƒŖ Connected: Elemen ditambahkan ke DOM');

    // ✅ Boleh: baca attributes, setup event listeners, fetch data
    const nama = this.getAttribute('nama') || 'World';
    this.render(nama);

    // Setup event listener
    this._handleClick = () => {
      this._state.count++;
      this.updateCounter();
    };
    this.shadowRoot.querySelector('button')
      ?.addEventListener('click', this._handleClick);

    // Fetch data
    this.loadData();
  }

  // 3. disconnectedCallback — saat elemen dihapus dari DOM
  disconnectedCallback() {
    console.log('3ī¸âƒŖ Disconnected: Elemen dihapus dari DOM');

    // ✅ Cleanup: hapus event listeners, stop timers, abort fetch
    this.shadowRoot.querySelector('button')
      ?.removeEventListener('click', this._handleClick);

    if (this._timer) clearInterval(this._timer);
    if (this._abortController) this._abortController.abort();
  }

  // 4. adoptedCallback — saat elemen dipindahkan ke dokumen baru
  adoptedCallback() {
    console.log('4ī¸âƒŖ Adopted: Elemen dipindahkan ke dokumen baru');

    // Jarang digunakan, tapi berguna untuk iframe atau document.adoptNode()
    const newDoc = document;
    console.log('Dipindahkan ke dokumen baru:', newDoc.title);
  }

  // 5. attributeChangedCallback — saat attribute diamati berubah
  static get observedAttributes() {
    return ['nama', 'count', 'disabled'];
  }

  attributeChangedCallback(name, oldVal, newVal) {
    console.log(`5ī¸âƒŖ Attribute "${name}" berubah: "${oldVal}" → "${newVal}"`);

    // ✅ Update UI berdasarkan attribute baru
    if (name === 'nama' && this.isConnected) {
      this.render(newVal);
    }
    if (name === 'disabled') {
      const btn = this.shadowRoot.querySelector('button');
      if (btn) btn.disabled = newVal !== null;
    }
  }

  // Helper methods
  render(nama) {
    this.shadowRoot.innerHTML = `
      <style>
        :host { display: block; padding: 16px; font-family: system-ui; }
        p { color: #cdd6f4; }
        button { padding: 8px 16px; border-radius: 6px; cursor: pointer; }
      </style>
      <p>Halo, ${nama}!</p>
      <p>Klik: <strong id="count">${this._state.count}</strong> kali</p>
      <button>Klik Saya</button>
    `;
  }

  updateCounter() {
    const el = this.shadowRoot.getElementById('count');
    if (el) el.textContent = this._state.count;
  }

  async loadData() {
    this._abortController = new AbortController();
    try {
      // const res = await fetch('/api/data', { signal: this._abortController.signal });
    } catch (e) {
      if (e.name !== 'AbortError') console.error(e);
    }
  }
}

customElements.define('lifecycle-demo', LifecycleDemo);

// Urutan lifecycle saat parsing HTML:
// 1. constructor()
// 2. attributeChangedCallback() (jika ada attribute)
// 3. connectedCallback()

// Urutan saat update attribute:
// 1. attributeChangedCallback()

// Urutan saat remove:
// 1. disconnectedCallback()
Diagram

attributeChangedCallback

attributeChangedCall.

disconnectedCallback

connectedCallback

adoptedCallback

constructor()



6. Observed Attributes & Properties

Custom Elements mendukung dua cara untuk mengkonfigurasi: attributes (HTML attributes, string) dan properties (JavaScript properties, tipe apapun). Keduanya perlu disinkronkan untuk pengalaman pengembang yang baik.

JavaScript — Attributes & Properties
// ====== Sinkronisasi Attribute ↔ Property ======
class RatingStars extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
    this._value = 0;
    this._max = 5;
    this._readonly = false;
  }

  // Daftar attribute yang diamati
  static get observedAttributes() {
    return ['value', 'max', 'readonly'];
  }

  // Getter & Setter untuk properties (JavaScript API)
  get value() { return this._value; }
  set value(val) {
    this._value = Math.max(0, Math.min(Number(val), this._max));
    // Sinkronkan ke attribute
    this.setAttribute('value', this._value);
    this._render();
  }

  get max() { return this._max; }
  set max(val) {
    this._max = Number(val) || 5;
    this.setAttribute('max', this._max);
    this._render();
  }

  get readonly() { return this._readonly; }
  set readonly(val) {
    this._readonly = val !== null && val !== false;
    if (this._readonly) {
      this.setAttribute('readonly', '');
    } else {
      this.removeAttribute('readonly');
    }
    this._render();
  }

  // Callback saat attribute berubah (dari HTML atau setAttribute)
  attributeChangedCallback(name, oldVal, newVal) {
    switch (name) {
      case 'value':
        this._value = Number(newVal) || 0;
        break;
      case 'max':
        this._max = Number(newVal) || 5;
        break;
      case 'readonly':
        this._readonly = newVal !== null;
        break;
    }
    if (this.isConnected) this._render();
  }

  connectedCallback() {
    this._render();
  }

  _render() {
    let stars = '';
    for (let i = 1; i <= this._max; i++) {
      const filled = i <= this._value ? '★' : '☆';
      const color = i <= this._value ? '#f9e2af' : '#585b70';
      stars += `<span class="star" data-value="${i}"
        style="color:${color};cursor:${this._readonly?'default':'pointer'};font-size:24px">
        ${filled}
      </span>`;
    }

    this.shadowRoot.innerHTML = `
      <style>
        :host { display: inline-flex; gap: 4px; align-items: center; }
        .star { transition: transform 0.1s; user-select: none; }
        .star:hover { transform: scale(1.2); }
      </style>
      ${stars}
      <span style="margin-left:8px;color:#a6adc8;font-size:14px">
        ${this._value}/${this._max}
      </span>
    `;

    if (!this._readonly) {
      this.shadowRoot.querySelectorAll('.star').forEach(star => {
        star.addEventListener('click', (e) => {
          this.value = Number(e.target.dataset.value);
          this.dispatchEvent(new CustomEvent('rating-change', {
            detail: { value: this._value },
            bubbles: true,
            composed: true,
          }));
        });
      });
    }
  }
}

customElements.define('rating-stars', RatingStars);

// ====== Penggunaan ======
// Via HTML (attributes — semua string):
// <rating-stars value="4" max="5"></rating-stars>
// <rating-stars value="3" max="10" readonly></rating-stars>

// Via JavaScript (properties — tipe apapun):
// const stars = document.querySelector('rating-stars');
// stars.value = 4;      // number
// stars.max = 5;        // number
// stars.readonly = true; // boolean


7. Custom Events & Communication

Web Components berkomunikasi dengan komponen lain melalui Custom Events. Ini adalah pola yang sama dengan event DOM native — dan memastikan komponen tetap loosely coupled.

JavaScript — Custom Events
// ====== Component yang mengirim event ======
class SearchInput extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
    this.shadowRoot.innerHTML = `
      <style>
        :host { display: block; }
        input {
          width: 100%;
          padding: 12px 16px;
          border: 2px solid #313244;
          border-radius: 8px;
          background: #181825;
          color: #cdd6f4;
          font-size: 16px;
          outline: none;
          box-sizing: border-box;
        }
        input:focus { border-color: #6366f1; }
      </style>
      <input type="search" placeholder="Cari...">
    `;
  }

  connectedCallback() {
    const input = this.shadowRoot.querySelector('input');
    let debounceTimer;

    input.addEventListener('input', (e) => {
      clearTimeout(debounceTimer);
      debounceTimer = setTimeout(() => {
        // Dispatch custom event dengan data
        this.dispatchEvent(new CustomEvent('search', {
          detail: {
            query: e.target.value,
            timestamp: Date.now(),
          },
          bubbles: true,    // Event naik ke parent
          composed: true,   // Event melewati shadow boundary
        }));
      }, 300); // Debounce 300ms
    });
  }
}

customElements.define('search-input', SearchInput);

// ====== Component yang mendengarkan event ======
class SearchResults extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
    this.shadowRoot.innerHTML = `<div class="results"></div>`;
  }

  connectedCallback() {
    // Dengarkan event dari search-input
    document.addEventListener('search', (e) => {
      this.handleSearch(e.detail.query);
    });
  }

  async handleSearch(query) {
    if (!query.trim()) {
      this.shadowRoot.querySelector('.results').innerHTML = '<p>Ketik untuk mulai mencari</p>';
      return;
    }

    // Simulasi pencarian
    const results = [
      'React.js Tutorial', 'Vue.js Guide', 'Svelte Basics',
      'Web Components', 'Angular Framework'
    ].filter(item => item.toLowerCase().includes(query.toLowerCase()));

    this.shadowRoot.querySelector('.results').innerHTML = results.length > 0
      ? results.map(r => `<div class="result-item">${r}</div>`).join('')
      : '<p>Tidak ditemukan</p>';
  }
}

customElements.define('search-results', SearchResults);

// ====== Penggunaan ======
// <search-input></search-input>
// <search-results></search-results>
//
// Event "search" dari search-input bubbles up dan di-dengar
// oleh search-results melalui document listener


8. Styling Web Components

Styling Web Components membutuhkan pemahaman khusus karena Shadow DOM menyediakan enkapsulasi. Ada beberapa cara untuk mengontrol styling dari dalam maupun dari luar komponen.

CSS — Styling Techniques
// ====== CSS Custom Properties (CSS Variables) — tembus Shadow DOM ======
// CSS Custom Properties bisa menembus Shadow DOM!

class ThemedCard extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
    this.shadowRoot.innerHTML = `
      <style>
        :host {
          display: block;
          padding: 20px;
          border-radius: 12px;
          /* Gunakan CSS custom properties dari luar */
          background: var(--card-bg, #1e1e2e);
          color: var(--card-text, #cdd6f4);
          border: 1px solid var(--card-border, #313244);
          font-family: var(--card-font, system-ui, sans-serif);
        }
        h3 {
          color: var(--card-title, #89b4fa);
          font-size: var(--card-title-size, 20px);
        }
        p { color: var(--card-text, #a6adc8); }
      </style>
      <h3><slot name="title">Judul</slot></h3>
      <p><slot>Konten...</slot></p>
    `;
  }
}

customElements.define('themed-card', ThemedCard);

// ====== Penggunaan dengan CSS Variables dari luar ======
/*
<style>
  /* Override tema untuk semua themed-card */
  themed-card {
    --card-bg: #11111b;
    --card-title: #f9e2af;
    --card-text: #bac2de;
    --card-border: #45475a;
  }

  /* Override khusus untuk satu card */
  .featured-card {
    --card-bg: #1e1e3e;
    --card-title: #f5c2e7;
    --card-border: #6366f1;
  }
</style>

<themed-card>
  <span slot="title">Tutorial Web</span>
  Belajar Web Components dengan mudah.
</themed-card>

<themed-card class="featured-card">
  <span slot="title">⭐ Featured</span>
  Komponen dengan tema kustom!
</themed-card>
*/

// ====== ::part() — Mengekspos bagian spesifik ======
class PartDemo extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
    this.shadowRoot.innerHTML = `
      <style>
        .wrapper { padding: 20px; background: #1e1e2e; border-radius: 12px; }
        .header { color: #cdd6f4; }
        .content { color: #a6adc8; }
        .footer { color: #6c7086; }
      </style>
      <div class="wrapper">
        <h2 part="header" class="header">Judul</h2>
        <div part="content" class="content"><slot></slot></div>
        <footer part="footer" class="footer">Footer</footer>
      </div>
    `;
  }
}

customElements.define('part-demo', PartDemo);

// Dari luar, bisa style bagian yang di-ekspos via ::part():
/*
part-demo::part(header) {
  color: #f9e2af;
  font-size: 28px;
}
part-demo::part(content) {
  padding: 16px;
  background: #313244;
  border-radius: 8px;
}
part-demo::part(footer) {
  border-top: 1px solid #45475a;
  padding-top: 12px;
}
*/


9. Teknik Lanjutan

Berikut beberapa teknik lanjutan untuk membuat Web Components yang lebih powerful dan profesional.

JavaScript — Advanced Patterns
// ====== 1. Form-Associated Custom Elements ======
// Komponen bisa berpartisipasi dalam form submission

class MyCheckbox extends HTMLElement {
  static formAssociated = true; // Aktifkan form association

  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
    // Akses internals untuk form integration
    this.internals = this.attachInternals();
    this._checked = false;
  }

  connectedCallback() {
    this.shadowRoot.innerHTML = `
      <style>
        :host { display: inline-flex; align-items: center; gap: 8px; cursor: pointer; }
        .box {
          width: 20px; height: 20px; border: 2px solid #6366f1;
          border-radius: 4px; display: flex; align-items: center;
          justify-content: center; transition: background 0.2s;
        }
        .box.checked { background: #6366f1; }
        label { color: #cdd6f4; user-select: none; }
      </style>
      <div class="box"></div>
      <label><slot>Checkbox</slot></label>
    `;

    this.addEventListener('click', () => this.toggle());
    this._render();
  }

  toggle() {
    this._checked = !this._checked;
    // Update form value
    this.internals.setFormValue(this._checked ? 'on' : null);
    this.internals.setValidity({});
    this._render();
    this.dispatchEvent(new Event('change', { bubbles: true }));
  }

  get checked() { return this._checked; }
  set checked(val) { this._checked = val; this._render(); }

  // Form reset callback
  formResetCallback() {
    this._checked = false;
    this._render();
  }

  _render() {
    const box = this.shadowRoot.querySelector('.box');
    if (box) box.classList.toggle('checked', this._checked);
  }
}

customElements.define('my-checkbox', MyCheckbox);

// <form>
//   <my-checkbox name="agree">Saya setuju</my-checkbox>
//   <button type="submit">Submit</button>
// </form>

// ====== 2. Autonomous mixin pattern ======
const ResizeMixin = (superclass) => class extends superclass {
  connectedCallback() {
    super.connectedCallback?.();
    this._resizeObserver = new ResizeObserver(entries => {
      for (const entry of entries) {
        this.onResize?.(entry.contentRect);
      }
    });
    this._resizeObserver.observe(this);
  }

  disconnectedCallback() {
    super.disconnectedCallback?.();
    this._resizeObserver?.disconnect();
  }
};

const IntersectionMixin = (superclass) => class extends superclass {
  connectedCallback() {
    super.connectedCallback?.();
    this._intersectionObserver = new IntersectionObserver(entries => {
      for (const entry of entries) {
        if (entry.isIntersecting) this.onVisible?.();
        else this.onHidden?.();
      }
    });
    this._intersectionObserver.observe(this);
  }

  disconnectedCallback() {
    super.disconnectedCallback?.();
    this._intersectionObserver?.disconnect();
  }
};

// Gunakan mixin untuk komponen yang responsif & lazy
class ResponsiveChart extends ResizeMixin(IntersectionMixin(HTMLElement)) {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
    this._isVisible = false;
  }

  onResize(rect) {
    console.log(`Chart resized: ${rect.width}x${rect.height}`);
    // Re-render chart sesuai ukuran baru
  }

  onVisible() {
    this._isVisible = true;
    console.log('Chart visible — mulai animasi');
  }

  onHidden() {
    this._isVisible = false;
    console.log('Chart hidden — pause animasi');
  }
}

customElements.define('responsive-chart', ResponsiveChart);


10. Quiz: Uji Pemahamanmu!

Setelah membaca tutorial di atas, jawablah 5 pertanyaan berikut untuk menguji pemahamanmu tentang Web Components:

Pertanyaan 1: Apa aturan penamaan untuk Custom Element?

a) Nama harus mengandung setidaknya satu tanda hubung (-)
b) Nama harus dimulai dengan angka
c) Nama harus menggunakan camelCase
d) Nama bebas, tidak ada aturan khusus

Pertanyaan 2: Apa fungsi utama Shadow DOM?

a) Membuat animasi CSS yang lebih halus
b) Mengenkapsulasi DOM, CSS, dan JS agar tidak saling mempengaruhi
c) Meningkatkan performa rendering browser
d) Menggantikan kebutuhan akan HTML Templates

Pertanyaan 3: Lifecycle callback mana yang dipanggil saat elemen ditambahkan ke DOM?

a) constructor()
b) connectedCallback()
c) adoptedCallback()
d) attributeChangedCallback()

Pertanyaan 4: Bagaimana cara mengirim data dari komponen anak ke komponen induk?

a) Menggunakan global variable
b) Menggunakan CustomEvent dengan bubbles: true
c) Mengubah props secara langsung
d) Menggunakan localStorage

Pertanyaan 5: Apa fungsi dari <slot> dalam Web Components?

a) Placeholder untuk konten yang di-supply dari luar komponen
b) Tempat menyimpan state komponen
c) Menghubungkan komponen dengan database
d) Membuat animasi transisi antar halaman
🔍 Zoom
100%
🎨 Tema