Bab 14 dari Eloquent JavaScript. Sebelumnya: 13 JavaScript dan Browser. Berikutnya: 15 Menangani Event.

Sayang sekali! Cerita lama yang sama! Begitu selesai bangun rumahmu, kamu nyadar kamu gak sengaja belajar sesuatu yang seharusnya udah kamu tahu, sebelum kamu mulai.

— Friedrich Nietzsche, Beyond Good and Evil

Ilustrasi pohon dengan huruf, gambar, dan roda gigi tergantung di dahannya

Waktu kamu buka halaman web, browser ngambil teks HTML halaman itu dan mem-parse-nya, mirip parser kita dari Bab 12 mem-parse program. Browser ngebangun model struktur dokumennya dan pakai model ini buat ngegambar halaman di layar.

Representasi dokumen ini adalah salah satu mainan yang tersedia buat program JavaScript di sandbox-nya. Dia struktur data yang bisa kamu baca atau modifikasi. Dia bertindak sebagai struktur data hidup (live): waktu dia dimodifikasi, halaman di layar diupdate buat nyerminin perubahannya.

Struktur Dokumen

Kamu bisa ngebayangin dokumen HTML sebagai sekumpulan kotak bersarang. Tag kaya <body> dan </body> ngelingkupin tag lain, yang pada gilirannya berisi tag lain atau teks. Ini dokumen contoh dari bab sebelumnya:

<!doctype html>
<html>
  <head>
    <title>My home page</title>
  </head>
  <body>
    <h1>My home page</h1>
    <p>Hello, I am Marijn and this is my home page.</p>
    <p>I also wrote a book! Read it
      <a href="http://eloquentjavascript.net">here</a>.</p>
  </body>
</html>

Halaman ini punya struktur berikut:

Diagram yang menunjukkan dokumen HTML sebagai sekumpulan kotak bersarang. Kotak terluar dilabeli 'html' dan berisi dua kotak dilabeli 'head' dan 'body'. Di dalamnya ada kotak lagi, dengan beberapa kotak terdalam berisi teks dokumennya.

Struktur data yang dipakai browser buat mewakili dokumennya ngikutin bentuk ini. Buat setiap kotak, ada objek, yang bisa kita ajak interaksi buat nyari tahu hal-hal kaya tag HTML apa yang dia wakili dan kotak dan teks apa yang dia isi. Representasi ini disebut Document Object Model, atau DOM singkatnya.

Binding global document ngasih kita akses ke objek-objek ini. Properti documentElement-nya nunjuk ke objek yang mewakili tag <html>. Karena setiap dokumen HTML punya head dan body, dia juga punya properti head dan body yang nunjuk ke elemen-elemen itu.

Pohon (Tree)

Ingat lagi syntax tree dari Bab 12 sebentar. Strukturnya sangat mirip struktur dokumen browser. Setiap node (simpul) bisa nunjuk ke node lain, children (anak), yang pada gilirannya bisa punya anak mereka sendiri. Bentuk ini khas dari struktur bersarang, di mana elemen bisa berisi subelemen yang mirip dirinya.

Kita nyebut struktur data tree (pohon) waktu dia punya struktur bercabang, gak ada siklus (node gak boleh berisi dirinya sendiri, langsung atau gak langsung), dan satu root (akar) yang terdefinisi dengan baik. Di kasus DOM, document.documentElement berfungsi sebagai root.

Pohon sering muncul di ilmu komputer. Selain mewakili struktur rekursif kaya dokumen HTML atau program, mereka sering dipakai buat ngejaga kumpulan data yang terurut karena elemen biasanya bisa ditemuin atau disisipin lebih efisien di pohon daripada di array pipih.

Pohon tipikal punya jenis node yang berbeda. Syntax tree buat bahasa Egg punya identifier, value, dan node application. Node application bisa punya anak, sedangkan identifier dan value adalah leaf (daun), atau node tanpa anak.

Sama juga buat DOM. Node buat elemen, yang mewakili tag HTML, nentuin struktur dokumennya. Ini bisa punya node anak. Contoh node kaya gitu adalah document.body. Beberapa anak ini bisa jadi node daun, kaya potongan teks atau node komentar.

Setiap objek node DOM punya properti nodeType, yang berisi kode (angka) yang ngidentifikasi jenis node-nya. Elemen punya kode 1, yang juga didefinisikan sebagai properti konstan Node.ELEMENT_NODE. Node teks, yang mewakili bagian teks di dokumen, dapet kode 3 (Node.TEXT_NODE). Komentar punya kode 8 (Node.COMMENT_NODE).

Cara lain buat memvisualisasikan pohon dokumen kita adalah kaya berikut:

Diagram yang menunjukkan dokumen HTML sebagai pohon, dengan panah dari node induk ke node anak

Daunnya adalah node teks, dan panahnya nunjukin hubungan induk-anak antar node.

Standarnya

Pakai kode numerik yang samar buat mewakili jenis node bukan hal yang mirip-JavaScript buat dilakuin. Nanti di bab ini, kita bakal lihat bahwa bagian lain dari interface DOM juga terasa berat dan asing. Ini karena interface DOM gak didesain buat JavaScript sendirian. Sebaliknya, dia mencoba jadi interface yang netral-bahasa yang bisa dipakai di sistem lain juga, gak cuma buat HTML tapi juga buat XML, yang merupakan format data generik dengan sintaks mirip-HTML.

Ini disayangkan. Standar sering berguna. Tapi di kasus ini, keuntungannya (konsistensi lintas-bahasa) gak terlalu meyakinkan. Punya interface yang terintegrasi dengan benar sama bahasa yang kamu pakai bakal ngehemat waktumu lebih banyak daripada punya interface yang familiar lintas bahasa.

Sebagai contoh integrasi buruk ini, lihat properti childNodes yang dipunya node elemen di DOM. Properti ini megang objek mirip-array dengan properti length dan properti yang dilabeli angka buat ngakses node anaknya. Tapi dia instance dari tipe NodeList, bukan array beneran, jadi dia gak punya method kaya slice dan map.

Lalu ada masalah yang cuma disebabkan desain buruk. Misalnya, gak ada cara buat bikin node baru dan langsung nambahin anak atau atribut ke dia. Sebagai gantinya, kamu harus pertama bikin dia lalu nambahin anak dan atributnya satu per satu, pakai side effect. Kode yang banyak berinteraksi sama DOM cenderung jadi panjang, berulang, dan jelek.

Tapi cacat ini gak fatal. Karena JavaScript ngebolehin kita bikin abstraksi kita sendiri, bisa aja ngedesain cara yang lebih baik buat ngekspresiin operasi yang kita lakuin. Banyak library yang dimaksudkan buat pemrograman browser datang dengan alat kaya gitu.

Bergerak Lewat Pohon

Node DOM berisi banyak link ke node terdekat lain. Diagram berikut ngilustrasiin ini:

Diagram yang nunjukin link antar node DOM. Node 'body' ditunjukin sebagai kotak, dengan panah 'firstChild' nunjuk ke node 'h1' di awalnya, panah 'lastChild' nunjuk ke node paragraf terakhir, dan panah 'childNodes' nunjuk ke array link ke semua anaknya. Paragraf tengah punya panah 'previousSibling' nunjuk ke node sebelumnya, panah 'nextSibling' ke node setelahnya, dan panah 'parentNode' nunjuk ke node 'body'.

Walaupun diagramnya cuma nunjukin satu link dari setiap jenis, setiap node punya properti parentNode yang nunjuk ke node tempat dia jadi bagian, kalau ada. Sama juga, setiap node elemen (jenis node 1) punya properti childNodes yang nunjuk ke objek mirip-array yang megang anaknya.

Secara teori, kamu bisa pindah ke mana pun di pohon cuma pakai link induk dan anak ini. Tapi JavaScript juga ngasih kamu akses ke sejumlah link kemudahan tambahan. Properti firstChild dan lastChild nunjuk ke elemen anak pertama dan terakhir atau punya nilai null buat node tanpa anak. Sama juga, previousSibling dan nextSibling nunjuk ke node yang bersebelahan, yaitu node dengan induk yang sama yang muncul tepat sebelum atau sesudah node itu sendiri. Buat anak pertama, previousSibling bakal null, dan buat anak terakhir, nextSibling bakal null.

Ada juga properti children, yang kaya childNodes tapi cuma berisi anak elemen (jenis 1), bukan jenis node anak lain. Ini bisa berguna waktu kamu gak tertarik sama node teks.

Waktu nanganin struktur data bersarang kaya yang ini, fungsi rekursif sering berguna. Fungsi berikut mindai dokumen buat node teks yang berisi string tertentu dan ngembaliin true waktu dia nemuin satu:

function talksAbout(node, string) {
  if (node.nodeType == Node.ELEMENT_NODE) {
    for (let child of node.childNodes) {
      if (talksAbout(child, string)) {
        return true;
      }
    }
    return false;
  } else if (node.nodeType == Node.TEXT_NODE) {
    return node.nodeValue.indexOf(string) > -1;
  }
}
 
console.log(talksAbout(document.body, "book"));
// → true

Properti nodeValue dari node teks megang string teks yang dia wakili.

Nyari Elemen

Menavigasi link ini antar induk, anak, dan sibling sering berguna. Tapi kalau kita mau nemuin node tertentu di dokumen, nyapainnya dengan mulai di document.body dan ngikutin jalur properti yang tetap adalah ide buruk. Ngelakuin itu ngebakar asumsi ke program kita soal struktur persis dokumennya, struktur yang mungkin kamu mau ubah nanti. Faktor rumit lainnya adalah node teks dibikin bahkan buat whitespace di antara node. Tag <body> dokumen contoh gak cuma punya tiga anak (<h1> dan dua elemen <p>), tapi tujuh: tiga itu, plus spasi sebelum, sesudah, dan di antara mereka.

Kalau kita mau dapet atribut href dari link di dokumen itu, kita gak mau bilang sesuatu kaya “Ambil anak kedua dari anak keenam dari body dokumen”. Bakal lebih baik kalau kita bisa bilang “Ambil link pertama di dokumen”. Dan kita bisa.

let link = document.body.getElementsByTagName("a")[0];
console.log(link.href);

Semua node elemen punya method getElementsByTagName, yang ngumpulin semua elemen dengan nama tag yang dikasih yang merupakan keturunan (anak langsung atau gak langsung) dari node itu dan ngembaliin mereka sebagai objek mirip-array.

Buat nemuin satu node tertentu, kamu bisa ngasih dia atribut id dan pakai document.getElementById sebagai gantinya.

<p>My ostrich Gertrude:</p>
<p><img id="gertrude" src="img/ostrich.png"></p>
 
<script>
  let ostrich = document.getElementById("gertrude");
  console.log(ostrich.src);
</script>

Method ketiga yang serupa adalah getElementsByClassName, yang, kaya getElementsByTagName, nyari lewat isi node elemen dan ngambil semua elemen yang punya string yang dikasih di atribut class mereka.

Ngubah Dokumen

Hampir semua soal struktur data DOM bisa diubah. Bentuk pohon dokumen bisa dimodifikasi dengan ngubah hubungan induk-anak. Node punya method remove buat ngehapus mereka dari node induk mereka saat ini. Buat nambahin node anak ke node elemen, kita bisa pakai appendChild, yang naruhnya di akhir daftar anak, atau insertBefore, yang nyisipin node yang dikasih sebagai argumen pertama sebelum node yang dikasih sebagai argumen kedua.

<p>One</p>
<p>Two</p>
<p>Three</p>
 
<script>
  let paragraphs = document.body.getElementsByTagName("p");
  document.body.insertBefore(paragraphs[2], paragraphs[0]);
</script>

Node cuma bisa ada di dokumen di satu tempat. Jadi, nyisipin paragraf Three di depan paragraf One bakal pertama ngehapusnya dari akhir dokumen lalu nyisipinnya di depan, ngehasilin Three/One/Two. Semua operasi yang nyisipin node di suatu tempat bakal, sebagai side effect, bikin dia dihapus dari posisinya saat ini (kalau dia punya).

Method replaceChild dipakai buat ngeganti node anak dengan yang lain. Dia ngambil dua node sebagai argumen: node baru dan node yang mau diganti. Node yang diganti harus jadi anak dari elemen tempat method-nya dipanggil. Perhatiin bahwa replaceChild maupun insertBefore ngarepin node baru sebagai argumen pertama mereka.

Bikin Node

Misalnya kita mau nulis script yang ngeganti semua gambar (tag <img>) di dokumen dengan teks yang dipegang di atribut alt mereka, yang nentuin representasi tekstual alternatif dari gambarnya. Ini melibatkan bukan cuma ngehapus gambarnya tapi juga nambahin node teks baru buat ngegantiin mereka.

<p>The <img src="img/cat.png" alt="Cat"> in the
  <img src="img/hat.png" alt="Hat">.</p>
 
<p><button onclick="replaceImages()">Replace</button></p>
 
<script>
  function replaceImages() {
    let images = document.body.getElementsByTagName("img");
    for (let i = images.length - 1; i >= 0; i--) {
      let image = images[i];
      if (image.alt) {
        let text = document.createTextNode(image.alt);
        image.parentNode.replaceChild(text, image);
      }
    }
  }
</script>

Dikasih string, createTextNode ngasih kita node teks yang bisa kita sisipin ke dokumen biar dia muncul di layar.

Loop yang ngelewatin gambarnya mulai di akhir daftar. Ini perlu karena daftar node yang dikembaliin method kaya getElementsByTagName (atau properti kaya childNodes) itu live (hidup). Yaitu, dia diupdate seiring dokumennya berubah. Kalau kita mulai dari depan, ngehapus gambar pertama bakal bikin daftarnya kehilangan elemen pertamanya jadi kedua kali loop-nya ngulang, di mana i adalah 1, dia bakal berhenti karena panjang koleksinya sekarang juga 1.

Kalau kamu mau koleksi node yang padat (solid), berbeda dari yang live, kamu bisa ngonversi koleksinya jadi array beneran dengan manggil Array.from.

let arrayish = {0: "one", 1: "two", length: 2};
let array = Array.from(arrayish);
console.log(array.map(s => s.toUpperCase()));
// → ["ONE", "TWO"]

Buat bikin node elemen, kamu bisa pakai method document.createElement. Method ini ngambil nama tag dan ngembaliin node kosong baru dari jenis yang dikasih.

Contoh berikut mendefinisikan utilitas elt, yang bikin node elemen dan memperlakukan sisa argumennya sebagai anak buat node itu. Fungsi ini lalu dipakai buat nambahin atribusi ke sebuah kutipan.

<blockquote id="quote">
  No book can ever be finished. While working on it we learn
  just enough to find it immature the moment we turn away
  from it.
</blockquote>
 
<script>
  function elt(type, ...children) {
    let node = document.createElement(type);
    for (let child of children) {
      if (typeof child != "string") node.appendChild(child);
      else node.appendChild(document.createTextNode(child));
    }
    return node;
  }
 
  document.getElementById("quote").appendChild(
    elt("footer", "—",
        elt("strong", "Karl Popper"),
        ", preface to the second edition of ",
        elt("em", "The Open Society and Its Enemies"),
        ", 1950"));
</script>

Atribut

Beberapa atribut elemen, kaya href buat link, bisa diakses lewat properti dengan nama yang sama di objek DOM elemennya. Ini kasus buat kebanyakan atribut standar yang umum dipakai.

HTML ngebolehin kamu nyetel atribut apa pun yang kamu mau di node. Ini bisa berguna karena ngebolehin kamu nyimpen informasi ekstra di dokumen. Buat baca atau ngubah atribut kustom, yang gak tersedia sebagai properti objek biasa, kamu harus pakai method getAttribute dan setAttribute.

<p data-classified="secret">The launch code is 00000000.</p>
<p data-classified="unclassified">I have two feet.</p>
 
<script>
  let paras = document.body.getElementsByTagName("p");
  for (let para of Array.from(paras)) {
    if (para.getAttribute("data-classified") == "secret") {
      para.remove();
    }
  }
</script>

Disarankan buat ngasih prefix data- ke nama atribut buatan kaya gitu buat mastiin mereka gak bentrok dengan atribut lain.

Ada atribut yang umum dipakai, class, yang merupakan keyword di bahasa JavaScript. Karena alasan sejarah (beberapa implementasi JavaScript lama gak bisa nanganin nama properti yang cocok dengan keyword), properti yang dipakai buat ngakses atribut ini disebut className. Kamu juga bisa ngaksesnya di bawah nama aslinya, "class", dengan method getAttribute dan setAttribute.

Layout

Kamu mungkin nyadar bahwa jenis elemen yang berbeda ditata secara berbeda. Beberapa, kaya paragraf (<p>) atau heading (<h1>), makan seluruh lebar dokumen dan dirender di baris terpisah. Ini disebut elemen block. Yang lain, kaya link (<a>) atau elemen <strong>, dirender di baris yang sama dengan teks di sekitarnya. Elemen kaya gitu disebut elemen inline.

Buat dokumen apa pun, browser bisa ngitung layout, yang ngasih setiap elemen ukuran dan posisi berdasarkan jenis dan isinya. Layout ini lalu dipakai buat beneran ngegambar dokumennya.

Ukuran dan posisi elemen bisa diakses dari JavaScript. Properti offsetWidth dan offsetHeight ngasih kamu ruang yang dimakan elemen dalam piksel. Piksel adalah unit pengukuran dasar di browser. Dia secara tradisional sesuai dengan titik terkecil yang bisa digambar layar, tapi di layar modern, yang bisa ngegambar titik yang sangat kecil, itu mungkin gak lagi jadi kasusnya, dan satu piksel browser bisa membentang beberapa titik layar.

Sama juga, clientWidth dan clientHeight ngasih kamu ukuran ruang di dalam elemen, ngabaikan lebar border.

<p style="border: 3px solid red">
  I'm boxed in
</p>
 
<script>
  let para = document.body.getElementsByTagName("p")[0];
  console.log("clientHeight:", para.clientHeight);
  // → 19
  console.log("offsetHeight:", para.offsetHeight);
  // → 25
</script>

Cara paling efektif buat nemuin posisi persis elemen di layar adalah method getBoundingClientRect. Dia ngembaliin objek dengan properti top, bottom, left, dan right, yang nunjukin posisi piksel dari sisi-sisi elemen relatif terhadap kiri atas layar. Kalau kamu mau posisi piksel relatif terhadap seluruh dokumen, kamu harus nambahin posisi scroll saat ini, yang bisa kamu temuin di binding pageXOffset dan pageYOffset.

Nata dokumen bisa jadi cukup banyak kerjaan. Demi kecepatan, mesin browser gak langsung nata ulang dokumen setiap kali kamu ngubahnya tapi nunggu selama mungkin sebelum ngelakuinnya. Waktu program JavaScript yang ngubah dokumen selesai jalan, browser bakal harus ngitung layout baru buat ngegambar dokumen yang berubah ke layar. Waktu program minta posisi atau ukuran sesuatu dengan baca properti kaya offsetHeight atau manggil getBoundingClientRect, nyediain informasi itu juga butuh ngitung layout.

Program yang berulang kali bergantian antara baca informasi layout DOM dan ngubah DOM maksa banyak perhitungan layout terjadi dan akibatnya bakal jalan sangat lambat. Kode berikut adalah contohnya. Dia berisi dua program berbeda yang ngebangun baris karakter X selebar 2.000 piksel dan ngukur waktu masing-masing.

<p><span id="one"></span></p>
<p><span id="two"></span></p>
 
<script>
  function time(name, action) {
    let start = Date.now(); // Waktu saat ini dalam milidetik
    action();
    console.log(name, "took", Date.now() - start, "ms");
  }
 
  time("naive", () => {
    let target = document.getElementById("one");
    while (target.offsetWidth < 2000) {
      target.appendChild(document.createTextNode("X"));
    }
  });
  // → naive took 32 ms
 
  time("clever", function() {
    let target = document.getElementById("two");
    target.appendChild(document.createTextNode("XXXXX"));
    let total = Math.ceil(2000 / (target.offsetWidth / 5));
    target.firstChild.nodeValue = "X".repeat(total);
  });
  // → clever took 1 ms
</script>

Styling

Kita udah lihat bahwa elemen HTML yang berbeda digambar secara berbeda. Beberapa ditampilin sebagai block, yang lain inline. Beberapa nambahin styling, <strong> bikin isinya tebal, dan <a> bikinnya biru dan menggarisbawahinya.

Cara tag <img> nampilin gambar atau tag <a> bikin link diikuti waktu diklik terkait kuat dengan jenis elemennya. Tapi kita bisa ngubah styling yang terkait dengan elemen, kaya warna teks atau garis bawah. Ini contoh yang pakai properti style:

<p><a href=".">Normal link</a></p>
<p><a href="." style="color: green">Green link</a></p>

Atribut style bisa berisi satu deklarasi atau lebih, yang merupakan properti (kaya color) diikuti titik dua dan nilai (kaya green). Waktu ada lebih dari satu deklarasi, mereka harus dipisah titik koma, kaya di "color: red; border: none".

Banyak aspek dokumen bisa dipengaruhi styling. Misalnya, properti display ngendaliin apakah elemen ditampilin sebagai block atau elemen inline.

This text is displayed <strong>inline</strong>,
<strong style="display: block">as a block</strong>, and
<strong style="display: none">not at all</strong>.

Tag block bakal berakhir di barisnya sendiri, karena elemen block gak ditampilin inline dengan teks di sekitarnya. Tag terakhir gak ditampilin sama sekali, display: none nyegah elemen muncul di layar. Ini cara buat nyembunyiin elemen. Ini sering lebih disukai daripada ngehapusnya dari dokumen sepenuhnya karena bikin gampang munculin mereka lagi nanti.

Kode JavaScript bisa langsung memanipulasi style elemen lewat properti style elemennya. Properti ini megang objek yang punya properti buat semua properti style yang mungkin. Nilai properti ini adalah string, yang bisa kita tulis buat ngubah aspek tertentu dari style elemennya.

<p id="para" style="color: purple">
  Nice text
</p>
 
<script>
  let para = document.getElementById("para");
  console.log(para.style.color);
  para.style.color = "magenta";
</script>

Beberapa nama properti style berisi tanda hubung, kaya font-family. Karena nama properti kaya gitu canggung buat dikerjain di JavaScript (kamu harus bilang style["font-family"]), nama properti di objek style buat properti kaya gitu tanda hubungnya dihapus dan huruf setelahnya dikapitalisasi (style.fontFamily).

Cascading Styles

Sistem styling buat HTML disebut CSS, dari Cascading Style Sheets. Style sheet adalah sekumpulan aturan buat cara nge-style elemen di dokumen. Dia bisa dikasih di dalam tag <style>.

<style>
  strong {
    font-style: italic;
    color: gray;
  }
</style>
<p>Now <strong>strong text</strong> is italic and gray.</p>

Kata cascading di namanya ngerujuk ke fakta bahwa beberapa aturan kaya gitu digabung buat ngehasilin style akhir buat sebuah elemen. Di contoh, styling default buat tag <strong>, yang ngasih mereka font-weight: bold, ditumpangi aturan di tag <style>, yang nambahin font-style dan color.

Waktu beberapa aturan mendefinisikan nilai buat properti yang sama, aturan yang paling baru dibaca dapet prioritas lebih tinggi dan menang. Misalnya, kalau aturan di tag <style> nyertain font-weight: normal, bertentangan dengan aturan font-weight default, teksnya bakal normal, bukan tebal. Style di atribut style yang diterapin langsung ke node punya prioritas tertinggi dan selalu menang.

Bisa aja nargetin hal-hal selain nama tag di aturan CSS. Aturan buat .abc berlaku buat semua elemen dengan "abc" di atribut class mereka. Aturan buat #xyz berlaku buat elemen dengan atribut id "xyz" (yang harusnya unik di dalam dokumen).

.subtle {
  color: gray;
  font-size: 80%;
}
#header {
  background: blue;
  color: white;
}
/* elemen p dengan id main dan dengan class a dan b */
p#main.a.b {
  margin-bottom: 20px;
}

Aturan prioritas yang mengunggulkan aturan yang paling baru didefinisikan cuma berlaku waktu aturannya punya specificity (kekhususan) yang sama. Specificity aturan adalah ukuran seberapa presisi dia menggambarkan elemen yang cocok, ditentuin oleh jumlah dan jenis (tag, class, atau ID) aspek elemen yang dia butuhin. Misalnya, aturan yang nargetin p.a lebih spesifik dari aturan yang nargetin p atau cuma .a dan jadi bakal ngambil prioritas atas mereka.

Notasi p > a {…} nerapin style yang dikasih ke semua tag <a> yang merupakan anak langsung dari tag <p>. Sama juga, p a {…} berlaku buat semua tag <a> di dalam tag <p>, entah mereka anak langsung atau gak langsung.

Query Selector

Kita gak bakal banyak pakai style sheet di buku ini. Memahaminya membantu waktu memprogram di browser, tapi mereka cukup rumit buat pantas dapet buku terpisah. Alasan utama saya ngenalin sintaks selector (notasi yang dipakai di style sheet buat nentuin elemen mana sekumpulan style berlaku) adalah kita bisa pakai bahasa mini yang sama ini sebagai cara efektif buat nemuin elemen DOM.

Method querySelectorAll, yang didefinisikan baik di objek document maupun di node elemen, ngambil string selector dan ngembaliin NodeList yang berisi semua elemen yang dia cocokin.

<p>And if you go chasing
  <span class="animal">rabbits</span></p>
<p>And you know you're going to fall</p>
<p>Tell 'em a <span class="character">hookah smoking
  <span class="animal">caterpillar</span></span></p>
<p>Has given you the call</p>
 
<script>
  function count(selector) {
    return document.querySelectorAll(selector).length;
  }
  console.log(count("p"));           // Semua elemen <p>
  // → 4
  console.log(count(".animal"));     // Class animal
  // → 2
  console.log(count("p .animal"));   // Animal di dalam <p>
  // → 2
  console.log(count("p > .animal")); // Anak langsung dari <p>
  // → 1
</script>

Berbeda dari method kaya getElementsByTagName, objek yang dikembaliin querySelectorAll itu gak live. Dia gak bakal berubah waktu kamu ngubah dokumen. Tapi dia tetap bukan array beneran, jadi kamu perlu manggil Array.from kalau kamu mau memperlakukannya kaya array.

Method querySelector (tanpa bagian All) bekerja dengan cara serupa. Yang ini berguna kalau kamu mau satu elemen tertentu. Dia bakal cuma ngembaliin elemen yang cocok pertama, atau null waktu gak ada elemen yang cocok.

Positioning dan Animasi

Properti style position mempengaruhi layout dengan cara yang kuat. Dia punya nilai default static, artinya elemennya duduk di tempat normalnya di dokumen. Waktu dia disetel ke relative, elemennya masih makan ruang di dokumen, tapi sekarang properti style top dan left bisa dipakai buat mindahinnya relatif terhadap tempat normal itu. Waktu position disetel ke absolute, elemennya dihapus dari aliran dokumen normal, yaitu, dia gak lagi makan ruang dan mungkin tumpang tindih dengan elemen lain. Properti top dan left-nya bisa dipakai buat memposisikannya secara absolut relatif terhadap sudut kiri atas elemen terdekat yang ngelingkupinya yang properti position-nya bukan static, atau relatif terhadap dokumen kalau gak ada elemen pengelilinginya kaya gitu.

Kita bisa pakai ini buat bikin animasi. Dokumen berikut nampilin gambar kucing yang bergerak di sekitar elips:

<p style="text-align: center">
  <img src="img/cat.png" style="position: relative">
</p>
<script>
  let cat = document.querySelector("img");
  let angle = Math.PI / 2;
  function animate(time, lastTime) {
    if (lastTime != null) {
      angle += (time - lastTime) * 0.001;
    }
    cat.style.top = (Math.sin(angle) * 20) + "px";
    cat.style.left = (Math.cos(angle) * 200) + "px";
    requestAnimationFrame(newTime => animate(newTime, time));
  }
  requestAnimationFrame(animate);
</script>

Gambar kita dipusatkan di halaman dan dikasih position relative. Kita bakal berulang kali ngupdate style top dan left gambar itu buat mindahinnya.

Script-nya pakai requestAnimationFrame buat menjadwalkan fungsi animate jalan setiap kali browser siap ngegambar ulang layar. Fungsi animate sendiri lagi-lagi manggil requestAnimationFrame buat menjadwalkan update berikutnya. Waktu jendela browser (atau tab) aktif, ini bakal bikin update terjadi dengan laju sekitar 60 per detik, yang cenderung ngehasilin animasi yang keliatan bagus.

Kalau kita cuma ngupdate DOM di loop, halamannya bakal freeze, dan gak ada yang bakal muncul di layar. Browser gak ngupdate tampilan mereka waktu program JavaScript lagi jalan, mereka juga gak ngebolehin interaksi apa pun dengan halamannya. Inilah kenapa kita butuh requestAnimationFrame, dia ngasih tahu browser bahwa kita udah selesai buat sekarang, dan dia bisa lanjut ngelakuin hal-hal yang dilakuin browser, kaya ngupdate layar dan merespons aksi pengguna.

Fungsi animasinya dikasih waktu saat ini sebagai argumen. Buat mastiin gerakan kucingnya per milidetik stabil, dia mendasarkan kecepatan perubahan sudutnya pada selisih antara waktu saat ini dan waktu terakhir fungsinya jalan. Kalau dia cuma mindahin sudutnya sebesar jumlah tetap per langkah, gerakannya bakal tersendat waktu, misalnya, tugas berat lain yang jalan di komputer yang sama nyegah fungsinya jalan buat sepersekian detik.

Bergerak dalam lingkaran dilakuin pakai fungsi trigonometri Math.cos dan Math.sin. Buat yang gak akrab sama ini, saya bakal ngenalin mereka sebentar, karena kita bakal sesekali pakai mereka di buku ini.

Math.cos dan Math.sin berguna buat nemuin titik yang terletak di lingkaran di sekitar titik (0, 0) dengan radius 1. Kedua fungsi menafsirkan argumennya sebagai posisi di lingkaran ini, dengan 0 nunjukin titik di paling kanan lingkaran, pergi searah jarum jam sampai 2π (sekitar 6,28) udah bawa kita mengelilingi seluruh lingkaran. Math.cos ngasih tahu kamu koordinat-x dari titik yang sesuai dengan posisi yang dikasih, dan Math.sin ngasih koordinat-y. Posisi (atau sudut) yang lebih besar dari 2π atau kurang dari 0 itu valid, rotasinya berulang jadi a+2π ngerujuk ke sudut yang sama dengan a.

Unit buat ngukur sudut ini disebut radian, satu lingkaran penuh adalah 2π radian, mirip gimana dia 360 derajat waktu ngukur dalam derajat. Konstanta π tersedia sebagai Math.PI di JavaScript.

Diagram yang menunjukkan penggunaan kosinus dan sinus buat ngitung koordinat. Lingkaran dengan radius 1 ditunjukin dengan dua titik di atasnya. Sudut dari sisi kanan lingkaran ke titik, dalam radian, dipakai buat ngitung posisi setiap titik dengan pakai 'cos(angle)' buat jarak horizontal dari pusat lingkaran dan sin(angle) buat jarak vertikal.

Kode animasi kucing nyimpen counter, angle, buat sudut saat ini dari animasinya dan naikin dia setiap kali fungsi animate dipanggil. Dia lalu bisa pakai sudut ini buat ngitung posisi saat ini dari elemen gambarnya. Style top dihitung dengan Math.sin dan dikali 20, yaitu radius vertikal elips kita. Style left didasarkan pada Math.cos dan dikali 200 biar elipsnya jauh lebih lebar daripada tinggi.

Perhatiin bahwa style biasanya butuh unit. Di kasus ini, kita harus nambahin "px" ke angkanya buat ngasih tahu browser bahwa kita ngitung dalam piksel (berbeda dari sentimeter, “em”, atau unit lain). Ini gampang dilupain. Pakai angka tanpa unit bakal ngehasilin style-mu diabaikan, kecuali kalau angkanya 0, yang selalu berarti hal yang sama, terlepas dari unitnya.

Ringkasan

Program JavaScript bisa meriksa dan mengganggu dokumen yang lagi ditampilin browser lewat struktur data yang disebut DOM. Struktur data ini mewakili model browser dari dokumennya, dan program JavaScript bisa memodifikasinya buat ngubah dokumen yang terlihat.

DOM diorganisir kaya pohon, di mana elemen disusun hierarkis sesuai struktur dokumennya. Objek yang mewakili elemen punya properti kaya parentNode dan childNodes, yang bisa dipakai buat menavigasi lewat pohon ini.

Cara dokumen ditampilin bisa dipengaruhi styling, baik dengan nempelin style ke node langsung dan dengan mendefinisikan aturan yang cocok dengan node tertentu. Ada banyak properti style berbeda, kaya color atau display. Kode JavaScript bisa memanipulasi style elemen langsung lewat properti style-nya.

Poin-poin pentingnya:

  1. DOM = model browser dari dokumen, berbentuk pohon node yang live (ubah DOM → tampilan ikut berubah). Root-nya document.documentElement.
  2. Jenis node: elemen (nodeType 1), teks (3), komentar (8). Navigasi: parentNode, childNodes, children, firstChild, lastChild, previousSibling, nextSibling.
  3. Nyari elemen: getElementById, getElementsByTagName, getElementsByClassName, dan yang paling fleksibel querySelector/querySelectorAll (pakai selector CSS).
  4. Ngubah dokumen: appendChild, insertBefore, replaceChild, remove. Bikin node: createElement, createTextNode. Node cuma bisa ada di satu tempat.
  5. Atribut: properti langsung (href) atau getAttribute/setAttribute (buat data-*). class diakses lewat className.
  6. Layout: offsetWidth/Height, clientWidth/Height, getBoundingClientRect. Jangan bolak-balik baca layout + ubah DOM (lambat).
  7. Styling: atribut style, tag <style> dengan CSS (cascading, specificity). Dari JS: element.style.color, element.style.fontFamily (tanda hubung dihapus + camelCase). Ingat unit "px".
  8. Animasi: requestAnimationFrame + position: relative/absolute + Math.sin/Math.cos buat gerakan melingkar.

Latihan

Bikin Tabel

Tabel HTML dibangun dengan struktur tag berikut:

<table>
  <tr>
    <th>name</th>
    <th>height</th>
    <th>place</th>
  </tr>
  <tr>
    <td>Kilimanjaro</td>
    <td>5895</td>
    <td>Tanzania</td>
  </tr>
</table>

Buat setiap baris, tag <table> berisi tag <tr>. Di dalam tag <tr> ini, kita bisa naruh elemen sel: entah sel heading (<th>) atau sel biasa (<td>).

Dikasih dataset gunung, array objek dengan properti name, height, dan place, hasilkan struktur DOM buat tabel yang ngenumerasi objeknya. Dia punya satu kolom per kunci dan satu baris per objek, plus baris header dengan elemen <th> di atas, ngedaftar nama kolomnya.

Tulis ini biar kolomnya otomatis diturunin dari objeknya, dengan ngambil nama properti dari objek pertama di data.

Tampilin tabel hasilnya di dokumen dengan menambahkannya ke elemen yang punya atribut id "mountains".

Begitu ini jalan, rata-kanankan sel yang berisi nilai angka dengan nyetel properti style.textAlign mereka ke "right".

<h1>Mountains</h1>
 
<div id="mountains"></div>
 
<script>
  const MOUNTAINS = [
    {name: "Kilimanjaro", height: 5895, place: "Tanzania"},
    {name: "Everest", height: 8848, place: "Nepal"},
    {name: "Mount Fuji", height: 3776, place: "Japan"},
    {name: "Vaalserberg", height: 323, place: "Netherlands"},
    {name: "Denali", height: 6168, place: "United States"},
    {name: "Popocatepetl", height: 5465, place: "Mexico"},
    {name: "Mont Blanc", height: 4808, place: "Italy/France"}
  ];
 
  // Kodemu di sini
</script>

Petunjuk:

Kamu bisa pakai document.createElement buat bikin node elemen baru, document.createTextNode buat bikin node teks, dan method appendChild buat naruh node ke node lain.

Kamu bakal mau ngeloop nama kunci sekali buat ngisi baris atas lalu lagi buat setiap objek di array buat nyusun baris data. Buat dapet array nama kunci dari objek pertama, Object.keys bakal berguna.

Buat nambahin tabel ke node induk yang benar, kamu bisa pakai document.getElementById atau document.querySelector dengan "#mountains" buat nemuin node-nya.

Solusi:

<h1>Mountains</h1>
 
<div id="mountains"></div>
 
<script>
  const MOUNTAINS = [
    {name: "Kilimanjaro", height: 5895, place: "Tanzania"},
    {name: "Everest", height: 8848, place: "Nepal"},
    {name: "Mount Fuji", height: 3776, place: "Japan"},
    {name: "Vaalserberg", height: 323, place: "Netherlands"},
    {name: "Denali", height: 6168, place: "United States"},
    {name: "Popocatepetl", height: 5465, place: "Mexico"},
    {name: "Mont Blanc", height: 4808, place: "Italy/France"}
  ];
 
  function buildTable(data) {
    let table = document.createElement("table");
 
    let fields = Object.keys(data[0]);
    let headRow = document.createElement("tr");
    fields.forEach(function(field) {
      let headCell = document.createElement("th");
      headCell.appendChild(document.createTextNode(field));
      headRow.appendChild(headCell);
    });
    table.appendChild(headRow);
 
    data.forEach(function(object) {
      let row = document.createElement("tr");
      fields.forEach(function(field) {
        let cell = document.createElement("td");
        cell.appendChild(document.createTextNode(object[field]));
        if (typeof object[field] == "number") {
          cell.style.textAlign = "right";
        }
        row.appendChild(cell);
      });
      table.appendChild(row);
    });
 
    return table;
  }
 
  document.querySelector("#mountains")
    .appendChild(buildTable(MOUNTAINS));
</script>

Elemen Berdasarkan Nama Tag

Method document.getElementsByTagName ngembaliin semua elemen anak dengan nama tag tertentu. Implementasikan versimu sendiri dari ini sebagai fungsi yang ngambil node dan string (nama tag) sebagai argumen dan ngembaliin array yang berisi semua node elemen keturunan dengan nama tag yang dikasih. Fungsimu harus ngelewatin dokumennya sendiri. Dia gak boleh pakai method kaya querySelectorAll buat ngelakuin kerjaannya.

Buat nemuin nama tag elemen, pakai properti nodeName-nya. Tapi perhatiin bahwa ini bakal ngembaliin nama tag dalam huruf besar semua. Pakai method string toLowerCase atau toUpperCase buat mengompensasi ini.

<h1>Heading with a <span>span</span> element.</h1>
<p>A paragraph with <span>one</span>, <span>two</span>
  spans.</p>
 
<script>
  function byTagName(node, tagName) {
    // Kodemu di sini.
  }
 
  console.log(byTagName(document.body, "h1").length);
  // → 1
  console.log(byTagName(document.body, "span").length);
  // → 3
  let para = document.querySelector("p");
  console.log(byTagName(para, "span").length);
  // → 2
</script>

Petunjuk:

Solusinya paling gampang diekspresikan dengan fungsi rekursif, mirip fungsi `talksAbout` yang didefinisikan sebelumnya di bab ini.

Kamu bisa manggil byTagName sendiri secara rekursif, ngegabungin array hasilnya buat ngehasilin output. Atau kamu bisa bikin fungsi dalam yang manggil dirinya sendiri secara rekursif dan yang punya akses ke binding array yang didefinisikan di fungsi luar, tempat dia bisa nambahin elemen cocok yang dia temuin. Jangan lupa manggil fungsi dalamnya sekali dari fungsi luar buat mulai prosesnya.

Fungsi rekursifnya harus ngecek jenis node-nya. Di sini kita cuma tertarik sama jenis node 1 (Node.ELEMENT_NODE). Buat node kaya gitu, kita harus ngeloop anak mereka dan, buat setiap anak, lihat apakah anaknya cocok dengan query-nya sambil juga ngelakuin pemanggilan rekursif di dia buat meriksa anaknya sendiri.

Solusi:

<h1>Heading with a <span>span</span> element.</h1>
<p>A paragraph with <span>one</span>, <span>two</span>
  spans.</p>
 
<script>
  function byTagName(node, tagName) {
    let found = [];
    tagName = tagName.toUpperCase();
 
    function explore(node) {
      for (let i = 0; i < node.childNodes.length; i++) {
        let child = node.childNodes[i];
        if (child.nodeType == document.ELEMENT_NODE) {
          if (child.nodeName == tagName) found.push(child);
          explore(child);
        }
      }
    }
 
    explore(node);
    return found;
  }
 
  console.log(byTagName(document.body, "h1").length);
  // → 1
  console.log(byTagName(document.body, "span").length);
  // → 3
  let para = document.querySelector("p");
  console.log(byTagName(para, "span").length);
  // → 2
</script>

Topi si Kucing

Perluas animasi kucing yang didefinisikan sebelumnya biar kucingnya dan topinya (<img src="img/hat.png">) mengorbit di sisi berlawanan dari elipsnya.

Atau bikin topinya mengelilingi kucingnya. Atau ubah animasinya dengan cara menarik lain.

Biar memposisikan beberapa objek lebih gampang, kamu mungkin mau beralih ke absolute positioning. Ini berarti top dan left dihitung relatif terhadap kiri atas dokumen. Buat ngehindarin pakai koordinat negatif, yang bakal bikin gambarnya bergerak keluar dari halaman yang terlihat, kamu bisa nambahin jumlah piksel tetap ke nilai posisinya.

<style>body { min-height: 200px }</style>
<img src="img/cat.png" id="cat" style="position: absolute">
<img src="img/hat.png" id="hat" style="position: absolute">
 
<script>
  let cat = document.querySelector("#cat");
  let hat = document.querySelector("#hat");
 
  let angle = 0;
  let lastTime = null;
  function animate(time) {
    if (lastTime != null) angle += (time - lastTime) * 0.001;
    lastTime = time;
    cat.style.top = (Math.sin(angle) * 40 + 40) + "px";
    cat.style.left = (Math.cos(angle) * 200 + 230) + "px";
 
    // Ekstensimu di sini.
 
    requestAnimationFrame(animate);
  }
  requestAnimationFrame(animate);
</script>

Petunjuk:

Math.cos dan Math.sin ngukur sudut dalam radian, di mana satu lingkaran penuh adalah 2π. Buat sudut tertentu, kamu bisa dapet sudut berlawanannya dengan nambahin setengah dari ini, yaitu Math.PI. Ini bisa berguna buat naruh topinya di sisi berlawanan dari orbitnya.

Solusi:

<style>body { min-height: 200px }</style>
<img src="img/cat.png" id="cat" style="position: absolute">
<img src="img/hat.png" id="hat" style="position: absolute">
 
<script>
  let cat = document.querySelector("#cat");
  let hat = document.querySelector("#hat");
 
  let angle = 0;
  let lastTime = null;
  function animate(time) {
    if (lastTime != null) angle += (time - lastTime) * 0.001;
    lastTime = time;
    cat.style.top = (Math.sin(angle) * 40 + 40) + "px";
    cat.style.left = (Math.cos(angle) * 200 + 230) + "px";
    hat.style.top = (Math.sin(angle + Math.PI) * 40 + 40) + "px";
    hat.style.left = (Math.cos(angle + Math.PI) * 200 + 230) + "px";
 
    requestAnimationFrame(animate);
  }
  requestAnimationFrame(animate);
</script>