Infinite scroll memuat halaman berikutnya sendiri saat pengguna menggulir sampai bawah, tanpa tombol "Muat lagi". Di htmx, caranya satu elemen di ujung daftar dengan hx-trigger="revealed": begitu elemen itu terlihat, ia meminta halaman berikutnya. Bayangin Bu Rini menyalin buku riwayat order untuk pelanggan, lima baris per lembar. Di baris terakhir lembar yang sedang dibaca, ia menyelipkan pembatas. Begitu matamu sampai di pembatas itu, lembar berikutnya diambilkan dan ditaruh di bawahnya, dengan pembatas baru di ujungnya. Lembar terakhir datang tanpa pembatas: selesai.
🧪 Tebakan cepat: Riwayat order Bu Rini berisi 11 order lama, 5 per halaman. Halaman 1 ikut termuat bersama halamannya. Kamu menggulir sampai habis. Berapa request yang dikirim htmx? Jawabannya ada di bagian "Berhenti di halaman terakhir".
Baris terakhir yang memicu
<ul id="riwayat">
<li id="order-11">#11 Bu Sari, 2 kg: <span>Diambil</span></li>
<li id="order-10">#10 Pak Joko, 4 kg: <span>Diambil</span></li>
<li id="order-9">#9 Mbak Tia, 3 kg: <span>Diambil</span></li>
<li id="order-8">#8 Bu Ani, 6 kg: <span>Diambil</span></li>
<li id="order-7" hx-get="/riwayat?page=2" hx-trigger="revealed" hx-swap="afterend">
#7 Mas Dodi, 2 kg: <span>Diambil</span>
</li>
</ul>
hx-trigger="revealed": request dikirim sekali, saat elemen ini pertama kali masuk layar karena digulir. htmx lalu menandainya dengandata-hx-revealed="true"supaya gak terpicu lagi.hx-get="/riwayat?page=2": alamat halaman berikutnya, nomornya di query.hx-swap="afterend": balasan ditaruh sesudah baris #7, masih di dalam<ul>yang sama. Baris #7 sendiri tetap. Tanpahx-target, target-nya baris itu sendiri.
Server membalas GET /riwayat?page=2 dengan lima baris berikutnya, dan baris terakhirnya membawa pemicu untuk halaman 3:
<li id="order-6">#6 Bu Wati, 5 kg: <span>Diambil</span></li>
...
<li id="order-2" hx-get="/riwayat?page=3" hx-trigger="revealed" hx-swap="afterend">#2 Pak Ujang, 3 kg: <span>Diambil</span></li>
Berhenti di halaman terakhir
| Kamu menggulir sampai | Request | Baris di daftar |
|---|---|---|
| (halaman dibuka) | belum ada, halaman 1 ikut halamannya | 5 |
| #7 | GET /riwayat?page=2 |
10 |
| #2 | GET /riwayat?page=3 |
11 |
| #1 | gak ada, #1 gak punya hx-get |
11 |
Halaman 3 cuma berisi #1, dan server gak memberinya pemicu. Gak ada pemicu lagi, jadi request berhenti: total 2 request. Itu jawaban tebakannya. Di Laravel, pemicu ditulis cuma kalau masih ada halaman: @if ($orders->hasMorePages()), dengan alamat dari $orders->nextPageUrl().
Pola sentinel: elemen "Memuat..." yang mengganti dirinya
<ul id="riwayat">
...lima baris...
<li hx-get="/riwayat?page=2" hx-trigger="revealed" hx-swap="outerHTML">Memuat...</li>
</ul>
- Sentinel adalah elemen penanda di ujung daftar.
hx-swap="outerHTML"mengganti sentinel itu sendiri dengan lima baris baru plus sentinel baru untuk halaman berikutnya. Halaman terakhir datang tanpa sentinel. - Di tabel, sentinel-nya
<tr>di dalam<tbody>:<tr hx-get="/api/rows?page=2" hx-trigger="revealed" hx-swap="outerHTML" hx-target="this"><td>Loading more...</td></tr>.hx-target="this"menegaskan bahwa baris itu sendiri yang diganti. - Bedanya:
afterendmembiarkan baris pemicunya,outerHTMLmembuang sentinel-nya. Keduanya berhenti saat server gak mengirim pemicu baru.
revealed atau intersect
revealedmengecek posisi terhadap jendela browser: htmx mendengarkan scroll diwindow, lalu mengecek tiap 200 ms. Di dalam kotak ber-overflow-y: auto, yang digulir kotaknya, bukan window. Jadirevealedgak pernah terpicu, atau malah terpicu terlalu awal.- Untuk daftar di dalam kotak, pakai
hx-trigger="intersect once".intersectmemakai IntersectionObserver, yang memperhitungkan kotaknya.onceperlu ditulis, karenaintersectterpicu setiap kali elemen masuk area pandang.
⚠️ Jebakan yang sering ketemu
- Lupa
hx-swap. BawaannyainnerHTML: baris-baris baru masuk ke dalam baris pemicu, jadi<li>di dalam<li>. - Halaman terakhir masih membawa pemicu. Server tetap diminta halaman 4, yang isinya kosong. Tulis pemicu cuma kalau masih ada halaman berikutnya.
revealeddi dalam kotak scroll. Gak jalan. Pakaiintersect once.- Halaman pertama terlalu pendek. Pemicunya langsung terlihat saat halaman dibuka, jadi halaman 2, 3, dan seterusnya dimuat beruntun tanpa digulir. Kirim cukup baris untuk mengisi layar.
- Di latihan: sentinel
<div hx-get="/api/feed?page=2">di dalam#feedbutuhhx-trigger="revealed"danhx-swap="outerHTML". Baris tabel<tr hx-get="/api/rows?page=2">butuhhx-trigger="revealed",hx-swap="outerHTML", danhx-target="this". Contoh balasannya punya halaman terakhir tanpa sentinel, jadi sesudah htmx selesai memuat, semua item sudah tampil dan gak adahx-getyang tertinggal.
TL;DR: Infinite scroll di htmx: elemen di ujung daftar dengan hx-get="/riwayat?page=2" dan hx-trigger="revealed", yang terpicu sekali saat terlihat. Pakai hx-swap="afterend" di baris terakhir (balasan masuk sesudahnya), atau sentinel "Memuat..." dengan hx-swap="outerHTML" (sentinel-nya diganti). Setiap balasan membawa pemicu baru untuk halaman berikutnya, kecuali halaman terakhir: di situ request berhenti. Tanpa hx-swap, baris baru masuk ke dalam pemicu. Di dalam kotak scroll, revealed gak jalan: pakai intersect once.