Dasar Manipulasi DOM dengan JavaScript
Membangun komponen daftar tugas kecil tanpa framework sambil memisahkan data, render, dan event agar perubahan antarmuka mudah diprediksi.
DOM adalah representasi dokumen yang dapat dibaca dan diubah melalui API browser. Tantangan sebenarnya bukan menemukan banyak method, melainkan menjaga hubungan antara data, elemen, dan interaksi agar tidak saling bertabrakan. Kita akan memakai daftar tugas kecil sebagai studi kasus.
- Form untuk menambahkan tugas.
- Render node dengan
textContent, bukan HTML mentah. - Satu event listener untuk item yang dibuat dinamis.
- Status kosong dan pesan yang dapat diumumkan teknologi bantu.
Mulai dari kontrak HTML
Gunakan atribut data-* sebagai hook JavaScript agar class tetap fokus pada tampilan. Form native memberi dukungan Enter dan validasi dasar tanpa menulis handler keyboard tambahan.
<form data-task-form>
<label for="task-title">Tugas baru</label>
<div class="field-row">
<input id="task-title" name="title" required maxlength="80">
<button type="submit">Tambahkan</button>
</div>
</form>
<p data-task-empty>Belum ada tugas.</p>
<ul data-task-list></ul>
<p class="sr-only" data-task-status aria-live="polite"></p>
aria-live="polite" cocok untuk pembaruan yang tidak mendesak. Jangan menaruh live region pada seluruh daftar karena setiap render dapat menghasilkan pengumuman yang terlalu panjang.
Simpan data sebagai sumber kebenaran
Jika data hanya dibaca kembali dari teks DOM, perubahan kecil akan cepat sulit dilacak. Gunakan array sebagai state, kemudian render DOM dari array tersebut.
const form = document.querySelector("[data-task-form]");
const list = document.querySelector("[data-task-list]");
const empty = document.querySelector("[data-task-empty]");
const status = document.querySelector("[data-task-status]");
if (!form || !list || !empty || !status) {
throw new Error("Komponen tugas tidak lengkap");
}
let tasks = [];
Pemeriksaan elemen dilakukan sekali di awal. Pada situs yang memakai script sama di beberapa halaman, kita bisa memilih return alih-alih melempar error. Pilih perilaku berdasarkan apakah komponen wajib ada atau opsional.
Buat node secara aman
Nilai dari input pengguna tidak boleh langsung dimasukkan ke innerHTML. Bangun node dengan API DOM dan isi teks melalui textContent.
function createTaskItem(task) {
const item = document.createElement("li");
item.dataset.taskId = task.id;
const label = document.createElement("span");
label.textContent = task.title;
const remove = document.createElement("button");
remove.type = "button";
remove.dataset.removeTask = "";
remove.setAttribute("aria-label", `Hapus ${task.title}`);
remove.textContent = "Hapus";
item.append(label, remove);
return item;
}
Template literal pada atribut label aman karena nilainya diberikan melalui DOM property. Risiko muncul ketika string tersebut diperlakukan sebagai markup. Untuk aplikasi yang memang harus menerima HTML, gunakan proses sanitasi yang sesuai, bukan daftar regex buatan sendiri.
Render perubahan minimum
function renderTasks() {
const fragment = document.createDocumentFragment();
for (const task of tasks) {
fragment.append(createTaskItem(task));
}
list.replaceChildren(fragment);
empty.hidden = tasks.length > 0;
}
DocumentFragment membantu menyiapkan kumpulan node sebelum dimasukkan ke daftar. Untuk daftar sangat besar, render ulang seluruh daftar mungkin tidak efisien; pada tahap itu kita dapat memperbarui item yang berubah saja. Untuk komponen kecil, fungsi render tunggal lebih mudah diuji dan biasanya cukup cepat.
Tangani submit sebagai satu alur
form.addEventListener("submit", (event) => {
event.preventDefault();
const data = new FormData(form);
const title = String(data.get("title") || "").trim();
if (!title) return;
const task = {
id: crypto.randomUUID(),
title
};
tasks = [...tasks, task];
renderTasks();
form.reset();
form.elements.title.focus();
status.textContent = `${title} ditambahkan`;
});
Handler melakukan urutan yang jelas: hentikan navigasi form, ambil data, normalisasi, ubah state, render, lalu pulihkan fokus. Validasi server tetap wajib jika data dikirim ke backend; validasi di browser terutama meningkatkan pengalaman pengguna.
Gunakan event delegation untuk item dinamis
Item belum ada ketika halaman pertama kali dimuat. Daripada memasang listener baru pada setiap tombol setelah render, dengarkan klik pada parent yang tetap ada.
list.addEventListener("click", (event) => {
const button = event.target.closest("[data-remove-task]");
if (!button || !list.contains(button)) return;
const item = button.closest("[data-task-id]");
const removed = tasks.find((task) => task.id === item?.dataset.taskId);
tasks = tasks.filter((task) => task.id !== item?.dataset.taskId);
renderTasks();
status.textContent = removed ? `${removed.title} dihapus` : "Tugas dihapus";
});
closest() membuat klik pada ikon di dalam tombol tetap dikenali. Pemeriksaan list.contains(button) mencegah elemen dari konteks lain ikut diproses.
Pisahkan state, render, dan efek samping
Ketika komponen berkembang, bedakan tiga pekerjaan: fungsi yang mengubah data, fungsi yang membuat DOM, dan efek samping seperti penyimpanan atau permintaan jaringan. Pemisahan ini membuat bug lebih mudah dilokalisasi.
function addTask(currentTasks, title) {
return [...currentTasks, { id: crypto.randomUUID(), title }];
}
function removeTask(currentTasks, id) {
return currentTasks.filter((task) => task.id !== id);
}
Dua fungsi tersebut tidak menyentuh dokumen sehingga dapat diuji dengan input dan output biasa. Event handler cukup menghubungkan aksi pengguna ke fungsi data dan render.
Bug yang sering muncul
- Selector terlalu umum:
document.querySelector("button")dapat mengambil tombol navigasi. Gunakan hook yang spesifik. - Listener dipasang setiap render: satu klik akhirnya menjalankan handler beberapa kali.
- ID berbasis index: menghapus item mengubah index dan dapat membuat target keliru.
- Fokus hilang: setelah modal ditutup atau item dihapus, pengguna keyboard tidak tahu posisi berikutnya.
- Markup mentah: input pengguna masuk ke
innerHTMLdan membuka risiko injeksi.
Rencana pengujian
- Submit kosong: browser menampilkan validasi dan state tidak berubah.
- Tambahkan dua tugas dengan judul yang sama: keduanya memiliki ID berbeda.
- Masukkan teks seperti
<img onerror=alert(1)>: teks harus tampil sebagai teks, bukan elemen. - Hapus item kedua: item pertama tetap ada dan status diumumkan.
- Operasikan form hanya dengan keyboard.
- Jalankan script pada halaman tanpa komponen dan pastikan perilaku fallback sesuai keputusan awal.