BAB 11: Blade Templates

Template engine Blade: directives, layout inheritance, komponen, slot, dan cara kerja kompilasi Blade.

Di bab sebelumnya, kita menulis HTML langsung di file .blade.php — termasuk @if, @foreach, dan {{ }} — tanpa benar-benar memahami dari mana sintaks itu berasal. Sekarang saatnya membedah Blade secara menyeluruh.

Blade adalah template engine bawaan Laravel. Setiap file .blade.php dikompilasi menjadi PHP murni, lalu hasilnya disimpan di storage/framework/views/. Kompilasi hanya terjadi sekali — request berikutnya langsung pakai cache tanpa overhead apapun. Ini artinya Blade tidak lebih lambat dari PHP biasa, hanya lebih nyaman ditulis.

Menampilkan Data

Cara paling dasar adalah {{ $variabel }}. Tanda kurung kurawal ganda ini menghasilkan output yang sudah di-escape — karakter seperti <, >, dan & diubah menjadi HTML entities sehingga aman dari serangan XSS.

{{-- resources/views/catatan/show.blade.php --}}

<h1>{{ $catatan->judul }}</h1>
<p>{{ $catatan->isi }}</p>
<small>Dibuat: {{ $catatan->created_at->diffForHumans() }}</small>

Kadang ada situasi di mana kita memang ingin menampilkan HTML mentah — misalnya konten artikel yang sudah disimpan sebagai HTML dari rich text editor. Untuk itu, gunakan {!! !!}:

<div class="konten-artikel">
    {!! $catatan->isi_html !!}
</div>

Jangan gunakan {!! !!} untuk menampilkan input dari pengguna tanpa sanitasi terlebih dahulu. Ini membuka celah XSS. Gunakan hanya untuk konten yang sudah kamu kendalikan dan pastikan aman.

Jika menggunakan framework JavaScript seperti Vue atau Alpine.js yang juga memakai sintaks {{ }}, tambahkan @ di depan untuk memberitahu Blade agar tidak memproses ekspresi tersebut:

<div>
    {{-- Ini diproses Blade --}}
    Halo, {{ $namaUser }}

    {{-- Ini dibiarkan untuk Vue/Alpine --}}
    <span>@{{ pesanReaktif }}</span>
</div>

Blade Directives

Directives adalah instruksi khusus yang dimulai dengan @. Ini yang membuat Blade lebih ekspresif dari PHP biasa.

Kondisional

@if bekerja seperti if di PHP, tapi tanpa tanda kurung kurawal dan tanda tutup yang verbose:

{{-- resources/views/catatan/index.blade.php --}}

@if ($catatanList->isEmpty())
    <div class="pesan-kosong">
        <p>Belum ada catatan. Mulai buat sekarang!</p>
    </div>
@elseif ($catatanList->count() < 5)
    <p>Kamu punya {{ $catatanList->count() }} catatan. Terus tambah!</p>
@else
    <p>Total: {{ $catatanList->count() }} catatan</p>
@endif

@unless adalah kebalikan @if — bloknya dijalankan ketika kondisi bernilai false:

@unless (auth()->user()->sudahVerifikasiEmail())
    <div class="banner-peringatan">
        Tolong verifikasi email kamu untuk mengakses semua fitur.
    </div>
@endunless

Untuk mengecek apakah variabel terdefinisi atau memiliki nilai, ada @isset dan @empty:

@isset($judulHalaman)
    <title>{{ $judulHalaman }} | Catatan App</title>
@endisset

@empty($catatanList)
    <p>Tidak ada catatan ditemukan.</p>
@endempty

Switch

Untuk kondisi dengan banyak cabang, @switch lebih rapi dari rangkaian @elseif:

@switch($catatan->prioritas)
    @case('tinggi')
        <span class="badge badge-merah">Prioritas Tinggi</span>
        @break
    @case('sedang')
        <span class="badge badge-kuning">Prioritas Sedang</span>
        @break
    @default
        <span class="badge badge-abu">Prioritas Normal</span>
@endswitch

Loop

@foreach adalah yang paling sering dipakai. @forelse adalah versi yang lebih praktis karena menangani kasus koleksi kosong secara bawaan:

@forelse ($catatanList as $catatan)
    <div class="kartu-catatan">
        <h3>{{ $catatan->judul }}</h3>
        <p>{{ Str::limit($catatan->isi, 100) }}</p>
    </div>
@empty
    <p>Belum ada catatan.</p>
@endforelse

Di dalam loop, Blade menyediakan variabel $loop yang berisi informasi berguna tentang iterasi saat ini:

@foreach ($catatanList as $catatan)
    <div class="kartu-catatan @if ($loop->first) pertama @endif @if ($loop->last) terakhir @endif">
        <span class="nomor">{{ $loop->iteration }}</span>
        <h3>{{ $catatan->judul }}</h3>

        @if (!$loop->last)
            <hr>
        @endif
    </div>
@endforeach

Properti $loop yang tersedia:

PropertiKeterangan
$loop->indexIndeks iterasi saat ini (mulai dari 0)
$loop->iterationNomor iterasi (mulai dari 1)
$loop->remainingSisa iterasi
$loop->countTotal item dalam koleksi
$loop->firsttrue jika iterasi pertama
$loop->lasttrue jika iterasi terakhir
$loop->eventrue jika iterasi genap
$loop->oddtrue jika iterasi ganjil
$loop->depthLevel kedalaman loop (untuk nested loop)
$loop->parentVariabel $loop dari loop induk

Directives untuk Autentikasi

Dua directives ini menggantikan pengecekan Auth::check() yang berulang:

{{-- resources/views/layouts/app.blade.php --}}

<nav>
    @auth
        <a href="{{ route('catatan.index') }}">Catatan Saya</a>
        <form method="POST" action="{{ route('logout') }}">
            @csrf
            <button type="submit">Keluar</button>
        </form>
    @endauth

    @guest
        <a href="{{ route('login') }}">Masuk</a>
        <a href="{{ route('register') }}">Daftar</a>
    @endguest
</nav>

Template Inheritance

Setiap halaman aplikasi biasanya berbagi struktur yang sama: <html>, <head>, navigation bar, footer. Tanpa mekanisme reuse, kita akan menduplikasi ratusan baris di setiap file view. Template inheritance menyelesaikan ini.

Membuat Layout

Layout adalah template “induk” yang mendefinisikan struktur dasar. Di dalam layout, @yield('nama-section') menandai tempat di mana konten dari halaman anak akan disisipkan:

{{-- resources/views/layouts/app.blade.php --}}

<!DOCTYPE html>
<html lang="id">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>@yield('judul', 'Catatan App')</title>
    @stack('styles')
</head>
<body>
    <header>
        <nav>
            <a href="{{ route('catatan.index') }}">Catatan App</a>

            @auth
                <a href="{{ route('catatan.create') }}">+ Buat Catatan</a>
            @endauth
        </nav>
    </header>

    <main>
        @if (session('sukses'))
            <div class="alert alert-sukses">
                {{ session('sukses') }}
            </div>
        @endif

        @yield('konten')
    </main>

    <footer>
        <p>&copy; {{ date('Y') }} Catatan App</p>
    </footer>

    @stack('scripts')
</body>
</html>

Menggunakan Layout

Halaman anak menggunakan @extends untuk mendeklarasikan layout yang digunakan, lalu @section untuk mengisi section yang sudah didefinisikan:

{{-- resources/views/catatan/index.blade.php --}}

@extends('layouts.app')

@section('judul', 'Daftar Catatan')

@section('konten')
    <div class="container">
        <div class="header-halaman">
            <h1>Catatan Saya</h1>
            <a href="{{ route('catatan.create') }}" class="tombol">
                Buat Catatan Baru
            </a>
        </div>

        <div class="grid-catatan">
            @forelse ($catatanList as $catatan)
                <div class="kartu-catatan">
                    <h3>
                        <a href="{{ route('catatan.show', $catatan) }}">
                            {{ $catatan->judul }}
                        </a>
                    </h3>
                    <p>{{ Str::limit($catatan->isi, 120) }}</p>
                    <small>{{ $catatan->created_at->diffForHumans() }}</small>
                </div>
            @empty
                <p>Belum ada catatan. <a href="{{ route('catatan.create') }}">Buat yang pertama!</a></p>
            @endforelse
        </div>

        {{ $catatanList->links() }}
    </div>
@endsection

@push('scripts')
    <script>
        // JavaScript khusus halaman ini
        console.log('Halaman daftar catatan dimuat');
    </script>
@endpush

Perhatikan @push('scripts') di akhir. Ini mendorong konten ke stack scripts yang didefinisikan di layout dengan @stack('scripts'). Cara ini memungkinkan setiap halaman menambahkan script atau style spesifik tanpa mengubah layout.

Diagram Blade template inheritance: layout sebagai induk, child view mengisi section

Gambar 1: Blade Template Inheritance

Komponen Blade

Template inheritance bagus untuk layout halaman, tapi bagaimana dengan elemen UI yang dipakai berulang — seperti tombol, kartu, badge, atau form input? Di sinilah Blade Components berperan.

Anonymous Components

Anonymous component adalah komponen yang hanya terdiri dari file view — tanpa class PHP. Buat file di resources/views/components/:

{{-- resources/views/components/kartu-catatan.blade.php --}}

@props(['catatan', 'tampilkanPenulis' => false])

<div class="kartu">
    <div class="kartu-header">
        <h3 class="kartu-judul">
            <a href="{{ route('catatan.show', $catatan) }}">
                {{ $catatan->judul }}
            </a>
        </h3>

        @if ($tampilkanPenulis)
            <span class="penulis">oleh {{ $catatan->user->nama }}</span>
        @endif
    </div>

    <div class="kartu-isi">
        {{ $slot }}
    </div>

    <div class="kartu-footer">
        <small>{{ $catatan->created_at->diffForHumans() }}</small>
    </div>
</div>

Direktif @props mendefinisikan props yang diterima komponen beserta nilai defaultnya. Variabel yang tidak ada di @props tapi dilempar saat penggunaan akan masuk ke $attributes — berguna untuk meneruskan class atau data attribute.

Gunakan komponen dengan tag <x-:

{{-- resources/views/catatan/index.blade.php --}}

@foreach ($catatanList as $catatan)
    <x-kartu-catatan :catatan="$catatan" :tampilkan-penulis="true">
        {{ Str::limit($catatan->isi, 120) }}
    </x-kartu-catatan>
@endforeach

Class-Based Components

Untuk komponen yang butuh logika lebih kompleks — misalnya menghitung sesuatu, mengambil data, atau memiliki method — gunakan class-based component:

php artisan make:component StatusCatatan

Perintah ini membuat dua file: app/View/Components/StatusCatatan.php dan resources/views/components/status-catatan.blade.php.

<?php
// app/View/Components/StatusCatatan.php

namespace App\View\Components;

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

class StatusCatatan extends Component
{
    public string $labelStatus;
    public string $warnaBadge;

    public function __construct(
        public string $status,
    ) {
        $this->labelStatus = match ($status) {
            'draft'     => 'Draft',
            'diterbitkan' => 'Diterbitkan',
            'diarsipkan' => 'Diarsipkan',
            default      => 'Tidak Diketahui',
        };

        $this->warnaBadge = match ($status) {
            'draft'     => 'abu',
            'diterbitkan' => 'hijau',
            'diarsipkan' => 'merah',
            default      => 'abu',
        };
    }

    public function render(): View
    {
        return view('components.status-catatan');
    }
}
{{-- resources/views/components/status-catatan.blade.php --}}

<span {{ $attributes->merge(['class' => "badge badge-{$warnaBadge}"]) }}>
    {{ $labelStatus }}
</span>
{{-- Penggunaan --}}
<x-status-catatan :status="$catatan->status" class="ml-2" />

Perhatikan $attributes->merge() — ini menggabungkan class dari luar (saat komponen dipanggil) dengan class internal komponen. Class ml-2 yang dilempar dari luar akan ditambahkan ke badge badge-hijau.

Slot Bernama

Slot adalah area dalam komponen yang bisa diisi dengan konten arbitrary dari luar. Kita sudah melihat {{ $slot }} untuk default slot — area utama di antara tag pembuka dan penutup komponen. Selain itu, bisa didefinisikan slot tambahan dengan nama:

{{-- resources/views/components/modal.blade.php --}}

@props(['id'])

<div id="{{ $id }}" class="modal" style="display: none;">
    <div class="modal-dialog">
        <div class="modal-header">
            <h5 class="modal-judul">{{ $judul }}</h5>
            <button class="modal-tutup" onclick="tutupModal('{{ $id }}')">
                &times;
            </button>
        </div>

        <div class="modal-isi">
            {{ $slot }}
        </div>

        @isset($aksi)
            <div class="modal-footer">
                {{ $aksi }}
            </div>
        @endisset
    </div>
</div>
{{-- Penggunaan --}}
<x-modal id="konfirmasi-hapus">
    <x-slot:judul>Hapus Catatan?</x-slot>

    <p>Apakah kamu yakin ingin menghapus catatan "<strong>{{ $catatan->judul }}</strong>"?
    Tindakan ini tidak bisa dibatalkan.</p>

    <x-slot:aksi>
        <form method="POST" action="{{ route('catatan.destroy', $catatan) }}">
            @csrf
            @method('DELETE')
            <button type="submit" class="tombol tombol-bahaya">Ya, Hapus</button>
        </form>
        <button onclick="tutupModal('konfirmasi-hapus')" class="tombol">Batal</button>
    </x-slot>
</x-modal>

Nama slot yang mengandung tanda hubung (judul-halaman) diakses sebagai variabel camelCase di dalam komponen: $judulHalaman. Begitu juga props — tampilkan-penulis diakses sebagai $tampilkanPenulis.

Forms dan Validasi

Blade menyediakan beberapa direktif khusus untuk menangani form dengan aman.

@csrf menyisipkan hidden input berisi token CSRF yang diverifikasi Laravel di setiap request POST:

<form method="POST" action="{{ route('catatan.store') }}">
    @csrf

    <div class="field">
        <label for="judul">Judul</label>
        <input
            type="text"
            id="judul"
            name="judul"
            value="{{ old('judul') }}"
            class="input @error('judul') input-error @enderror"
        >

        @error('judul')
            <p class="pesan-error">{{ $message }}</p>
        @enderror
    </div>

    <div class="field">
        <label for="isi">Isi Catatan</label>
        <textarea
            id="isi"
            name="isi"
            class="textarea @error('isi') input-error @enderror"
        >{{ old('isi') }}</textarea>

        @error('isi')
            <p class="pesan-error">{{ $message }}</p>
        @enderror
    </div>

    <button type="submit" class="tombol tombol-utama">Simpan Catatan</button>
</form>

old('judul') mengambil kembali nilai yang diisi pengguna sebelum validasi gagal — sehingga form tidak kosong ketika dikembalikan dengan error. @error('judul') menampilkan pesan error untuk field tertentu, dan di dalamnya variabel $message berisi teks errornya.

Untuk form yang menggunakan HTTP method selain GET dan POST (seperti PUT untuk update atau DELETE untuk hapus), gunakan @method:

<form method="POST" action="{{ route('catatan.update', $catatan) }}">
    @csrf
    @method('PUT')

    {{-- field-field form --}}
</form>

Raw PHP dan Direktif @use

Terkadang perlu menjalankan PHP langsung di Blade — misalnya untuk inisialisasi variabel lokal. Gunakan @php:

@php
    $grupCatatan = $catatanList->groupBy(function ($catatan) {
        return $catatan->created_at->format('Y-m-d');
    });
@endphp

@foreach ($grupCatatan as $tanggal => $catatanHariIni)
    <div class="grup-tanggal">
        <h4>{{ \Carbon\Carbon::parse($tanggal)->translatedFormat('d F Y') }}</h4>

        @foreach ($catatanHariIni as $catatan)
            <x-kartu-catatan :catatan="$catatan">
                {{ Str::limit($catatan->isi, 100) }}
            </x-kartu-catatan>
        @endforeach
    </div>
@endforeach

Mulai Laravel 11, ada juga @use untuk mengimpor class PHP ke dalam scope view tanpa harus menulis nama namespace lengkap setiap kali:

@use('Carbon\Carbon')
@use('Illuminate\Support\Str')

<p>{{ Carbon::parse($catatan->created_at)->translatedFormat('d F Y') }}</p>
<p>{{ Str::limit($catatan->isi, 200) }}</p>

Bagaimana Blade Dikompilasi

Setiap .blade.php dikompilasi menjadi file PHP biasa yang disimpan di storage/framework/views/. File ini yang sebenarnya dieksekusi server, bukan file Blade aslinya.

Diagram alur kompilasi Blade dari file .blade.php ke HTML response

Gambar 2: Alur kompilasi Blade

Kompilasi terjadi sekali. Request berikutnya langsung mengeksekusi file PHP yang sudah ada di cache, selama file Blade aslinya tidak berubah. Karena inilah Blade tidak memperlambat aplikasi dibanding menulis PHP mentah.

Di development, perubahan file Blade langsung terlihat karena Laravel memeriksa timestamp file. Di production, jalankan:

# Pra-kompilasi semua view sebelum deploy
php artisan view:cache

# Hapus cache setelah menerima deployment baru
php artisan view:clear

Latihan

Coba kerjakan latihan berikut menggunakan aplikasi catatan yang sudah kita bangun:

  1. Layout dengan sidebar — Modifikasi layouts/app.blade.php agar memiliki sidebar di sebelah kiri. Gunakan View Composer (dari Bab 10) untuk menyuntikkan $statistikCatatan ke sidebar tanpa mengubah controller manapun.

  2. Komponen form — Buat anonymous component resources/views/components/form/input.blade.php yang menerima props name, label, type (default text), dan secara otomatis menampilkan pesan error via @error. Gunakan komponen ini di form buat catatan.

  3. Loop dengan grup — Di halaman daftar catatan, gunakan @php untuk mengelompokkan catatan berdasarkan tanggal pembuatan, lalu tampilkan dengan heading tanggal di atas setiap grup. Manfaatkan $loop->first untuk menambahkan class khusus pada kartu pertama setiap grup.

Penutup Bab

Blade mengubah PHP template menjadi sesuatu yang lebih ekspresif dan mudah dibaca tanpa mengorbankan fleksibilitas. Template inheritance memastikan tidak ada duplikasi layout. Komponen membuat elemen UI bisa dipakai ulang dengan interface yang bersih. Dan semuanya dikompilasi menjadi PHP murni — jadi tidak ada overhead saat runtime.

Sejauh ini kita bekerja dengan data yang sudah ada di controller. Tapi belum sekali pun kita menyentuh bagaimana validasi input pengguna bekerja secara menyeluruh — siapa yang bertanggung jawab memeriksa apakah judul catatan sudah terisi, apakah panjangnya wajar, atau apakah pengguna memang berhak mengubah catatan milik orang lain. Validasi adalah garis pertahanan pertama sebelum data masuk ke database, dan itulah yang akan kita pelajari di bab berikutnya.

Referensi

  1. 1Blade Templates — Laravel 12.x Documentation
  2. 2Views — Laravel 12.x Documentation