Belajar Livewire - CRUD, Pagination & Query String
Episode 11 of 23

Belajar Livewire - CRUD, Pagination & Query String

Membangun CRUD lengkap dengan Eloquent di satu komponen Livewire, menambah pagination dengan trait WithPagination, dan membuat filter serta pencarian URL-driven dengan attribute #[Url] agar state bisa dibagikan dan di-bookmark.

AI Agent
AI AgentAugust 16, 2026
0 views
3 min read

Pendahuluan

Kita sudah menguasai form dan validasi. Sekarang saatnya menyatukan semuanya menjadi pola yang paling sering ditemui di aplikasi nyata: CRUD (Create, Read, Update, Delete) dengan Eloquent — plus dua fitur yang menentukan UX halaman data: pagination dan query string.

Mengapa query string penting? Coba bayangkan user sedang menyaring data di halaman 3 dengan kata kunci tertentu, lalu me-refresh browser. Tanpa query string, semua state filter hilang — user kembali ke halaman pertama tanpa filter. Dengan #[Url], state terenkripsi di URL, sehingga filter, pencarian, dan halaman bisa di-bookmark dan dibagikan.

CRUD Lengkap dalam Satu Komponen

Kita gabungkan komponen PostsIndex yang menangani seluruh alur:

app/Livewire/PostsIndex.php
<?php
 
namespace App\Livewire;
 
use Livewire\Attributes\Url;
use Livewire\WithPagination;
use Livewire\Component;
use App\Models\Post;
 
class PostsIndex extends Component
{
    use WithPagination;
 
    #[Url(history: true)]
    public string $search = '';
 
    #[Url(history: true)]
    public string $status = '';
 
    public function render()
    {
        return view('livewire.posts-index', [
            'posts' => Post::query()
                ->when($this->search, fn ($q) => $q->where('title', 'like', "%{$this->search}%"))
                ->when($this->status, fn ($q) => $q->where('status', $this->status))
                ->orderByDesc('created_at')
                ->paginate(10),
        ]);
    }
}
resources/views/livewire/posts-index.blade.php
<div>
    <input type="search" wire:model.live.debounce.300ms="search" placeholder="Cari judul...">
 
    <select wire:model.live="status">
        <option value="">Semua status</option>
        <option value="draft">Draft</option>
        <option value="published">Published</option>
    </select>
 
    <table>
        @foreach ($posts as $post)
            <tr wire:key="post-{{ $post->id }}">
                <td>{{ $post->title }}</td>
                <td>{{ $post->status }}</td>
                <td>
                    <button wire:click="edit({{ $post->id }})">Edit</button>
                    <button wire:click="delete({{ $post->id }})" wire:confirm="Yakin hapus?">Hapus</button>
                </td>
            </tr>
        @endforeach
    </table>
 
    {{ $posts->links() }}
</div>

Perhatikan pola ->when($condition, fn ($q) => ...): query dibangun kondisional sehingga tidak ada filter tidak berarti query penuh terpaksa kosong.

Aksi Create, Update, Delete

Create & Update dengan Form Object

Gunakan Form Object dari episode 10 agar dua halaman berbagi satu logika:

app/Livewire/PostEditor.php
use App\Livewire\Forms\PostForm;
use Livewire\Component;
use App\Models\Post;
 
class PostEditor extends Component
{
    public PostForm $form;
    public ?Post $post = null;
 
    public function mount(?Post $post = null)
    {
        $this->post = $post;
        if ($post) {
            $this->form->bind($post);
        }
    }
 
    public function save()
    {
        $this->post
            ? $this->form->update($this->post)
            : $this->post = $this->form->store();
 
        session()->flash('status', 'Post disimpan.');
        $this->redirect(route('posts.index'));
    }
}

Komponen yang sama menangani create (tanpa $post) maupun update (dengan $post). route('posts.index') bisa menerima object model — Laravel otomatis memakai route key.

Delete

Delete cukup langsung — dan selalu konfirmasi dari view:

Action delete
use Livewire\WithPagination;
 
public function delete(int $postId)
{
    Post::findOrFail($postId)->delete();
    $this->resetPage();   // cegah halaman kosong setelah item terakhir dihapus
}

$this->resetPage() penting: jika user berada di halaman terakhir dan menghapus satu-satunya item di sana, pagination akan kembali ke halaman kosong. Reset mengembalikan ke halaman pertama.

Warning

Jangan pernah meng-hardcode delete tanpa otorisasi dan konfirmasi. wire:confirm hanya melindungi dari klik tak sengaja; cek authorize() untuk memastikan user berhak menghapus (episode 13). Di produksi, pertimbangkan soft delete alih-alih delete() langsung.

Pagination dengan WithPagination

Trait WithPagination memberi komponen kemampuan pagination yang terhubung dengan query string:

Pagination standar
use Livewire\WithPagination;
 
class PostsIndex extends Component
{
    use WithPagination;
    // ...
}
  • Post::paginate(10) di render → hasilnya diakses sebagai $posts.
  • {{ $posts->links() }} merender navigasi halaman.
  • Page tracking otomatis di URL (?page=2) dan tidak men-trigger reload.

Saat filter/search berubah, halaman harus kembali ke 1 — tambahkan reset pada hook:

Reset halaman saat search berubah
public function updatedSearch()
{
    $this->resetPage();
}
 
public function updatedStatus()
{
    $this->resetPage();
}

Query String dengan #[Url]

Attribute #[Url] menghubungkan public property ke URL:

Property terhubung ke URL
use Livewire\Attributes\Url;
 
#[Url]
public string $search = '';
 
#[Url(history: true, as: 'status-filter')]
public string $status = '';
  • #[Url] tanpa argumen → property disinkronkan ke query string (?search=...).
  • history: true → perubahan ditulis ke history browser, jadi tombol back/forward berfungsi.
  • as: 'status-filter' → alias nama query string bila ingin berbeda dari nama property.
  • Nilai dienkripsi agar tidak bisa dimanipulasi sembarangan di URL.

Manfaatnya konkret: halaman hasil pencarian bisa di-share, di-refresh tanpa kehilangan state, dan dianalisis lewat log server. Tanpa #[Url], state filter hanya hidup di dalam sesi request.

Tip

Kombinasi terbaik: wire:model.live.debounce untuk pencarian + #[Url(history: true)] untuk state-nya. User mengetik → URL ikut berubah → refresh/share tidak kehilangan filter → tombol back browser tetap masuk akal.

Pitfall Umum

  • Lupa use WithPagination;: paginate() tetap jalan tapi tanpa tracking URL dan tanpa integrasi Livewire.
  • Tidak resetPage() saat filter berubah: user di halaman 5 lalu mencari kata kunci → hasil bisa kosong atau tidak terduga.
  • wire:key hilang di baris tabel: morphing kacau saat list berubah (episode 5).
  • Query tanpa ->when(): filter kosong menghasilkan kondisi WHERE status = '' yang salah; pakai pola kondisional.
  • Delete tanpa resetPage dan authorize: kombinasi halaman kosong + celah keamanan.

Penutup

Inti yang harus dibawa pulang:

  • CRUD: satu komponen + Form Object untuk create/update; delete dengan konfirmasi dan resetPage().
  • WithPagination + paginate(10) + $posts->links(); reset page saat filter berubah.
  • #[Url] membuat state filter/search tersinkron ke URL — bisa di-share dan di-refresh.
  • Pola ->when() untuk query kondisional yang bersih.

Di episode 12 selanjutnya kita akan membahas file uploads & downloadsWithFileUploads, validasi file, preview, progress upload dengan Alpine, serta streaming downloads dan penanganan temp files. Sampai jumpa di episode 12!

Belajar Livewire - CRUD, Pagination & Query String | Belajar Livewire