
Liveware Fullcalendar
Je continue l’exploration des possibilités de Livewire, cette fois associé à la plus célèbre librairie de calendrier : Fullcalendar. C’est une librairie très complète avec un affichage esthétique sous différentes formes d’un calendrier avec des événements. Il est totalement paramétrable et gratuit et open source pour l’essentiel de ses possibilités. Sur le site officiel on trouve de nombreuses démonstrations : glisser-déposer d’événement, entrée d’événement avec un clic sur le jour, événements en fond, changement de thème ou de locale… On a là tout ce qu’il faut pour traiter une application fondée sur des événements temporels.
C’est la lecture de cet article qui m’a donné envie de m’intéresser à cette histoire.
Fullcalendar est une librairie frontend qui fonctionne en Javascript, il n’y a donc aucune persistance des données. Livewire est très doué pour gérer ce genre de situation sans qu’on se soucie de la gestion des requêtes Ajax. On va voir dans cet article comment articuler les deux.
Vous pouvez télécharger le code final de cet article ici.
Installation
Laravel
On fait une nouvelle installation de Laravel :
composer create-project laravel/laravel livewirecalendar --prefer-dist
On crée une base de données et on informe le fichier .env :
DB_CONNECTION=mysql DB_HOST=127.0.0.1 DB_PORT=3306 DB_DATABASE=livewirecalendar DB_USERNAME=root DB_PASSWORD=
On peut alors créer les tables de base :
php artisan migrate
Livewire
Maintenant on installe Livewire :
composer require livewire/livewire
Et un composant :
php artisan make:livewire calendar
On a ainsi créé une classe :
Route et vue
On crée une vue pour l’affichage du calendrier :
Là on va faire simple :
<html> <head> @livewireStyles </head> <body> <livewire:calendar /> @livewireScripts @stack('scripts') </body> </html>
Par défaut Livewire ne supporte pas la balise <script> directement dans la vue d’un composant (c’est expliqué ici). Blade propose la directive @stack qui permet de contourner cette limitation. On crée ici une pile nommée et on pourra pousser (@push) un script à cet emplacement.
On crée aussi la route pour afficher cette vue :
Route::get('/', function () { return view('home'); });
On crée un calendrier
Maintenant on peut se lancer en créant le premier calendrier. On code le composant Livewire :
<style> #calendar-container { position: fixed; top: 0; left: 0; right: 0; bottom: 0; } #calendar { margin: 10px auto; padding: 10px; max-width: 1100px; height: 700px; } </style> <div> <div id='calendar-container' wire:ignore> <div id='calendar'></div> </div> </div> @push('scripts') <script src='https://cdn.jsdelivr.net/npm/fullcalendar@5.6.0/main.min.js'></script> <script> document.addEventListener('livewire:load', function () { const Calendar = FullCalendar.Calendar; const calendarEl = document.getElementById('calendar'); const calendar = new Calendar(calendarEl); calendar.render(); }); </script> <link href='https://cdn.jsdelivr.net/npm/fullcalendar@5.6.0/main.min.css' rel='stylesheet' /> @endpush
On charge Fullcalendar avec son CDN. Ensuite on a un simple conteneur et la prestation minimale pour le calendrier.
Pour le moment Livewire ne nous sert à rien, mais c’est juste pour voir si le calendrier s’affiche correctement :
On a les réglages par défaut : affichage du mois courant et boutons pour le défilement par mois.
On peut déjà modifier la locale :
<script src="https://cdn.jsdelivr.net/npm/fullcalendar@5.6.0/locales-all.min.js"></script> <script> document.addEventListener('livewire:load', function () { ... const calendar = new Calendar(calendarEl, { locale: '{{ config('app.locale') }}', }); ...
Si on met la locale en fr on a tout en français :
Dommage qu’il n’y ait pas une majuscule pour le mois. D’ailleurs les noms des mois ne se trouvent pas dans le fichier JS chargé avec le CDN. Je n’ai pas cherché plus loin, mais j’ai remarqué que moment.js est utilisé…
On va aussi enrichir et réorganiser la barre de boutons :
const calendar = new Calendar(calendarEl, { headerToolbar: { left: 'prev,next today', center: 'title', right: 'dayGridMonth,timeGridWeek,timeGridDay,listWeek' }, locale: '{{ config('app.locale') }}', });
On peut maintenant facilement changer l’affichage : par mois, par semaine, par jour, sous forme de liste d’événements.
Mais notre calendrier est vide et on n’a encore aucun moyen de le remplir…
Persistance des données
Maintenant qu’on sait comment créer un calendrier il va falloir le remplir. Côté Laravel on crée une table (avec modèle et seeder) pour mémoriser les événements :
php artisan make:model Event -ms
Pour les colonnes de notre table, il faut déjà savoir quelles sont les informations utilisées par Fullcalendar pour ses événements. On trouve l’objet Event dans la documentation. On se rend compte qu’il y a beaucoup de possibilités. On va rester simple et prévoir seulement :
- id : l’identifiant de l’événement
- title : le titre de l’événement tel qu’il apparaît
- start : date et heure éventuelle de début de l’événement
- end : date et heure de fin de l’événement (si null c’est que l’événement concerne la journée entière)
On code en conséquence la migration :
public function up() { Schema::create('events', function (Blueprint $table) { $table->uuid('id')->primary(); $table->string('title'); $table->string('start'); $table->string('end')->nullable(); }); }
Avec Eloquent on n’est pas obligé d’avoir une clé primaire en nombre entier auto-incrémenté, ici j’utilise un uuid. Il serait délicat, pour ne pas dire impossible, d’utiliser dans le calendrier des entiers en série continue comme identifiant et ensuite de les gérer simplement.
Dans le modèle Event on prévoit l’assignement de masse, que notre clé n’est pas un entier auto-incrémenté et on précise également qu’on n’aura pas de timestamp :
public $timestamps = false; public $incrementing = false; protected $keyType = 'string'; protected $fillable = ['id', 'title', 'start', 'end',];
Dans le seeder on code 3 événements :
<?php namespace Database\Seeders; use Illuminate\Database\Seeder; use App\Models\Event; use Illuminate\Support\Str; class EventSeeder extends Seeder { /** * Run the database seeds. * * @return void */ public function run() { Event::insert([ [ 'id' => Str::uuid(), 'title' => 'Evenement 1', 'start' => '2021-04-02', 'end' => null ], [ 'id' => Str::uuid(), 'title' => 'Evenement 2', 'start' => '2021-04-10T08:00:00', 'end' => '2021-04-10T16:00:00' ], [ 'id' => Str::uuid(), 'title' => 'Evenement 3', 'start' => '2021-04-20', 'end' => '2021-04-22' ], ]); } }
Dans Databaseeder il faut appeler la classe créée :
public function run() { $this->call([EventSeeder::class,]); }
On lance :
php artisan migrate --seed
Maintenant qu’on a des événements on utilise Livewire pour les afficher :
<?php namespace App\Http\Livewire; use Livewire\Component; use App\Models\Event; class Calendar extends Component { public $events = []; public function render() { $this->events = json_encode(Event::all()); return view('livewire.calendar'); } }
Dans le composant on récupère ces événements :
document.addEventListener('livewire:load', function () { ... const calendar = new Calendar(calendarEl, { ... events: JSON.parse(@this.events) }); calendar.render(); });
On vérifie sur le calendrier :
En affichage semaine on a le détail de l’événement 2:
On peut rendre les événements sur le calendrier modifiable :
const calendar = new Calendar(calendarEl, { ... editable: true, });
On peut maintenant modifier les événements, mais évidemment ça ne met pas à jour notre base de données !
Répercussion des modifications
Ajustement de la fin d’un événement en changeant sa dimension
On peut modifier un événement en ajustant la fin. L’événement déclenché est eventResize. Livewire va nous permettre de gérer ça facilement. Dans le composant :
const calendar = new Calendar(calendarEl, { ... eventResize: info => @this.eventChange(info.event) });
Et dans la classe on crée une méthode pour la mise à jour dans la base :
... use Illuminate\Support\Arr; class Calendar extends Component { public $events = []; public function eventChange($event) { $e = Event::find($event['id']); $e->start = $event['start']; if(Arr::exists($event, 'end')) { $e->end = $event['end']; } $e->save(); } ...
Ici la méthode est généralisée pour pouvoir être utilisée aussi pour le glisser-déposer qu’on va voir juste après. On teste l’existence de la donnée de fin d’événement parce que les événements d’une journée ne comportent pas cette information.
Glisser-déposer
On peut aussi déplacer un événement avec un glisser-déposer et dans ce cas on modifie à la fois le début et la fin. L’événement déclenché est eventDrop. On ajoute la gestion de cet événement dans le composant :
const calendarEl = document.getElementById('calendar'); const calendar = new Calendar(calendarEl, { ... eventDrop: info => @this.eventChange(info.event) });
Dans la classe on utilise la même méthode qu’on a créée précédemment.
Notre calendrier a avancé ! Nos événements sont stockés dans une table, on les affiche au chargement et ensuite on peut les modifier directement sur le calendrier et les modifications sont automatiquement répercutées dans la table.
Ajouter un événement
Il faut aussi pouvoir ajouter des événements sur le calendrier, pour le moment on n’en a pas la possibilité. Il existe deux manières pour ajouter un événement : utiliser l’événement select pour ouvrir par exempleune boîte de dialogue à la suite d’un clic sur un jour ou une série de jours ou glisser-déposer un événement sur le calendrier.
Boîte de dialogue
Par défaut la fonctionnalité de sélection est désactivée, il faut donc l’activer :
const calendar = new Calendar(calendarEl, { ... selectable: true, });
Maintenant avec un clic sur un jour on déclenche l’événement select. On ajoute le code dans le composant :
create_UUID = () => { let dt = new Date().getTime(); const uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => { let r = (dt + Math.random() * 16) % 16 | 0; dt = Math.floor(dt / 16); return (c == 'x' ? r :(r&0x3|0x8)).toString(16); }); return uuid; } document.addEventListener('livewire:load', function () { const Calendar = FullCalendar.Calendar; const calendarEl = document.getElementById('calendar'); const calendar = new Calendar(calendarEl, { headerToolbar: { left: 'prev,next today', center: 'title', right: 'dayGridMonth,timeGridWeek,timeGridDay,listWeek' }, locale: '{{ config('app.locale') }}', events: JSON.parse(@this.events), editable: true, eventResize: info => @this.eventChange(info.event), eventDrop: info => @this.eventChange(info.event), selectable: true, select: arg => { const title = prompt('Titre :'); const id = create_UUID(); if (title) { calendar.addEvent({ id: id, title: title, start: arg.start, end: arg.end, allDay: arg.allDay }); @this.eventAdd(calendar.getEventById(id)); }; calendar.unselect(); }, }); calendar.render(); });
J’ai copié tout le script pour ceux qui risquaient de se perdre.
On fait plusieurs choses :
- on définit le texte de la boîte de dialogue
- on définit l’uuid (j’ai prévu une fonction que j’ai récupérée ici)
- on crée l’événement dans le calendrier
- on utilise Livewire pour envoyer les informations pour la mise à jour de la base
Dans la classe on crée la méthode :
public function eventAdd($event) { Event::create($event); }
On peut maintenant ajouter des événements ! Par défaut ils occupent la journée entière.
Glisser-déposer
La deuxième façon d’ajouter un événement est d’en avoir des disponibles à côté du calendrier et de les faire glisser sur un jour du calendrier. Dans la documentation on trouve des explications ici. C’est sommaire mais précis.
On va un peu changer la mise en page parce qu’on doit ajouter les événement à glisser-déposer. Voici le nouveau HTML et style :
<style> #calendar-container { display: grid; grid-template-columns: 200px 1fr; padding: 20px; } #events { grid-column: 1; } #calendar { grid-column: 2; height: 700px; } .dropEvent { background-color: DodgerBlue; color: white; padding: 5px 16px; margin-bottom: 10px; text-align: center; display: inline-block; font-size: 16px; border-radius: 4px; cursor:pointer; } </style> <div> <div id="calendar-container" wire:ignore> <div id="events"> <div data-event='{"title":"Evénement A"}' class="dropEvent">Evénement A</div> <div data-event='{"title":"Evénement B"}' class="dropEvent">Evénement B</div> </div> <div id="calendar"></div> </div> </div>
Pour la mise en page j’utilise Grid et pour le reste c’est du CSS classique. On a deux événements à glisser à côté du calendrier :
Comme expliqué dans la documentation les informations des événements sont placés en JSON dans un dataset :
<div data-event='{"title":"Evénement A"}' class="dropEvent">Evénement A</div>
Il faut utiliser la classe Draggable et lui dire où trouver les événements à glisser, ici j’utilise la classe :
document.addEventListener('livewire:load', function () { ... const Draggable = FullCalendar.Draggable; new Draggable(document.getElementById('events'), { itemSelector: '.dropEvent' });
Ensuite on utilise l’événement eventReceive pour récupérer les informations et on utilise encore Livewire pour l’ajout dans la base :
document.addEventListener('livewire:load', function () { ... const calendar = new Calendar(calendarEl, { ... eventReceive: info => { const id = create_UUID(); info.event.setProp('id', id); @this.eventAdd(info.event); }, }); calendar.render(); });
On génère un nouvel identifiant pour chaque événement. Et ça marche !
Supprimer un événement
Il faut aussi pouvoir supprimer un événement. Curieusement dans toutes les démonstrations du site on n’a jamais le cas…
Alors on va faire simple et juste gérer un clic sur un événement en ouvrant un boîte de confirmation classique :
document.addEventListener('livewire:load', function () { ... const calendar = new Calendar(calendarEl, { ... eventClick: info => { if (confirm("Voulez-vous vraiment supprimer cet événement ?")) { info.event.remove(); @this.eventRemove(info.event.id); } } }); calendar.render(); });
Et dans la classe on fait la suppression pour la base :
public function eventRemove($id) { Event::destroy($id); }
Conclusion
On a vu dans cet article comment gérer Fullcalendar avec Livewire. On en arrive à un code très léger côté Laravel ! J’ai traité les possibilités de base de ce superbe calendrier mais ça ouvre pas mal de perspectives de création d’application à base de gestion temporelle. ca peut rapidement se compliquer si un même calendrier est utilisé par plusieurs personnes et qu’on veut empêcher des simultanéités…


14 commentaires
Nyleor
Hello,
« Avec Eloquent on n’est pas obligé d’avoir une clé primaire en nombre entier auto-incrémenté, ici j’utilise un uuid. Il serait délicat, pour ne pas dire impossible, d’utiliser dans le calendrier des entiers en série continue comme identifiant et ensuite de les gérer simplement. »
Pourquoi donc ?
bestmomo
Salut.
Comme on crée un événement côté client on ne connaît pas la valeur de la dernière clé primaire utilisée. On pourrait aller la lire ou la transmettre mais j’ai trouvé plus simple de passer par un UUID facile à gérer côté client.
softcode
bonjour best momo ma préocupation est de savoir comment ciblé le bloc de jour pour diminuer ça taille au lieu que ça soit de gros carré, je voulais avoir une taille un peu reduite, j’ai essayé de cibler et utiliser le css ça n’a pas marché, Merci pour votre aide
bestmomo
Salut,
Tu peux agir globalement sur le calendrier, par exemple pour la hauteur :
calendar.setOption('contentHeight', 400);
Ce qui va réduire les cases des jours. Tu ne peux pas agir directement sur les jours.
La documentation pour les tailles est ici.
softcode
Merci infinement bestmomo
armelbelem
comment afficher toutes les informations de la table event ? par exemple au niveau du calendrier au lieu d’affchier le nom de l’évènement , on affiche aussi l’heure et la date! une autre facon aussi est de pouvoir cliker et voir les informations de l’évènement. Aidez si possible ,!!!! merci
bestmomo
Salut,
Je n’ai pas utilisé ce composant depuis que j’ai écrit cet article et il faudrait vraiment se replonger dedans pour répondre à la question, d’autant que tout ça a encore évolué depuis.
foantje@gmail.com
I don’t understand why u make start & end with a string ?
Shoudn’t it be a Timestamp or dateTime? I try to change it to this but then i get error Invalid datetime format
bestmomo
Hello,
I made this script more than a year ago but I think it was to easily fit with Javascript Date Object.
Velkacem
Merci pour votre aide
Velkacem
Salut bestmomo et merci pour vos efforts, si tu peux m’aider avec un exemple de systeme de reversation ou un package a me conseiller
bestmomo
Salut,
Je suppose que tu veux dire réservation ? Je n’ai jamais creusé cette question même si en ce moment je m’amuse avec Fullcalendar qui est très facile à gérer avec Litewire, mais un système de réservation complet est assez lourd à mettre en place. Au niveau des packages existants celui-ci semble intéressant, actualisé et bien noté.
Velkacem
Merci Bestmomo, je veux votre concenrnant un crud d’un model exemple post avec de multiples images exemple en les sauvegardant les images dans un tableau serialiseren base de donnée en utilisant ajax
bestmomo
Salut,
Il existe un package intéressant pour associer toutes sortes de fichiers, y compris des images, avec un modèle Eloquent.