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