Kenapa ini penting
Saat browser membuka file HTML, ia tidak menyimpan HTML itu sebagai teks panjang. Browser membacanya lalu membangun struktur di memori yang disebut DOM (Document Object Model). DOM inilah yang digambar ke layar, dan DOM juga yang bisa dibaca serta diubah oleh JavaScript. Hampir semua yang kamu lihat di web modern, dari tombol "like", notifikasi, sampai keranjang belanja, terjadi karena JavaScript mengutak-atik DOM.
Sebelum bisa mengubah sesuatu, kamu harus bisa menemukannya dulu. Analoginya seperti kurir paket. Kurir tidak bisa mengantar barang kalau tidak tahu alamatnya: provinsi, kota, jalan, nomor rumah. DOM juga punya "alamat". Ada elemen induk, anak, dan saudara. Kemampuan menavigasi DOM adalah kemampuan membaca peta itu. Modul ini fokus ke "membaca dan menemukan"; mengubah dan bereaksi terhadap pengguna dibahas di modul berikutnya.
Selain DOM, browser juga menyediakan object global lain yang berguna: window (jendela browser), location (alamat URL), dan history (riwayat navigasi). Ketiganya dipakai untuk hal-hal seperti membaca parameter pencarian di URL, mengarahkan pengguna ke halaman lain, atau membuat tombol "kembali".
Konsep inti
DOM tree: halaman sebagai pohon keluarga
Ambil HTML sederhana ini:
<body>
<header>
<h1>Toko Kopi</h1>
</header>
<main>
<ul id="menu">
<li>Espresso</li>
<li>Latte</li>
</ul>
</main>
</body>
Browser mengubahnya menjadi pohon. body adalah induk (parent) dari header dan main. header dan main bersaudara (siblings). ul adalah anak (child) dari main, dan kedua li adalah anak dari ul. Setiap kotak di pohon itu disebut node.
Ada beberapa jenis node. Yang paling penting:
- Element node: tag HTML seperti
<li>atau<h1>. - Text node: teks di dalam elemen, termasuk spasi dan baris baru di antara tag.
- Comment node: komentar
<!-- -->.
Poin tentang text node ini akan muncul lagi di bagian traversing, karena spasi dan enter di antara tag juga dihitung sebagai node.
document: pintu masuk ke DOM
Object document mewakili seluruh halaman. Beberapa properti langsung: document.documentElement (tag <html>), document.head, document.body, dan document.title (judul tab yang bisa dibaca dan diubah).
Memilih elemen
| Method | Mengembalikan | Catatan |
|---|---|---|
document.getElementById("menu") |
satu elemen atau null |
Paling cepat, khusus id |
document.querySelector("css selector") |
elemen pertama yang cocok atau null |
Paling fleksibel |
document.querySelectorAll("css selector") |
NodeList semua yang cocok (bisa kosong) |
Statis, tidak ikut berubah |
document.getElementsByClassName("kelas") |
HTMLCollection |
Live, ikut berubah saat DOM berubah |
Kekuatan querySelector adalah ia memakai selector CSS yang sudah kamu kuasai: "#menu", ".kartu", "ul > li", "input[type='email']", "li:nth-child(2)", "a[href^='https']". Kalau bisa kamu pilih di CSS, bisa juga kamu pilih di JavaScript.
querySelector juga bisa dipanggil pada elemen, bukan hanya document. menu.querySelector("li") hanya mencari di dalam menu. Ini lebih cepat dan lebih aman karena tidak salah ambil elemen lain di halaman.
NodeList vs array. Hasil querySelectorAll mirip array dan punya forEach, tapi tidak punya map, filter, atau reduce. Ubah dulu dengan Array.from(list) atau [...list].
Traversing: berjalan dari satu elemen ke elemen lain
Kadang kamu sudah memegang satu elemen dan ingin ke kerabatnya. Ada dua keluarga properti:
| Hanya elemen (biasanya ini yang kamu mau) | Semua node (termasuk text) |
|---|---|
parentElement |
parentNode |
children |
childNodes |
firstElementChild / lastElementChild |
firstChild / lastChild |
nextElementSibling / previousElementSibling |
nextSibling / previousSibling |
Ada satu method yang sangat berguna: elemen.closest("selector"). Ia naik ke atas (mulai dari elemen itu sendiri) sampai menemukan leluhur yang cocok dengan selector. Contohnya, dari tombol "hapus" kamu bisa langsung menemukan kartu produk tempat tombol itu berada. Kebalikannya, elemen.matches("selector") mengecek apakah elemen itu sendiri cocok dengan selector. Keduanya akan sering kamu pakai di event delegation nanti.
Atribut dan dataset
Atribut HTML seperti href, src, alt, disabled bisa diakses dengan:
el.getAttribute("href"),el.setAttribute("href", "/baru"),el.hasAttribute("disabled"),el.removeAttribute("disabled").- Banyak atribut juga tersedia sebagai properti langsung:
img.src,a.href,input.value,button.disabled.
Ada beda halus. a.getAttribute("href") mengembalikan apa yang tertulis di HTML (misalnya "/produk"), sedangkan a.href mengembalikan URL lengkap hasil olahan browser ("https://toko.com/produk"). Sama halnya, input.getAttribute("value") adalah nilai awal di HTML, sementara input.value adalah nilai yang sedang diketik pengguna.
Atribut data-* adalah cara resmi menyimpan data tambahan di elemen tanpa mengarang atribut sendiri. Tulis <button data-product-id="17" data-harga="25000"> di HTML, lalu baca lewat el.dataset. Nama dengan strip diubah menjadi camelCase: data-product-id dibaca sebagai dataset.productId. Semua nilai dataset berbentuk string, jadi ubah dengan Number() sebelum dihitung.
window, location, history
windowadalah object global di browser. Variabeldocument,console,localStorage, danalertsebenarnya adalah propertiwindow. Ukuran jendela ada diwindow.innerWidthdanwindow.innerHeight, posisi scroll diwindow.scrollY, danwindow.scrollTo({ top: 0, behavior: "smooth" })menggulung halaman.locationmewakili URL halaman. Properti penting:href(URL lengkap),protocol,host,pathname,search(bagian?q=kopi),hash(bagian#kontak).location.href = "/login"ataulocation.assign("/login")berpindah halaman,location.replace("/login")berpindah tanpa menambah riwayat (tombol back tidak kembali ke halaman ini), danlocation.reload()memuat ulang. Untuk membaca query string, pakainew URLSearchParams(location.search).historyadalah riwayat tab.history.back()danhistory.forward()sama seperti tombol browser.history.pushState(state, "", "/url-baru")mengubah URL di address bar tanpa memuat ulang halaman, dan inilah dasar cara kerja router di React atau Next.js. Saat pengguna menekan back setelahpushState, browser memicu eventpopstate.
Contoh kode
Simpan HTML ini sebagai index.html, buka lewat server lokal (misalnya ekstensi Live Server di VS Code), lalu jalankan potongan JavaScript-nya di tab Console DevTools atau di tag <script> sebelum </body>.
<!doctype html>
<html lang="id">
<head>
<meta charset="utf-8">
<title>Toko Kopi Senja</title>
</head>
<body>
<header class="site-header">
<h1>Toko Kopi Senja</h1>
<nav>
<a href="/">Beranda</a>
<a href="/menu" class="aktif">Menu</a>
<a href="https://instagram.com/kopisenja" target="_blank">Instagram</a>
</nav>
</header>
<main>
<ul id="daftar-menu">
<li class="item" data-id="1" data-harga="18000" data-kategori="kopi">Espresso</li>
<li class="item" data-id="2" data-harga="25000" data-kategori="kopi">Latte</li>
<li class="item habis" data-id="3" data-harga="15000" data-kategori="teh">Teh Tarik</li>
</ul>
</main>
</body>
</html>
Memilih elemen dan membaca isinya
const judul = document.querySelector("h1");
console.log(judul.textContent); // → 'Toko Kopi Senja'
const daftar = document.getElementById("daftar-menu");
const semuaItem = document.querySelectorAll(".item");
console.log(semuaItem.length); // → 3
// Selector CSS kompleks
const itemKedua = document.querySelector("#daftar-menu li:nth-child(2)");
console.log(itemKedua.textContent); // → 'Latte'
const linkLuar = document.querySelectorAll('a[href^="https"]');
console.log(linkLuar.length); // → 1
const tersedia = document.querySelectorAll(".item:not(.habis)");
console.log(tersedia.length); // → 2
// NodeList → array supaya bisa map/filter
const namaMenu = [...semuaItem].map((li) => li.textContent);
console.log(namaMenu); // → [ 'Espresso', 'Latte', 'Teh Tarik' ]
// querySelector selalu bisa null. Biasakan cek.
const promo = document.querySelector(".promo");
console.log(promo); // → null
promo?.classList.add("tampil"); // aman, tidak error
Traversing
const latte = document.querySelector('[data-id="2"]');
console.log(latte.parentElement.id); // → 'daftar-menu'
console.log(latte.previousElementSibling.textContent); // → 'Espresso'
console.log(latte.nextElementSibling.textContent); // → 'Teh Tarik'
console.log(latte.nextElementSibling.nextElementSibling); // → null (sudah item terakhir)
const ul = latte.parentElement;
console.log(ul.children.length); // → 3 (hanya elemen <li>)
console.log(ul.childNodes.length); // → 7 (3 <li> + 4 text node berisi enter & spasi)
console.log(ul.firstElementChild.textContent); // → 'Espresso'
// closest: naik ke leluhur terdekat yang cocok
console.log(latte.closest("main").tagName); // → 'MAIN'
console.log(latte.closest(".site-header")); // → null (latte bukan di dalam header)
// matches: apakah elemen ini cocok dengan selector?
console.log(latte.matches(".item")); // → true
console.log(latte.matches(".habis")); // → false
// Fungsi kecil: kumpulkan "jalur" dari elemen sampai <html>
function jalur(el) {
const langkah = [];
while (el) {
langkah.unshift(el.tagName.toLowerCase() + (el.id ? "#" + el.id : ""));
el = el.parentElement;
}
return langkah.join(" > ");
}
console.log(jalur(latte)); // → 'html > body > main > ul#daftar-menu > li'
Atribut dan dataset
const menuLink = document.querySelector("nav a.aktif");
console.log(menuLink.getAttribute("href")); // → '/menu'
console.log(menuLink.href); // → 'http://127.0.0.1:5500/menu' (URL lengkap, tergantung server)
const igLink = document.querySelector('a[target="_blank"]');
// Link ke tab baru sebaiknya diberi rel="noopener" demi keamanan
igLink.setAttribute("rel", "noopener noreferrer");
console.log(igLink.hasAttribute("rel")); // → true
// dataset: baca data-* sebagai object
const espresso = document.querySelector('[data-id="1"]');
console.log({ ...espresso.dataset }); // → { id: '1', harga: '18000', kategori: 'kopi' }
console.log(typeof espresso.dataset.harga); // → 'string'
// Hitung total harga semua menu yang tidak habis
const total = [...document.querySelectorAll(".item:not(.habis)")]
.reduce((jml, li) => jml + Number(li.dataset.harga), 0);
console.log(total); // → 43000
// Kelompokkan per kategori
const perKategori = {};
document.querySelectorAll(".item").forEach((li) => {
const k = li.dataset.kategori;
perKategori[k] = (perKategori[k] ?? 0) + 1;
});
console.log(perKategori); // → { kopi: 2, teh: 1 }
// Menulis dataset otomatis membuat atribut data-*
espresso.dataset.stokTersisa = "12";
console.log(espresso.getAttribute("data-stok-tersisa")); // → '12'
window, location, history
// window
console.log(window.innerWidth > 768 ? "desktop" : "mobile"); // → tergantung lebar jendela
console.log(window.document === document); // → true (document adalah properti window)
// location. Misal URL: https://kopisenja.com/menu?kategori=kopi&urut=harga#daftar
console.log(location.pathname); // → '/menu'
console.log(location.search); // → '?kategori=kopi&urut=harga'
console.log(location.hash); // → '#daftar'
const params = new URLSearchParams(location.search);
console.log(params.get("kategori")); // → 'kopi'
console.log(params.get("halaman")); // → null (tidak ada)
console.log(params.has("urut")); // → true
// Membangun URL dengan aman (spasi & simbol otomatis di-encode)
const url = new URL("https://kopisenja.com/cari");
url.searchParams.set("q", "kopi susu & gula aren");
console.log(url.toString()); // → 'https://kopisenja.com/cari?q=kopi+susu+%26+gula+aren'
// history: ubah URL tanpa reload, misalnya saat filter kategori dipilih
function pilihKategori(kategori) {
const p = new URLSearchParams(location.search);
p.set("kategori", kategori);
history.pushState({ kategori }, "", `?${p}`);
console.log("Tampilkan kategori:", kategori);
}
pilihKategori("teh"); // address bar jadi /menu?kategori=teh&urut=harga, halaman tidak reload
// Saat pengguna menekan tombol back
window.addEventListener("popstate", (e) => {
console.log("Kembali ke state:", e.state); // → null untuk entri awal, atau { kategori: '...' }
});
Pola "simpan filter di URL" ini kelihatan sepele tapi besar manfaatnya. Pengguna bisa membagikan link hasil filter ke teman, dan tombol back bekerja sesuai harapan. Kamu akan memakainya di proyek akhir.
Kesalahan umum
- Script jalan sebelum HTML selesai dibaca. Kalau
<script>ditaruh di<head>tanpa atribut apa pun,document.querySelector("#daftar-menu")bernilainullkarena elemennya belum ada. Solusi: taruh script sebelum</body>, atau pakai<script src="app.js" defer></script>di head. Atributdefermenunda eksekusi sampai HTML selesai di-parse. Scripttype="module"otomatis berperilaku sepertidefer. - Memperlakukan NodeList seperti array.
document.querySelectorAll("li").map(...)error karenamaptidak ada. Ubah dulu:[...document.querySelectorAll("li")].map(...). - Memakai
childNodes/nextSiblinglalu bingung kenapa dapat text node. Enter dan spasi di HTML adalah text node. Untuk navigasi antar elemen, pakai versiElement:children,nextElementSibling,firstElementChild. - Lupa titik atau pagar di selector.
querySelector("item")mencari tag<item>, bukan class. Untuk class tulis".item", untuk id"#daftar-menu". - Menghitung langsung dari dataset.
li.dataset.harga + 1000menghasilkan"180001000"karena dataset selalu string. Ubah denganNumber()dulu.
Latihan
- Mudah, Dari HTML contoh di atas, tampilkan di console daftar nama menu yang berkategori
kopisaja, memakai selector atribut. - Sedang, Buat function
infoItem(id)yang menerima id menu, lalu mengembalikan object{ nama, harga, kategori, posisi, sebelum, sesudah }.posisiadalah urutan (mulai 1),sebelumdansesudahadalah nama menu tetangga ataunull. - Menantang, Buat function
bacaFilterDariURL()yang membaca?kategori=...&maks=...dari URL, lalu memberi classtersembunyipada setiap.itemyang tidak cocok (kategori berbeda atau harga di atasmaks). Kalau parameter tidak ada, jangan sembunyikan apa pun.
Lihat solusi
// 1. Mudah
const kopi = [...document.querySelectorAll('.item[data-kategori="kopi"]')].map((li) => li.textContent);
console.log(kopi); // → [ 'Espresso', 'Latte' ]
// 2. Sedang
function infoItem(id) {
const li = document.querySelector(`.item[data-id="${id}"]`);
if (!li) return null;
const saudara = [...li.parentElement.children];
return {
nama: li.textContent,
harga: Number(li.dataset.harga),
kategori: li.dataset.kategori,
posisi: saudara.indexOf(li) + 1,
sebelum: li.previousElementSibling?.textContent ?? null,
sesudah: li.nextElementSibling?.textContent ?? null,
};
}
console.log(infoItem(1));
// → { nama: 'Espresso', harga: 18000, kategori: 'kopi', posisi: 1, sebelum: null, sesudah: 'Latte' }
// 3. Menantang
function bacaFilterDariURL() {
const p = new URLSearchParams(location.search);
const kategori = p.get("kategori");
const maks = p.has("maks") ? Number(p.get("maks")) : Infinity;
document.querySelectorAll(".item").forEach((li) => {
const cocokKategori = !kategori || li.dataset.kategori === kategori;
const cocokHarga = Number(li.dataset.harga) <= maks;
li.classList.toggle("tersembunyi", !(cocokKategori && cocokHarga));
});
}
// Uji: buka index.html?kategori=kopi&maks=20000 → hanya Espresso yang tampil
bacaFilterDariURL();
Di CSS, tambahkan .tersembunyi { display: none; } supaya efeknya terlihat. classList.toggle dengan argumen kedua akan menambah class jika true dan menghapusnya jika false. Method ini dibahas lebih dalam di modul berikutnya.
Ringkasan
- Browser mengubah HTML menjadi DOM tree; setiap tag, teks, dan komentar adalah node dengan hubungan parent, child, dan sibling.
querySelector/querySelectorAllmemakai selector CSS dan bisa dipanggil dari elemen mana pun untuk mempersempit pencarian; hasilnya bisanullatau NodeList kosong.- Untuk traversing, pakai properti versi Element (
parentElement,children,nextElementSibling), plusclosest()danmatches(). - Atribut dibaca dengan
getAttribute/properti; data custom disimpan didata-*dan dibaca lewatdataset(selalu string). locationmembaca dan mengubah URL (pakaiURLSearchParamsuntuk query),history.pushStatemengubah URL tanpa reload,windowadalah object global browser.