Day 3 — Blade Components & Layout Lanjutan
Setelah menguasai sintaks dan layout Blade dasar, hari ini kita merapikan potongan antarmuka yang berulang menjadi component yang konsisten, mudah dipakai ulang, dan mudah dirawat.
Target Belajar Hari Ini
Pada Day 2 kita sudah memakai {{ }}, @if, @foreach, serta layout dengan @extends dan @section. Fondasi itu tetap dipakai; hari ini kita naik satu tingkat dengan memecah tampilan menjadi bagian yang memiliki nama dan tanggung jawab jelas.
Tujuan utamanya bukan sekadar membuat file lebih banyak. Kita ingin perubahan tampilan cukup dilakukan di satu tempat, sedangkan halaman yang memakai tampilan tersebut langsung mendapat hasil yang konsisten.
- Memahami apa itu Blade Component dan alasan fitur ini ada.
- Mengetahui kapan memakai Component dan kapan cukup memakai
@include. - Membuat class-based component dan anonymous component.
- Mengirim data melalui props serta isi melalui default dan named slot.
- Mengelola component agar tetap mudah ditemukan ketika jumlahnya bertambah.
Selesai materi ini, kamu diharapkan bisa melihat HTML yang berulang lalu memilih bentuk abstraksi yang tepat—bukan otomatis mengubah semua potongan HTML menjadi component.
<x-alert>. Mulailah dari pertanyaan: “bagian mana yang berulang, data apa yang berubah, dan isi apa yang perlu fleksibel?” Dari jawaban itulah props dan slot ditentukan.Kenapa perlu Component
Aplikasi nyata cepat dipenuhi pola HTML yang sama: alert, tombol, kartu siswa, badge status, dan kotak form. Jika setiap halaman menyalin markup tersebut, satu perubahan kelas CSS harus diedit berkali-kali dan hasil akhirnya mudah tidak konsisten.
Bayangkan component sebagai cetakan kue. Kita membuat bentuk cetakan sekali, lalu mengganti adonan atau hiasannya untuk setiap kue. Tanpa cetakan, kita membentuk setiap kue secara manual; selama jumlahnya dua mungkin terasa mudah, tetapi puluhan kue akan berbeda ukuran dan menyita waktu.
Sebelum memakai component, halaman menyalin seluruh struktur alert. Perhatikan bahwa yang sebenarnya berubah hanya jenis dan pesannya.
<!-- halaman profil -->
<div class="alert alert-success">
Profil berhasil disimpan.
</div>
<!-- halaman kelas -->
<div class="alert alert-danger">
Kelas gagal dihapus.
</div>
Sesudah dibuat sebagai component, pemanggil menyatakan maksud, sementara detail HTML berada di satu file. Ketika desain alert berubah, kita mengedit component sekali saja.
<x-alert type="success">
Profil berhasil disimpan.
</x-alert>
<x-alert type="danger">
Kelas gagal dihapus.
</x-alert>
<p> atau <div> menjadi component justru menyulitkan pembacaan. Perbaiki dengan mengekstrak pola yang benar-benar berulang, punya makna UI, atau memiliki variasi yang jelas.Blade Component Class-based
Class-based component terdiri dari class PHP dan view Blade. Class menjadi tempat menerima serta menyiapkan data, sedangkan view fokus menghasilkan HTML. Pemisahan ini cocok ketika component memiliki nilai default, transformasi data, atau logika presentasi yang mulai berarti.
Buat component bernama Alert melalui Artisan. Laravel akan menghasilkan dua file yang saling berpasangan.
php artisan make:component Alert
# File yang dibuat:
app/View/Components/Alert.php
resources/views/components/alert.blade.php
Data yang ditulis sebagai atribut pada <x-alert> masuk ke constructor publik. Properti publik $type kemudian tersedia di view component.
<?php
namespace App\View\Components;
use Closure;
use Illuminate\Contracts\View\View;
use Illuminate\View\Component;
class Alert extends Component
{
public function __construct(
public string $type = 'info',
) {}
public function render(): View|Closure|string
{
return view('components.alert');
}
}
<!-- view component -->
<div class="alert alert-{{ $type }}">
{{ $slot }}
</div>
<!-- di halaman mana pun -->
<x-alert type="success">
Data siswa berhasil disimpan.
</x-alert>
Nama class Alert otomatis dipanggil dengan tag kebab-case <x-alert>. Untuk nama bertingkat seperti FormInput, tag lazimnya menjadi <x-form-input>.
render()Method render() harus mengembalikan view, closure, atau string HTML. Jika hanya menulis view('components.alert'); tanpa return, component tidak punya hasil render. Perbaiki menjadi return view('components.alert');.Anonymous Component
Anonymous component adalah versi ringan yang hanya membutuhkan file Blade, tanpa class PHP pasangan. Laravel mencari tag <x-badge> di resources/views/components/badge.blade.php.
Karena tidak ada constructor, kontrak input dideklarasikan dengan @props di awal file. Nilai di sebelah kanan menjadi default jika atribut tidak dikirim oleh pemanggil.
@props([
'color' => 'gray',
])
<span class="badge badge-{{ $color }}">
{{ $slot }}
</span>
Pemanggilannya tetap memakai sintaks x- yang sama, sehingga halaman tidak perlu tahu apakah implementasinya class-based atau anonymous. Detail tersebut dapat diubah kemudian tanpa mengganti cara pemakaian selama kontrak props tetap sama.
<x-badge color="green">Aktif</x-badge>
<x-badge>Belum diverifikasi</x-badge>
Anonymous component juga dapat menerima atribut HTML tambahan melalui $attributes. Dengan merge(), class dasar tetap ada sementara pemanggil boleh menambahkan class lain.
<span {{ $attributes->merge(['class' => 'badge badge-' . $color]) }}>
{{ $slot }}
</span>
<x-badge color="green" class="text-sm">Aktif</x-badge>
@propsTanpa deklarasi @props(['color' => 'gray']), variabel $color belum tentu tersedia seperti yang diharapkan dan atribut dapat tercampur dengan attribute bag. Taruh @props pada baris awal component serta berikan default yang aman.@include untuk Partial Views
@include menyisipkan isi satu file Blade ke file lain. Ini cocok untuk partial view: fragmen seperti header, footer, atau potongan navigasi yang tidak memerlukan API component formal.
Berbeda dari component, partial tidak dipanggil memakai tag <x-...>, tidak memiliki slot, dan biasanya lebih bergantung pada konteks halaman. Ia adalah cara sederhana untuk memecah file panjang, bukan selalu sebuah unit UI mandiri.
<header>
<h1>{{ $judul }}</h1>
</header>
<!-- partial footer -->
<footer>© {{ date('Y') }} Kelas Laravel</footer>
<!-- pemakaian pada halaman -->
@include('partials.header', [
'judul' => 'Daftar Siswa',
])
<main>...</main>
@include('partials.footer')
Laravel memang meneruskan variabel yang tersedia pada view parent ke partial saat @include dijalankan. Namun, mengandalkan variabel implisit membuat partial rapuh: begitu dipanggil dari parent lain yang tidak memiliki variabel tersebut, muncul error undefined variable. Mengirim input secara eksplisit membuat kebutuhan partial terlihat.
Jadi, anggap array argumen kedua sebagai dokumentasi mini. Pembaca langsung tahu bahwa header memerlukan $judul, tanpa harus menelusuri seluruh variabel milik parent.
@include('partials.header', ['judul' => $judulHalaman]), atau gunakan nilai default bila sesuai.@includePakai @include untuk potongan struktural sederhana yang erat dengan halaman, seperti header dan footer. Pakai component jika fragmen merupakan elemen reusable formal, punya props/slot, butuh attribute forwarding, atau ingin kontrak pemakaian yang konsisten.Props & Slot
Props adalah data bernama yang mengatur component, misalnya warna, judul, atau status. Slot adalah ruang untuk menyisipkan isi dari pemanggil. Analogi sederhananya: props adalah pengaturan bingkai foto, sedangkan slot adalah foto yang dimasukkan ke dalam bingkai.
Slot tanpa nama disebut default slot dan tersedia sebagai $slot. Pada component kartu berikut, props $variant mengatur gaya, sedangkan isi paragraf dari pemanggil masuk ke area body.
@props([
'variant' => 'default',
])
<article {{ $attributes->merge([
'class' => 'card card-' . $variant,
]) }}>
<div class="card-body">
{{ $slot }}
</div>
</article>
Jika component membutuhkan beberapa area isi, gunakan named slot. Contohnya, judul masuk ke slot $title, sementara isi utama tetap masuk ke $slot.
<!-- components/card.blade.php -->
@props(['variant' => 'default'])
<article class="card card-{{ $variant }}">
<header class="card-title">{{ $title }}</header>
<div class="card-body">{{ $slot }}</div>
</article>
<!-- pemakaian -->
<x-card variant="highlight">
<x-slot:title>Profil Siswa</x-slot:title>
<p>Nama: {{ $siswa->nama }}</p>
</x-card>
Atribut statis ditulis seperti HTML biasa, misalnya variant="highlight". Untuk mengirim ekspresi PHP atau variabel, gunakan awalan titik dua, misalnya :variant="$warnaKartu"; tanpa titik dua nilainya dianggap string literal.
<!-- string tetap -->
<x-card variant="highlight">...</x-card>
<!-- ekspresi/variabel PHP -->
<x-card :variant="$warnaKartu">...</x-card>
{{ $slot }}Isi di antara tag pembuka dan penutup tidak muncul otomatis. View component harus menentukan posisinya dengan {{ $slot }}; named slot juga harus dicetak, misalnya {{ $title }}.Mengelola Component
Membuat component pertama biasanya mudah. Tantangan sebenarnya muncul setelah aplikasi membesar: file sulit ditemukan, variasi tampilan mulai disalin, dan logika menumpuk di Blade. Enam kasus berikut membantu menjaga component tetap sebagai alat perapihan—bukan sumber kerumitan baru.
Kasus 1 — Component makin banyak, folder berantakan
Masalah: semua file disimpan langsung di resources/views/components/. Saat jumlahnya puluhan, nama seperti input.blade.php, card.blade.php, dan header.blade.php tidak lagi menjelaskan konteksnya.
Solusi: kelompokkan berdasarkan keluarga UI, bukan berdasarkan halaman pemakai. Misalnya input form masuk ke form/, variasi kartu ke card/, dan kerangka halaman ke layout/. Subfolder menjadi bagian nama tag component.
resources/views/components/
├── form/
│ ├── input.blade.php # <x-form.input>
│ └── select.blade.php # <x-form.select>
├── card/
│ ├── siswa.blade.php # <x-card.siswa>
│ └── kelas.blade.php # <x-card.kelas>
└── layout/
└── panel.blade.php # <x-layout.panel>
Gunakan nama yang menyatakan peran, bukan warna sesaat: alert.blade.php dengan prop type lebih tahan lama daripada file terpisah alert-merah.blade.php. Hindari folder terlalu dalam; satu atau dua tingkat biasanya cukup.
<x-card.siswa>, struktur atau penamaannya perlu disederhanakan.Kasus 2 — Component butuh CSS sendiri
Masalah: component punya gaya khusus, tetapi menaruh <style> di setiap pemanggilan membuat CSS tercetak berulang. Class umum seperti .title juga mudah menimpa elemen lain.
Solusi: untuk contoh sangat kecil atau component yang hanya muncul sekali, class utility/inline dapat diterima. Untuk component reusable, taruh CSS di file asset seperti resources/css/components/alert.css, lalu import satu kali dari entry CSS utama yang dibangun Vite. Beri nama class terisolasi dengan pola seperti .c-alert dan .c-alert__title.
/* resources/css/app.css */
@import './components/alert.css';
/* resources/css/components/alert.css */
.c-alert { padding: 1rem; border-radius: .5rem; }
.c-alert--success { background: #dcfce7; }
.c-alert__title { font-weight: 700; }
Jangan menambahkan tag <link> dari dalam setiap card. Asset sebaiknya dimuat oleh layout melalui @vite. Jika memang ada CSS yang hanya diperlukan satu halaman, gunakan stack layout seperti @push('styles') secara sadar dan pastikan tidak didorong berulang.
.card, .header, atau .active terlalu umum. Pakai prefix component atau utility framework secara konsisten; jangan memakai !important sebagai obat pertama.Kasus 3 — Dipakai di banyak halaman, tetapi tampilannya sedikit berbeda
Masalah: card yang sama memerlukan badge di halaman daftar, tombol di halaman detail kelas, dan kadang tanpa footer. Menduplikasi menjadi siswa-card-a dan siswa-card-b membuat perbaikan markup harus dilakukan dua kali.
Solusi: pertahankan satu component. Gunakan prop opsional dengan nilai default untuk variasi kecil yang terstruktur, serta named slot untuk blok HTML fleksibel. Default membuat pemanggilan sederhana tetap ringkas.
<!-- components/card/siswa.blade.php -->
@props(['compact' => false])
<article class="student-card {{ $compact ? 'student-card--compact' : '' }}">
<h3>{{ $nama }}</h3>
{{ $slot }}
@isset($actions)
<footer>{{ $actions }}</footer>
@endisset
</article>
<!-- pemakaian -->
<x-card.siswa nama="Alya" :compact="true">
<x-slot:actions><a href="/siswa/1">Detail</a></x-slot:actions>
</x-card.siswa>
Jika prop boolean mulai menjadi lima atau enam sakelar yang saling memengaruhi, kontrak component sudah terlalu rumit. Pertimbangkan satu prop variant yang jelas atau pecah component berdasarkan tanggung jawab yang benar-benar berbeda.
Kasus 4 — Component perlu logic PHP agak rumit
Masalah: anonymous component mulai berisi banyak @php: memilih ikon, menghitung label, memformat data, dan menentukan beberapa class. View sulit dibaca dan sulit diuji.
Solusi: anonymous component tetap tepat bila hanya memiliki props, default, kondisi singkat, dan markup. Naikkan menjadi class-based component ketika perlu normalisasi data, method bantu, dependency injection, atau perhitungan presentasi yang digunakan beberapa kali.
class StudentCard extends Component
{
public function __construct(public Student $student) {}
public function statusLabel(): string
{
return $this->student->active ? 'Aktif' : 'Nonaktif';
}
}
Component class tetap seharusnya menangani logika presentasi. Query bisnis besar, perubahan database, atau aturan domain bukan tanggung jawab component; siapkan data di controller/service lalu kirimkan ke view.
Kasus 5 — Component dipanggil tetapi data salah atau kosong
Masalah: nama siswa kosong, muncul undefined variable, prop selalu berupa teks, atau Laravel mengatakan component tidak ditemukan. Debug secara berurutan agar tidak menebak-nebak.
- Periksa pemanggilan. Pastikan prop memang dikirim.
<x-card.siswa :siswa="$siswa" />mengirim variabel, sedangkansiswa="$siswa"mengirim string literal. - Cocokkan nama. Prop
student-namedipetakan ke variabel$studentName, bukan$student_name. Nama di@propsatau constructor harus sama dengan yang dipakai view. - Inspeksi data sementara. Gunakan
@dump($siswa)ataudd($siswa)di controller/component saat lokal. Hapus kembali setelah selesai dan jangan melakukan dump data sensitif di production. - Periksa lokasi file dan tag.
resources/views/components/card/siswa.blade.phpdipanggil sebagai<x-card.siswa>. Pastikan ekstensi tepat.blade.php, huruf/nama folder cocok, dan bukan tersimpan diresources/view/. - Bersihkan cache bila file baru masih tidak terdeteksi. Jalankan
php artisan view:cleardan, bila perlu,php artisan optimize:clear.
<!-- benar: nilai variabel PHP -->
<x-card.siswa :nama="$siswa->nama" />
<!-- salah bila yang diinginkan isi variabel -->
<x-card.siswa nama="$siswa->nama" />
Kasus 6 — Kapan jangan membuat component
Masalah: keinginan “membuat semuanya reusable” menghasilkan terlalu banyak file dan tag abstrak. Pembaca harus bolak-balik membuka file hanya untuk memahami tiga baris HTML yang dipakai sekali.
Solusi: jangan buat component jika markup hanya dipakai sekali, sangat spesifik pada satu halaman, atau bentuknya belum stabil. Tulis HTML langsung terlebih dahulu. Jika tujuanmu hanya memecah file halaman yang panjang dan fragmen masih bergantung pada variabel parent, @include sering lebih jujur.
| Situasi | Pilihan | Alasan |
|---|---|---|
| Tiga baris profil khusus satu halaman | HTML biasa | Belum ada reuse atau kontrak. |
| Header/footer struktural sederhana | @include | Memecah file tanpa API props/slot. |
| Alert/card dipakai lintas halaman | Component | Punya pola, input, dan tampilan konsisten. |
| UI reusable dengan persiapan data | Class-based component | Logika presentasi dapat dipisah dari markup. |
Praktik Mini — Alert, Header, dan Footer Sekolah
Kita akan membuat satu anonymous component <x-alert> untuk pesan reusable, lalu dua partial sederhana untuk kerangka halaman sekolah. Hasil akhirnya memperlihatkan kapan component dan @include bekerja berdampingan.
Langkah 1 — Siapkan file
mkdir -p resources/views/components resources/views/partials
# Buat file berikut melalui editor:
resources/views/components/alert.blade.php
resources/views/partials/header.blade.php
resources/views/partials/footer.blade.php
resources/views/sekolah.blade.php
Langkah 2 — Buat component alert
Deklarasikan prop type dengan default info. $slot menjadi pesan yang ditulis di antara tag pembuka dan penutup.
@props(['type' => 'info'])
<div {{ $attributes->merge([
'class' => 'alert alert-' . $type,
]) }} role="alert">
{{ $slot }}
</div>
Langkah 3 — Buat partial header dan footer
<header class="site-header">
<h1>{{ $namaSekolah }}</h1>
<nav><a href="/">Beranda</a> · <a href="/siswa">Siswa</a></nav>
</header>
<footer>
© {{ date('Y') }} {{ $namaSekolah }}
</footer>
Langkah 4 — Gabungkan di halaman sekolah
<!DOCTYPE html>
<html lang="id">
<body>
@include('partials.header', ['namaSekolah' => 'SMP Harapan'])
<main>
<x-alert type="success" class="mb-4">
Data siswa berhasil diperbarui.
</x-alert>
<x-alert>Jadwal semester baru sudah tersedia.</x-alert>
</main>
@include('partials.footer', ['namaSekolah' => 'SMP Harapan'])
</body>
</html>
Langkah 5 — Tambahkan route dan uji
Route::get('/sekolah', fn () => view('sekolah'));
Jalankan php artisan serve, buka /sekolah, lalu cek dua hal: kedua alert memiliki markup dasar yang sama meski tipenya berbeda, dan header/footer dapat diubah dari file partial tanpa menyentuh halaman.
warning, lalu kirim atribut id="pengumuman" ke <x-alert>. Periksa melalui DevTools bahwa atribut tersebut diteruskan oleh $attributes.Tugas / PR — Uji Pemahaman Hari Ini
Studi kasus: sekolah membutuhkan card siswa berisi foto, nama, dan kelas. Card yang sama harus dapat dipakai berulang pada halaman daftar siswa dan halaman detail kelas. Kerjakan bertingkat; jangan menyalin markup card ke setiap halaman.
Soal A — Dasar: buat kontrak card siswa
Buat anonymous component <x-card.siswa> yang menerima data foto, nama, dan kelas melalui props. Panggil minimal tiga kali dengan data berbeda di halaman daftar siswa.
Kriteria selesai:
- File berada di folder component yang rapi dan nama tag cocok dengan lokasinya.
- Semua foto memiliki
altbermakna; nama serta kelas berasal dari props. - Tidak ada tiga salinan struktur HTML card di halaman daftar.
@props(['foto', 'nama', 'kelas']).Soal B — Menengah: gunakan dari dua konteks
Pakai component yang sama pada halaman daftar siswa dan detail kelas. Tambahkan variasi opsional untuk mode ringkas atau badge status tanpa membuat file card kedua.
Kriteria selesai:
- Kedua halaman memanggil tag component yang sama.
- Variasi memiliki default sehingga pemanggilan lama tetap bekerja.
- Data dinamis dikirim dengan sintaks
:, bukan menjadi string literal. - Tampilan standar dan variasi ringkas/status dapat dibedakan dengan jelas.
'compact' => false atau satu named slot.Soal C — Tantangan terbuka: aksi yang fleksibel
Tambahkan area aksi opsional. Di daftar siswa area ini berisi tautan “Lihat”, sedangkan di detail kelas dapat berisi “Pindahkan kelas”; card tetap rapi jika area aksi tidak diberikan. Putuskan apakah kebutuhan ini lebih cocok menjadi named slot atau prop, lalu tulis alasanmu dalam 2–3 kalimat di catatan PR.
Kriteria selesai:
- Area aksi menerima HTML yang berbeda tanpa duplikasi component.
- Card tanpa aksi tidak menghasilkan footer kosong.
- Fokus keyboard, teks link/tombol, dan struktur HTML tetap masuk akal.
- PR menyertakan tangkapan layar dua halaman serta penjelasan pilihan props/slot.
@isset($actions).Playground PHP — Simulasi Component
Blade Component membutuhkan Laravel, sedangkan playground ini menjalankan PHP murni di browser memakai WebAssembly (php-wasm). Fungsi PHP di bawah mensimulasikan ide component: menerima “props”, memberi nilai default, mengamankan data, lalu menghasilkan tampilan yang konsisten.
Mesin PHP sedang disiapkan…
?? seperti default pada @props, dan argumen konten seperti slot. Di Laravel, Blade menangani rendering component dengan sintaks yang lebih nyaman.Latihan Mandiri & Checklist Selesai
Latihan mandiri
- Di playground, tambahkan siswa ketiga lalu beri status berbeda. Pastikan format card tetap konsisten.
- Pilih contoh prop default dan ubah nilai defaultnya; amati pemanggilan yang tidak mengirim prop tersebut.
- Di proyek Laravel, pindahkan satu pola HTML berulang menjadi anonymous component.
- Tambahkan satu named slot opsional dan pastikan component tetap benar ketika slot tidak diisi.
- Susun component ke subfolder berdasarkan keluarga UI, lalu sesuaikan tag pemanggilannya.
Checklist sebelum lanjut Day 4
- Bisa menjelaskan perbedaan HTML biasa,
@include, anonymous component, dan class-based component. - Bisa membuat serta memanggil component dengan nama folder/tag yang benar.
- Bisa mengirim prop statis dan dinamis, termasuk memberi nilai default.
- Bisa memakai default slot dan named slot tanpa menduplikasi markup.
- Bisa menelusuri data kosong dari view pemanggil sampai view component.
- Component proyek tersusun rapi dan tidak dibuat untuk HTML sekali pakai.
🏁 Milestone Day 3 Tercapai!
Hari ini kamu sudah memahami Blade Component, props, slot, partial, serta cara memilih dan mengorganisir kode reusable. Kamu sekarang mampu membuat UI konsisten tanpa menyalin markup ke banyak halaman.