FullCalendar is probably the most popular jQuery-based calendar solution, but it doesn’t have event editing function. Let’s build that with Laravel and use Bootstrap modal to edit events without refreshing the page.

See below short video how the result will look like, our goal is this:

  1. You click on any event in the calendar
  2. You see modal appear with start/finish time to edit
  3. When you click Save in modal, data should be saved and event should re-appear in correct place in calendar without refreshing the page

Let’s begin.


Step 1. Preparation: Base Code

As a base project, we will take our own QuickAdminPanel-based Appointments Demo, but you don’t need to know QuickAdminPanel here. The code is pretty straightforward Laravel+Blade+jQuery.

Notice: I’m not showing FULL code here, only the parts that are relevant to the calendar.

app/Http/Controllers/Admin/AppointmentsController.php:

public function index()
{
    $appointments = Appointment::all();
    return view('admin.appointments.index', compact('appointments'));
}

resources/views/admin/appointments/index.blade.php:

<link rel='stylesheet' href='https://cdnjs.cloudflare.com/ajax/libs/fullcalendar/3.1.0/fullcalendar.min.css' />
<div id='calendar'></div>

<script src='https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.17.1/moment.min.js'></script>
<script src='https://cdnjs.cloudflare.com/ajax/libs/fullcalendar/3.1.0/fullcalendar.min.js'></script>
<script>
    $(document).ready(function() {
        // page is now ready, initialize the calendar...
        $('#calendar').fullCalendar({
            // put your options and callbacks here
            defaultView: 'agendaWeek',
            events : [
                @foreach($appointments as $appointment)
                {
                    title : '{{ $appointment->client->first_name . ' ' . $appointment->client->last_name }}',
                    start : '{{ $appointment->start_time }}',
                    @if ($appointment->finish_time)
                            end: '{{ $appointment->finish_time }}',
                    @endif
                },
                @endforeach
            ],
        });
    });
</script>

A few lines of explanations:

  • According to DB structure, Appointment has belongsTo(Client::class) relationship;
  • Appointment start_time and finish_time fields have DATETIME DB type
  • We’re using FullCalendar’s option defaultView: ‘agendaWeek’ to show weekly calendar, see other options here

Ok, we’ve got the starting point, now let’s begin editing the event.


Step 2. Edit with Bootstrap Modal

In the same resources/views/admin/appointments/index.blade.php file we add this HTML for the modal:

<div class="modal fade" id="editModal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel">
    <div class="modal-dialog" role="document">
        <div class="modal-content">
            <div class="modal-body">
                <h4>Edit Appointment</h4>

                Start time:
                <br />
                <input type="text" class="form-control" name="start_time" id="start_time">

                End time:
                <br />
                <input type="text" class="form-control" name="finish_time" id="finish_time">
            </div>

            <div class="modal-footer">
                <button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
                <input type="button" class="btn btn-primary" id="appointment_update" value="Save">
            </div>
        </div>
    </div>
</div>

Now, we need to call that modal by a click on calendar event. To do that, we add one event to FullCalendar JavaScript called eventClick.

$('#calendar').fullCalendar({
    events : [
        @foreach($appointments as $appointment)
        {
            title : '{{ $appointment->client->first_name . ' ' . $appointment->client->last_name }}',
            start : '{{ $appointment->start_time }}',
            @if ($appointment->finish_time)
                    end: '{{ $appointment->finish_time }}',
            @endif
        },
        @endforeach
    ],
    eventClick: function(calEvent, jsEvent, view) {
        $('#start_time').val(moment(calEvent.start).format('YYYY-MM-DD HH:mm:ss'));
        $('#finish_time').val(moment(calEvent.end).format('YYYY-MM-DD HH:mm:ss'));
        $('#editModal').modal();
    }
});

Notice we need to use Moment.js to format the datetime value properly.

So, at this point we show the modal and fill in the input field values.
Final stage – saving the changes!


Step 3. Save Changes and Re-Draw Calendar

This part is the most difficult and not documented in FullCalendar. Here are a few things we need to do:

  • Create Laravel route/controller to update the data
  • Call that route from JavaScript and pass appointment ID to update
  • Identify the event ID in FullCalendar (which is different than database’s appointment ID) to delete the old one and draw the new updated one

I’ve come up with this structure in routes – just another method within same Controller:

routes/web.php:

Route::group(['middleware' => ['auth'], 'prefix' => 'admin', 'as' => 'admin.'], function () {
    // ...
    Route::resource('appointments', 'Admin\AppointmentsController');
    Route::post('appointments_ajax_update', 
        ['uses' => 'Admin\AppointmentsController@ajaxUpdate', 'as' => 'appointments.ajax_update']);
});

app/Http/Controllers/Admin/AppointmentsController.php:

public function ajaxUpdate(Request $request)
{
    $appointment = Appointment::with('client')->findOrFail($request->appointment_id);
    $appointment->update($request->all());

    return response()->json(['appointment' => $appointment]);
}

So, we need to pass POST data within $request, including appointments.id value that should come as $request->appointment_id, we currently don’t pass it to Bootstrap modal at all.

Also, I’m doing Appointment::with(‘client’) to be able to draw client’s data on a new appointment’s card after update.

Now, let’s pass our appointment ID. Also, we need Fullcalendar’s event ID, which is not the same.
For that, we add those hidden inputs to the Bootstrap modal:

<div class="modal fade" id="editModal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel">
    <div class="modal-dialog" role="document">
        <div class="modal-content">
            <input type="hidden" name="event_id" id="event_id" value="" />
            <input type="hidden" name="appointment_id" id="appointment_id" value="" />
            <div class="modal-body"> 
            ...

Next, we modify the bolded places in original FullCalendar call:

    $('#calendar').fullCalendar({
        // put your options and callbacks here
        defaultView: 'agendaWeek',
        events : [
            @foreach($appointments as $appointment)
            {
                id: '{{ $appointment->id }}',
                title : '{{ $appointment->client->first_name . ' ' . $appointment->client->last_name }}',
                start : '{{ $appointment->start_time }}',
                @if ($appointment->finish_time)
                        end: '{{ $appointment->finish_time }}',
                @endif
            },
            @endforeach
        ],
        eventClick: function(calEvent, jsEvent, view) {
            $('#event_id').val(calEvent._id);
            $('#appointment_id').val(calEvent.id);
            $('#start_time').val(moment(calEvent.start).format('YYYY-MM-DD HH:mm:ss'));
            $('#finish_time').val(moment(calEvent.end).format('YYYY-MM-DD HH:mm:ss'));
            $('#editModal').modal();
        }
    });

You can pass whatever parameters you want to events array, they won’t be visible anywhere but then you will be able to access them and pass to other events. That’s what we’re doing with $appointment->id here.

And FullCalendar’s internal ID of every event is stored in _id property of JavaScript object, so we’re using it as calEvent._id.

Final step is to have a JavaScript method on clicking Save in the modal. We create it outside of FullCalendar, but within the same jQuery block:

resources/views/admin/appointments/index.blade.php:

$(document).ready(function() {
    // ... All the calendar functionality

    $('#appointment_update').click(function(e) {
        e.preventDefault();
        var data = {
            _token: '{{ csrf_token() }}',
            appointment_id: $('#appointment_id').val(),
            start_time: $('#start_time').val(),
            finish_time: $('#finish_time').val(),
        };

        $.post('{{ route('admin.appointments.ajax_update') }}', data, function( result ) {
            $('#calendar').fullCalendar('removeEvents', $('#event_id').val());

            $('#calendar').fullCalendar('renderEvent', {
                title: result.appointment.client.first_name + ' ' + result.appointment.client.last_name,
                start: result.appointment.start_time,
                end: result.appointment.finish_time
            }, true);

            $('#editModal').modal('hide');
        });
    });
});

Step-by-step, what we’re doing in the block above:

  • Forming the data object to POST with current modal values, including CSRF token field
  • Doing POST request with that data
  • Deleting current event in the calendar, using $(‘#event_id’).val() value
  • Rendering new update event in the calendar, using result from AJAX call
  • Hiding Bootstrap modal

That’s it! May sound pretty complicated, but I hope I broke it down for you in steps so you could actually use it in your Laravel+jQuery projects.