Blade Component untuk UI Reusable

Coba hitung berapa kali markup tombol, kartu, atau input form yang sama disalin di proyek Laravel Anda. Ketika desainer meminta warna tombol diganti atau pesan error ditampilkan de...

Blade Component untuk UI Reusable

Coba hitung berapa kali markup tombol, kartu, atau input form yang sama disalin di proyek Laravel Anda. Ketika desainer meminta warna tombol diganti atau pesan error ditampilkan dengan gaya baru, Anda harus mengedit puluhan file, dan biasanya ada satu halaman yang terlewat dan terlihat berbeda sendiri.

Blade Component menyelesaikan masalah ini: satu potongan UI ditulis sekali, lalu dipakai seperti tag HTML. Panduan ini membahas Blade Component di Laravel 11 mulai dari dasar sampai pola yang dipakai di aplikasi nyata, termasuk komponen input form lengkap dengan pesan validasi.

1. Class Component vs Anonymous Component

AspekAnonymous componentClass component
FileHanya resources/views/components/*.blade.phpKelas di app/View/Components + view
Deklarasi props@props([...])Parameter constructor
Logika PHPMinimal, sebatas tampilanBebas: format data, method helper, query ringan
Cocok untukButton, badge, card, alertMenu dinamis, tabel dengan format angka, widget statistik

Aturan praktis: mulai dari anonymous. Naikkan ke class component hanya ketika file Blade mulai berisi blok @php yang panjang.

2. Membuat Anonymous Component

Buat file resources/views/components/button.blade.php:

@props([
    'variant' => 'primary',
    'type'    => 'button',
])

@php
    $classes = match ($variant) {
        'danger'  => 'btn btn-danger',
        'outline' => 'btn btn-outline-primary',
        default   => 'btn btn-primary',
    };
@endphp

<button type="{{ $type }}" {{ $attributes->merge(['class' => $classes]) }}>
    {{ $slot }}
</button>

Dipakai di halaman mana pun:

<x-button type="submit">Simpan</x-button>
<x-button variant="danger" class="w-100" wire:confirm="Yakin?">Hapus</x-button>

Atribut yang tidak dideklarasikan di @props (seperti class dan wire:confirm) masuk ke $attributes. Method merge() menggabungkan class tambahan dengan class bawaan, bukan menimpanya.

3. Mengirim Data: String vs Ekspresi PHP

Ini sumber kebingungan nomor satu. Atribut biasa dikirim sebagai string literal. Untuk mengirim variabel atau ekspresi PHP, beri awalan titik dua:

<x-product-card title="Kopi Susu" />            {{-- string "Kopi Susu" --}}
<x-product-card :product="$product" />           {{-- objek Product --}}
<x-product-card :stock="0" />                    {{-- integer 0 --}}
<x-product-card :featured="true" />              {{-- boolean --}}

Nama atribut kebab-case otomatis dipetakan ke camelCase. Atribut show-price diterima sebagai $showPrice.

4. Slot dan Named Slot

$slot berisi konten di antara tag pembuka dan penutup. Untuk area lain, gunakan named slot:

{{-- resources/views/components/card.blade.php --}}
@props(['title' => null])

<div {{ $attributes->merge(['class' => 'card']) }}>
    @if ($title)
        <div class="card-header">{{ $title }}</div>
    @endif

    <div class="card-body">{{ $slot }}</div>

    @isset($footer)
        <div {{ $footer->attributes->merge(['class' => 'card-footer']) }}>
            {{ $footer }}
        </div>
    @endisset
</div>
<x-card title="Ringkasan Penjualan">
    Total hari ini: {{ number_format($total, 0, ',', '.') }}

    <x-slot:footer class="text-end">
        <x-button>Lihat Detail</x-button>
    </x-slot:footer>
</x-card>

Named slot juga punya attribute bag sendiri, sehingga pemakai komponen bisa menambah class ke footer tanpa mengubah komponennya. Untuk mengecek slot kosong, gunakan $slot->isEmpty().

5. Komponen Input Form yang Benar-Benar Berguna

Bagian form adalah tempat duplikasi paling parah: label, input, nilai lama dari old(), dan pesan error. Satu komponen bisa menggantikan belasan baris per field:

{{-- resources/views/components/form/input.blade.php --}}
@props([
    'name',
    'label' => null,
    'type'  => 'text',
    'value' => null,
])

<div class="mb-3">
    @if ($label)
        <label for="{{ $name }}" class="form-label">{{ $label }}</label>
    @endif

    <input
        id="{{ $name }}"
        name="{{ $name }}"
        type="{{ $type }}"
        value="{{ old($name, $value) }}"
        {{ $attributes->class(['form-control', 'is-invalid' => $errors->has($name)]) }}
    >

    @error($name)
        <div class="invalid-feedback">{{ $message }}</div>
    @enderror
</div>

Karena file berada di subfolder form, pemanggilannya memakai titik:

<x-form.input name="nama" label="Nama Produk" :value="$product->nama" required />
<x-form.input name="harga" label="Harga" type="number" min="0" :value="$product->harga" />

Method class() pada attribute bag menerima array kondisional, jadi class is-invalid hanya muncul jika field itu gagal validasi.

6. Class Component untuk Logika

php artisan make:component StockBadge
namespace App\View\Components;

use Illuminate\View\Component;
use Illuminate\Contracts\View\View;

class StockBadge extends Component
{
    public function __construct(public int $stock, public int $minimum = 5) {}

    public function level(): string
    {
        return match (true) {
            $this->stock === 0              => 'habis',
            $this->stock <= $this->minimum => 'menipis',
            default                         => 'aman',
        };
    }

    public function render(): View
    {
        return view('components.stock-badge');
    }
}
{{-- resources/views/components/stock-badge.blade.php --}}
<span class="badge badge-{{ $level() }}">{{ $stock }} ({{ $level() }})</span>

Method publik pada kelas bisa dipanggil langsung di view seperti variabel yang berupa closure. Logika penentuan level mudah dibuat unit test tanpa merender HTML.

7. Layout sebagai Komponen

Breeze dan Jetstream sudah memakai pola ini. Buat resources/views/components/layouts/app.blade.php berisi kerangka HTML dengan {{ $slot }} di bagian konten dan {{ $title ?? config('app.name') }} di tag title. Lalu setiap halaman cukup ditulis:

<x-layouts.app>
    <x-slot:title>Daftar Produk</x-slot:title>

    ...isi halaman...
</x-layouts.app>

8. Komponen Dinamis:.when, @props[], dan Looping

Tiga fitur kecil ini yang paling sering membuat komponen sulit dibaca. Memahami kapan memakainya membuat pustaka komponen jauh lebih bersih:

FiturPakai saatContoh
:variant="..."Propsi bisa berisi nilai dari sekumpulan pilihanTombol primary / secondary / danger
{{ $variant ?? 'primary' }}Nilai bawaan perlu, tapi bisa ditimpa pemanggilWarna default tombol
@props(['active' => false])Boolean yang nilai awalnya matiStatus aktif pada menu
<x-loop :data="$items" as="item" />Tag yang sama diulang banyak kaliDaftar tag, baris tabel
@foreach di dalam viewButuh logika di dalam markupBaris tabel dengan kondisi

Perhatikan sel boolean di tabel. Menulis :active="true" dan :active="false" membuat pemanggil lebih panjang, sedangkan mendeklarasikan sekali di @props membuat pemakaiannya cukup <x-menu-item active />. Itu perbedaan kecil yang langsung terasa kalau komponen dipakai puluhan kali.

Untuk kasus yang benar-benar perlu, <x-dynamic-component :component="$type" /> memilih komponen berdasarkan nilai runtime. Pola ini bekerja untuk tabel yang punya beberapa jenis baris, tapi jangan menjadikannya pilihan utama. Menyebut komponen secara eksplisit jauh lebih mudah dibaca daripada mencarinya lewat variabel.

9. Attribute Bag: Cara yang Benar Menggabungkan Class

Ini detail yang membedakan komponen yang bisa di-style ulang dari yang kaku. Tanpa $attributes->merge(), class bawaan komponen menimpa class yang Anda kirim dari pemanggil:

{{-- salah: class pemanggil hilang --}}
<button {{ $attributes->merge(['class' => 'btn btn-primary']) }}>

{{-- benar: keduanya gabung, class pemanggil menang --}}
<button {{ $attributes->merge(['class' => 'btn btn-primary']) }}>

{{-- pakai kelas dari config Tailwind, mis. memaksa warna --}}
<button {{ $attributes->merge(['class' => 'btn']) }}>

{{-- kelas kondisional + merge sekaligus --}}
<button {{ $attributes->merge(['class' => ['btn', 'py-2', $variant => 'btn-primary', $disabled ? 'opacity-50' : '']]) }}>

Begini cara kerjanya. $attributes adalah Illuminate\View\ComponentAttributeBag yang berisi semua atribut yang tidak dideklarasikan di @props. Method merge() menggabungkan class bawaan dengan class dari pemanggil, dan ketika keduanya sama, nilai pemanggil yang dipertahankan. Itu persis perilaku yang Anda inginkan saat menulis <x-button class="w-full">.

Untuk perilaku yang lebih halus, Laravel menyediakan class() yang menerima kelas bawaan, kelas dari variant yang Anda definisikan sendiri, atau kelas dari framework:

{{-- theme application's own button variants --}}
<button {{ $attributes->class(['btn', 'btn-primary' => $variant === 'primary']) }}>

{{-- menyatukan utility milik Tailwind --}}
<div {{ $attributes->class(['p-4', 'rounded-lg' => $rounded, 'shadow' => $shadow]) }}></div>

Selalu lakukan merge pada elemen terluar komponen, bukan pada elemen di dalam. Kalau Anda merge di elemen yang tidak holding slot, semua atribut yang dikirim pemanggil akan hilang begitu saja tanpa error — penyebab paling umum "kenapa class saya nggak masuk?"

10. Komponen yang Menyimpan State: @aware dan Broadcast

PHP bersifat satu arah: data masuk lewat props, lalu view selesai. Untuk hal yang benar-benar membutuhkan state bersama, seperti panel yang bisa dibuka semua sekaligus atau filter yang menempel di sidebar, Blade menyediakan dua alat:

{{-- komponen induk: state.php --}}
@props(['panelOpen' => false])
<div x-data="{ open: @js($panelOpen) }">
    {{ $slot }}
</div>

{{-- komponen anak membaca state induk tanpa meneruskannya manual --}}
@aware(['panelOpen'])
<button @click="$dispatch('toggle-panel')">...</button>

@aware membaca props dari komponen induk tanpa harus menulis rantai :panel-open="$panelOpen" di setiap anak. Ini menghemat banyak pekerjaan saat pohon komponen membesar, tapi ada aturan yang jelas: @aware hanya untuk state yang benar-benar bersama. Kalau sebuah komponen butuh nilai tertentu, teruskan sebagai props: lebih jelas dan lebih mudah dites.

Alternatif yang lebih ringan adalah @props(['message']) plus attribute events: komponen anak memanggil $dispatch('saved') dengan Alpine, dan induk mendengarkan @saved.window. Pola ini membuat komponen tetap stateless dan mudah diuji, dan biasanya itu pilihan yang lebih baik untuk aplikasi bisnis.

11. Kesalahan Umum dan Perbaikannya

  • Variabel terkirim sebagai teks "$product": lupa titik dua. Tulis :product="$product".
  • Class kustom hilang: komponen menulis class="..." langsung tanpa $attributes->merge().
  • Atribut muncul dua kali di HTML: prop yang sama tidak dideklarasikan di @props, sehingga ikut masuk attribute bag.
  • Perubahan tidak terlihat: view ter-cache. Jalankan php artisan view:clear.
  • Query database di dalam komponen yang dipakai di loop: menyebabkan N+1. Ambil data di controller, kirim lewat props.
  • Komponen melakukan navigasi atau menyimpan form: komponen sebaiknya presentation-only. Letakkan aksi ini di form action atau controller, bukan di dalam view.
  • Nama komponen tidak mengikuti konvensi: <x-user-card /> lebih baik daripada <x-card-user /> supaya autofill tag bekerja dan pustaka komponen mudah dibaca.

Checklist Pustaka Komponen

  • Satu komponen, satu tanggung jawab.
  • Semua komponen meneruskan $attributes ke elemen terluarnya.
  • Props punya nilai default yang masuk akal, dan boolean ditulis active, bukan :active="true".
  • Komponen form menangani old() dan @error.
  • Subfolder dipakai untuk kelompok (form, layouts, ui).
  • Tidak ada query di dalam view; semua data masuk lewat props.
  • Daftar komponen dan contoh pemakaiannya dicatat di README proyek.

Jika Anda baru mengenal struktur proyek, mulai dari struktur folder project Laravel. Untuk menyesuaikan tampilan source code siap pakai tanpa merusak fungsi lain, baca cara kustomisasi source code tanpa merusak; mengganti markup berulang menjadi komponen adalah salah satu langkah teraman. Padding dan spacing yang konsisten paling mudah ditulis sebagai komponen <x-section />. Latihan composants dengan_attribute merging akan terbayar cepat begitu proyek mulai punya puluhan view.

Ingin langsung mempraktikkannya?

Buat akun gratis untuk mengunduh contoh aplikasi Laravel, menyimpan tutorial favorit, dan mencoba kursus. Gratis, bisa daftar dengan akun Google.

Daftar Gratis Lihat katalog aplikasi
Yudhi
Ditulis oleh
Yudhi
Founder & Lead Developer, GudangCode

Yudhi adalah founder GudangCode dan developer Laravel dengan pengalaman membangun puluhan sistem informasi bisnis siap pakai — mulai dari POS, HRIS, hingga aplikasi manajemen. Ia menulis panduan dan artikel di GudangCode untuk membantu developer Indonesia menjalankan, memahami, dan men-deploy source code Laravel dengan benar.

Laravel PHP MySQL Sistem Informasi Bisnis
Lihat semua artikel Yudhi
Blade Component Laravel Programming & Laravel
Bacaan Paling Dicari
📚 Free Learning Hub

Learn Coding for Free at DhieCoderWeb

Explore Laravel, PHP, JavaScript tutorials, source code, web development guides, and practical programming tips.

DhieCoderWeb
100+
Tutorials
Free
Learning
SEO
Tips
Visit Dhiecoderweb.com →