Why FullCalendar?
We use FullCalendar in a lot of projects and I have yet to find anything that is as powerful and flexible for creating calendars. We’ve used plain FullCalendar before and also the Vue integration, but lately Livewire has taken my interest. I wanted to see if I could integrate this nicely. I’m happy with the result so far and wanted to share some of the things I encountered. We don’t cover the basics of creating your Livewire component, but you can read about it in the Quickstart. I created a Laravel Playgrounds for each of the steps so you can follow it and see the result in action.
See the final example on Laravel Playground here.
Drag & Drop
The starting-point of my journey was the Drag & Drop Demo (Docs) which shows the Javascript side. This is a common pattern for planning/scheduling for use; take a list of events and plan them. Of course the changes need to be stored Server Side, which is were Livewire comes in.
Rendering the Calendar once
To start, we can use the same code from a CDN and put it in a simple Livewire Component. Scripts should just run once, but you can also run the script outside of your component. For example by using @stack('scripts')
in your layout file and @push('scripts')
in your component as described in Inline Code docs
The Calendar itself gets rendered inside the component, but we want to persist is across livewire events. So we add the wire:ignore
attribute to our Calendar container. This will let Livewire ignore the contents.
<div id="calendar-container" wire:ignore>
<div id="calendar"></div>
</div>
Now each time Livewire reloads, the FullCalendar is still rendered. See the Playground with a Select that refreshes the view on change.
Calling actions on Events
To store the events, we need to pass the data to the back-end. With Livewire this is super easy. We can use @this.<myaction>
to call functions in PHP, straight from the Javascript FullCalendar events:
{
...,
eventReceive: info => @this.eventReceive(info.event),
eventDrop: info => @this.eventDrop(info.event, info.oldEvent)
}
This will call the functions in your component:
public function eventReceive($event)
{
// Handle new events
}
public function eventDrop($event, $oldEvent)
{
// Modify existing events
}
The $event will contain the EventObject from FullCalendar, so you can pass an ID to match it, or supply additional data. You can use the data-event
attribute on the list-item, to pass any data to the event using the @json
directive:
<div data-event='@json($event)' class='fc-event ...'>
Refreshing the data
One of the benefits of FulLCalendar is the Event Source API, which makes it easy to load your events from an API. The benefit from Livewire is that you can reload your data easily, but we want to use the API to optimize the Event loading when required.
We can add Dynamic Parameters to our Event Source to make use of the Livewire inputs if required. We can reference the properties with @this.<property>
as described in the Livewire Inline Scripts docs.
calendar.addEventSource( {
url: '/calendar/events',
extraParams: function() {
return {
name: @this.name
};
}
});
The start/end time will be appended to the QueryString, together with the extraParams. So you can only load the current month/week etc.
By default, this will load only once, at the start. But we want to trigger it from our PHP Component. So we can add a listener using @this.on()
:
@this.on(`refreshCalendar`, () => {
calendar.refetchEvents()
});
Now we can emit this event from inside our component, eg. when the name changes:
public function updatedName()
{
$this->emit("refreshCalendar");
}
Now we can decide when to reload any data, and it will be dynamically using the correct name!
To make sure that the initial data is correct, we can listen to the livewire:load
event instead of DOMContentLoaded
:
document.addEventListener('livewire:load', function() {
..
}
And if you want to clear the ‘dropped’ events without an actual source when refreshing, you can use the Loading callback in the Calendar options to remove those events after a refresh:
loading: function(isLoading) {
if (!isLoading) {
this.getEvents().forEach(function(e){
if (e.source === null) {
e.remove();
}
});
}
}
You can see the final working example here
Wrapping up
This only explains some basic stuff but it shows a lot of potential, for me. It removes a lot of boilerplate and simplifies the handling of changes in the Events, but moving it all to PHP. By keeping in control of the refreshing of data, it still feels very fast, without using too much javascript.
Let me know if you have any questions or suggestions!