Ciao, oggi volevo giocherellare un po' con Filament.
Questo pacchetto per Laravel promette sviluppo rapido e funzionale per pannelli amministrativi.
Devo dire che mantiene la promessa: lavorare con Filament è piacevole e tutto scorre liscio, anche se devo ancora approfondire bene la combo Livewire + Alpine.
Per prendere confidenza, integrerò un calendario JavaScript custom senza usare i plugin della community (ottimi e numerosi). La risorsa scelta è Event Calendar - simile a FullCalendar ma con le viste resource gratuite.
Cosa esploreremo:
- Asset lazy loading con
loadedOnRequest() - Interazione Livewire/Alpine
- Azioni modali su widget personalizzati
Iniziamo
Installiamo Laravel e l’ultima versione di Filament.
Creiamo il modello Event con migrazione, facciamolo semplicissimo per gestire gli eventi del calendario.
php artisan make:model Event -m
public function up(): void
{
Schema::create('events', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->dateTime('start');
$table->dateTime('end');
$table->string('description');
$table->timestamps();
});
}
Lanciamo il comando per creare la risorsa in Filament
php artisan make:filament-resource Event --simple
Ora creiamo il widget myCalendar
php artisan make:filament-widget myCalendar
Scegliamo la voce custom. Adesso Filament ha creato per noi il componente Livewire (livewire/mycalendar) con relativa vista blade (resources/views/livewire/my-calendar.blade.php).
Integrazione
Ci sono due modi per integrare la libreria:
- CDN - Via più rapida, si include direttamente nel widget
- NPM + Vite - Si installa e si espone globalmente per Alpine
La CDN offre un bundle completo già pronto, perfetto per il nostro caso. Usiamo quella.
Alpine
Alpine.js è un framework JavaScript leggero che permette di aggiungere interattività direttamente negli attributi HTML.
Direttive principali:
x-data- stato del componentex-on- ascolta eventix-ref- seleziona elementix-init- codice all'avvio
Personalmente preferisco Vue + Inertia (come Laravel Nova), ma Nova è a pagamento mentre Filament è gratuito, potente e si integra perfettamente con Livewire. Una scelta pragmatica.
Ok, non perdiamoci per strada con le preferenze personali e procediamo a inizializzare il nostro calendario. Ecco il primo step: gestire gli assets con Filament.
Di base possiamo utilizzare FilamentAsset::register() per registrarli globalmente, ma questo li carica subito all'avvio del pannello.
Per una CDN veloce come questa non è strettamente necessario il lazy loading, ma è un ottimo modo per capire il meccanismo degli asset di Filament - molto utile quando si lavora con librerie pesanti o bundle custom.
Esploriamo quindi il lazy loading con due feature:
loadedOnRequest()- rende l'asset disponibile solo su richiestax-load-jsdi Alpine - carica lo script al momento del bisogno
Qui entra in gioco la sincronizzazione: il nostro codice JavaScript deve partire solo dopo aver caricato il bundle della CDN. Come fare?
Andiamo in AppServiceProvider e registriamo l'asset con il metodo loadedOnRequest():
Alpine prevede anche l’attributo x-load-css ma in questo caso lo userò solo per JavaScript.
//appserviceprovider.php
public function boot(): void
{
FilamentAsset::register([
Css::make('eventcalendar-css','https://cdn.jsdelivr.net/npm/@event-calendar/build@5.4.1/dist/event-calendar.min.css'),
Js::make('eventcalendar-js', 'https://cdn.jsdelivr.net/npm/@event-calendar/build@5.4.0/dist/event-calendar.min.js')->loadedOnRequest(),
]);
}
Nella vista blade completiamo il lavoro.
//my-calendar.blade.php
<x-filament::section>
<div wire:ignore>
<div
x-load-js="[@js(\Filament\Support\Facades\FilamentAsset::getScriptSrc('eventcalendar-js'))]"
data-dispatch="asset-loaded"
x-on:asset-loaded-js.window="createCalendar"
x-data="{
mycal: null,
createCalendar(){
//qui calendario...
}">
<div x-ref="cal" class="cal"></div>
</div>
</div>
</x-filament::section>
x-load-js ci permette di richiamare e caricare eventcalendar-js, data-dispatch definisce quale evento lanciare quando il bundle è caricato mentre x-data definisce lo stato del componente con la funzione createCalendar() che viene eseguita automaticamente quando x-on:asset-loaded-js riceve l'evento.
Importantissimo racchiudere tutto in un div con wire:ignore per ignorare le richieste AJAX di Livewire per evitare di “piallare” il calendario.
Usiamo x-ref per “selezionare” l’elemento target del calendario e passarlo a EventCalendar.create con this.$refs.cal.
//my-calendar.blade.php
<x-filament::section>
<div wire:ignore>
<div
x-load-js="[@js(\Filament\Support\Facades\FilamentAsset::getScriptSrc('eventcalendar-js'))]"
data-dispatch="asset-loaded"
x-on:asset-loaded-js.window="createCalendar"
x-data="{
mycal: null,
createCalendar(){
mycal = EventCalendar.create(this.$refs.cal, {
view:'timeGridWeek',
locale:'it',
firstDay:1,
events:[],
})
}
}">
<div x-ref="cal" class="cal"></div>
</div>
</div>
</x-filament::section>
Ok, primo step completato.
Ora dobbiamo passare i dati del nostro modello Event in maniera dinamica.
Eventi
eventSources nella documentazione del calendario è quello che ci serve. È la funzione che creerà per noi le chiamate Ajax per ottenere gli eventi. Tramite il parametro info recuperiamo informazioni preziose per la nostra query (date di inizio/fine della vista).
Sostituiamo events:[] scritto in precedenza con il nostro eventSources:
//my-calendar.blade.php
eventSources:[{
events:function(info){
console.log(info);
}
}],
info restituisce la data di inizio e di fine del calendario in base alla vista corrente e se si naviga tra i pulsanti next e prev del mese.
Non ci resta che settare la nostra query in base ai dati che abbiamo e giocare con l'interazione Livewire/Alpine. Dichiariamo una funzione getEvents() nel nostro widget. La query riceverà start ed end e restituirà gli eventi come array:
//mycalendar.php
public function getEvents($start, $end): array
{
return Event::query()
->where('start', '>=', $start)
->where('end', '<=', $end)
->get()
->map(fn($event) => [
'id' => $event->id,
'title' => $event->title,
'start' => $event->start,
'end' => $event->end,
])
->toArray();
}
}
Richiamiamo questa funzione in Alpine grazie a this.$wire, questo metodo è il “ponte” che ci permette di comunicare da Alpine a LiveWire tramite una richiesta AJAX.
//my-calendar.blade.php
eventSources:[{
events: (info) => {
return this.$wire.getEvents(info.startStr, info.endStr);
}
}],
Magicamente vediamo gli eventi del modello all’interno del nostro calendario.
Possiamo, in caso, aggiungere uno spinner per indicare il caricamento usando la funzione loading:
//my-calendar.blade.php
loading: function(isLoading) {
if(isLoading){
//add spinner
console.log('loading')
}else{
//remove spinner
console.log('finish')
}
}
Azioni e Schema
Adesso dedichiamoci alle azioni.
Vogliamo, ad esempio, cliccare sull’evento e aprire il modale di Filament per aggiornare i dati.
Per sfruttare e renderizzare le azioni in un componente LiveWire custom in Filament ci serve importare i trait InteractsWithActions e InteractsWithSchemas ed implementare la classe MyCalendar con HasActions e HasSchemas per utilizzare azioni e schemi. Qui la documentazione.
//mycalendar.php
namespace App\Livewire;
use App\Models\Event;
use Filament\Actions\Action;
use Filament\Actions\Concerns\InteractsWithActions;
use Filament\Actions\Contracts\HasActions;
use Filament\Schemas\Concerns\InteractsWithSchemas;
use Filament\Schemas\Contracts\HasSchemas;
use Filament\Widgets\Widget;
class MyCalendar extends Widget implements HasActions, HasSchemas
{
use InteractsWithActions;
use InteractsWithSchemas;
Questo ci permetterà di usare il metodo mountAction() per lanciare la finestra modale dalla vista del nostro widget. Andiamo nel file blade ed aggiungiamo, subito, il tag magico per renderizzare il componente modal prima di dimenticarcelo.
//my-calendar.blade.php
<x-filament-actions::modals />
</x-filament::section>
Aggiungiamo la funzione eventClick per recuperare le informazioni associate al click e lanciamo mountAction() per aprire la finestra modale passando l’id dell’evento.
Usiamo sempre il “ponte” this.$wire per interagire da Alpine a LiveWire.
//my-calendar.blade.php
eventClick: (info) => {
this.$wire.mountAction('update', { id: info.event.id } )
},
Torniamo al nostro widget.
Definiamo l’azione recuperando il record da modificare con relativi input.
//mycalendar.php
public function updateAction(): EditAction
{
return EditAction::make('update')
->record(fn (array $arguments) => Event::find($arguments['id']))
->schema([
TextInput::make('title')
->required(),
DateTimePicker::make('start')
->required(),
DateTimePicker::make('end')
->required(),
TextInput::make('description')
->required(),
])
->after(function (Event $record) {
$this->dispatch('event-updated', ...$record->toArray());
});
}
Ho ricopiato gli input per velocità ma In realtà Filament, suggerisce di creare una classe separata con lo schema e poi riutilizzarlo grazie al metodo configure().
Mi piace che ci sia una pagina dedicata nella documentazione per questi consigli “salva vita”.
Dispaccio
Una volta aggiornato l’evento abbiamo bisogno di una risposta “reattiva” che notifichi ad Alpine che il dato è cambiato per aggiornare il calendario senza refreshiare tutti gli eventi. Sfruttiamo Livewire per propagare un avviso con dispatch() passando il record appena modificato.
//mycalendar.php
public function updateAction(): EditAction
{
return EditAction::make('update')
->record(fn (array $arguments) => Event::find($arguments['id']))
->schema([
TextInput::make('title')
->required(),
DateTimePicker::make('start')
->required(),
DateTimePicker::make('end')
->required(),
TextInput::make('description')
->required(),
])
->after(function (Event $record) {
//dispatch dopo l'update con i dati del record
$this->dispatch('event-updated', ...$record->toArray());
});
}
Mettiamo in ascolto Alpine con l’attributo x-on: con riferimento il dispaccio event-updated. Quando arriverà l’evento di Livewire lanciamo updateEvent per aggiornare il singolo evento recuperando i dati tramite $event.detail.
//my-calendar.blade.php
<x-filament-widgets::widget>
<x-filament::section>
<div wire:ignore>
<div
x-load-js="[@js(\Filament\Support\Facades\FilamentAsset::getScriptSrc('eventcalendar-js'))]"
data-dispatch="asset-loaded"
x-on:asset-loaded-js.window="createCalendar"
x-on:event-updated.window="mycal.updateEvent($event.detail)"
x-data="{
mycal: null,
createCalendar(){
mycal = EventCalendar.create(this.$refs.cal, {
view:'dayGridMonth',
locale:'it',
firstDay:1,
eventSources:[{
events: (info) => {
return this.$wire.getEvents(info.startStr, info.endStr);
}
}],
//clic su evento
eventClick: (info) => {
this.$wire.mountAction('update', { id: info.event.id } )
},
loading: function(isLoading) {
if(isLoading){
//add spinner
console.log('loading')
}else{
//remove spinner
console.log('finish')
}
},
})
}
}">
<div x-ref="cal" class="cal"></div>
</div>
</div>
<x-filament-actions::modals />
</x-filament::section>
</x-filament-widgets::widget>
Conclusioni
Abbiamo costruito un calendario interattivo in Filament esplorando:
- Lazy loading con
loadedOnRequest()- asset caricati on-demand - Comunicazione Livewire/Alpine - dispatch ed eventi reattivi
- Azioni custom - modali su widget personalizzati
- Update real-time - calendario aggiornato senza ricaricare tutto
Filament mi piace. Lo stack Livewire + Alpine non è il mio preferito (resto un fan di Vue + Inertia), ma l'ecosistema è talmente solido che sarebbe sciocco ignorarlo.
Per progetti dove serve velocità di sviluppo e un admin robusto, Filament è una scelta eccellente. La prossima volta, prima di chiamare Inertia per la pizza del venerdì sera, magari faccio uno squillo a Filament per un panino veloce.
Grazie per aver letto, alla prossima!
