Skip to content

Tutorial Laravel 13 + Livewire 4+ Flux UI: Panduan Lengkap CRUD Category

Yhotie
Published date:
Edit this post

Kombinasi Laravel 13, Livewire 4, dan Flux UI merupakan salah satu stack paling modern dan efisien untuk membangun aplikasi web interaktif tanpa harus keluar dari ekosistem PHP.

Flux UI adalah library komponen UI resmi yang dirancang oleh Caleb Porzio (pencipta Livewire). Dengan Flux, kita bisa membuat antarmuka yang sangat indah, responsif, dan kaya fitur (seperti modal, sidebar, tabel, dll.) dengan sintaksis HTML-like yang bersih.

Dalam panduan ini, kita akan belajar langkah demi langkah membangun fitur CRUD Category (Manajemen Kategori) secara lengkap dan rapi.


Prasyarat & Persiapan Awal

Sebelum memulai, pastikan Anda telah menginstal proyek Laravel baru, mengonfigurasi database, serta memasang Livewire 3 dan Flux UI.

Jika semua sudah siap, pastikan database sudah dimigrasi dan server aset Anda berjalan:

# Menjalankan migrasi awal database
php artisan migrate

# Menjalankan build tools (Vite/Tailwind)
npm run dev

Langkah 1: Membuat Model dan Migration Category

Pertama, kita butuh tabel di database untuk menyimpan data kategori. Kita bisa membuat model Eloquent sekaligus file migrasinya dengan satu perintah:

php artisan make:model Category -m

Perintah di atas akan menghasilkan dua file penting:

  1. Model: app/Models/Category.php
  2. Migration: database/migrations/xxxx_xx_xx_create_categories_table.php

1. Konfigurasi Migration

Buka file migration yang baru dibuat, lalu tambahkan kolom name dan description seperti di bawah ini:

// database/migrations/xxxx_xx_xx_create_categories_table.php
Schema::create('categories', function (Blueprint $table) {
    $table->id();
    $table->string('name')->unique();
    $table->text('description')->nullable();
    $table->timestamps();
});

2. Konfigurasi Model

Buka file model Category.php dan tambahkan properti $fillable agar kolom tersebut bisa diisi secara massal (mass assignment):

// app/Models/Category.php
namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Category extends Model
{
    protected $fillable = ['name', 'description'];
}

Setelah itu, jalankan migrasi untuk membuat tabel categories di database Anda:

php artisan migrate

Langkah 2: Membuat Komponen Livewire Index

Untuk menampilkan daftar kategori, kita akan membuat sebuah komponen Livewire khusus halaman (Full-page Component). Pada Livewire 4, format default yang digunakan adalah Single-File Component (SFC) di mana logika PHP dan markup HTML disatukan dalam satu file Blade tunggal untuk mengurangi boilerplate.

Jalankan perintah berikut untuk membuatnya:

php artisan make:livewire pages::category.index

Perintah di atas akan menghasilkan satu file komponen tunggal:

Mendaftarkan Route

Agar halaman ini bisa diakses lewat browser, tambahkan route baru di file routes/web.php menggunakan directive Route::livewire():

// routes/web.php
use Illuminate\Support\Facades\Route;

Route::livewire('/categories', 'pages::category.index')
    ->middleware(['auth'])
    ->name('category.index');

Langkah 3: Membuat Livewire Form Object (CategoryForm)

Praktik terbaik di Livewire 3 untuk menangani input dan validasi formulir adalah menggunakan Form Object. Ini menjaga agar class komponen utama tetap bersih dan fokus pada logika halaman.

Buat Form Object baru dengan perintah:

php artisan livewire:form CategoryForm

Buka file app/Livewire/Forms/CategoryForm.php yang dihasilkan, lalu sesuaikan isinya untuk menangani proses validasi dan penyimpanan:

<?php

namespace App\Livewire\Forms;

use App\Models\Category;
use Illuminate\Validation\Rule;
use Livewire\Form;

class CategoryForm extends Form
{
    public string $name = '';
    
    public string $description = '';

    public ?Category $category = null;

    public function rules(): array
    {
        return [
            'name' => [
                'required',
                'string',
                'min:3',
                'max:255',
                Rule::unique('categories', 'name')->ignore($this->category?->id),
            ],
            'description' => [
                'nullable',
                'string',
                'max:1000',
            ],
        ];
    }

    public function setCategory(Category $category): void
    {
        $this->category = $category;
        $this->name = $category->name;
        $this->description = $category->description ?? '';
    }

    public function store()
    {
        $this->validate();
        Category::create($this->only(['name', 'description']));
        $this->reset();
    }

    // update
    public function update()
    {
        $this->validate();
        $this->category->update($this->only(['name', 'description']));
    }
}

Langkah 4: Menulis Logika Komponen Livewire Index

Buka file komponen utama di resources/views/livewire/pages/category/index.blade.php. Karena komponen ini dibuat sebagai Single-File Component, pertama kita akan menulis blok logika PHP (<?php ... ?>) di bagian paling atas file tersebut:

<?php

use Livewire\Component;
use Livewire\Attributes\Computed;
use Livewire\WithPagination;
use App\Models\Category;

new class extends Component
{
    use WithPagination;

    public $sortBy = 'name';
    public $sortDirection = 'desc';

    public function sort($column) {
        if ($this->sortBy === $column) {
            $this->sortDirection = $this->sortDirection === 'asc' ? 'desc' : 'asc';
        } else {
            $this->sortBy = $column;
            $this->sortDirection = 'asc';
        }
    }
    
    #[Computed]
    public function categories()
    {
        // return Category::latest()->paginate(2);
        
        return Category::query()
            ->tap(fn ($query) => $this->sortBy ? $query->orderBy($this->sortBy, $this->sortDirection) : $query)
            ->paginate(5);
    }

    public function edit($id){
        $this->dispatch('edit-category', id: $id);
    }
};
?>


Langkah 5: Mendesain UI Modern dengan Flux UI

Tepat di bawah blok PHP (?>) pada file komponen resources/views/livewire/pages/category/index.blade.php yang sama, tuliskan kode Blade berikut untuk menyusun tampilan antarmuka (UI) menggunakan Flux UI:

<div class="max-w-7xl mx-auto space-y-4">
    <flux:heading size="xl" class="text-zinc-800 dark:text-white">Category</flux:heading>
    <flux:subheading size="lg" class="text-zinc-600 dark:text-zinc-400">Manage your categories</flux:subheading>
    <flux:separator variant="subtle" />

    <!-- modal -->
    <flux:modal.trigger name="create-category">
        <flux:button variant="primary" icon="plus" color="primary">Add Category</flux:button>
    </flux:modal.trigger>

    <livewire:category.create />
    <livewire:category.edit />

    <x-flash-message />

    {{-- table --}}
    <div class="overflow-x-auto">
       <flux:table :paginate="$this->categories">
            <flux:table.columns>
                <flux:table.column sortable :sorted="$sortBy === 'name'" :direction="$sortDirection" wire:click="sort('name')">Name</flux:table.column>
                <flux:table.column>Description</flux:table.column>
                <flux:table.column sortable :sorted="$sortBy === 'created_at'" :direction="$sortDirection" wire:click="sort('created_at')">Created At</flux:table.column>
                <flux:table.column></flux:table.column>
            </flux:table.columns>

            <flux:table.rows>
                @foreach ($this->categories as $category)
                    <flux:table.row :key="$category->id">
                        
                        <flux:table.cell class="flex items-center gap-3">
                            {{ $category->name }}
                        </flux:table.cell>

                        <flux:table.cell class="text-zinc-500 dark:text-zinc-400">
                            {{ $category->description ?? '-' }}
                        </flux:table.cell>

                        <flux:table.cell class="whitespace-nowrap">{{ $category->created_at->diffForHumans() }}</flux:table.cell>

                        <flux:table.cell>


                            <flux:dropdown>
                                <flux:button variant="ghost" size="sm" icon="ellipsis-horizontal" inset="top bottom"></flux:button>

                                <flux:menu>
                                    <flux:menu.item icon="pencil" wire:click="edit({{ $category->id }})">Edit</flux:menu.item>

                                    <flux:menu.separator />

                                    {{-- <flux:menu.item variant="danger" icon="trash" wire:click="$dispatch('confirm-delete', id: $category->id)">Delete</flux:menu.item> --}}
                                    <flux:menu.item variant="danger" icon="trash" wire:click="$dispatch('confirm-delete', {id: {{ $category->id }}})">Delete</flux:menu.item>
                                </flux:menu>
                            </flux:dropdown>
                        </flux:table.cell>
                    </flux:table.row>
                @endforeach
            </flux:table.rows>
        </flux:table>


    </div>
   
</div>

Langkah 6: Membuat Komponen Create Category (Single-File Component)

Selanjutnya, kita akan membuat komponen untuk menambahkan kategori baru. Komponen ini dirancang dalam bentuk modal dialog interaktif yang dipicu dari tombol Add Category pada halaman Index.

Dengan fitur Single-File Component (SFC) bawaan Livewire 4, seluruh logika PHP dan tampilan HTML (Blade) dapat disatukan di dalam satu file komponen tunggal. Buatlah file baru di resources/views/livewire/category/create.blade.php dengan perintah di bawah dan tambahkan kode berikut:

php artisan make:livewire category.create

<?php

use App\Livewire\Forms\CategoryForm;
use Livewire\Component;

new class extends Component
{
    public CategoryForm $form;

    public function save()
    {
        $this->form->store();
        Flux::modal('create-category')->close();

        // session
        session()->flash('success', 'Category created successfully');

        $this->redirectRoute('category.index',navigate: true);
    }

    public function resetForm()
    {
        $this->resetValidation();
        $this->form->reset();
    }
};
?>

<div>
    <flux:modal 
        name="create-category" 
        class="md:w-150" 
        x-on:close="$wire.resetForm()" 
    >
        <form class="space-y-8" wire:submit.prevent="save">
            {{-- header --}}
            <div class="space-y-2">
                <flux:heading size="lg" class="text-zinc-900 dark:text-white">
                    Create Category
                </flux:heading>
                <flux:text class="text-zinc-500 dark:text-zinc-400">
                    Add a new category to your account
                </flux:text>
            </div>

            {{-- form field --}}
            <div class="space-y-6">
                <flux:input
                    label="Name"
                    placeholder="Enter category name"
                    wire:model="form.name"
                />

                <flux:textarea
                    label="Description"
                    placeholder="Enter category description"
                    wire:model="form.description"
                />
            </div>
    
            {{-- footer --}}
            <div class="flex items-center justify-end gap-3 pt-4 border-t border-zinc-200 dark:border-zinc-800">
                <flux:modal.close>
                    <flux:button variant="outline" color="neutral">Cancel</flux:button>
                </flux:modal.close>
                <flux:button variant="primary" color="primary" type="submit">Create</flux:button>
            </div>
                

        </form>
    </flux:modal>
</div>

Langkah 7: Membuat Komponen Edit & Delete Category (Single-File Component)

Untuk melengkapi fungsionalitas CRUD secara menyeluruh, kita akan membuat satu komponen Single-File Component lagi yang bertugas menangani aksi pengubahan (Edit) dan penghapusan (Delete) kategori. Menggabungkan kedua aksi ini sangat efisien karena keduanya membutuhkan referensi data model kategori yang sama yang sedang dipilih oleh pengguna.

Buatlah file komponen baru di resources/views/livewire/category/edit.blade.php dengan perintah dan isi kode sebagai berikut:

php artisan make:livewire category.edit
<?php

use Livewire\Component;
use Livewire\Attributes\On;
use App\Livewire\Forms\CategoryForm;
use App\Models\Category;

new class extends Component
{
    public CategoryForm $form;

    #[On('edit-category')]
    public function editCategory($id){
        $category = Category::find($id);
        $this->form->setCategory($category);
        Flux::modal('edit-category')->show();
    }

    public function updateCategory() {
        $this->form->update();
        Flux::modal('edit-category')->close();
        session()->flash('success', 'Category updated successfully');
        $this->redirectRoute('category.index', navigate: true);
    }

    public function resetForm()
    {
        $this->resetValidation();
        $this->form->reset();
    }

    #[On('confirm-delete')]
    public function confirmDelete($id)
    {
        $category = Category::find($id);
        $this->form->setCategory($category);
        Flux::modal('delete-category')->show();
    }

    public function deleteCategory() {
        $this->form->category->delete();
        Flux::modal('delete-category')->close();
        session()->flash('success', 'Category deleted successfully');
        $this->redirectRoute('category.index', navigate: true);
    }
};
?>

<div>
    <flux:modal 
        name="edit-category" 
        class="md:w-150" 
        x-on:close="$wire.resetForm()" 
    >
        <form class="space-y-8" wire:submit.prevent="updateCategory">
            {{-- header --}}
            <div class="space-y-2">
                <flux:heading size="lg" class="text-zinc-900 dark:text-white">
                    Edit Category
                </flux:heading>
                <flux:text class="text-zinc-500 dark:text-zinc-400">
                    Edit your category details below
                </flux:text>
            </div>

            {{-- form field --}}
            <div class="space-y-6">
                <flux:input
                    label="Name"
                    placeholder="Enter category name"
                    wire:model="form.name"
                    wire:dirty.class.text-red-500
                />

                <flux:textarea
                    label="Description"
                    placeholder="Enter category description"
                    wire:model="form.description"
                    wire:dirty.class.text-red-500
                />
            </div>

            <div 
                wire:show ="$dirty"
                class="text-red-500 dark:text-red-400"
            >
                you have unsaved changes
            </div>
    
            {{-- footer --}}
            <div class="flex items-center justify-end gap-3 pt-4 border-t border-zinc-200 dark:border-zinc-800">
                <flux:modal.close>
                    <flux:button variant="outline" color="neutral">Cancel</flux:button>
                </flux:modal.close>
                <flux:button variant="primary" color="primary" type="submit">Update</flux:button>
            </div>
                

        </form>
    </flux:modal>


    {{-- delete modal --}}

    <flux:modal 
        name="delete-category" 
        class="md:w-150" 
        x-on:close="$wire.resetForm()" 
    >
        <form class="space-y-8" wire:submit.prevent="deleteCategory">
            {{-- header --}}
            <div class="space-y-2">
                <flux:heading size="lg" class="text-zinc-900 dark:text-white">
                    Delete Category
                </flux:heading>
                <flux:text class="text-zinc-500 dark:text-zinc-400">
                    this action cannot be undone
                </flux:text>
            </div>

            {{-- footer --}}
            <div class="flex items-center justify-end gap-3 pt-4 border-t border-zinc-200 dark:border-zinc-800">
                <flux:modal.close>
                    <flux:button variant="outline" color="neutral">Cancel</flux:button>
                </flux:modal.close>
                <flux:button variant="primary" color="danger" type="submit">Delete</flux:button>
            </div>
                

        </form>
    </flux:modal>
</div>

    

Langkah 8: Memperbarui Sidebar Navigasi

Agar pengguna dapat berpindah ke halaman Kategori dengan mudah, kita perlu mendaftarkannya pada menu sidebar navigasi utama aplikasi Anda (biasanya berada di layout utama seperti layouts.app atau komponen sidebar khusus).

Tambahkan baris berikut di dalam komponen sidebar Anda:


<flux:sidebar.item icon="folder" 
	:href="route('category.index')" 
	:current="request()->routeIs('category.index')" 
	wire:navigate
>

{{ __('Category') }}

</flux:sidebar.item>

Tips Tambahan: Membuat Komponen Notifikasi (Flash Message) Kustom

Jika Anda ingin notifikasi sukses yang tampil lebih modular di berbagai halaman, Anda bisa membuat komponen Blade kustom:

php artisan make:component FlashMessage --view

Buka file komponen di resources/views/components/flash-message.blade.php dan buatlah komponen notifikasi yang cantik:

@foreach (['success','error','warning','info'] as $type)
    @php
        $bgColor = match($type) {
            'success' => 'bg-green-600 text-white',
            'error' => 'bg-red-600 text-white',
            'warning' => 'bg-yellow-600 text-white',
            'info' => 'bg-blue-600 text-white',
        };
    @endphp

    @if (session()->has($type))
        <div 
            x-data="{ show: true }"
            x-show="show"
            x-init="setTimeout(() => show = false, 3000)"
            x-transition:enter="transition ease-out duration-300"
            x-transition:enter-start="opacity-0 transform -translate-y-4"
            x-transition:enter-end="opacity-100 transform translate-y-0"
            x-transition:leave="transition ease-in duration-300"
            x-transition:leave-start="opacity-100 transform translate-y-0"
            x-transition:leave-end="opacity-0 transform -translate-y-4"
            class="{{ $bgColor }} fixed top-4 right-4 z-50 p-4 mb-4 text-sm rounded-lg" role="alert">
            <span class="font-medium">{{ ucfirst($type) }}!</span> {{ session($type) }}
        </div>
    @endif
@endforeach

Sekarang, Anda cukup meletakkan <x-flash-message /> di layout utama aplikasi (app.blade.php), dan setiap notifikasi sukses yang dikirim lewat session()->flash('success') akan muncul secara elegan dengan animasi halus di pojok kanan bawah!


Ringkasan Alur Kerja

Dengan kombinasi ini, alur kerja pengembangan Anda menjadi sangat efisien:

  1. Model & Migration untuk mendefinisikan database schema.
  2. Form Object (CategoryForm) untuk merapikan urusan validasi formulir dan binding data.
  3. Livewire Component (Index) sebagai pengendali logika halaman.
  4. Flux UI Components untuk membangun antarmuka premium secara deklaratif tanpa pusing menulis CSS tambahan atau library JS eksternal.

Selamat mencoba membangun aplikasi hebat Anda berikutnya dengan Laravel, Livewire, dan Flux UI!

Next
Install dan Konfigurasi MariaDB di Arch Linux