In this post, I attempt to show how to develop a calendar using vanilla Javascript and CSS Grid Layout. Following CodePen shows the final code.
We start with an HTML page that has a single <div>
element.
<div data-app="calendar-app"></div>
We will use Javascript to build and render our calendar in this <div>
.
In our Javascript file, we start by defining an array for the months and an array for days of the week.
var months = ['January','February','March','April','May','June','July','August','September','October','November','December'];
var daysOfWeek = ['S','M','T','W','TH','F','SA'];
The application level state
object stores the month
and year
being displayed. We initialize the state
object with the current month and year.
var state = {
month: new Date().getMonth(),
year: new Date().getFullYear()
}
Now, let's take a look at datesForGrid
function, which is the most important function of our application. We pass a year
and month
as arguments and the function returns an array of 42 entries to fill up the 7x6 date grid for the given year and month. month
is 0-indexed.
At the beginning of the function, we declare an empty dates
array to store the date entries.
//month is 0-indexed
function datesForGrid(year, month) {
var dates= [];
...
}
firstDay
stores the day of the week on which the month starts.
...
//Date.getDay() method is 0-indexed.
var firstDay = new Date(year, month).getDay();
...
If value of firstDay
is 2, that means the month starts on a Tuesday (Sunday=0, Monday=1, and so on).
Next, we have to find out how many total days are in the month
...
var totalDaysInMonth = new Date(year, month + 1, 0).getDate();
...
By incrementing the the month
value by 1 and passing 0
for the date argument in the Date()
function, we get the last date of the month in question and hence the total number of days in the month.
We are also interested in the total number of days in the previous month (you'll soon see why).
...
var totalDaysInPrevMonth = new Date(year, month, 0).getDate();
...
Again, by passing the current month
value and 0
for the date argument in the Date()
function we get the last day of the previous month.
Now, we start populating the dates
array.
First, we check how many dates from the previous month do we have to display in the calendar. For example, if the current month starts on a Tuesday, then we have to show 2 dates from the previous month for Sunday and Monday.
We start by adding entries for the previous month in the dates
array.
...
// Days from prev month to show in the grid
for(var i = 1; i <= firstDay; i++) {
var prevMonthDate = totalDaysInPrevMonth - firstDay + i;
var key = new Date(state.year, state.month -1, prevMonthDate).toLocaleString();
dates.push({key: key, date: prevMonthDate, monthClass:'prev'});
}
...
An entry in dates
array is an object. We store a key
property which is a string representation of the date, date
is the numerical date value, monthClass:'prev'
indicates that the date belongs to the previous month.
var prevMonthDate = totalDaysInPrevMonth - firstDay + i
computes the numerical date value to be stored in the date
property of the object.
Let's say the current month is Sept 2020, which means the last month was Aug 2020. In that case, totalDaysInPrevMonth = 31
.
Since Sept 1st, 2020 will be a Tuesday, so firstDay = 2
. Therefore, prevMonthDate
value for i = 1
will be 31 - 2 + 1 = 30
.
So, for Sept 2020, the first two entries in the dates
array will be as follows
//Example for September 2020
dates[0] = {key: '8/30/2020, 12:00:00 AM', date: 30, monthClass: 'prev'};
dates[1] = {key: '8/31/2020, 12:00:00 AM', date: 31, monthClass: 'prev'};
Next, we populate the dates
array with entries for the current month
...
// Days of the current month to show in the grid
var today = new Date();
for(var i = 1; i <= totalDaysInMonth; i++) {
var key = new Date(state.year, state.month, i).toLocaleString();
if(i === today.getDate() && state.month === today.getMonth() && state.year === today.getFullYear()) {
dates.push({key: key, date: i, monthClass: 'current', todayClass: 'today'});
} else{
dates.push({key: key, date: i, monthClass: 'current'});
}
}
...
Here we add an additional property todayClass:'today'
to the object for today's date.
So, for Sept 1, 2020 the dates
array will look like this
//Example for September 2020
...
dates[2] = {key: '9/1/2020, 12:00:00 AM', date: 1, monthClass: 'current', todayClass:'current'};
dates[3] = {key: '9/2/2020, 12:00:00 AM', date: 2, monthClass: 'current'};
...
After adding all the entries for the month to be displayed, we check if there is still space left in the grid. If the size of dates
array is less than 42, then we enter the dates for the next month in the dates
array.
...
var gridsize = 42;
// If there is space left over in the grid, then show the dates for the next month
if(dates.length < gridsize) {
var count = gridsize - dates.length;
for(var i = 1; i <= count; i++) {
var key = new Date(state.year, state.month + 1, i).toLocaleString();
dates.push({key: key, date: i, monthClass:'next'});
}
}
...
render()
function is responsible for rendering the calendar on the webpage.
function render() {
var calendarApp = document.querySelector('[data-app=calendar-app]');
// Building the calendar app HTML from the data
calendarApp.innerHTML = `
<div class="calendar-nav">
<button id="prev-month">Previous</button>
<h2>${months[state.month]} ${state.year}</h2>
<button id="next-month">Next</button>
</div>
<div class='calendar-grid'>
${ daysOfWeek.map(day => `<div>${day}</div>` ).join('') }
${ datesForGrid(state.year, state.month).map(date => `<div id="${date.key}" class="${date.monthClass} ${date.todayClass ? date.todayClass : ''}">${date.date}</div>`).join('') }
</div>
`;
}
.calendar-grid
css class defines a simple grid layout for the calendar. Each grid cell is a <div>
. Following is an example of rendered HTML
<div class='calendar-nav'>
<button id="prev-month">Previous</button>
<h2>September 2020</h2>
<button id="next-month">Next</button>
</div>
<div class='calendar-grid'>
...
<div id="8/31/2020, 12:00:00 AM" class="prev">31<div>
<div id="9/1/2020, 12:00:00 AM" class="current today">1<div>
<div id="9/2/2020, 12:00:00 AM" class="current">2<div>
<div id="9/3/2020, 12:00:00 AM" class="current">3<div>
...
<div id="10/1/2020, 12:00:00 AM" class="next">1<div>
...
</div>
showCalendar()
function is the main entry point. It takes one numerical argument prevNextIndicator
.
//valid prevNextIndicator values
-1 = previous month
0 = current month
1 = next month
showCalendar()
function sets the state
variable based on the value of prevNextIndicator
and calls the render()
function
function showCalendar(prevNextIndicator) {
var date = new Date(state.year, state.month + prevNextIndicator);
//Update the state
state.year = date.getFullYear();
state.month = date.getMonth();
render();
}
// Show the current month by default
showCalendar(0);
Finally, we add event listeners for the Previous (<) and Next (>) buttons.
document.addEventListener('click', function(ev) {
if(ev.target.id === 'prev-month') {
showCalendar(-1);
}
if(ev.target.id === 'next-month') {
showCalendar(1);
}
});
That's our calendar. With less than 100 lines of Javascript code and a few lines of CSS, we created a fully functional calendar.
Top comments (4)
Is there a way to change the first day of the week to Monday?
I’ve seen this request too. Would be nice to support.
Great post, thanks.
You forgot to include "return dates" at the bottom of your datesForGrid function (but remembered it in the codepen example). Without that we get a JS error
Thank you for this clean cut example. I’ll be using your example as a basis for my calendar web component library I’m working on at webcomponents.engineer