Proyek ini menyatukan semua pelajaran htmx jadi satu halaman utuh: papan order Laundry Bu Rini di http://localhost:8000/papan. Bayangin meja kasir Bu Rini di pagi yang ramai. Di depan kasir ada satu papan: cucian masuk ditempel di bawah, kartu status digeser, nama pelanggan dicari, kartu yang sudah diambil dicabut, dan angka "order aktif" di pojok ikut berubah. Kasir gak pernah mengganti seluruh papan, cuma kartu yang berubah. Tiap pelajaran sebelumnya memberi satu alat. Di sini semuanya dipakai bersama.
🧪 Tebakan cepat: htmx gagal dimuat di HP kasir, jadi halaman jalan tanpa JavaScript. Mana yang masih bisa: menerima cucian, mencari nama, atau menaikkan status? Jawabannya ada di bagian "Tetap jalan tanpa JavaScript".
Halaman papan, lengkap
<header>
<h1>Laundry Bu Rini</h1>
<span id="jumlah-aktif">3 order aktif</span>
</header>
<form action="/cari" hx-get="/cari" hx-target="#hasil"
hx-trigger="input changed delay:500ms, submit">
<input name="cari" type="search" placeholder="Nama pelanggan">
</form>
<ul id="hasil"></ul>
<ul id="papan-order" hx-get="/papan" hx-trigger="every 30s" hx-swap="outerHTML">
<li id="order-12">#12 Pak Darto, 3 kg: <span id="status-12">Dicuci</span>
<button hx-post="/order/12/status" hx-vals='{"status": "Disetrika"}'
hx-target="closest li" hx-swap="outerHTML">Disetrika</button></li>
<li id="order-13">#13 Mbak Wulan, 2 kg: <span id="status-13">Disetrika</span>
<button hx-post="/order/13/status" hx-vals='{"status": "Siap diambil"}'
hx-target="closest li" hx-swap="outerHTML">Siap diambil</button></li>
<li id="order-14">#14 Mas Yoga, 5 kg: <span id="status-14">Siap diambil</span>
<button hx-delete="/order/14" hx-confirm="Order #14 sudah diambil Mas Yoga?"
hx-target="closest li" hx-swap="outerHTML">Sudah diambil</button></li>
</ul>
<form id="terima" action="/order" method="post"
hx-post="/order" hx-target="#papan-order" hx-swap="beforeend"
hx-indicator="#memuat" hx-disabled-elt="find button">
<input name="nama" placeholder="Nama" required>
<input name="berat" placeholder="kg" required>
<button>Terima cucian</button> <span id="memuat" class="htmx-indicator">Menyimpan...</span>
</form>
#jumlah-aktifdiperbarui lewat potongan OOB di balasan (pelajaran OOB swap).- Form cari adalah active search:
input changed delay:500msmenunggu ketikan terakhir, dansubmitmenangkap Enter (pelajaran hx-trigger dan active search). Atributnya dipasang di<form action="/cari">, bukan di<input>, supaya Enter tetap mengirim form itu walau JavaScript mati. - Papan menyegarkan diri tiap 30 detik (polling, pelajaran hx-trigger), jadi status yang diubah Bu Rini di belakang ikut muncul. SSE (pelajaran SSE) bisa menggantikannya kalau perubahan harus langsung tampil.
- Tombol status mengirim
hx-vals(pelajaran hx-vals), menunjukclosest li(pelajaran hx-target), dan menukar seluruh baris denganouterHTML(pelajaran hx-swap). - Sudah diambil memakai
hx-deletedanhx-confirm(pelajaran konfirmasi dialog). - Form terima memakai
hx-postdanbeforeend(pelajaran hx-post dan form), plus "Menyimpan..." dan tombol nonaktif selama request (pelajaran indikator loading).
Peta request dan balasan
| Fitur | Request | Pemicu | Target, swap | Balasan |
|---|---|---|---|---|
| Buka papan | GET /papan |
alamat diketik | seluruh halaman | 200, halaman penuh |
| Cari nama | GET /cari?cari=wulan |
ketik lalu jeda 500 ms, atau Enter | #hasil, innerHTML |
200, baris <li> |
| Segarkan papan | GET /papan |
tiap 30 detik | #papan-order, outerHTML |
200, <ul id="papan-order"> |
| Naikkan status | POST /order/12/status, body status=Disetrika |
klik | closest li, outerHTML |
200, baris #12 yang baru |
| Terima cucian | POST /order, body nama=Bu%20Tini&berat=4 |
submit | #papan-order, beforeend |
200 baris baru, atau 422 |
| Serahkan cucian | DELETE /order/14 |
klik lalu OK | closest li, outerHTML |
200, cuma potongan OOB |
Setiap request htmx membawa header HX-Request: true. Itu sebabnya GET /papan bisa punya dua bentuk: halaman penuh untuk browser, dan <ul> saja untuk polling. Server yang memilih, dan caranya di Laravel dibahas di pelajaran berikutnya.
Satu order dari awal sampai akhir
Bu Tini menyerahkan 4 kg. Kasir mengetik nama dan berat, lalu menekan Terima cucian:
| Waktu | Yang terjadi |
|---|---|
| 0 ms | validasi browser lolos (dua kotak required terisi), htmx mengirim POST /order |
| 0 ms | "Menyimpan..." muncul, tombol nonaktif |
| 600 ms | 200 OK: baris #15 masuk di akhir papan, header jadi "4 order aktif", form kosong yang baru menggantikan yang lama |
Balasan server:
<li id="order-15">#15 Bu Tini, 4 kg: <span id="status-15">Antre</span>
<button hx-post="/order/15/status" hx-vals='{"status": "Dicuci"}'
hx-target="closest li" hx-swap="outerHTML">Dicuci</button></li>
<span id="jumlah-aktif" hx-swap-oob="true">4 order aktif</span>
<!-- lalu form #terima yang kosong, dengan hx-swap-oob="true" -->
Baris baru itu sudah membawa tombol langkah berikutnya, Dicuci. Browser gak perlu tahu urutan status: server yang menaruh tombol yang boleh ditekan. Inilah arsitektur hypermedia (Hypermedia-Driven Application): server mengirim HTML yang berisi data sekaligus aksinya, bukan JSON yang harus dirakit jadi tampilan oleh JavaScript di browser. Makanya htmx cocok dengan framework server seperti Laravel atau Django: server merender HTML dengan template-nya, tanpa API JSON terpisah.
Saat server menolak: 422
- Kotak kosong ditahan browser lewat
required, tanpa request. - Aturan "berat maksimal 30 kg" cuma server yang tahu. Untuk 40 kg, server membalas 422 berisi form yang sama, dengan isian kasir dan
<p class="galat">Berat maksimal 30 kg.</p>, plus headerHX-Retarget: #terimadanHX-Reswap: outerHTMLsupaya form itu yang ditukar, bukan papan. - Secara bawaan htmx gak menukar balasan 4xx (pelajaran hx-post), dan
HX-Retargetgak mengubah itu. Izinkan 422 sekali di<head>, persis contoh dokumentasi htmx:
<meta name="htmx-config" content='{"responseHandling":[{"code":"204","swap":false},{"code":"[23]..","swap":true},{"code":"422","swap":true},{"code":"[45]..","swap":false,"error":true},{"code":"...","swap":true}]}'>
- Balasan sukses membawa form kosong sebagai potongan OOB, supaya pesan "Berat maksimal 30 kg." ikut hilang. Cara yang lebih pendek dari pelajaran form,
hx-on::after-request="if (event.detail.successful) this.reset()", cuma mengosongkan kotak isian. Pesan error-nya tetap tampil.
Tetap jalan tanpa JavaScript
| Fitur | Tanpa JavaScript |
|---|---|
| Terima cucian | jalan: form punya action dan method, browser mengirim POST /order biasa, server membalas redirect ke /papan |
| Cari nama | jalan dengan Enter: GET /cari?cari=wulan membuka halaman penuh berisi hasilnya |
| Naikkan status, Sudah diambil | gak jalan: tombol di luar form gak mengirim apa pun |
| Polling, indikator, konfirmasi | gak jalan, tapi tiap reload tetap membawa data terbaru |
Itu jawaban tebakannya: menerima cucian dan mencari nama tetap bisa, menaikkan status gak. Pola ini disebut progressive enhancement: mulai dari halaman HTML yang berfungsi sendiri, lalu htmx membuatnya lebih mulus. Supaya tombol status ikut jalan, jadikan form kecil: <form action="/order/12/status" method="post"> berisi <input type="hidden" name="status" value="Disetrika"> dan tombolnya, dengan hx-post yang sama di form itu.
Checklist sebelum selesai
- Tiap form dan tombol punya target yang jelas, dan daftar yang bertambah memakai
beforeend. - Balasan cuma berisi potongan yang dibutuhkan. Potongan di luar target bertanda
hx-swap-oob="true", dengan id yang persis sama. - Aksi yang menghapus punya
hx-confirmberisi kalimat. - Request yang terasa lambat punya indikator, dan tombolnya nonaktif selama menunggu.
- Form punya
actiondanmethodsungguhan. - 422 diizinkan ditukar, dan balasan sukses membersihkan pesan error.
- Pencarian memakai
delay, dan polling punya jeda yang masuk akal (30 detik = 120 request per jam per tab).
⚠️ Jebakan yang sering ketemu
- Lupa
beforeenddi daftar. BawaannyainnerHTML, jadi order baru menghapus seluruh papan. Di latihan Todo App, formhx-post="/api/todos"harus tetaphx-swap="beforeend". - Tombol hapus tanpa konfirmasi, atau konfirmasi kosong. Satu klik salah, cucian orang hilang dari papan. Di latihan, tombol
hx-deletedi tiap item butuhhx-confirmberisi kalimat, misalnyahx-confirm="Hapus todo ini?". - Penghitung tanpa
hx-swap-oob="true". Potongannya ikut masuk ke daftar, dan angkanya gak berubah. Di latihan,<span id="todo-count" hx-swap-oob="true">ditaruh di contoh balasan server. - Pencarian tanpa jeda. Satu request per huruf. Di latihan Contact Book, kolom cari memakai
hx-get="/api/contacts/search"danhx-triggerdengandelay:300ms.
TL;DR: Papan order adalah semua pelajaran htmx dalam satu halaman: active search dengan delay:500ms, polling every 30s, tombol status dengan hx-vals dan outerHTML, hx-delete dengan hx-confirm, form hx-post dengan beforeend, indikator dan hx-disabled-elt, dan penghitung lewat hx-swap-oob="true". Tiap fitur punya satu baris di peta request: method, alamat, target, swap, dan status balasan. Server membalas potongan HTML yang membawa aksi berikutnya: itulah arsitektur hypermedia, dan alasan htmx cocok dengan Laravel atau Django yang merender HTML sendiri. Penolakan dibalas 422 dengan HX-Retarget dan HX-Reswap, yang baru ditukar kalau 422 diizinkan di htmx-config. Form dan link sungguhan membuat fitur utama tetap jalan tanpa JavaScript.