Elemen kustom adalah class yang didaftarkan terhadap nama tag melalui customElements.define(). Pendaftaran dan siklus proses dasar sudah dipahami dengan baik; aturan di bawah menargetkan bagian yang sering kali salah diterapkan.
Siklus proses: bagian yang salah diterapkan
Callback itu sendiri didokumentasikan dengan baik; berikut adalah kesalahpahaman yang menyebabkan bug:
connectedCallbackdapat berjalan lebih dari sekali. Peristiwa ini diaktifkan pada setiap penyisipan, sehingga memindahkan elemen akan menjalankannya kembali. Buat penyiapan yang idempoten — jangan asumsikan satu kali operasi pertama.connectedMoveCallbackmelewati pasangan pemutusan/penghubungan kembali saat pemindahan. Saat elemen dipindahkan denganNode.moveBefore(), callback ini akan diaktifkan alih-alihdisconnectedCallback→connectedCallback, sehingga status, fokus, dan pemutaran iframe/media tetap bertahan setelah pemindahan. Tentukan saat penyiapan atau penguraian Anda mahal atau dijalankan ulang secara nyata.- Penghapusan dalam
disconnectedCallback. Hapus pemroses, timer, dan observer global atau Anda akan mengalami kebocoran; tetapi elemen dapat disisipkan kembali, jadi jangan menganggap penghapusan sebagai permanen. - Panggil
super()terlebih dahulu di konstruktor, dan jangan bergantung pada keberadaan DOM lengkap. Memanggilsuper()setelah menyentuhthisakan menampilkanReferenceError. Atribut biasanya (tetapi tidak selalu) ada; daftar panjang turunan mungkin belum selesai diuraikan. GunakanconnectedCallbackuntuk inisialisasi satu kali,slotchangeuntuk memantau turunan langsung, danattributeChangedCallbackuntuk memantau atribut.connectedCallbackmungkin masih diaktifkan sebelum semua turunan diuraikan, jadi jangan abaikan panggilanslotchange/attributeChangedCallbackberikutnya. attributeChangedCallbackhanya diaktifkan untukobservedAttributes, dan harus tetap murah untuk atribut yang tidak memengaruhi output.- Hook siklus proses dan
observedAttributesdi-snapshot saat pendaftaran.customElements.define()membaca keduanya sekali; menambahkan hook atau memperluasobservedAttributespada class setelah panggilan akan diabaikan tanpa ada pemberitahuan. Secara khusus,attributeChangedCallbacktidak akan pernah diaktifkan kecuali jikaobservedAttributessudah ada pada waktudefine().
Waktu upgrade dan :defined
Elemen dapat ada di DOM sebelum class-nya didaftarkan (dalam HTML yang dirender server, atau saat skrip dimuat terlambat). Sebelum ditentukan, elemen ini adalah HTMLElement inert tanpa perilaku Anda; saat customElements.define() berjalan, browser akan mengupgrade setiap elemen yang cocok yang sudah ada dalam dokumen, menjalankan konstruktornya, lalu connectedCallback.
Dua konsekuensi yang sering diabaikan oleh model:
- Atribut dan turunan mungkin sudah ada saat konstruktor/
connectedCallbackAnda berjalan saat upgrade. Baca atribut yang ada diconnectedCallbackuntuk menyinkronkan status awal, dan jangan pernah berasumsi bahwa elemen dimulai dalam keadaan kosong. - Anak mungkin masih hilang bahkan di
connectedCallback— parser HTML dapat menghasilkan pertengahan penguraian daftar anak yang panjang, sehingga callback terkadang berjalan sebelum setiap anak dilampirkan. Lihat poin konstruktor di Siklus proses di atas untuk pola (slotchange+attributeChangedCallback).
Sembunyikan elemen yang tidak ditentukan untuk menghindari flash konten tanpa gaya — tetapi gagal terbuka, sehingga konten tetap dapat dijangkau jika skrip tidak pernah dimuat. Animasikan opacity dengan penundaan, bukan menyembunyikannya secara permanen dengan visibility: hidden, sehingga elemen akan muncul setelah waktu tunggu habis meskipun tidak pernah diupgrade:
my-element:not(:defined) {
opacity: 0;
/* Reveal anyway after 2s, so a failed or slow script never hides content for good. */
animation: defined-fallback 0.2s 2s forwards;
}
@keyframes defined-fallback { to { opacity: 1; } }
Cakup guard ini ke elemen yang memerlukannya — atau ke subhierarki tertentu melalui :has(:not(:defined)) — bukan ke seluruh body. Satu widget yang lambat dan tidak penting (misalnya, tombol bagikan) tidak boleh menunda render untuk seluruh halaman.
Refleksi atribut ⇄ properti
Pastikan atribut HTML dan properti JS tetap sinkron agar elemen dapat digunakan dari markup dan skrip. Nilai atribut selalu berupa string; paksa nilai tersebut.
class MyToggle extends HTMLElement {
static observedAttributes = ['label', 'count', 'disabled'];
// String attribute reflected via getter/setter.
get label() { return this.getAttribute('label') ?? ''; }
set label(v) {
v ? this.setAttribute('label', v) : this.removeAttribute('label');
}
// Numeric attribute: coerce from string on read, serialize on write.
get count() { return Number(this.getAttribute('count')) || 0; }
set count(v) { this.setAttribute('count', String(v)); }
// Boolean attribute: present = true, absent = false. Never set it to "false".
get disabled() { return this.hasAttribute('disabled'); }
set disabled(on) { this.toggleAttribute('disabled', !!on); }
attributeChangedCallback(name, oldVal, newVal) {
if (oldVal !== newVal) this.render();
}
}
Tempat nilai berada
Contoh di atas mempertahankan DOM sebagai sumber kebenaran — setiap pengambil membaca getAttribute. Ini adalah default yang tepat untuk beberapa atribut sederhana yang mirip string: hanya ada satu tempat untuk menyimpan status, sehingga tidak dapat didesinkronisasi.
Untuk komponen yang lebih kaya, simpan sumber kebenaran di kolom JS dan refleksikan ke luar ke atribut hanya saat sesuatu yang lain harus melihatnya — penargetan CSS, atau konsumen yang membacanya. Membaca kolom jauh lebih murah daripada mengurai dan memaksa atribut pada setiap akses, dan nilai non-string tidak pernah melakukan perjalanan bolak-balik melalui serialisasi. Inilah alasan library seperti Lit menyimpan status di JS dan merefleksikan secara selektif, bukan memperlakukan atribut sebagai penyimpanan: hanya merefleksikan apa yang harus diamati oleh dunia luar, bukan setiap properti.
Kesalahan:
- Disiplin
observedAttributes: hanya cantumkan atribut yang perubahannya harus merender ulang atau memicu efek samping. Mengamati semuanya akan menambahkan callback ke setiap mutasi tanpa manfaat. - Loop refleksi: setter yang menulis atribut akan memasukkan ulang
attributeChangedCallback. Lindungi dengan pemeriksaanoldVal !== newVal, seperti di atas. - Status khusus gaya: untuk status internal yang hanya ada untuk mendorong gaya visual (pemuatan, aktif, diperluas), pilih status kustom daripada mencerminkan atribut sintetis — lihat Status kustom di bawah. Refleksikan ke atribut hanya jika konsumen harus dapat menetapkan status dari markup.
Status kustom
Status kustom adalah flag boolean yang diekspos elemen tentang dirinya sendiri untuk ditargetkan CSS, ditetapkan melalui ElementInternals dan dicocokkan dengan pseudo-class :state(). Gunakan untuk status UI runtime internal — busy, expanded, invalid — alih-alih mencerminkan atribut sintetis: status tersebut tidak dapat bertabrakan dengan atribut konsumen, tidak pernah muncul di DOM, dan bersifat hanya baca dari luar (konsumen dapat menata gaya :state(), tetapi tidak dapat memalsukannya).
class AsyncButton extends HTMLElement {
#internals;
#action = async () => {};
constructor() {
super();
this.#internals = this.attachInternals();
}
set action(fn) { this.#action = fn; } // host supplies the work to run
async run() {
if (this.#internals.states.has('busy')) return; // guard re-entry
this.#internals.states.add('busy');
this.#internals.states.delete('success');
try {
await this.#action();
this.#internals.states.add('success');
} catch {
this.#internals.states.add('error');
} finally {
this.#internals.states.delete('busy');
}
}
}
CustomStateSet adalah set, sehingga status independen menyusun (busy plus error) tanpa menciptakan enum data-status gabungan.
Status kustom hanya runtime. Status ini tidak diserialisasi ke DOM, sehingga tidak akan bertahan saat pemuatan ulang, pembuatan ulang elemen, atau rendering server. Jika status harus ditetapkan secara deklaratif dalam markup atau dipertahankan di seluruh pemuatan, itu adalah tugas untuk atribut (lihat Tempat nilai berada) — pulihkan di connectedCallback dari sumber tepercaya Anda. Aturan praktis: status kustom untuk apa yang diperoleh dan dimiliki komponen saat runtime dan hanya perlu gaya; atribut untuk apa yang harus ditetapkan dari markup atau diserialisasi.
Untuk menata gaya status ini — baik di dalam komponen maupun dari stylesheet konsumen — lihat Menata Gaya Komponen Web.
Konvensi penamaan
- Status benar-benar pribadi: kolom
#native (#count) untuk enkapsulasi ketat; awalan_hanya jika subclass masih harus menjangkaunya. Perhatikan bahwa kolom#tidak diwariskan — subclass tidak dapat melihat kolom pribadi class dasar. - Properti dengan kunci simbol adalah jalan tengah: disembunyikan dari enumerasi biasa, tetapi dapat dijangkau oleh kode apa pun yang menyimpan simbol (ekspor, atau ekspos sebagai kolom
staticdi class). Karena setiap simbol bersifat unik, dua simbol yang memiliki deskripsi yang sama tidak akan pernah bertabrakan — berguna saat subclass atau sibling memerlukan akses terkontrol yang tidak dapat diberikan oleh kolom#. - Jangan membayangi atribut global (
style,class,id,slot,part,title,lang,dir) dengan properti Anda sendiri; tindakan ini akan merusak perilaku bawaannya. Perhatikan bahwadisabledhanya bersifat global pada kontrol formulir; pada elemen lain, Anda harus menerapkan efeknya sendiri.
Saat memberi nama platform API baru, ikuti konvensi platform itu sendiri: gunakan format kebab case huruf kecil dengan setidaknya satu tanda hubung untuk nama tag elemen kustom (misalnya my-toggle), lowerCamelCase untuk properti dan metode, serta nama atribut huruf kecil yang disinkronkan dengan properti yang sesuai.