FullCalendar in Lightning web component

fullcalendar in lwc
Sample fullcalendar project in lwc

In this post we are going to see how we can use fullcalendar.io library in lightning web component.

FullCalendar.io is the most popular full-sized Javascript calendar and it is used in different usecases such as logging hours in Time Sheet, logging events, assigning resources to different slots & etc.

I have tried different versions to achieve this using lightning web component. Finally i am able to render the full-sized calendar with the following version and its dependencies. I would recommend to use these versions to avoid compatability with lwc framework. I have used this github as base to implement this project.

Prerequisistes
fullcalendar.io v3.10.0 library
jquery – jQuery v3.3.1 version
moment – v2.23.0 version

Usecase

Ability to log a private event and show it on full-sized calendar with month, week and day view.

Demo

github repository

I would recommend to look at the following repository to quick spin the scratch org and test.

https://github.com/brahmajitammana/lwc-fullcalendarjs

fullcalendarjs.html

<!-- 
@description: Lightning web component using Fullcalendar.io js library to display most recent events
@author: Brahmaji tammana from www.auraenabled.com 
@jslibrary: https://fullcalendar.io/ -->
<template>

    <!-- Spinner to show on waiting screens -->
    <template if:true={openSpinner}>
        <lightning-spinner alternative-text="Loading" size="medium"></lightning-spinner>
    </template>

   <div class="slds-grid slds-wrap slds-theme_default">
        <div class="slds-col slds-size_3-of-12">
            <!-- To display list of events or any parent records  
                TODO: add drag items in this div to drop on fullcalendar.
            -->
            <div class=" slds-p-around_medium slds-border_right slds-scrollable_y" style="height:800px">
                <div class="slds-clearfix">
                    <div class="slds-float_right">
                        <lightning-button icon-name="utility:add" slot="actions" 
                                        alternative-text="add" title="Add" size="small"
                                        class="slds-p-around_medium"
                                        label="Add Event"
                                        onclick={addEvent}>
                        </lightning-button>
                    </div>
                  </div>
                
                <template for:each={events} for:item="eachevent">
                    <lightning-card key={eachevent.id}
                                    class="slds-p-left_medium slds-p-right_small">
                        <h3 slot="title">
                            <span class="slds-p-right_small">
                                <lightning-icon icon-name="standard:event" size="small">

                                </lightning-icon>
                            </span>
                            {eachevent.title} 
                        </h3>
                        <lightning-button-icon icon-name="action:remove" slot="actions" 
                                                alternative-text="remove" title="Remove"
                                                value={eachevent.id} size="small"
                                                onclick={removeEvent}>

                        </lightning-button-icon>
                        
                        <p class="slds-p-horizontal_small"> Start: <lightning-formatted-date-time value={eachevent.start} year="numeric" month="numeric" day="numeric" hour="2-digit"
                            minute="2-digit" time-zone="GMT" time-zone-name="short" hour12="true"></lightning-formatted-date-time></p>

                        <p class="slds-p-horizontal_small">End <lightning-formatted-date-time value={eachevent.end} year="numeric" month="numeric" day="numeric" hour="2-digit"
                            minute="2-digit" time-zone="GMT" time-zone-name="short" hour12="true"></lightning-formatted-date-time></p>
                        
                    </lightning-card>
                </template>
            </div>
        </div>
        <div class="slds-col slds-size_9-of-12">
                <!-- fullcalendar sits in this div -->
                <div id="calendar" class="fullcalendarjs"></div>
        </div>
   </div>

   <!-- Open a modal with new event form  -->
   <template if:true={openModal}>
       <div data-modal="custommodal" class="modalclass">
            <section
            role="dialog"
            tabindex="-1"
            aria-labelledby="modal-heading-01"
            aria-modal="true"
            aria-describedby="modal-content-id-1"
            class="slds-modal slds-fade-in-open">
            <div class="slds-modal__container">
                <header class="slds-modal__header">
                    <lightning-button-icon icon-name="utility:close" 
                                            class="slds-modal__close " 
                                            alternative-text="Close" 
                                            title="Close"
                                            size="large"
                                            variant="bare-inverse"
                                            onclick={handleCancel} >

                    </lightning-button-icon>
                    <h2 id="modal-heading-01" class="slds-modal__title slds-hyphenate">New Event</h2>
                </header>
                <div class="slds-modal__content slds-p-around_medium"
                    id="modal-content-id-1">
                    <lightning-input label="Title" name="title" type="text" required onkeyup={handleKeyup}></lightning-input>
                    <lightning-input label="Start Date" name="start" type="datetime" required value={startDate}></lightning-input>
                    <lightning-input label="End Date" name="end" type="datetime" required value={endDate}></lightning-input>
                </div>
                <footer class="slds-modal__footer">
                    <lightning-button-group>
                        <lightning-button label="Close" title="Close" icon-name="utility:close" onclick={handleCancel}></lightning-button>
                        <lightning-button label="Save" title="Save" variant="brand" icon-name="utility:save" onclick={handleSave}></lightning-button>
                    </lightning-button-group>
                    
                </footer>
            </div>
        </section>
        <div class="slds-backdrop slds-backdrop_open"></div>
       </div>
    </template>
</template>

fullcalendarJs.js

import { LightningElement, track, wire } from 'lwc';
import { loadScript, loadStyle } from 'lightning/platformResourceLoader';
import { ShowToastEvent } from 'lightning/platformShowToastEvent';
import FullCalendarJS from '@salesforce/resourceUrl/FullCalendarJS';
import fetchEvents from '@salesforce/apex/FullCalendarController.fetchEvents';
import createEvent from '@salesforce/apex/FullCalendarController.createEvent';
import deleteEvent from '@salesforce/apex/FullCalendarController.deleteEvent';
import { refreshApex } from '@salesforce/apex';
/**
 * @description: FullcalendarJs class with all the dependencies
 */
export default class FullCalendarJs extends LightningElement {
    //To avoid the recursion from renderedcallback
    fullCalendarJsInitialised = false;

    //Fields to store the event data -- add all other fields you want to add
    title;
    startDate;
    endDate;

    eventsRendered = false;//To render initial events only once
    openSpinner = false; //To open the spinner in waiting screens
    openModal = false; //To open form

    @track
    events = []; //all calendar events are stored in this field

    //To store the orignal wire object to use in refreshApex method
    eventOriginalData = [];

    //Get data from server - in this example, it fetches from the event object
    @wire(fetchEvents)
    eventObj(value){
        this.eventOriginalData = value; //To use in refresh cache

        const {data, error} = value;
        if(data){
            //format as fullcalendar event object
            console.log(data);
            let events = data.map(event => {
                return { id : event.Id, 
                        title : event.Subject, 
                        start : event.StartDateTime,
                        end : event.EndDateTime,
                        allDay : event.IsAllDayEvent};
            });
            this.events = JSON.parse(JSON.stringify(events));
            console.log(this.events);
            this.error = undefined;

            //load only on first wire call - 
            // if events are not rendered, try to remove this 'if' condition and add directly 
            if(! this.eventsRendered){
                //Add events to calendar
                const ele = this.template.querySelector("div.fullcalendarjs");
                $(ele).fullCalendar('renderEvents', this.events, true);
                this.eventsRendered = true;
            }
        }else if(error){
            this.events = [];
            this.error = 'No events are found';
        }
   }

   /**
    * Load the fullcalendar.io in this lifecycle hook method
    */
   renderedCallback() {
      // Performs this operation only on first render
      if (this.fullCalendarJsInitialised) {
         return;
      }
      this.fullCalendarJsInitialised = true;

      // Executes all loadScript and loadStyle promises
      // and only resolves them once all promises are done
        Promise.all([
            loadScript(this, FullCalendarJS + "/FullCalendarJS/jquery.min.js"),
            loadScript(this, FullCalendarJS + "/FullCalendarJS/moment.min.js"),
            loadScript(this, FullCalendarJS + "/FullCalendarJS/fullcalendar.min.js"),
            loadStyle(this, FullCalendarJS + "/FullCalendarJS/fullcalendar.min.css"),
        ])
        .then(() => {
            //initialize the full calendar
        this.initialiseFullCalendarJs();
        })
        .catch((error) => {
        console.error({
            message: "Error occured on FullCalendarJS",
            error,
        });
        });
   }

    initialiseFullCalendarJs() {
        const ele = this.template.querySelector("div.fullcalendarjs");
        const modal = this.template.querySelector('div.modalclass');
        console.log(FullCalendar);

        var self = this;

        //To open the form with predefined fields
        //TODO: to be moved outside this function
        function openActivityForm(startDate, endDate){
            self.startDate = startDate;
            self.endDate = endDate;
            self.openModal = true;
        }
        //Actual fullcalendar renders here - https://fullcalendar.io/docs/v3/view-specific-options
        $(ele).fullCalendar({
            header: {
                left: "prev,next today",
                center: "title",
                right: "month,agendaWeek,agendaDay",
            },
            defaultDate: new Date(), // default day is today - to show the current date
            defaultView : 'agendaWeek', //To display the default view - as of now it is set to week view
            navLinks: true, // can click day/week names to navigate views
            // editable: true, // To move the events on calendar - TODO 
            selectable: true, //To select the period of time

            //To select the time period : https://fullcalendar.io/docs/v3/select-method
            select: function (startDate, endDate) {
                let stDate = startDate.format();
                let edDate = endDate.format();
                
                openActivityForm(stDate, edDate);
            },
            eventLimit: true, // allow "more" link when too many events
            events: this.events, // all the events that are to be rendered - can be a duplicate statement here
        });
    }

    //TODO: add the logic to support multiple input texts
    handleKeyup(event) {
        this.title = event.target.value;
    }
    
    //To close the modal form
    handleCancel(event) {
        this.openModal = false;
    }

   //To save the event
    handleSave(event) {
        let events = this.events;
        this.openSpinner = true;

        //get all the field values - as of now they all are mandatory to create a standard event
        //TODO- you need to add your logic here.
        this.template.querySelectorAll('lightning-input').forEach(ele => {
            if(ele.name === 'title'){
               this.title = ele.value;
           }
           if(ele.name === 'start'){
                this.startDate = ele.value.includes('.000Z') ? ele.value : ele.value + '.000Z';
            }
            if(ele.name === 'end'){
                this.endDate = ele.value.includes('.000Z') ? ele.value : ele.value + '.000Z';
            }
        });
       
        //format as per fullcalendar event object to create and render
        let newevent = {title : this.title, start : this.startDate, end: this.endDate};
        console.log(this.events);

        //Close the modal
        this.openModal = false;
        //Server call to create the event
        createEvent({'event' : JSON.stringify(newevent)})
        .then( result => {
            const ele = this.template.querySelector("div.fullcalendarjs");

            //To populate the event on fullcalendar object
            //Id should be unique and useful to remove the event from UI - calendar
            newevent.id = result;
            
            //renderEvent is a fullcalendar method to add the event to calendar on UI
            //Documentation: https://fullcalendar.io/docs/v3/renderEvent
            $(ele).fullCalendar( 'renderEvent', newevent, true );
            
            //To display on UI with id from server
            this.events.push(newevent);

            //To close spinner and modal
            this.openSpinner = false;

            //show toast message
            this.showNotification('Success!!', 'Your event has been logged', 'success');

        })
        .catch( error => {
            console.log(error);
            this.openSpinner = false;

            //show toast message - TODO 
            this.showNotification('Oops', 'Something went wrong, please review console', 'error');
        })
   }
   
   /**
    * @description: remove the event with id
    * @documentation: https://fullcalendar.io/docs/v3/removeEvents
    */
   removeEvent(event) {
        //open the spinner
        this.openSpinner = true;

        //delete the event from server and then remove from UI
        let eventid = event.target.value;
        deleteEvent({'eventid' : eventid})
        .then( result => {
            console.log(result);
            const ele = this.template.querySelector("div.fullcalendarjs");
            console.log(eventid);
            $(ele).fullCalendar( 'removeEvents', [eventid] );

            this.openSpinner = false;
            
            //refresh the grid
            return refreshApex(this.eventOriginalData);

        })
        .catch( error => {
            console.log(error);
            this.openSpinner = false;
        });
   }

   /**
    *  @description open the modal by nullifying the inputs
    */
    addEvent(event) {
        this.startDate = null;
        this.endDate = null;
        this.title = null;
        this.openModal = true;
    }

    /**
     * @description method to show toast events
     */
    showNotification(title, message, variant) {
        console.log('enter');
        const evt = new ShowToastEvent({
            title: title,
            message: message,
            variant: variant,
        });
        this.dispatchEvent(evt);
    }
}

fullcalendarjs.css

.fullcalendarjs {
    max-width: 1000px;
    margin: 40px auto;
  }

Resources:

fullcalendar.io documentation

Please follow and like us: