Building a working calendar from scratch with Vue.js and CSS grid (no libraries!)
Hello there!
I’m going to share with you a simple calendar I made just recently using Vue.js and CSS grid (no external libraries included). The logic is in JavaScript, of course, and I’m going to walk you through the whole process of making it. It’s quite simpler than you may think 😉
What are we building?
So here’s a preview of what we’re going to produce at the end of this process:
The code is few, and everything is done from scratch.
What you should know beforehand.
These are some basic things you should know to follow along without getting too confused:
- A basic understanding of Vue.js (obviously). At least, you should understand how components and props work.
- A basic understanding of JavaScript. The main thing we will use here is its
Date
object. - A basic understanding of CSS grid. The image you saw at the beginning is styled mainly using CSS grid. So you should understand how elements are positioned in the grid. We’re going to be determining their positions using JavaScript in a minute. How fun is that?
As you can see, everything is pretty basic.
Let’s Begin!
Everything starts with trying to get all the dates in a given month. And this is done, like I said, in JavaScript, using its Date
object.
But first, keep in mind as we go along:
- ‘Date’ refers to the number on the calendar. For example, the 12th of May — the date here is 12.
- ‘Day’ refers to the day of the week (Monday, Tuesday, etc.).
- Months in JavaScript are numbered, and they begin at 0 and end at 11 (because we have 12 months). So January is 0, February - 1 and so on. December is 11.
- Days are also numbered, beginning from 0, which is Sunday, 1 being Monday and so on till 6, being Saturday.
So let us create a function which accepts two arguments: Year and Month, so we can generate the dates in that specific month.
Here’s the code. I’ll explain what every line does:
function getDates(year, month) {
var d = new Date(year, month);
while(d.getMonth() == month) {
console.log(d.getDate());
d = new Date(d.getTime() + (1000 * 60 * 60 * 24));
}
}
Explanation:
function getDates(year, month){…}
-Creates a functiongetDates
which accepts two arguments, ‘year’ and ‘month’ as integers.var d = new Date(year, month);
-Creates a variable namedd
and gives it aDate
object which is passed theyear
andmonth
values, the same ones which were passed into thegetDates
function. TheDate
object can take in more arguments:new Date(year, month, date, hour, minutes, seconds, milliseconds)
. The year and month are the least it can take. Arguments can be omitted from the right. In that case, those are set to default. If you’re not so familiar with the date object, here’s a link to a tutorial on w3schools: https://www.w3schools.com/js/js_dates.asp.while (d.getMonth() == month){…}
-This while loop makes sure the dates produced are within the bounds of the given month. As we increment the date object by 24 hours (1000*60*60*24 milliseconds), the date value we’re passing increases without bound. And interestingly, passing 32nd January into aDate
object, as innew Date(2018,0,32)
won’t give you an error, but will be translated into 1st February of that year. So in our case, we’re telling JavaScript that it should increase the date by 1 as long as the month stays the same. Got it?console.log(d.getDate());
- This logs the generated date values to the console.d = new Date(d.getTime() + (1000 * 60 * 60 * 24));
-Increments the date object by 24 hours (that’s a day). thed.getTime()
method returns the millisecond value of the date objectd
, that is the number of milliseconds since January 01, 1970, 00:00:00 UTC till the time of creation of that object. So it gets that value, adds 24 hours (in milliseconds), and passes the new value into the same object. So we now have the next day.
Now if we call getDates(2018,0)
(that’s for January 2018), we get 1 through 31 logged to the console. getDates(2016,1)
will give you 1 through 29. Meaning 2016 was a leap year.
Okay so now we have our central code done.
What next?
We’re now going to create our vue component. Let’s name it monthView.vue
.
This is the template we’re going to use. It’s not complete yet, but I’ll explain how it works in a second:
As it is, nothing is displayed yet. We will insert the needed codes later. For now, let me explain how this is supposed to work.
The component has some data properties:
months
is an array containing the names of the months in ascending order. This allows us to easily get the name of a month given us by theDate
object, using it as an index since the array is also zero based.dayNames
is also an array containing the names of the days of the week in ascending order, allowing us to retrieve a day given it’s corresponding number.dates
-an empty array which will be populated with all the dates in a given month when thegetDates
function is called.firstSatDate
- This might sound strange for now, but we’ll use this property to hold the date of the first Saturday of any month we generate. I’ll tell you why we need this as we go along.
Note: The component receives the year and month values to use as ‘props’. This enables us to pass in different values, and the component renders dynamically. How convenient!
Now for the methods, we have just one: getDates
. This is the same function we created at the beginning, to generate all the dates in a given month. Just a few additions, and I’ll explain them:
line 28: this.dates = [];
-What this does is to reset thedates
data property of the component anytime thegetDates
function is called. This is to avoid appending data to the existing one when the function is called again.line 30: if (!!d.getTime() && mth <= 11 && mth >= 0) {
- This if statement is simply to handle the error of invalid arguments passed to the function. For example, a negative value formonth
. If so, then an empty object is passed intodates
as seen inline 37
.line 32: this.dates.push({date: d.getDate(), day: d.getDay()});
-Now instead of logging the values to the console, we are appending objects to thedates
array. Every one of these objects has two properties:date
, for the actual date, andday
for the day it falls on. So if the month starts on a Wednesday, thedates
array will contain[{1, 3},{2, 4},...]
, in that order. Note that theday
will get to 6 (Saturday) and reset to 0 as the date increases.line 35: this.firstSatDate = 7 — this.dates[0].day;
-After the dates have been generated, we calculate the date of the first Saturday of the given month. We do this by subtracting the day (not date) value of the first day of the month from 7 (because we have seven days in a week). So if the first day of the month is a Tuesday, for example, it’s day value is 2. and 7 -2 = 5. Meaning the first Saturday of that month is the 5th. Simple, right?- The watchers are there to perform if the
year
andmonth
prop values are changed, so the component is re-rendered. Without them, the component will NOT react (no pun intended), even when the props change.
We’re almost done!
Great, we’re virtually done. We now have to insert the code to display the values. So let’s do that:
Okay now, first we have the name of our month displayed (line 3). It just takes the value of the month
prop, and uses it as an index to the months
array, to return the corresponding name.
Next, we have line 6. This creates HTML <div>
elements for each day in the dayNames
array, and gives each of them a class of ‘dayName’ and the actual day’s name. So a div would be rendered as, for example, <div class="dayName MON" ...></div>
. They also have keys in the form year-month-dayName
which will look like 2018-0-SUN
. This way, if there are multiple components for different months on the same page, there won’t be any duplicate keys. Then, the day names are printed in <h5>
tags. So we have something like <div ...><h5>SUN</h5></div>
for each day.
Finally we have line 9, which creates <div>
elements for each date in the month. They each also have two classes: ‘date’ and the day which they fall on. So we have something like <div class="date WED" ...>
. They also have ID’s and keys in the form year-month-date
to distinguish each date’s <div>
on a page.
Now let’s add some styles, because now everything is displayed in it’s own line. Like so:
DECEMBER
SUN
MON
TUE
...
SAT
1
2
3
...
31
So let’s add some CSS:
These are very basic styles, setting up the grid template and adding some basic rules. If you understand CSS grid, this should be a breeze. Notice in the last rule that I styled every div
with a class of ‘SUN’ differently, giving them a light red background and red text, so the Sunday column is distinguished. Just like real calendars.
Now here’s the problem:
Every month is shown to start on a Sunday. So how do we determine where the dates are placed in the grid based on their day value? Here’s where the math comes in. Don’t worry, this is one of the simplest algorithms you could come across!
First, we can determine the column a date should be placed in, based on its day value (this is a better way to do it, you’ll understand why). We just increase the value by 1, and we get the column line it should begin at. So, for example, if it’s a Sunday, its date value is 0. and 0 + 1 = 1. So it begins at column line 1 (remember grid lines start at 1, and the days are zero-based), and ends at the next grid line.
Next, we need to determine the row in which the date div will be placed in the grid.
Remember firstSatDate
? This is where it comes into play! Now there are different ways you could do this, but this is the algorithm that came to mind:
We know that, using a calendar which is Sunday-based, the first days in a month until the first Saturday (inclusive) are all on the first row. After the first Saturday, the next 7 dates wrap to the next row and so on. So what we’re going to do is to look at the offset of every date from the first Saturday’s date, and perform some math with it. We will only look at the ones after the first Saturday, because the ones until the first Saturday (inclusive) will, by default, be on the first row. So now the next Sunday is offset by 1, next Monday offset by 2 and so on until the next Saturday, offset by 7 (that’s the next week). Notice that all those are on the second row. So now we can see a pattern here: Offsets 1 -7 are on row 2, Offsets 8 - 14 are on row 3 and so on.
So how do we compute this? Simple! divide the offset by 7, and use the ceiling function on the result. Note: The ceiling function just rounds up a proper (non-whole-number) floating point value to the nearest larger integer, no matter the decimal point. It is mostly referred to as ceil
. So ceil(1.1) = 2, ceil(1.9) = 2,
but ceil(1.0) = 1
.
Therefore offset 1 will give us 1/7 = 0.14…
, offset 7 will give us 7/7 = 1
. Everything in-between will give us a value between 0.14… and 1. Using the ceiling function, we get 1 for all of them. That tells us they’re all on one row. But since we’re looking at the offset from the first Saturday, the first row is already taken. So we will add 1 to the result to tell us the correct row they should be in.
offsets 1 -7 gave us 1 after the computation, so we add 1 to give us 2. This tells us they’re to be on the 2nd row. So now offsets 8 -14 (Yet the next Sunday through Saturday) will give us 2 after the computation: ceil(8/7) = 2, ceil(14/7) = 2
. So we add 1 to give us 3, to tell us those are on the 3rd row.
That does it! Now we can add a grid-area
style rule to position each date. I’ll use the shorthand value which is in the form grid-area: row-line-start / column-line-start / row-line-end / column-line-end;
. For the line-end values, I’ll replace both with span 1
, since each element is spanning 1 row and 1 column. So we get: grid-area: row-line-start / column-line-start / span 1 / span 1;
. Now let’s update our code:
Now we’ve added a new attribute:
:style=”`grid-area: ${(date.date > firstSatDate) ? (Math.ceil((date.date — firstSatDate)/7))+1 : 1}/${date.day + 1}/span 1/span 1`”
First of all, the colon (:) before the ‘style’ attribute is short-hand for v-bind:
, which allows us to put JavaScript expressions in the quotes. And here, I’m using the ES6 template string. So now we determine the row-line-start
value with the code (date.date > firstSatDate) ? (Math.ceil((date.date — firstSatDate)/7))+1 : 1
.
Explanation: This first asks JavaScript if the current date value is greater than (is a positive offset from) the first Saturday’s date. If yes, Then it calls JavaScripts Math.ceil()
function, and does our math with it to determine the row number. If not, it takes the value 1. (if it isn’t offset positively from the first Saturday’s date, then the date is either before, or on that Saturday. So it is on row 1). Also it determines the column-line-start
by just adding 1 to the date’s day value, as we spoke about.
Voila!
So there we have it! You can give this component any year and month, and it will display the dates like we saw in the image at the beginning.
So to use this component, we have to nest it in a parent component and pass it the props. like so:
<template> <month-view year=2018 month=11 /></template>
More usefully, you could display all the months in a given year, like a real calendar!
<template> <month-view v-for="month in months" :month=month year=2018 /></template><script>
import monthView from 'monthView.vue'; export default{
components: {
monthView
} data(){
return {
months: [0,1,2,3,4,5,6,7,8,9,10,11]
}
}
}
</script>
Very simple, isn’t it? And you could do all kinds of fancy stuff with it. Like I added an input box where I could type in a year and press enter, then the months will re-render to form the calendar for that year.
So that’s how to create a calendar using Vue.js and CSS grid!
Spoiler…🤫
I realized through the devTools console that I kept getting an error (warning) for duplicate keys in the monthView
component. This was because some months were producing duplicates of a particular date. For example, October 28, 2018 was duplicated, before moving on to 29, then 30, etc. So it had 32 items in it’s dates
array!.
The problem was with JavaScript itself: Consider a date object new Date(2018,9,28)
. It is currently set at 12am on that day. But increasing it’s time (millisecond) value by 24 hours (1000*60*60*24 milliseconds) will return a date set at Oct 28, 2018 11:00:00pm instead of Oct 29, 2018 12:00:00am. Interesting. So I did some research and found out that it happens due to some Daylight Saving gobbledegook. I don’t really know about that for now, but at least I knew it was valid.
But here’s the good news… Because, in the CSS, we determined where each date will be positioned based on it’s date and day using JavaScript, this duplicate did not affect the layout, because they were all competing for the same spot!
Cooool! This is why it isn’t a good idea (as I had at first) to fill the empty days at the beginning of the month (if there are any), with empty <div>
elements, and make the rest of the dates fall in place as the grid allows. That way, the duplicates WILL BE SHOWN, and will affect the rest of the dates, distorting the calendar!
But I fixed it.
Though seeing that CSS took care of the problem for me, I still didn’t feel good about that red bar in the console. And, a perfectionist that I am, I came up with a very simple fix to the problem:
In the getDates
function, I tweaked the default Date
object a little:
var d = new Date(yr, mth, 1, 1);
What I did there was to set the first day’s time to 1:00am. So that every 24-hour increment will give me the next day, with time at 1:00am. But this way, if an hour is lost as in October 28, 2018, I’ll end up at 12:00am, which will still give me the next day, October 29!
Classic!, I thought.
The End!
So we’ve come a long way. Thank you for staying with me. Feel free to leave a comment below. And if there’s anything you’d want to ask concerning this, or any suggestions you have, you’re welcome! I’d love to see what you guys could make out of this, so do well to share.
See you next time!
And oh!… Merry Christmas! 😉.