Bab 18 dari Eloquent JavaScript. Sebelumnya: 17 Menggambar di Canvas. Berikutnya: 19 Proyek Editor Pixel Art.

Yang sering susah dipahami orang soal desainnya adalah gak ada apa pun di luar URL, HTTP, dan HTML. Gak ada komputer pusat yang ‘mengendalikan’ web, gak ada satu jaringan tempat protokol ini bekerja, bahkan gak ada organisasi di mana pun yang ‘menjalankan’ Web. Web bukan ‘benda’ fisik yang ada di suatu ‘tempat’. Dia ‘ruang’ tempat informasi bisa ada.

— Tim Berners-Lee

Ilustrasi form pendaftaran web di gulungan perkamen

Hypertext Transfer Protocol, diperkenalkan di Bab 13, adalah mekanisme yang lewatnya data diminta dan disediain di World Wide Web. Bab ini menggambarkan protokolnya lebih detail dan ngejelasin cara JavaScript browser punya akses ke dia.

Protokolnya

Kalau kamu ngetik eloquentjavascript.net/18_http.html di bar alamat browsermu, browser pertama nyari alamat server yang terkait dengan eloquentjavascript.net dan nyoba buka koneksi TCP ke dia di port 80, port default buat lalu lintas HTTP. Kalau server-nya ada dan nerima koneksinya, browser mungkin ngirim sesuatu kaya gini:

GET /18_http.html HTTP/1.1
Host: eloquentjavascript.net
User-Agent: Your browser's name

Lalu server-nya merespons, lewat koneksi yang sama itu.

HTTP/1.1 200 OK
Content-Length: 87320
Content-Type: text/html
Last-Modified: Fri, 13 Oct 2023 10:05:41 GMT
 
<!doctype html>
... sisa dokumennya

Browser ngambil bagian respons setelah baris kosong, body-nya (jangan ketuker sama tag HTML <body>), dan nampilinnya sebagai dokumen HTML.

Informasi yang dikirim client disebut request (permintaan). Dia mulai dengan baris ini:

GET /18_http.html HTTP/1.1

Kata pertama adalah method dari request-nya. GET berarti kita mau dapet (get) sumber daya yang ditentuin. Method umum lainnya adalah DELETE buat ngehapus sumber daya, PUT buat bikin atau ngeganti dia, dan POST buat ngirim informasi ke dia. Perhatiin bahwa server-nya gak wajib ngelakuin setiap request yang dia dapet. Kalau kamu nyamperin website acak dan nyuruh dia DELETE halaman utamanya, dia mungkin bakal nolak.

Bagian setelah nama method adalah path dari resource (sumber daya) yang request-nya berlaku. Di kasus paling sederhana, resource cuma file di server, tapi protokolnya gak ngeharusin itu. Resource bisa apa pun yang bisa ditransfer seolah dia file. Banyak server ngehasilin respons yang mereka bikin secara langsung. Misalnya, kalau kamu buka https://github.com/marijnh, server-nya nyari di database-nya buat pengguna bernama “marijnh”, dan kalau dia nemuin satu, dia bakal ngehasilin halaman profil buat pengguna itu.

Setelah path resource, baris pertama request nyebutin HTTP/1.1 buat nunjukin versi protokol HTTP yang dia pakai.

Dalam praktik, banyak situs pakai HTTP versi 2, yang mendukung konsep yang sama dengan versi 1.1 tapi jauh lebih rumit biar dia bisa lebih cepat. Browser bakal otomatis beralih ke versi protokol yang tepat waktu ngomong ke server tertentu, dan hasil request-nya sama terlepas dari versi mana yang dipakai. Karena versi 1.1 lebih lugas dan lebih gampang dimainkan, kita bakal pakai itu buat ngilustrasiin protokolnya.

Respons server-nya bakal mulai dengan versi juga, diikuti status respons-nya, pertama sebagai kode status tiga digit lalu sebagai string yang bisa dibaca manusia.

HTTP/1.1 200 OK

Kode status yang mulai dengan 2 nunjukin request-nya berhasil. Kode yang mulai dengan 4 berarti ada yang salah dengan request-nya. Kode status HTTP paling terkenal mungkin 404, yang berarti resource-nya gak bisa ditemuin. Kode yang mulai dengan 5 berarti error terjadi di server dan request-nya gak salah.

Baris pertama request atau respons boleh diikuti berapa pun header. Ini baris dalam bentuk name: value yang nentuin informasi ekstra soal request atau respons-nya. Header ini bagian dari respons contoh:

Content-Length: 87320
Content-Type: text/html
Last-Modified: Fri, 13 Oct 2023 10:05:41 GMT

Ini ngasih tahu kita ukuran dan jenis dokumen respons-nya. Di kasus ini, dia dokumen HTML sebesar 87.320 byte. Dia juga ngasih tahu kita kapan dokumen itu terakhir dimodifikasi.

Client dan server bebas mutusin header apa yang disertain di request atau respons mereka. Tapi beberapa dari mereka perlu biar hal-hal bekerja. Misalnya, tanpa header Content-Type di respons-nya, browser gak bakal tahu cara nampilin dokumennya.

Setelah header-nya, request maupun respons boleh nyertain baris kosong diikuti body, yang berisi dokumen aktual yang dikirim. Request GET dan DELETE gak ngirim data apa pun, tapi request PUT dan POST iya. Beberapa jenis respons, kaya respons error, juga gak butuh body.

Browser dan HTTP

Kaya yang kita lihat, browser bakal bikin request waktu kita masukin URL di bar alamatnya. Waktu halaman HTML hasilnya ngerujuk file lain, kaya gambar dan file JavaScript, dia bakal ngambil itu juga.

Website yang cukup rumit dengan gampang bisa nyertain di mana pun dari 10 sampai 200 resource. Biar bisa ngambil itu dengan cepat, browser bakal bikin beberapa request GET sekaligus, bukannya nunggu respons satu per satu.

Halaman HTML boleh nyertain form, yang ngebolehin pengguna ngisi informasi dan ngirimnya ke server. Ini contoh form:

<form method="GET" action="example/message.html">
  <p>Name: <input type="text" name="name"></p>
  <p>Message:<br><textarea name="message"></textarea></p>
  <p><button type="submit">Send</button></p>
</form>

Kode ini menggambarkan form dengan dua field: yang kecil minta nama dan yang lebih besar buat nulis pesan. Waktu kamu klik tombol Send, form-nya di-submit, artinya isi field-nya dikemas ke request HTTP dan browser menavigasi ke hasil request itu.

Waktu atribut method elemen <form> adalah GET (atau dihilangin), informasi di form-nya ditambahin ke akhir URL action sebagai query string. Browser mungkin bikin request ke URL ini:

GET /example/message.html?name=Jean&message=Yes%3F HTTP/1.1

Tanda tanya nunjukin akhir bagian path URL dan awal query-nya. Dia diikuti pasangan nama dan nilai, sesuai dengan atribut name di elemen field form dan isi elemen itu. Karakter ampersand (&) dipakai buat misahin pasangannya.

Pesan aktual yang dikodekan di URL adalah “Yes?” tapi tanda tanyanya diganti dengan kode aneh. Beberapa karakter di query string harus di-escape. Tanda tanya, diwakili sebagai %3F, salah satunya. Sepertinya ada aturan tak tertulis bahwa setiap format butuh cara sendiri buat escape karakter. Yang ini, disebut URL encoding, pakai tanda persen diikuti dua digit heksadesimal (basis 16) yang mengkodekan kode karakternya. Di kasus ini, 3F, yang 63 di notasi desimal, adalah kode karakter tanda tanya. JavaScript nyediain fungsi encodeURIComponent dan decodeURIComponent buat mengkodekan dan mendekodekan format ini.

console.log(encodeURIComponent("Yes?"));
// → Yes%3F
console.log(decodeURIComponent("Yes%3F"));
// → Yes?

Kalau kita ngubah atribut method dari form HTML di contoh yang kita lihat sebelumnya jadi POST, request HTTP yang dibikin buat submit form-nya bakal pakai method POST dan naruh query string di body request-nya bukannya nambahinnya ke URL.

POST /example/message.html HTTP/1.1
Content-length: 24
Content-type: application/x-www-form-urlencoded
 
name=Jean&message=Yes%3F

Request GET harusnya dipakai buat request yang gak punya side effect tapi cuma minta informasi. Request yang ngubah sesuatu di server, misalnya bikin akun baru atau posting pesan, harusnya diekspresikan dengan method lain, kaya POST. Software di sisi client kaya browser tahu bahwa dia gak boleh membabi buta bikin request POST tapi sering bakal secara implisit bikin request GET, buat mengambil-lebih-awal (prefetch) resource yang dia percaya bakal segera dibutuhin pengguna, misalnya.

Kita bakal balik ke form dan cara berinteraksi dengan mereka dari JavaScript nanti di bab.

Fetch

Interface yang lewatnya JavaScript browser bisa bikin request HTTP disebut fetch.

fetch("example/data.txt").then(response => {
  console.log(response.status);
  // → 200
  console.log(response.headers.get("Content-Type"));
  // → text/plain
});

Manggil fetch ngembaliin promise yang resolve ke objek Response yang megang informasi soal respons server, kaya kode status dan header-nya. Header-nya dibungkus dalam objek mirip-Map yang memperlakukan kunci-kuncinya (nama header) sebagai case insensitive karena nama header seharusnya gak case sensitive. Ini berarti headers.get("Content-Type") dan headers.get("content-TYPE") bakal ngembaliin nilai yang sama.

Perhatiin bahwa promise yang dikembaliin fetch resolve dengan sukses bahkan kalau server-nya merespons dengan kode error. Dia juga bisa di-reject kalau ada error jaringan atau kalau server yang dituju request-nya gak bisa ditemuin.

Argumen pertama ke fetch adalah URL yang harus diminta. Waktu URL itu gak mulai dengan nama protokol (kaya http:), dia diperlakukan sebagai relatif, yang berarti dia ditafsirin relatif terhadap dokumen saat ini. Waktu dia mulai dengan garis miring (/), dia ngeganti path saat ini, yaitu bagian setelah nama server. Waktu dia gak, bagian path saat ini sampai dan termasuk karakter garis miring terakhirnya ditaruh di depan URL relatif-nya.

Buat ngambil isi aktual respons, kamu bisa pakai method text-nya. Karena promise awalnya di-resolve begitu header respons-nya udah diterima dan karena baca body respons-nya mungkin butuh waktu lebih lama, ini lagi-lagi ngembaliin promise.

fetch("example/data.txt")
  .then(resp => resp.text())
  .then(text => console.log(text));
// → This is the content of data.txt

Method serupa, disebut json, ngembaliin promise yang resolve ke nilai yang kamu dapet waktu mem-parse body-nya sebagai JSON atau reject kalau dia bukan JSON yang valid.

Secara default, fetch pakai method GET buat bikin request-nya dan gak nyertain body request. Kamu bisa mengonfigurasinya secara berbeda dengan ngasih objek dengan opsi ekstra sebagai argumen kedua. Misalnya, request ini nyoba ngehapus example/data.txt:

fetch("example/data.txt", {method: "DELETE"}).then(resp => {
  console.log(resp.status);
  // → 405
});

Kode status 405 berarti “method not allowed”, cara server HTTP bilang “Maaf saya gak bisa ngelakuin itu”.

Buat nambahin body request buat request PUT atau POST, kamu bisa nyertain opsi body. Buat nyetel header, ada opsi headers. Misalnya, request ini nyertain header Range, yang nginstruksiin server buat ngembaliin cuma sebagian dokumen.

fetch("example/data.txt", {headers: {Range: "bytes=8-19"}})
  .then(resp => resp.text())
  .then(console.log);
// → the content

Browser bakal otomatis nambahin beberapa header request, kaya “Host” dan yang dibutuhin server buat nyari tahu ukuran body-nya. Tapi nambahin header sendiri sering berguna buat nyertain hal kaya informasi autentikasi atau buat ngasih tahu server format file mana yang kamu mau terima.

Sandboxing HTTP

Bikin request HTTP di script halaman web lagi-lagi memunculkan kekhawatiran soal keamanan. Orang yang ngendaliin script-nya mungkin gak punya kepentingan yang sama dengan orang yang di komputernya dia jalan. Lebih spesifiknya, kalau saya ngunjungin themafia.org, saya gak mau script-nya bisa bikin request ke mybank.com, pakai informasi identifikasi dari browser saya, dengan instruksi buat mentransfer semua uang saya.

Karena alasan ini, browser melindungi kita dengan ngelarang script bikin request HTTP ke domain lain (nama kaya themafia.org dan mybank.com).

Ini bisa jadi masalah yang menyebalkan waktu ngebangun sistem yang mau ngakses beberapa domain karena alasan yang sah. Untungnya, server bisa nyertain header kaya ini di respons mereka buat secara eksplisit nunjukin ke browser bahwa gak apa-apa request-nya datang dari domain lain:

Access-Control-Allow-Origin: *

Menghargai HTTP

Waktu ngebangun sistem yang butuh komunikasi antara program JavaScript yang jalan di browser (sisi client) dan program di server (sisi server), ada beberapa cara berbeda buat memodelkan komunikasi ini.

Model yang umum dipakai adalah remote procedure call. Di model ini, komunikasi ngikutin pola pemanggilan fungsi normal, kecuali fungsinya sebenernya jalan di mesin lain. Manggilnya melibatkan bikin request ke server yang nyertain nama fungsi dan argumennya. Respons ke request itu berisi nilai yang dikembaliin.

Waktu mikir dalam kerangka remote procedure call, HTTP cuma kendaraan buat komunikasi, dan kamu kemungkinan bakal nulis lapisan abstraksi yang nyembunyiinnya sepenuhnya.

Pendekatan lain adalah ngebangun komunikasimu di sekitar konsep resource dan method HTTP. Bukannya remote procedure bernama addUser, kamu pakai request PUT ke /users/larry. Bukannya mengkodekan properti pengguna itu di argumen fungsi, kamu mendefinisikan format dokumen JSON (atau pakai format yang udah ada) yang mewakili pengguna. Body request PUT buat bikin resource baru lalu jadi dokumen kaya gitu. Resource diambil dengan bikin request GET ke URL resource-nya (misalnya, /users/larry), yang lagi ngembaliin dokumen yang mewakili resource-nya.

Pendekatan kedua ini bikin lebih gampang pakai beberapa fitur yang disediain HTTP, kaya dukungan buat caching resource (nyimpen salinan resource di client buat akses cepat). Konsep yang dipakai di HTTP, yang didesain dengan baik, bisa nyediain sekumpulan prinsip yang membantu buat ngedesain interface server-mu di sekitarnya.

Keamanan dan HTTPS

Data yang bepergian lewat internet cenderung ngikutin jalan yang panjang dan berbahaya. Buat nyampe tujuannya, dia harus melompat lewat apa pun dari hotspot Wi-Fi kedai kopi sampai jaringan yang dikendalikan berbagai perusahaan dan negara. Di titik mana pun sepanjang rutenya, dia bisa diperiksa atau bahkan dimodifikasi.

Kalau penting bahwa sesuatu tetap rahasia, kaya password akun email-mu, atau bahwa dia nyampe tujuannya tanpa dimodifikasi, kaya nomor rekening yang kamu transfer uang lewat website bank-mu, HTTP biasa gak cukup baik.

Protokol HTTP aman, dipakai buat URL yang mulai dengan https://, ngebungkus lalu lintas HTTP dengan cara yang bikin lebih susah dibaca dan diutak-atik. Sebelum bertukar data, client memverifikasi bahwa server-nya adalah siapa yang dia klaim dengan minta dia membuktikan bahwa dia punya sertifikat kriptografi yang diterbitkan sertifikat authority yang dikenali browser. Berikutnya, semua data yang lewat koneksinya dienkripsi dengan cara yang harusnya nyegah penyadapan dan pengutak-atikan.

Jadi, waktu dia bekerja dengan benar, HTTPS nyegah orang lain menyamar sebagai website yang lagi kamu coba ajak ngomong dan menguping komunikasimu. Dia gak sempurna, dan udah ada berbagai insiden di mana HTTPS gagal karena sertifikat palsu atau curian dan software rusak, tapi dia jauh lebih aman dari HTTP biasa.

Field Form

Form awalnya didesain buat web pra-JavaScript buat ngebolehin website ngirim informasi yang di-submit pengguna di request HTTP. Desain ini berasumsi bahwa interaksi dengan server selalu terjadi dengan menavigasi ke halaman baru.

Tapi, elemen form itu bagian dari DOM, kaya sisa halaman, dan elemen DOM yang mewakili field form mendukung sejumlah properti dan event yang gak ada di elemen lain. Ini bikin mungkin buat meriksa dan ngendaliin field input kaya gitu dengan program JavaScript dan ngelakuin hal kaya nambahin fungsionalitas baru ke form atau pakai form dan field sebagai blok bangunan di aplikasi JavaScript.

Form web terdiri dari berapa pun field input yang dikelompokin dalam tag <form>. HTML ngebolehin beberapa gaya field berbeda, mulai dari checkbox on/off sederhana sampai menu dropdown dan field buat input teks. Buku ini gak bakal nyoba ngebahas semua jenis field secara menyeluruh, tapi kita bakal mulai dengan gambaran kasar.

Banyak jenis field pakai tag <input>. Atribut type tag ini dipakai buat milih gaya field-nya. Ini beberapa jenis <input> yang umum dipakai:

TypeDeskripsi
textField teks satu baris
passwordSama kaya text tapi nyembunyiin teks yang diketik
checkboxSaklar on/off
colorWarna
dateTanggal kalender
radio(Bagian dari) field pilihan ganda
fileNgebolehin pengguna milih file dari komputernya

Field form gak harus muncul di tag <form>. Kamu bisa naruh mereka di mana pun di halaman. Field tanpa-form kaya gitu gak bisa di-submit (cuma form secara keseluruhan yang bisa), tapi waktu merespons input dengan JavaScript, kita sering gak mau submit field kita secara normal juga.

<p><input type="text" value="abc"> (text)</p>
<p><input type="password" value="abc"> (password)</p>
<p><input type="checkbox" checked> (checkbox)</p>
<p><input type="color" value="orange"> (color)</p>
<p><input type="date" value="2023-10-13"> (date)</p>
<p><input type="radio" value="A" name="choice">
   <input type="radio" value="B" name="choice" checked>
   <input type="radio" value="C" name="choice"> (radio)</p>
<p><input type="file"> (file)</p>

Interface JavaScript buat elemen kaya gitu beda dengan jenis elemennya.

Field teks multi-baris punya tag sendiri, <textarea>, terutama karena pakai atribut buat nentuin nilai awal multi-baris bakal canggung. Tag <textarea> butuh tag penutup </textarea> yang cocok dan pakai teks di antara dua itu, bukannya atribut value, sebagai teks awal.

<textarea>
one
two
three
</textarea>

Terakhir, tag <select> dipakai buat bikin field yang ngebolehin pengguna milih dari sejumlah opsi yang udah ditentuin.

<select>
  <option>Pancakes</option>
  <option>Pudding</option>
  <option>Ice cream</option>
</select>

Setiap kali nilai field form berubah, dia bakal nembakin event "change".

Focus

Berbeda dari kebanyakan elemen di dokumen HTML, field form bisa dapet keyboard focus. Waktu diklik, dipindahi dengan tab, atau diaktifin dengan cara lain, mereka jadi elemen yang aktif saat ini dan penerima input keyboard.

Jadi, kamu cuma bisa ngetik di field teks waktu dia fokus. Field lain merespons event keyboard secara berbeda. Misalnya, menu <select> nyoba pindah ke opsi yang berisi teks yang diketik pengguna dan merespons tombol panah dengan mindahin seleksinya naik dan turun.

Kita bisa ngendaliin focus dari JavaScript dengan method focus dan blur. Yang pertama mindahin focus ke elemen DOM tempat dia dipanggil, dan yang kedua ngehapus focus. Nilai di document.activeElement sesuai dengan elemen yang saat ini fokus.

<input type="text">
<script>
  document.querySelector("input").focus();
  console.log(document.activeElement.tagName);
  // → INPUT
  document.querySelector("input").blur();
  console.log(document.activeElement.tagName);
  // → BODY
</script>

Buat beberapa halaman, pengguna diharapkan mau berinteraksi dengan field form langsung. JavaScript bisa dipakai buat fokusin field ini waktu dokumennya dimuat, tapi HTML juga nyediain atribut autofocus, yang ngehasilin efek yang sama sambil ngasih tahu browser apa yang kita coba capai. Ini ngasih browser opsi buat menonaktifkan perilakunya waktu dia gak tepat, kaya waktu pengguna udah naruh focus di sesuatu yang lain.

Browser ngebolehin pengguna mindahin focus lewat dokumen dengan mencet tab buat pindah ke elemen yang bisa difokus berikutnya, dan shift-tab buat pindah balik ke elemen sebelumnya. Secara default, elemen dikunjungi dalam urutan mereka muncul di dokumen. Bisa aja pakai atribut tabindex buat ngubah urutan ini. Dokumen contoh berikut bakal ngebiarin focus lompat dari input teks ke tombol OK, bukannya ngelewatin link help dulu:

<input type="text" tabindex=1> <a href=".">(help)</a>
<button onclick="console.log('ok')" tabindex=2>OK</button>

Secara default, kebanyakan jenis elemen HTML gak bisa difokus. Kamu bisa nambahin atribut tabindex ke elemen apa pun buat bikinnya bisa difokus. tabindex 0 bikin elemen bisa difokus tanpa mempengaruhi urutan focus.

Field yang Dinonaktifkan

Semua field form bisa dinonaktifkan (disabled) lewat atribut disabled mereka. Ini atribut yang bisa ditentuin tanpa nilai, fakta bahwa dia ada sama sekali menonaktifkan elemennya.

<button>I'm all right</button>
<button disabled>I'm out</button>

Field yang dinonaktifkan gak bisa difokus atau diubah, dan browser bikin mereka keliatan abu-abu dan pudar.

Waktu program lagi dalam proses nanganin aksi yang disebabkan suatu tombol atau kontrol lain yang mungkin butuh komunikasi dengan server dan jadi butuh waktu, bisa jadi ide bagus buat nonaktifin kontrolnya sampai aksinya selesai. Dengan cara itu, waktu pengguna jadi tidak sabar dan ngekliknya lagi, mereka gak gak sengaja ngulang aksinya.

Form Secara Keseluruhan

Waktu field terkandung di elemen <form>, elemen DOM-nya bakal punya properti form yang nge-link balik ke elemen DOM form-nya. Elemen <form>, pada gilirannya, punya properti bernama elements yang berisi koleksi mirip-array dari field di dalamnya.

Atribut name dari field form nentuin cara nilainya bakal diidentifikasi waktu form-nya di-submit. Dia juga bisa dipakai sebagai nama properti waktu ngakses properti elements form, yang bertindak baik sebagai objek mirip-array (bisa diakses dengan angka) dan map (bisa diakses dengan nama).

<form action="example/submit.html">
  Name: <input type="text" name="name"><br>
  Password: <input type="password" name="password"><br>
  <button type="submit">Log in</button>
</form>
<script>
  let form = document.querySelector("form");
  console.log(form.elements[1].type);
  // → password
  console.log(form.elements.password.type);
  // → password
  console.log(form.elements.name.form == form);
  // → true
</script>

Tombol dengan atribut type submit bakal, waktu ditekan, bikin form-nya di-submit. Mencet enter waktu field form fokus punya efek yang sama.

Men-submit form biasanya berarti browser menavigasi ke halaman yang ditunjukin atribut action form, pakai request GET atau POST. Tapi sebelum itu terjadi, event "submit" ditembakin. Kamu bisa nanganin event ini dengan JavaScript dan nyegah perilaku default ini dengan manggil preventDefault di event object.

<form>
  Value: <input type="text" name="value">
  <button type="submit">Save</button>
</form>
<script>
  let form = document.querySelector("form");
  form.addEventListener("submit", event => {
    console.log("Saving value", form.elements.value.value);
    event.preventDefault();
  });
</script>

Mencegat event "submit" di JavaScript punya berbagai kegunaan. Kita bisa nulis kode buat memverifikasi bahwa nilai yang dimasukin pengguna masuk akal dan langsung nampilin pesan error bukannya men-submit form-nya. Atau kita bisa nonaktifin cara biasa men-submit form-nya sepenuhnya, kaya di contoh, dan bikin program kita nanganin input-nya, mungkin pakai fetch buat ngirimnya ke server tanpa me-reload halaman.

Field Teks

Field yang dibikin tag <textarea>, atau tag <input> dengan type text atau password, berbagi interface yang sama. Elemen DOM mereka punya properti value yang megang isi mereka saat ini sebagai nilai string. Nyetel properti ini ke string lain ngubah isi field-nya.

Properti selectionStart dan selectionEnd dari field teks ngasih kita informasi soal kursor dan seleksi di teksnya. Waktu gak ada yang terpilih, dua properti ini megang angka yang sama, nunjukin posisi kursornya. Misalnya, 0 nunjukin awal teks, dan 10 nunjukin kursornya setelah karakter ke-10. Waktu sebagian field terpilih, dua propertinya bakal beda, ngasih kita awal dan akhir teks yang terpilih. Kaya value, properti ini juga bisa ditulis.

Bayangin kamu lagi nulis artikel soal Khasekhemwy, firaun terakhir Dinasti Kedua, tapi punya sedikit kesulitan ngeja namanya. Kode berikut nyambungin tag <textarea> dengan event handler yang, waktu kamu mencet F2, nyisipin string “Khasekhemwy” buatmu.

<textarea></textarea>
<script>
  let textarea = document.querySelector("textarea");
  textarea.addEventListener("keydown", event => {
    if (event.key == "F2") {
      replaceSelection(textarea, "Khasekhemwy");
      event.preventDefault();
    }
  });
  function replaceSelection(field, word) {
    let from = field.selectionStart, to = field.selectionEnd;
    field.value = field.value.slice(0, from) + word +
                  field.value.slice(to);
    // Taruh kursor setelah katanya
    field.selectionStart = from + word.length;
    field.selectionEnd = from + word.length;
  }
</script>

Fungsi replaceSelection ngeganti bagian yang terpilih saat ini dari isi field teks dengan kata yang dikasih lalu mindahin kursor setelah kata itu biar pengguna bisa lanjut ngetik.

Event "change" buat field teks gak nembak setiap kali sesuatu diketik. Sebaliknya, dia nembak waktu field-nya kehilangan focus setelah isinya diubah. Buat merespons langsung ke perubahan di field teks, kamu harus mendaftarkan handler buat event "input" sebagai gantinya, yang nembak setiap kali pengguna ngetik karakter, ngehapus teks, atau dengan cara lain memanipulasi isi field-nya.

Contoh berikut nunjukin field teks dan counter yang nampilin panjang teks saat ini di field-nya:

<input type="text"> length: <span id="length">0</span>
<script>
  let text = document.querySelector("input");
  let output = document.querySelector("#length");
  text.addEventListener("input", () => {
    output.textContent = text.value.length;
  });
</script>

Checkbox dan Radio Button

Field checkbox adalah toggle biner. Nilainya bisa diekstrak atau diubah lewat properti checked-nya, yang megang nilai Boolean.

<label>
  <input type="checkbox" id="purple"> Make this page purple
</label>
<script>
  let checkbox = document.querySelector("#purple");
  checkbox.addEventListener("change", () => {
    document.body.style.background =
      checkbox.checked ? "mediumpurple" : "";
  });
</script>

Tag <label> ngaitin potongan dokumen dengan field input. Ngeklik di mana pun di label bakal ngaktifin field-nya, yang memfokusnya dan men-toggle nilainya waktu dia checkbox atau radio button.

Radio button mirip checkbox, tapi dia secara implisit terhubung ke radio button lain dengan atribut name yang sama biar cuma satu dari mereka yang bisa aktif kapan pun.

Color:
<label>
  <input type="radio" name="color" value="orange"> Orange
</label>
<label>
  <input type="radio" name="color" value="lightgreen"> Green
</label>
<label>
  <input type="radio" name="color" value="lightblue"> Blue
</label>
<script>
  let buttons = document.querySelectorAll("[name=color]");
  for (let button of Array.from(buttons)) {
    button.addEventListener("change", () => {
      document.body.style.background = button.value;
    });
  }
</script>

Kurung siku di query CSS yang dikasih ke querySelectorAll dipakai buat nyocokin atribut. Dia milih elemen yang atribut name-nya "color".

Field Select

Field select secara konseptual mirip radio button, mereka juga ngebolehin pengguna milih dari sekumpulan opsi. Tapi di mana radio button naruh layout opsinya di bawah kendali kita, tampilan tag <select> ditentuin browser.

Field select juga punya varian yang lebih mirip daftar checkbox daripada kotak radio. Waktu dikasih atribut multiple, tag <select> bakal ngebolehin pengguna milih berapa pun opsi, bukannya cuma satu opsi. Sementara field select biasa digambar sebagai kontrol dropdown, yang nunjukin opsi tidak-aktif cuma waktu kamu ngebukanya, field dengan multiple diaktifin nunjukin beberapa opsi di saat yang sama, ngebolehin pengguna ngaktifin atau menonaktifkan mereka satu per satu.

Setiap tag <option> punya nilai. Nilai ini bisa didefinisikan dengan atribut value. Waktu itu gak dikasih, teks di dalam opsinya bakal dihitung sebagai nilainya. Properti value dari elemen <select> mencerminkan opsi yang saat ini terpilih. Buat field multiple, tapi, properti ini gak berarti banyak, karena dia bakal ngasih nilai cuma satu dari opsi yang saat ini terpilih.

Tag <option> buat field <select> bisa diakses sebagai objek mirip-array lewat properti options field-nya. Setiap opsi punya properti bernama selected, yang nunjukin apakah opsi itu saat ini terpilih. Properti itu juga bisa ditulis buat milih atau membatalkan pilihan opsi.

Contoh ini ngekstrak nilai yang terpilih dari field select multiple dan pakai mereka buat nyusun angka biner dari bit individual. Tahan ctrl (atau command di Mac) buat milih beberapa opsi.

<select multiple>
  <option value="1">0001</option>
  <option value="2">0010</option>
  <option value="4">0100</option>
  <option value="8">1000</option>
</select> = <span id="output">0</span>
<script>
  let select = document.querySelector("select");
  let output = document.querySelector("#output");
  select.addEventListener("change", () => {
    let number = 0;
    for (let option of Array.from(select.options)) {
      if (option.selected) {
        number += Number(option.value);
      }
    }
    output.textContent = number;
  });
</script>

Field File

Field file awalnya didesain sebagai cara buat mengunggah file dari mesin pengguna lewat form. Di browser modern, mereka juga nyediain cara buat baca file kaya gitu dari program JavaScript. Field-nya bertindak sebagai semacam penjaga gerbang. Script-nya gak bisa cuma mulai baca file privat dari komputer pengguna, tapi kalau pengguna milih file di field kaya gitu, browser menafsirkan aksi itu berarti script-nya boleh baca file-nya.

Field file biasanya keliatan kaya tombol berlabel sesuatu kaya “choose file” atau “browse”, dengan informasi soal file yang dipilih di sampingnya.

<input type="file">
<script>
  let input = document.querySelector("input");
  input.addEventListener("change", () => {
    if (input.files.length > 0) {
      let file = input.files[0];
      console.log("You chose", file.name);
      if (file.type) console.log("It has type", file.type);
    }
  });
</script>

Properti files dari elemen field file adalah objek mirip-array (lagi-lagi, bukan array beneran) yang berisi file yang dipilih di field-nya. Dia awalnya kosong. Alasan gak ada cuma properti file adalah field file juga mendukung atribut multiple, yang bikin mungkin buat milih beberapa file di saat yang sama.

Objek di files punya properti kaya name (nama file), size (ukuran file dalam byte, yang potongan 8 bit), dan type (media type file-nya, kaya text/plain atau image/jpeg).

Yang dia gak punya adalah properti yang berisi isi file-nya. Ngambil itu sedikit lebih rumit. Karena baca file dari disk bisa butuh waktu, interface-nya asinkron buat ngehindarin membekukan jendela.

<input type="file" multiple>
<script>
  let input = document.querySelector("input");
  input.addEventListener("change", () => {
    for (let file of Array.from(input.files)) {
      let reader = new FileReader();
      reader.addEventListener("load", () => {
        console.log("File", file.name, "starts with",
                    reader.result.slice(0, 20));
      });
      reader.readAsText(file);
    }
  });
</script>

Baca file dilakuin dengan bikin objek FileReader, mendaftarkan event handler "load" buat dia, dan manggil method readAsText-nya, ngasihnya file yang mau kita baca. Begitu pemuatan selesai, properti result reader-nya berisi isi file-nya.

FileReader juga nembakin event "error" waktu baca file-nya gagal karena alasan apa pun. Objek error-nya sendiri bakal berakhir di properti error reader. Interface ini didesain sebelum promise jadi bagian dari bahasa. Kamu bisa ngebungkusnya dalam promise kaya gini:

function readFileText(file) {
  return new Promise((resolve, reject) => {
    let reader = new FileReader();
    reader.addEventListener(
      "load", () => resolve(reader.result));
    reader.addEventListener(
      "error", () => reject(reader.error));
    reader.readAsText(file);
  });
}

Nyimpen Data di Sisi Client

Halaman HTML sederhana dengan sedikit JavaScript bisa jadi format bagus buat “aplikasi mini”, program pembantu kecil yang mengotomatisasi tugas dasar. Dengan nyambungin beberapa field form dengan event handler, kamu bisa ngelakuin apa pun dari ngonversi antara sentimeter dan inci sampai ngitung password dari master password dan nama website.

Waktu aplikasi kaya gitu perlu ngingat sesuatu antar sesi, kamu gak bisa pakai binding JavaScript, itu dibuang setiap kali halamannya ditutup. Kamu bisa nyiapin server, nyambungin dia ke internet, dan bikin aplikasimu nyimpen sesuatu di sana (kita bakal lihat cara ngelakuin itu di Bab 20). Tapi itu banyak kerja dan kerumitan ekstra. Kadang cukup buat cuma ngejaga datanya di browser.

Objek localStorage bisa dipakai buat nyimpen data dengan cara yang bertahan melewati reload halaman. Objek ini ngebolehin kamu ngearsipkan nilai string di bawah nama.

localStorage.setItem("username", "marijn");
console.log(localStorage.getItem("username"));
// → marijn
localStorage.removeItem("username");

Nilai di localStorage tetap ada sampai dia ditimpa atau dihapus dengan removeItem, atau pengguna ngebersihin data lokal mereka.

Situs dari domain berbeda dapet kompartemen penyimpanan berbeda. Itu berarti data yang disimpan di localStorage oleh website tertentu, pada prinsipnya, cuma bisa dibaca (dan ditimpa) oleh script di situs yang sama.

Browser emang memberlakukan batas ukuran data yang bisa disimpan situs di localStorage. Batasan itu, bareng fakta bahwa ngisi hard drive orang dengan sampah gak terlalu menguntungkan, nyegah fitur ini makan terlalu banyak ruang.

Kode berikut ngimplementasiin aplikasi pencatatan sederhana. Dia ngejaga sekumpulan catatan bernama dan ngebolehin pengguna ngedit catatan dan bikin yang baru.

Notes: <select></select> <button>Add</button><br>
<textarea style="width: 100%"></textarea>
 
<script>
  let list = document.querySelector("select");
  let note = document.querySelector("textarea");
 
  let state;
  function setState(newState) {
    list.textContent = "";
    for (let name of Object.keys(newState.notes)) {
      let option = document.createElement("option");
      option.textContent = name;
      if (newState.selected == name) option.selected = true;
      list.appendChild(option);
    }
    note.value = newState.notes[newState.selected];
 
    localStorage.setItem("Notes", JSON.stringify(newState));
    state = newState;
  }
  setState(JSON.parse(localStorage.getItem("Notes")) ?? {
    notes: {"shopping list": "Carrots\nRaisins"},
    selected: "shopping list"
  });
 
  list.addEventListener("change", () => {
    setState({notes: state.notes, selected: list.value});
  });
  note.addEventListener("change", () => {
    let {selected} = state;
    setState({
      notes: {...state.notes, [selected]: note.value},
      selected
    });
  });
  document.querySelector("button")
    .addEventListener("click", () => {
      let name = prompt("Note name");
      if (name) setState({
        notes: {...state.notes, [name]: ""},
        selected: name
      });
    });
</script>

Script-nya dapet keadaan awalnya dari nilai "Notes" yang disimpan di localStorage atau, kalau itu hilang, bikin keadaan contoh yang cuma punya shopping list di dalamnya. Baca field yang gak ada dari localStorage bakal ngasih null. Ngasih null ke JSON.parse bakal bikin dia mem-parse string "null" dan ngembaliin null. Jadi, operator ?? bisa dipakai buat nyediain nilai default di situasi kaya ini.

Method setState mastiin DOM lagi nunjukin keadaan tertentu dan nyimpen keadaan baru ke localStorage. Event handler manggil fungsi ini buat pindah ke keadaan baru.

Sintaks ... di contoh dipakai buat bikin objek baru yang merupakan klon dari state.notes lama, tapi dengan satu properti ditambahin atau ditimpa. Dia pakai sintaks spread buat pertama nambahin properti dari objek lama lalu nyetel properti baru. Notasi kurung siku di objek literal dipakai buat bikin properti yang namanya didasarkan pada suatu nilai dinamis.

Ada objek lain, mirip localStorage, disebut sessionStorage. Perbedaan antara keduanya adalah isi sessionStorage dilupain di akhir setiap sesi, yang buat kebanyakan browser berarti setiap kali browser-nya ditutup.

Ringkasan

Di bab ini, kita ngebahas cara protokol HTTP bekerja. Client ngirim request, yang berisi method (biasanya GET) dan path yang ngidentifikasi resource. Server lalu mutusin apa yang mau dilakuin dengan request-nya dan merespons dengan kode status dan body respons. Request maupun respons boleh berisi header yang nyediain informasi tambahan.

Interface yang lewatnya JavaScript browser bisa bikin request HTTP disebut fetch. Bikin request keliatan kaya gini:

fetch("/18_http.html").then(r => r.text()).then(text => {
  console.log(`The page starts with ${text.slice(0, 15)}`);
});

Browser bikin request GET buat ngambil resource yang dibutuhin buat nampilin halaman web. Halaman juga boleh berisi form, yang ngebolehin informasi yang dimasukin pengguna dikirim sebagai request buat halaman baru waktu form-nya di-submit.

HTML bisa mewakili berbagai jenis field form, kaya field teks, checkbox, field pilihan ganda, dan pemilih file. Field kaya gitu bisa diperiksa dan dimanipulasi dengan JavaScript. Mereka nembakin event "change" waktu diubah, nembakin event "input" waktu teks diketik, dan nerima event keyboard waktu mereka punya keyboard focus. Properti kaya value (buat field teks dan select) atau checked (buat checkbox dan radio button) dipakai buat baca atau nyetel isi field-nya.

Waktu form di-submit, event "submit" ditembakin di dia. Handler JavaScript bisa manggil preventDefault di event itu buat nonaktifin perilaku default browser. Elemen field form juga boleh muncul di luar tag form.

Waktu pengguna udah milih file dari filesystem lokal mereka di field pemilih file, interface FileReader bisa dipakai buat ngakses isi file ini dari program JavaScript.

Objek localStorage dan sessionStorage bisa dipakai buat nyimpen informasi dengan cara yang bertahan melewati reload halaman. Objek pertama nyimpen datanya selamanya (atau sampai pengguna mutusin ngebersihinnya), dan yang kedua nyimpennya sampai browser-nya ditutup.

Poin-poin pentingnya:

  1. HTTP: client ngirim request (method + path + header + body opsional), server balas response (kode status + header + body). Method: GET (ambil), POST (kirim), PUT (bikin/ganti), DELETE (hapus). Kode status: 2xx sukses, 4xx salah client, 5xx error server.
  2. fetch(url, opsi) ngembaliin promise ke objek Response. .text()/.json() buat baca body. Opsi: method, body, headers.
  3. Sandboxing: browser ngeblok request lintas-domain kecuali server ngasih header Access-Control-Allow-Origin. HTTPS ngenkripsi + memverifikasi identitas server.
  4. Form field: <input type="...">, <textarea>, <select>. Event "change" (setelah hilang fokus), "input" (tiap ketik). Baca lewat value/checked/selected.
  5. Focus: .focus()/.blur(), document.activeElement, atribut autofocus dan tabindex. disabled buat nonaktifin field.
  6. Submit: event "submit", preventDefault() buat nanganin sendiri (biasanya + fetch). Form punya .elements (akses per nama/nomor).
  7. FileReader (readAsText + event "load") buat baca isi file yang dipilih pengguna.
  8. localStorage/sessionStorage buat nyimpen string yang bertahan melewati reload (localStorage permanen, sessionStorage sampai browser ditutup).

Latihan

Content Negotiation

Salah satu hal yang bisa dilakuin HTTP disebut content negotiation. Header request Accept dipakai buat ngasih tahu server jenis dokumen apa yang mau didapet client. Banyak server ngabaikan header ini, tapi waktu server tahu berbagai cara buat mengkodekan resource, dia bisa ngelihat header ini dan ngirim yang lebih disukai client.

URL https://eloquentjavascript.net/author dikonfigurasi buat merespons dengan plaintext, HTML, atau JSON, tergantung apa yang diminta client. Format ini diidentifikasi dengan media type yang distandarkan text/plain, text/html, dan application/json.

Kirim request buat ngambil ketiga format resource ini. Pakai properti headers di objek opsi yang dikasih ke fetch buat nyetel header bernama Accept ke media type yang diinginkan.

Terakhir, coba minta media type application/rainbows+unicorns dan lihat kode status apa yang dihasilin itu.

// Kodemu di sini.

Petunjuk:

Dasarkan kodemu pada contoh fetch sebelumnya di bab.

Minta media type palsu bakal ngembaliin respons dengan kode 406, “Not acceptable”, yang merupakan kode yang harus dikembaliin server waktu dia gak bisa memenuhi header Accept.

Solusi:

const url = "https://eloquentjavascript.net/author";
const types = ["text/plain",
               "text/html",
               "application/json",
               "application/rainbows+unicorns"];
 
async function showTypes() {
  for (let type of types) {
    let resp = await fetch(url, {headers: {accept: type}});
    console.log(`${type}: ${await resp.text()}\n`);
  }
}
 
showTypes();

Meja Kerja JavaScript

Bangun interface yang ngebolehin pengguna ngetik dan ngejalanin potongan kode JavaScript.

Taruh tombol di samping field <textarea> yang, waktu ditekan, pakai constructor Function yang kita lihat di Bab 10 buat ngebungkus teksnya dalam fungsi dan manggilnya. Konversi nilai kembalian fungsinya, atau error apa pun yang dia angkat, jadi string dan tampilin di bawah field teksnya.

<textarea id="code">return "hi";</textarea>
<button id="button">Run</button>
<pre id="output"></pre>
 
<script>
  // Kodemu di sini.
</script>

Petunjuk:

Pakai document.querySelector atau document.getElementById buat dapet akses ke elemen yang didefinisikan di HTML-mu. Event handler buat event "click" atau "mousedown" di tombolnya bisa dapet properti value dari field teksnya dan manggil Function di dia.

Pastiin kamu ngebungkus pemanggilan Function maupun pemanggilan hasilnya dalam block try biar kamu bisa nangkep exception yang dia hasilin. Di kasus ini, kita beneran gak tahu jenis exception apa yang kita cari, jadi tangkep semuanya.

Properti textContent dari elemen output bisa dipakai buat ngisinya dengan pesan string.

Solusi:

<textarea id="code">return "hi";</textarea>
<button id="button">Run</button>
<pre id="output"></pre>
 
<script>
  document.querySelector("#button").addEventListener("click", () => {
    let code = document.querySelector("#code").value;
    let outputNode = document.querySelector("#output");
    try {
      let result = Function(code)();
      outputNode.innerText = String(result);
    } catch (e) {
      outputNode.innerText = "Error: " + e;
    }
  });
</script>

Game of Life-nya Conway

Game of Life-nya Conway adalah simulasi sederhana yang bikin “kehidupan” buatan di grid, tiap selnya entah hidup atau gak. Di setiap generasi (giliran), aturan berikut diterapin:

  • Sel hidup mana pun dengan kurang dari dua atau lebih dari tiga tetangga hidup mati.
  • Sel hidup mana pun dengan dua atau tiga tetangga hidup tetap hidup ke generasi berikutnya.
  • Sel mati mana pun dengan persis tiga tetangga hidup jadi sel hidup.

Tetangga didefinisikan sebagai sel bersebelahan mana pun, termasuk yang bersebelahan diagonal.

Perhatiin bahwa aturan ini diterapin ke seluruh grid sekaligus, bukan satu kotak dalam satu waktu. Itu berarti penghitungan tetangga didasarkan pada situasi di awal generasinya, dan perubahan yang terjadi ke sel tetangga selama generasi ini gak boleh mempengaruhi keadaan baru dari sel tertentu.

Implementasikan game ini pakai struktur data mana pun yang kamu rasa tepat. Pakai Math.random buat mengisi grid-nya dengan pola acak di awal. Tampilin dia sebagai grid field checkbox, dengan tombol di sampingnya buat maju ke generasi berikutnya. Waktu pengguna mencentang atau membatalkan centang checkbox-nya, perubahan mereka harus disertain waktu ngitung generasi berikutnya.

<div id="grid"></div>
<button id="next">Next generation</button>
 
<script>
  // Kodemu di sini.
</script>

Petunjuk:

Buat nyelesaiin masalah punya perubahan yang secara konseptual terjadi di saat yang sama, coba lihat perhitungan generasi sebagai fungsi murni, yang ngambil satu grid dan ngehasilin grid baru yang mewakili giliran berikutnya.

Mewakili matriksnya bisa dilakuin dengan satu array width × height elemen, nyimpen nilai baris demi baris, jadi, misalnya, elemen ketiga di baris kelima disimpan (pakai indeks berbasis nol) di posisi 4 × width + 2. Kamu bisa ngitung tetangga hidup dengan dua loop bersarang, ngeloop koordinat bersebelahan di kedua dimensi. Hati-hati buat gak ngitung sel di luar field dan buat ngabaikan sel di tengah, yang tetangganya lagi kita hitung.

Mastiin perubahan ke checkbox berefek di generasi berikutnya bisa dilakuin dua cara. Event handler bisa nyadar perubahan ini dan ngupdate grid saat ini buat nyerminin mereka, atau kamu bisa ngehasilin grid segar dari nilai di checkbox sebelum ngitung giliran berikutnya.

Buat menggambar grid checkbox-nya, kamu bisa pakai elemen <table> (lihat Bab 14) atau cuma naruh semua mereka di elemen yang sama dan naruh elemen <br> (line break) di antara barisnya.

Solusi:

<div id="grid"></div>
<button id="next">Next generation</button>
<button id="run">Auto run</button>
 
<script>
  const width = 30, height = 15;
 
  // Grid diwakili sebagai array of boolean.
 
  let gridNode = document.querySelector("#grid");
  let checkboxes = [];
  for (let y = 0; y < height; y++) {
    for (let x = 0; x < width; x++) {
      let box = document.createElement("input");
      box.type = "checkbox";
      gridNode.appendChild(box);
      checkboxes.push(box);
    }
    gridNode.appendChild(document.createElement("br"));
  }
 
  function gridFromCheckboxes() {
    return checkboxes.map(box => box.checked);
  }
  function checkboxesFromGrid(grid) {
    grid.forEach((value, i) => checkboxes[i].checked = value);
  }
  function randomGrid() {
    let result = [];
    for (let i = 0; i < width * height; i++) {
      result.push(Math.random() < 0.3);
    }
    return result;
  }
 
  checkboxesFromGrid(randomGrid());
 
  function countNeighbors(grid, x, y) {
    let count = 0;
    for (let y1 = Math.max(0, y - 1); y1 <= Math.min(height - 1, y + 1); y1++) {
      for (let x1 = Math.max(0, x - 1); x1 <= Math.min(width - 1, x + 1); x1++) {
        if ((x1 != x || y1 != y) && grid[x1 + y1 * width]) {
          count++;
        }
      }
    }
    return count;
  }
 
  function nextGeneration(grid) {
    let newGrid = new Array(width * height);
    for (let y = 0; y < height; y++) {
      for (let x = 0; x < width; x++) {
        let neighbors = countNeighbors(grid, x, y);
        let offset = x + y * width;
        if (neighbors < 2 || neighbors > 3) {
          newGrid[offset] = false;
        } else if (neighbors == 2) {
          newGrid[offset] = grid[offset];
        } else {
          newGrid[offset] = true;
        }
      }
    }
    return newGrid;
  }
 
  function turn() {
    checkboxesFromGrid(nextGeneration(gridFromCheckboxes()));
  }
 
  document.querySelector("#next").addEventListener("click", turn);
 
  let running = null;
  document.querySelector("#run").addEventListener("click", () => {
    if (running) {
      clearInterval(running);
      running = null;
    } else {
      running = setInterval(turn, 400);
    }
  });
</script>