kodingup.camp

Beranda/Advanced JavaScript: Interaksi Browser

Navigasi Dokumen Browser

Modul 2 dari 56 menit bacaBrowser Document Navigation

Isi modul

    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:

    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:

    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

    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

    1. Script jalan sebelum HTML selesai dibaca. Kalau <script> ditaruh di <head> tanpa atribut apa pun, document.querySelector("#daftar-menu") bernilai null karena elemennya belum ada. Solusi: taruh script sebelum </body>, atau pakai <script src="app.js" defer></script> di head. Atribut defer menunda eksekusi sampai HTML selesai di-parse. Script type="module" otomatis berperilaku seperti defer.
    2. Memperlakukan NodeList seperti array. document.querySelectorAll("li").map(...) error karena map tidak ada. Ubah dulu: [...document.querySelectorAll("li")].map(...).
    3. Memakai childNodes/nextSibling lalu bingung kenapa dapat text node. Enter dan spasi di HTML adalah text node. Untuk navigasi antar elemen, pakai versi Element: children, nextElementSibling, firstElementChild.
    4. Lupa titik atau pagar di selector. querySelector("item") mencari tag <item>, bukan class. Untuk class tulis ".item", untuk id "#daftar-menu".
    5. Menghitung langsung dari dataset. li.dataset.harga + 1000 menghasilkan "180001000" karena dataset selalu string. Ubah dengan Number() dulu.

    Latihan

    1. Mudah, Dari HTML contoh di atas, tampilkan di console daftar nama menu yang berkategori kopi saja, memakai selector atribut.
    2. Sedang, Buat function infoItem(id) yang menerima id menu, lalu mengembalikan object { nama, harga, kategori, posisi, sebelum, sesudah }. posisi adalah urutan (mulai 1), sebelum dan sesudah adalah nama menu tetangga atau null.
    3. Menantang, Buat function bacaFilterDariURL() yang membaca ?kategori=...&maks=... dari URL, lalu memberi class tersembunyi pada setiap .item yang tidak cocok (kategori berbeda atau harga di atas maks). 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

    Kuis pemahaman

    Pilih satu jawaban per soal. Penjelasan muncul setelah kamu menjawab.