Agar handler klik tetap berfungsi setelah mengganti konten DOM tampilan, pasang satu listener pada elemen induk yang bertahan selama pembaruan. Panduan ini menunjukkan cara menerapkan dan memeriksa pola tersebut di Safari pada Mac menggunakan pemilih ubin sintetis sederhana.

Tugasnya spesifik: bangun ulang konten tampilan JavaScript sambil menjaga tombolnya tetap responsif dan pilihan tetap tersimpan. Anda memerlukan pengetahuan dasar JavaScript dan browser Safari terkini. Contoh ini menggunakan API browser standar dan tidak memerlukan framework atau pemasangan paket.

Mengapa klik berhenti berfungsi setelah DOM diganti?

Listener yang didaftarkan dengan addEventListener() melekat pada target peristiwa tempat listener itu didaftarkan. Penetapan nilai ke innerHTML sebuah wadah mengurai markup yang diberikan dan mengganti elemen turunannya. Akibatnya, tombol yang baru dibuat tidak mewarisi listener yang terpasang pada tombol sebelumnya, sekalipun ID dan labelnya sama. Lihat referensi innerHTML MDN dan model listener peristiwa pada DOM Standard.

Perbedaan pentingnya adalah antara mengganti konten sebuah wadah dan mengganti wadah itu sendiri. Jika kode Anda mempertahankan wadah dan hanya membangun ulang anak-anaknya, listener pada wadah tersebut dapat tetap terpasang. Jika proses render luar juga mengganti wadah itu, Anda harus memilih leluhur yang tetap bertahan atau memasang listener saat wadah baru diinisialisasi.

Untuk peristiwa klik biasa yang mengalami bubbling, delegasi peristiwa memungkinkan elemen induk menangani interaksi yang berasal dari elemen turunannya. Alih-alih memasang listener pada setiap tombol pengganti, pasang pada elemen induk yang stabil, lalu identifikasi tombol yang relevan saat peristiwa diterima. Panduan bubbling peristiwa MDN menjelaskan pola ini.

Di mana listener sebaiknya dipasang?

Mulailah dengan menemukan batas DOM tepat yang diganti oleh operasi render Anda. Lalu pilih elemen induk paling dekat yang sesuai dan berada di luar batas tersebut. Panduan keputusan berikut menerapkan perilaku penggantian dan listener yang terdokumentasi pada pola pembaruan umum. MDN: replaceChildren(), DOM Standard: EventTarget.

Pembaruan Anda Penempatan listener Persyaratan siklus hidup
Membangun ulang anak-anak dari akar tampilan yang sudah ada Delegasikan klik dari akar tersebut Daftarkan sekali saat akar dibuat
Mengganti akar tampilan juga Delegasikan dari wadah luar yang tetap bertahan, atau pasang pada akar baru Bersihkan pendaftaran tampilan sebelumnya
Membuat tombol yang memerlukan handler masing-masing Pasang pada setiap tombol baru selama proses pembuatan Selesaikan pemasangan sebelum kontrol baru tersedia
Memuat ulang seluruh dokumen Inisialisasikan listener di dokumen baru Jalankan kembali inisialisasi halaman seperti biasa

Untuk contoh di bawah ini, strukturnya tetap berupa akar tampilan yang berisi judul, status, kumpulan pilihan ubin berurutan, dan kontrol penyegaran. Setiap render membangun ulang anak-anak tersebut dalam urutan yang sama. Hanya akar tampilan, listener, dan status JavaScript yang tetap ada.

Bangun tampilan lengkap yang aman terhadap penggantian di Safari

Pertama, siapkan tab browser sementara. Di Safari pada Mac Anda, buka Settings…, pilih Advanced, lalu aktifkan Show features for web developers. Buka Web Inspector melalui Develop → Show Web Inspector, atau tekan Option-Command-I. Langkah-langkah ini didokumentasikan dalam panduan WebKit untuk mengaktifkan Web Inspector.

Buka about:blank, buka Web Inspector, lalu pilih Console. Tempelkan seluruh program di bawah ini ke prompt-nya, kemudian jalankan. WebKit mendokumentasikan bahwa Prompt Console menjalankan JavaScript dalam konteks eksekusi terpilih pada halaman yang diperiksa. Gunakan tab kosong agar demonstrasi tetap terpisah dari aplikasi Anda.

Ini adalah program browser lengkap: program ini membuat DOM-nya sendiri, menentukan data contoh, memasang tampilan, dan menyediakan metode pembersihan. Program ini mengganti anak-anak tampilan menggunakan replaceChildren(), yang menerima objek node dan mengganti kumpulan anak yang ada. Referensi metode MDN menjelaskan operasi tersebut.

(() => {
  window.domClickDemo?.destroy();

  const root = document.createElement("section");
  root.id = "dom-click-demo";
  const controller = new AbortController();
  const items = [
    { id: "a", label: "Tile A" },
    { id: "b", label: "Tile B" },
    { id: "c", label: "Tile C" }
  ];
  const state = { selected: "None", refreshes: 0 };

  function makeButton(action, label, id) {
    const button = document.createElement("button");
    button.type = "button";
    button.dataset.action = action;
    if (id !== undefined) button.dataset.id = id;
    const caption = document.createElement("span");
    caption.textContent = label;
    button.append(caption);
    return button;
  }

  function render() {
    const heading = document.createElement("h2");
    heading.textContent = "Tile picker";
    const status = document.createElement("p");
    status.textContent =
      `Selected: ${state.selected}; refreshes: ${state.refreshes}`;
    const list = document.createElement("ul");
    for (const item of items) {
      const row = document.createElement("li");
      row.append(makeButton("select", item.label, item.id));
      list.append(row);
    }
    const refresh = makeButton("refresh", "Replace view content");
    root.replaceChildren(heading, status, list, refresh);
  }

  function handleClick(event) {
    if (!(event.target instanceof Element)) return;
    const button = event.target.closest("button[data-action]");
    if (!button || !root.contains(button) || button.disabled) return;

    if (button.dataset.action === "select") {
      const item = items.find(item => item.id === button.dataset.id);
      if (!item) return;
      state.selected = item.label;
    } else if (button.dataset.action === "refresh") {
      state.refreshes += 1;
    } else {
      return;
    }
    render();
  }

  root.addEventListener("click", handleClick, {
    signal: controller.signal
  });
  render();
  document.body.append(root);

  window.domClickDemo = {
    root,
    destroy() {
      controller.abort();
      root.remove();
    }
  };
})();

Listener didaftarkan di luar render(). Setiap pilihan dan penyegaran membuat tombol baru, tetapi semua tombol tersebut tetap berada di bawah akar yang sama. Objek status juga berada di luar subtree yang dirender, sehingga penggantian paragraf status tidak mereset ubin yang dipilih atau jumlah penyegaran.

Teks keterangan bersarang itu disengaja. Klik pada teks tombol dapat menargetkan elemen anaknya. closest("button[data-action]") mencari kontrol yang cocok pada target dan leluhurnya, sehingga handler tidak hanya berfungsi ketika tombol itu sendiri menjadi target peristiwa. Referensi closest() MDN menjelaskan pencarian tersebut dan hasil null jika tidak ada yang cocok.

Pemeriksaan ketercakupan menjaga kontrol yang cocok tetap berada dalam batas tampilan ini. DOM Standard mendefinisikan contains() berdasarkan apakah node yang diberikan adalah node itu sendiri atau salah satu turunannya. DOM Standard: Node.contains().

Bedakan inisialisasi, render, dan pembuangan

Contoh ini memiliki tiga tanggung jawab yang jelas. Inisialisasi membuat akar dan mendaftarkan listener-nya. Render memperbarui anak-anak akar berdasarkan status. Pembuangan menghapus listener dan akar tersebut. Pembagian ini merupakan kontrak siklus hidup implementasi; pertahankan tanggung jawab yang setara saat menyesuaikannya dengan aplikasi Anda.

Listener menerima AbortSignal, dan destroy() membatalkan controllernya. Pembatalan menghapus pendaftaran terkait, sebagaimana dijelaskan dalam referensi addEventListener() MDN. Menjalankan kembali seluruh program terlebih dahulu membuang demonstrasi sebelumnya, sehingga beberapa tampilan demo tidak menumpuk.

Jangan menaruh listener anonim baru di dalam setiap render. Ekspresi fungsi anonim yang berulang membuat objek fungsi yang berbeda, sehingga pendaftarannya dapat menumpuk pada akar yang tetap bertahan. Menggunakan kembali callback, jenis peristiwa, dan pengaturan capture yang sama mencegah pendaftaran duplikat, tetapi menempatkan pemasangan di tahap inisialisasi membuat kepemilikannya lebih jelas. MDN: addEventListener(), DOM Standard: menambahkan listener.

Periksa penggantian berulang, pilihan, dan pembersihan

Ikuti urutan ini untuk menguji contoh. Hasil yang diharapkan ini mengikuti pembaruan status yang dinyatakan secara eksplisit dalam program; hasil ini bukan klaim bahwa pengujian Safari telah diselesaikan.

  1. Klik Tile B. Status seharusnya berbunyi Selected: Tile B; refreshes: 0.
  2. Klik Replace view content tiga kali. Status seharusnya berbunyi Selected: Tile B; refreshes: 3.
  3. Klik teks Tile C. Status seharusnya berubah menjadi Selected: Tile C; refreshes: 3.
  4. Klik judul atau paragraf status. Kedua nilai seharusnya tidak berubah.
  5. Jalankan window.domClickDemo.destroy() di Console. Demonstrasi seharusnya menghilang.
  6. Jalankan kembali seluruh program. Satu tampilan baru seharusnya muncul dengan Selected: None; refreshes: 0.

Tiga pemeriksaan pertama mencakup penggantian, status yang dipertahankan, dan target klik bersarang. Pemeriksaan keempat menguji penyaringan tindakan. Dua pemeriksaan terakhir menguji alur pembuangan dan inisialisasi. Secara keseluruhan, langkah-langkah ini menguji siklus hidup yang sebenarnya, bukan sekadar memastikan tombol awal berfungsi.

Sintaks JavaScript program ini diperiksa dengan Node, dan pemeriksaan EventTarget deterministik terpisah mengonfirmasi deduplikasi callback serta penghapusan melalui sinyal pembatalan. Pemeriksaan itu tidak menjalankan render DOM browser maupun interaksi Safari; gunakan urutan di atas untuk memvalidasinya di Mac Anda.

Bagaimana jika handler tetap gagal di aplikasi Anda?

Selidiki titik pertama saat perilaku menyimpang. Pertahankan cakupan contoh tetap kecil saat memeriksa batas penggantian, jalur peristiwa, dan pembaruan status aplikasi Anda.

Gejala Periksa terlebih dahulu Perbaikan yang sesuai
Klik berfungsi sampai tampilan luar disegarkan Apakah akar listener juga diganti? Pasang pada akar baru saat inisialisasi atau pilih elemen induk yang tetap bertahan
Klik diterima, tetapi tidak ada tindakan yang berjalan Apakah kontrol baru masih cocok dengan selektor dan nilai tindakannya? Pertahankan kontrak selektor dan tindakan pada kontrol
Klik pada teks tombol berperilaku berbeda Apakah handler hanya memeriksa target persisnya? Cari kontrol dengan closest()
Satu klik memperbarui status beberapa kali Apakah ada beberapa pendaftaran atau handler langsung dan terdelegasi yang sama-sama aktif? Tetapkan satu jalur inisialisasi sebagai pemilik tindakan
Handler berjalan, tetapi tampilan mereset Apakah status dibuat ulang di dalam proses render? Simpan status yang diperlukan di luar subtree yang diganti

Periksa juga perambatan. Handler elemen turunan yang memanggil stopPropagation() dapat mencegah peristiwa bubbling mencapai listener terdelegasi Anda. MDN menjelaskan penghentian perambatan dan delegasi peristiwa. Tinjau alasan perambatan dihentikan sebelum mengubah fase listener, karena perubahan fase juga mengubah urutan eksekusi.

Jika Anda harus menggunakan listener tombol langsung, pasang listener setiap kali membuat tombol pengganti. Kueri dan pemasangan satu kali tidak dapat menginisialisasi node yang dibuat oleh render berikutnya: pendaftaran melekat pada target peristiwanya. Demikian pula, delegasi tidak dapat mempertahankan listener pada akar yang dibuang oleh renderer Anda. Batasan ini mengikuti model listener pada DOM Standard.

Untuk tugas ini, susunan yang tahan lama terdiri dari akar tampilan yang stabil, satu listener terdelegasi, status yang disimpan di luar anak-anak yang dirender, dan jalur pembersihan yang eksplisit. Identifikasi node yang bertahan pada setiap pembaruan, tempatkan listener sesuai kebutuhan, dan verifikasi beberapa penggantian berturut-turut sebelum mengintegrasikan perubahan ke tampilan yang lebih besar.

Sumber