I template Blade presentati nella precedente lezione ci hanno permesso non solo di renderizzare un contenuto dinamico, ma anche di creare delle view componibili. Le direttive @yeld e @section ci hanno permesso di separare file di view condivisi e dedicati alla definizione del layout comune da file di view specifici dedicati al contenuto più customizzato.
La modalità con cui view di layout e view child interagiscono, però, potrebbe non essere immediatamente comprensibile, proprio in virtù della sua modalità di funzionamento per ereditarietà.
Laravel e Blade offrono un’alternativa all’approccio basato su section, layout e include: tale approccio fa uso di quelli che sono definiti component e slot.
NOTA: i component di Blade si sono evoluti molto ad ogni rilascio di Laravel; in questa guida facciamo riferimento a quanto disponibile nella versione 9.x di Laravel
Confronto tra layout/section e component/slots in Laravel
Supponiamo che la nostra applicazione Laravel gestisca una sola pagina, https://example.com/welcome. Vediamo tre alternative con cui è possibile scrivere template di view che realizzeranno lo stesso output HTML.
Singolo template
{{-- resources/views/welcome.blade.php --}} <html lang="en"> <head> {{-- many stuff in head here ... --}} </head> <body> <h1>Welcome Laravel</h1> </body> </html>
Section e layout
{{-- resources/views/layout.blade.php --}} <html lang="en"> <head> {{-- many stuff in head here ... --}} </head> <body> @yeld('content') </body> </html> {{-- resources/views/welcome.blade.php --}} @extends('layout') @section('content') <h1>Welcome Laravel</h1> @endsection
Il template che useremo per le view (welcome) deve estendere il template che usa come layout. Entrambi devono fare riferimento alla stessa section (content), uno con la direttiva @yeld, l’altro con la direttiva @section.
Component e slot
{{-- resource/views/components/layout.blade.php --}} <html lang="en"> <head> {{-- many stuff in head here ... --}} </head> <body> {{ $slot }} </body> </html> {{-- resources/views/welcome.blade.php --}} <x-layout> <h1>Welcome Laravel</h1> <x-layout>
Viene creato un template di tipo component creando il file nella directory riservata resource/views/components/. Tale componente include un echo statement della variabile $slot nel punto in cui andrà a inserire contenuto passato dall’esterno.
Il template che andremo a usare per la view (welcome) richiama il componente tramite uno pseudo elemento HTML (x- seguito dal nome del componente) e specifica tra l’apertura e chiusura di questo elemento il valore che assumerà la variabile $slot.
Componente e view che lo utilizza non devono dichiarare la stessa section. I due non sono più legati da una relazione di ereditarietà e la modalità di utilizzo “alla linguaggio HTML” senza più direttive, rende i template più facili da leggere (fai il rendering di questo componente qui, usando questo contenuto).
Rendering dei Blade Component
Un componente Blade è renderizzato in un template utilizzando la sintassi <x-.
{{-- nel template chiedo di fare il rendering del componente --}} <x-alert></x-alert> {{-- nel componente resources/views/components/alert.blade.php definisco i dettagli --}} <div> <span>This is a static alert content by now...</span> <button>OK</button> </div>
È possibile passare dati ai componenti utilizzando una sintassi analoga a quella degli attributi HTML. Gli attributi che contengono espressioni e variabili PHP devono avere : come prefisso.
Nella definizione del componente è possibile definire gli attributi che dovrebbero essere usati come variabili per i dati tramite la direttiva @props
{{-- il valore di message verrà recuperato dalla variable $message --}} <x-alert type="warning" :message="$message"></x-alert> @props([ // questa prop ha un default 'type' => 'info', 'message', ]) <div class="alert type-{{ $type }}"> <span>{{ $message }}</span> <button>OK</button> </div>
Un modo alternativo per passare dati a un componente è tramite gli slot. Invece di usare il meccanismo degli attributi HTML, con gli slot viene passato l’intero contenuto indicato tra i tag di apertura e chiusura.
{{-- rendering nel template --}} <x-alert type="warning">{{ $message }}</x-alert> @props([ 'type' => 'info', ]) <div class="alert type-{{ $type }}"> <span>{{ $slot }}</span> <button>OK</button> </div>
NOTA: è anche possibile passare più slot al componente, con un meccanismo chiamato named slot. Per approfondimenti rimandiamo alla guida dei Blade component
È disponibile un meccanismo definito “attribute bag” che permette di leggere dal componente tutti gli altri eventuali attributi aggiuntivi (rispetto a quelli definiti come props del componente) passati al rendering del componente.
{{-- $id = 6758 --}} {{-- rendering nel template --}} <x-alert type="warning" role="big-alert" data-id="{{ $id }}">{{ $message }}</x-alert> @props([ 'type' => 'info', ]) {{-- $attributes sarà: role="big-alert" data-id="6758" --}} <div class="alert type-{{ $type }} {{ $attributes }}"> <span>{{ $slot }}</span> <button>OK</button> </div>
Non dimenticare che un componente Blade può usare le varie direttive messe a disposizione dal php template engine di Blade.
{{-- $alerts = [ ['type' => 'info', 'message' => 'info message one'], ['type' => 'warning', 'message' => 'warning message one'], ['type' => 'error', 'message' => 'error message one'], ['type' => 'info', 'message' => 'info message tho'], ] --}} <x-alert-box :alerts="$alerts"><x-alert-box> {{-- resources/views/components/alert-box.blade.php --}} <div> @foreach($alerts as $alert) <x-alert :type="$alert['type']">{{ $alert['message'] }}</x-alert> @endforeach </div>
Componenti anonimi e class based in Blade
Blade mette a disposizione due tipi diversi di componente, class based e anonymous, che possono essere usati in contesti diversi. La scelta dell’uno o dell’altro dipende molto dal tipo di elaborazione che serve tra gli attributi che passiamo al componente nel momento in cui ne chiediamo il rendering e gli effettivi dati che andremo a mostrare all’interno del componente renderizzato.
Gli esempi di componenti visti finora sono stati tutti di componenti anonymous. Al componente veniva passato ciò che effettivamente veniva renderizzato (ad esempio, il testo di un messaggio di alert). Già nell’ultimo esempio, però, ci è stato necessario “elaborare” quanto ricevuto per estrarre informazioni, affidandoci al fatto che sapevamo di ricevere una determinata struttura dati.
Laravel e Blade mettono a disposizione degli sviluppatori php un altro tipo di componente, definito per l’appunto “class based”, per il quale, oltre al file di template blade, è presente anche una classe PHP nella directory app/View/Components di supporto al rendering del componente .
Classe e template sono legati nel seguente modo:
- nel momento in cui un template blade chiede il rendering di un componente, l’attributo del componente è passato al costruttore della classe PHP del componente
- la classe PHP del componente presenta un metodo render, che implementa il metodo view richiamando il template del componente
- tutte le property pubbliche della classe del componente vengono passate in modo implicito all’esecuzione di questo metodo view
Vediamo, con un esempio, come sfruttare queste caratteristiche dei componenti class based.
Esempio di componente class based in Blade
Vogliamo realizzare un componente che ci permetta di mostrare tra quanti anni/mesi/giorni/ore finirà una certa offerta, partendo dalla data in cui l’offerta terminerà.
Per creare un nuovo componente class based è più semplice utilizzare il comando Artisan make:component, che crea sia il template Blade che la classe PHP del componente.
$php artisan make:component DateTime/Countdown
INFO Component [app/View/Components/DateTime/Countdown.php] created successfully.
NOTA: abbiamo deciso di creare il componente in una sottodirectory, quindi il file blade sarà in resources/views/components/date-time/countdown.blade.php e potrà essere inserito in un altro template usando <x-date-time.countdown.
Sappiamo di voler passare al nostro componente una data futura e di voler renderizzare quanti giorni mancano. Traendo ispirazione dal componente Countdown di una libreria di componenti Blade disponibili online, possiamo optare per una soluzione come la seguente:
{{-- nel template della view che richiede il componente, il valore di $date sarà un DateTime --}} {{-- $date = new DateTime('@' . mktime(23, 59, 59, 12, 31, 2099)) --}} <x-date-time.countdown :expires="$date"/>
Artisan avrà creato per noi la classe e il template vuoto. Andiamo, quindi, a modificare la classe indicando che al componente verrà passato un argomento $expire di tipo DateTimeInterface, inserendolo come parametro nel costruttore di classe.
// app/View/Components/DateTime/Countdown.php namespace App\View\Components\DateTime; use DateInterval; use DateTimeInterface; use Illuminate\View\Component; use Illuminate\Contracts\View\View; class Countdown extends Component { public DateTimeInterface $expires; public function __construct(DateTimeInterface $expires) { $this->expires = $expires; } public function render(): View { return view('components.date-time.countdown'); } }
Notare che, trattandosi di un componente class based, è possibile sfruttare in type hinting di PHP, andando, quindi, a scegliere che tipo di dato ci aspettiamo per il valore passato al componente.
Potremmo passare alla template del costruttore la property $expires della classe ma, in questo modo, l’unico vantaggio rispetto a un componente anonimo sarebbe quello di avere il type hinting su tale valore.
Qui interviene la caratteristica principale di un componente class based, ossia quello di rendere disponibile al template collegato le property e i metodi pubblici della classe corrispondente.
Possiamo, quindi, aggiungere direttamente nella classe Countdown i vari metodi necessari a calcolare quanti mesi/giorni/ore mancano alla scadenza indicata rispetto a now().
// nella classe Countdown in app/View/Components/DateTime/Countdown.php // ... private function difference(): DateInterval { return $this->expires->diff(now()); } public function days(): string { return sprintf('%02d', $this->difference()->d); } public function hours(): string { return sprintf('%02d', $this->difference()->h); } // ...
Infine, possiamo utilizzare i metodi della classe direttamente nel template del componente:
{{-- resources/views/components/date-time/countdown.blade.php --}} <div> <span>Will end in </span> <span>{{ $years() }} years </span> : <span>{{ $months() }} months </span> : <span>{{ $days() }} days </span> : <span>{{ $hours() }} hours </span> : <span>{{ $minutes() }} minutes </span> : <span>{{ $seconds() }} seconds </span> </div>
Notare come l’attributo expires che è necessario passare per richiedere il rendering del componente nel template di una view, non è, poi, utilizzato all’interno del template del componente, ma rimane “confinato” all’interno della classe PHP.
I componenti Blade class based sono, quindi, particolarmente utili nel caso in cui sia necessario o utile effettuare delle elaborazioni prima di passare il dato al template del componente, oppure connettersi ad altre parti della propria applicazione Laravel, per esempio, tramite dependency injection di un service provider