Proyek HTMX — HTMX

Saatnya membangun proyek mini menggunakan HTMX! Spesifikasi: CRUD Todo App Tambah todo baru via form POST Tampilkan list todos Toggle selesai via PATCH Hapus…

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>

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

<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}]}'>

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

  1. Tiap form dan tombol punya target yang jelas, dan daftar yang bertambah memakai beforeend.
  2. Balasan cuma berisi potongan yang dibutuhkan. Potongan di luar target bertanda hx-swap-oob="true", dengan id yang persis sama.
  3. Aksi yang menghapus punya hx-confirm berisi kalimat.
  4. Request yang terasa lambat punya indikator, dan tombolnya nonaktif selama menunggu.
  5. Form punya action dan method sungguhan.
  6. 422 diizinkan ditukar, dan balasan sukses membersihkan pesan error.
  7. Pencarian memakai delay, dan polling punya jeda yang masuk akal (30 detik = 120 request per jam per tab).

⚠️ Jebakan yang sering ketemu

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.

Yang akan kamu pelajari