Create a custom calendar in LWC — Part 2 (Styling the calendar)

Rohit Tanwani
11 min readJul 22, 2023

--

We discussed using the Fullcalendar.io library in a Lightning Web Component in the previous part. If you missed that part here is the link.

Custom Calendar Part 1

After Part 1, you will see the above custom calendar with header actions working and no data displayed in the calendar. Great!! isn’t? So let’s start with styling the calendar header.

Currently, the calendar header is not looking like a Salesforce custom component. Even the default buttons provided by Fullcalendar.io are looking too odd. Hence the first option which we are providing in the optionsObject is to set the header toolbar to false. But isn’t this going to hide the header completely? Well yes, because you cannot style these buttons. That’s why we are going to create our header for the custom calendar.

In the js file, write the following code in the initializeCalendar method.

const calendarEl = this.template.querySelector('div.fullcalendar');
const calendar = new FullCalendar.Calendar(calendarEl, {
headerToolbar: false
}
calendar.render();

After deploying this code, the header will disappear.

Custom Calendar without header.

Now create two variables at the class level in the js file,

  1. calendar title variable — This variable will be used to store the calendar title when switching the calendar mode.
  2. calendar variable — This variable will be used to store the calendar information outside of the initializeCalendar method.

Great! After creating these two variables. Go to the HTML file and write the following code to create a header. Replace the code in the HTML file with the below code.

<lightning-card>
<div class="calendar-card">
<div class="slds-theme_shade slds-box">
<div class="slds-grid slds-gutters">
<div class="slds-col slds-size_1-of-3">
<div class="slds-media slds-no-space slds-grow">
<div class="slds-media__figure">
<lightning-icon icon-name="standard:event" size="medium"></lightning-icon>
</div>
<div class="slds-media__body">
<p class="slds-line-height_reset" data-aura-rendered-by="168:1393;a">Calendar</p>
<h1 class="slds-page-header__title slds-m-right_small slds-align-middle slds-truncate" data-aura-rendered-by="170:1393;a">{calendarTitle}</h1>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="fullcalendar"></div>
</lightning-card>

The above code simply creates a dark grey header at the top of the calendar days section. It will look something like this. Your header may not display the title right away but don’t worry we will fix it.

Custom calendar with header without actions

Now let’s create some actions on the right side. We try to keep all the actions the same as the Salesforce calendar which means Previous, Next, Today, Mode Selection and New.

Now let’s just add two lightning-button-icon, one for the previous and one for the next action. Add one lightning button which will have the text ‘Today’. Again add a lightning-button-icon which will have a refresh icon. Again add a lightning button which will have the text as “New”. The ‘on click’ method of all of these button icons and buttons will be the same. We name it as calendarActionsHandler(You can have a separate method for each action). The HTML code will look something like this.

<lightning-button-icon 
variant="border-filled"
icon-name="utility:chevronleft"
value="previous"
onclick={calendarActionsHandler}>
</lightning-button-icon>

<lightning-button-icon
variant="border-filled"
icon-name="utility:chevronright"
value="next"
onclick={calendarActionsHandler}>
</lightning-button-icon>

<lightning-button
label="Today"
class="slds-m-horizontal_small"
value="today"
onclick={calendarActionsHandler}>
</lightning-button>

<lightning-button-icon
variant="border-filled"
icon-name="utility:refresh"
class="slds-m-left_medium"
value="refresh"
onclick={calendarActionsHandler}>
</lightning-button-icon>

<lightning-button
label={buttonLabel}
class="slds-m-horizontal_small"
value="new"
onclick={calendarActionsHandler}>
</lightning-button>

Wait why does each action have a value attribute? So that we can get which action is triggered in the js method. Go to the JS method and write the implementation for calendarActionsHandler. Now get the value of the action by event.target.value and assign it to the constant variable actionName. After getting it go to Fullcalendar.io documentation to look for appropriate methods which will help us in the following case. Here is the required documentation link. Fullcalendar.io have

  1. Prev method: For moving the calendar one step back (by a month or week for example).
  2. Next method: For moving the calendar one step ahead.
  3. Today's method: Moves the calendar to the current date.

Copy the following code into your JS file.

 calendarActionsHandler(event) {
const actionName = event.target.value;
if(actionName === 'previous') {
this.calendar.prev();
} else if(actionName === 'next') {
this.calendar.next();
} else if(actionName === 'today') {
this.calendar.today();
} else if(actionName === 'new') {

} else if(actionName === 'refresh') {

}
// changes the calendar title when switching to different months or days
this.calendarTitle = this.calendar.view.title;
}

Now in JS, create one more class-level variable as viewOptions which is an array-type variable which will have the following elements.

viewOptions = [
{
label: 'Day',
viewName: 'timeGridDay',
checked: false
},
{
label: 'Week',
viewName: 'timeGridWeek',
checked: false
},
{
label: 'Month',
viewName: 'dayGridMonth',
checked: true
},
{
label: 'Table',
viewName: 'listView',
checked: false
}
];

Each element contains a Label property to show it to the end user. ViewName is the Fullcalendar viewName which we will pass to the calendar variable to switch the view(listView is not a defined Fullcalendar.io viewName, this view will be used to navigate to the recently viewed list view of the object. Sounds confusing don’t worry it will get cleared soon). The checked property will be used to show a check icon beside the selected option.

Go to the HTML File and add a lightning-button-menu tag with the variant as border-filled, icon-name as a utility:event, menu-alignment as auto, with a small horizontal margin and onselect handler as changeViewHandler. Inside this lightning-button-menu tag run a for each loop on viewOptions defined in the JS file and create a lightning-menu-item. Wait, Rohit, you are not making sense. Okay got it, just copy the code in the HTML file below the refresh action.

 <lightning-button-menu alternative-text="Show menu" variant="border-filled" 
icon-name="utility:event" class="slds-m-horizontal_small"
menu-alignment="auto" onselect={changeViewHandler}>
<template for:each={viewOptions} for:item="menuItem">
<lightning-menu-item
key={menuItem.viewName}
value={menuItem.viewName}
label={menuItem.label}
checked={menuItem.checked}>
</lightning-menu-item>
</template>
</lightning-button-menu>

Now head back to JS File and write an implementation for changeViewHandler. First of all, look into Fullcalendar.io documentation that is there any method available for changing the view in the calendar. Luckily there is with the name ‘changeView’.

So we have to just call the changeView method and pass the viewName it will automatically change the view but how we will get the view name? Remember in viewOptions we have the viewName variable also which we have passed as the value in the lightning-menu-item. Second problem, we don’t have any view name as listView in the Fullcalendar.io so we have to write a if condition for that. In case, if the viewName is listView navigate to the recently list view page of the object. We also have to change the checked value in the options. So keeping these things in mind let’s just write changeViewHandler implementation.

changeViewHandler(event) {
const viewName = event.detail.value;
if(viewName != 'listView') {
this.calendar.changeView(viewName);
const viewOptions = [...this.viewOptions];
for(let viewOption of viewOptions) {
viewOption.checked = false;
if(viewOption.viewName === viewName) {
viewOption.checked = true;
}
}
this.viewOptions = viewOptions;
this.calendarTitle = this.calendar.view.title;
} else {
this.handleListViewNavigation(this.objectApiName);
}
}

handleListViewNavigation(objectName) {
this[NavigationMixin.Navigate]({
type: 'standard__objectPage',
attributes: {
objectApiName: objectName,
actionName: 'list'
},
state: {
filterName: 'Recent'
}
});
}

objectApiName passed to handleListViewNavigation method is a class-level variable whose default value we have set as Account. To use NavigationMixin.Navigate, first import the NavigationMixin library through the import statement and change the class declaration as

import { NavigationMixin } from "lightning/navigation";

export default class CustomCalendar extends NavigationMixin(LightningElement) {

Why we are using NavigationMixin? Two reasons :

  1. Because of the ‘Table’ view option. If you go to Salesforce Calendar and click on the table option it will navigate you to the Event object Recently Viewed list view. We are also trying to provide the same functionality here.
  2. For the New button created earlier, we will use the NavigationMixin to navigate to the new record form.

Create a method in the JS file to navigate to the new record page.

navigateToNewRecordPage(objectName) {
this[NavigationMixin.Navigate]({
type: "standard__objectPage",
attributes: {
objectApiName: objectName,
actionName: "new",
},
});
}

And call this method for action name equals “new”.

calendarActionsHandler(event) {
const actionName = event.target.value;
if(actionName === 'previous') {
this.calendar.prev();
} else if(actionName === 'next') {
this.calendar.next();
} else if(actionName === 'today') {
this.calendar.today();
} else if(actionName === 'new') {
this.navigateToNewRecordPage(this.objectApiName);
} else if(actionName === 'refresh') {

}
this.calendarTitle = this.calendar.view.title;
}

After doing all these changes just put all of these actions in a div tag and float this div tag to the right by applying the “slds-float_right” class. As float will break the element's flow. Hence encase this div with a div tag having class “slds-clearfix” and place this div inside the slds-grid div tag. Just copy the entire HTML code.

<template>
<lightning-card>
<div class="calendar-card">
<div class="slds-theme_shade slds-box">
<div class="slds-grid slds-gutters">
<div class="slds-col slds-size_1-of-3">
<div class="slds-media slds-no-space slds-grow">
<div class="slds-media__figure">
<lightning-icon icon-name="standard:event" size="medium"></lightning-icon>
</div>
<div class="slds-media__body">
<p class="slds-line-height_reset" data-aura-rendered-by="168:1393;a">Calendar</p>
<h1 class="slds-page-header__title slds-m-right_small slds-align-middle slds-truncate" data-aura-rendered-by="170:1393;a">{calendarTitle}</h1>
</div>
</div>
</div>

<div class="slds-col slds-size_2-of-3 slds-clearfix">
<div class="slds-float_right slds-p-top_xx-small">
<lightning-button-icon
variant="border-filled"
icon-name="utility:chevronleft"
value="previous"
onclick={calendarActionsHandler}>
</lightning-button-icon>

<lightning-button-icon
variant="border-filled"
icon-name="utility:chevronright"
value="next"
onclick={calendarActionsHandler}>
</lightning-button-icon>

<lightning-button
label="Today"
class="slds-m-horizontal_small"
value="today"
onclick={calendarActionsHandler}>
</lightning-button>

<lightning-button-icon
variant="border-filled"
icon-name="utility:refresh"
class="slds-m-left_medium"
value="refresh"
onclick={calendarActionsHandler}>
</lightning-button-icon>

<lightning-button-menu alternative-text="Show menu" variant="border-filled"
icon-name="utility:event" class="slds-m-horizontal_small"
menu-alignment="auto" onselect={changeViewHandler}>
<template for:each={viewOptions} for:item="menuItem">
<lightning-menu-item
key={menuItem.viewName}
value={menuItem.viewName}
label={menuItem.label}
checked={menuItem.checked}>
</lightning-menu-item>
</template>
</lightning-button-menu>

<lightning-button
label={buttonLabel}
class="slds-m-horizontal_small"
value="new"
onclick={calendarActionsHandler}>
</lightning-button>
</div>
</div>
</div>
</div>
</div>
<div class="fullcalendar"></div>
</lightning-card>
</template>

We are done with HTML. Now again go to the JS file. In the intializeCalendar method after the calendar. render(). Add the following code.

calendar.setOption('contentHeight', 550);
this.calendarTitle = calendar.view.title;
this.calendar = calendar;

content height option will set the height of the view area to 550 px. Setting the calendar title based on the calendar view. And to use calendar variable in different js methods we are assigning the rendered calendar instance to class level calendar variable.

Let’s just change a few settings in the calendar. In the calendar optionsObject below headerToolbar is set to false. Add the following options and see their implications.

const calendar = new FullCalendar.Calendar(calendarEl, {
headerToolbar: false,
initialDate: new Date(),
showNonCurrentDates: false,
fixedWeekCount: false,
allDaySlot: false,
navLinks: false,
});

You guys can look into Fullcalendar.io documentation to see what each option is doing. After doing these changes, deploy it to see the changes. The calendar will look like this.

After doing all these changes deploy the changes. JS file complete code for reference.

import { LightningElement } from 'lwc';

import FullCalendarJS from '@salesforce/resourceUrl/FullCalendar';
import { loadStyle, loadScript } from 'lightning/platformResourceLoader';
import { NavigationMixin } from "lightning/navigation";

export default class CustomCalendar extends NavigationMixin(LightningElement) {

calendar;
calendarTitle;
objectApiName = 'Account';
viewOptions = [
{
label: 'Day',
viewName: 'timeGridDay',
checked: false
},
{
label: 'Week',
viewName: 'timeGridWeek',
checked: false
},
{
label: 'Month',
viewName: 'dayGridMonth',
checked: true
},
{
label: 'Table',
viewName: 'listView',
checked: false
}
];

changeViewHandler(event) {
const viewName = event.detail.value;
if(viewName != 'listView') {
this.calendar.changeView(viewName);
const viewOptions = [...this.viewOptions];
for(let viewOption of viewOptions) {
viewOption.checked = false;
if(viewOption.viewName === viewName) {
viewOption.checked = true;
}
}
this.viewOptions = viewOptions;
this.calendarTitle = this.calendar.view.title;
} else {
this.handleListViewNavigation(this.objectApiName);
}
}

handleListViewNavigation(objectName) {
this[NavigationMixin.Navigate]({
type: 'standard__objectPage',
attributes: {
objectApiName: objectName,
actionName: 'list'
},
state: {
filterName: 'Recent'
}
});
}

calendarActionsHandler(event) {
const actionName = event.target.value;
if(actionName === 'previous') {
this.calendar.prev();
} else if(actionName === 'next') {
this.calendar.next();
} else if(actionName === 'today') {
this.calendar.today();
} else if(actionName === 'new') {
this.navigateToNewRecordPage(this.objectApiName);
} else if(actionName === 'refresh') {

}
this.calendarTitle = this.calendar.view.title;
}

navigateToNewRecordPage(objectName) {
this[NavigationMixin.Navigate]({
type: "standard__objectPage",
attributes: {
objectApiName: objectName,
actionName: "new",
},
});
}

connectedCallback() {
Promise.all([
loadStyle(this, FullCalendarJS + '/lib/main.css'),
loadScript(this, FullCalendarJS + '/lib/main.js')
])
.then(() => {
this.initializeCalendar();
})
.catch(error => console.log(error))
}

initializeCalendar() {
const calendarEl = this.template.querySelector('div.fullcalendar');
const calendar = new FullCalendar.Calendar(calendarEl, {
headerToolbar: false,
initialDate: new Date(),
showNonCurrentDates: false,
fixedWeekCount: false,
allDaySlot: false,
navLinks: false,
});
calendar.render();
calendar.setOption('contentHeight', 550);
this.calendarTitle = calendar.view.title;
this.calendar = calendar;
}
}

After deploying the calendar will look something like this.

Custom Calendar in month view
Custom Calendar in week view

One last thing, we are trying to change every blue colour link in the calendar to a shade of black. This is a matter of personal preference you can skip this step if you like but we are going to use these concepts in the later parts also.

You can say this is too easy. Just add a stylesheet in our customCalendar component and use those styles but we have one problem here. We can’t add any classes or ids to the FullCalendar.Calendar instance. So we can see the classes used by Fullcalendar.io developers and overwrite those with the custom CSS. This also will not work if you try to overwrite the CSS with a component CSS file. You have to import a CSS file after the FullCalendar files are loaded successfully. I know this doesn’t make any sense. Just create a CSS file and upload it as a static resource.

Some of the styles I have added in the CSS file are listed below.

.fc-col-header-cell .fc-scrollgrid-sync-inner {
padding: 8px;
}

.fc-col-header-cell-cushion, .fc-daygrid-day-number {
text-decoration: none;
color: rgb(24, 24, 24);
font-weight: normal;
text-transform: uppercase;
}

.fc-col-header-cell-cushion:hover, .fc-daygrid-day-number:hover {
text-decoration: none;
cursor: none;
color: rgb(24, 24, 24);
}

What is happening with the above styles? The first style increases the size of the day name row by adding the padding of 8px on top and bottom. And second and third rows just provide a black colour to the date links and remove the pointer cursor from them and remove any text decoration applied to the links.

How do I get these classes' names to overwrite the CSS? Well once the calendar renders you can right-click on the element and click Inspect in the menu to view the HTML code of the calendar there you will find the classes along with the CSS properties. You can play and change properties as you like. Once you get the desired result add those properties into this CSS file. Now to use this CSS file in our component import this static resource(I have named my static resource as FullCalendarCustom) with the following code.

import FullCalendarCustom from '@salesforce/resourceUrl/FullCalendarCustom';

In the connectedCallback method after loading the FullCalendar main.js and main.css, write a third import inside Promise.all.

loadStyle(this, FullCalendarCustom)

After adding the following code, deploy this code to the org. The custom calendar will look like this.

Custom Calendar: Month view with styled date links

I know not much but we will use this custom stylesheet to style the events in the next part. If you have any queries regarding the article please write them in the comments, we will be happy to help you. If you like this part please clap and share this article with friends and colleagues.

Here is the link for Part 3. Please keep showing support like this. Thanks Everyone.

--

--

Rohit Tanwani

Consultant at Deloitte USI | Salesforce Developer | Cinephile