JS Calendar
Create your own Vanilla JS calendar!
When I started at one of my more recent shops, I was criticized for criticizing the use of Moment.js. Sure, Moment.js is fine and dandy and can do alot of really cool stuff, but for most projects it’s overkill. Just another dependency you have to f*ck with.
So, I set out to learn all I could about the vanilla new Date class. Long story short, I ended up creating my own Calendar class. You can dump it into any JS framework you like (React/React Native, Vue, Knockout, Angular, etc…).
Constructor
First we need to define the class and constructor. It takes a month, day and year.
class Calendar {
constructor(m, d, y) {}
}
next, we set our internal properties and define today.
class Calendar {
constructor(m, d, y) {
this.m = m
this.d = d
this.y = y
this.today = new Date(`${this.m}/${this.d}/${this.y}`)
}
}
Our goal is to first create a 1D array of Date instances that represents the month. But to make it pretty, we’ll also need the days leading up to this month and the days trailing into the following month (calendars are usually 6 weeks with leading and trailing days).
But, what about leap years? What about long months? Any month can have 28, 29, 30 or 31 days in it! How do we handle that?!?!
Stay calm. We can use case statements, or if statements, but I want to keep this codebase small and sleek-ish. We’ll be using the often ignored function isNaN. Basically, if you try creating a date that doesn’t exist, isNaN will notice!
But won’t we have to still handle day numbers and convolutions? Nope!
The new Date() class usually takes in a string date… but it also takes in milliseconds since 1969(or some anachronistic date)! This means we can loop through some large number while incrementing i some other large number. For example:
86400000 /* Amount of milliseconds in a day */
therefore:
class Calendar {
constructor(m, d, y) {
... this.month = this.createMonth()
this.monthArr = this.getMonthArr(this.today)
} createMonth() { /* Creates day 1 of the month */
return new Date(`${this.m}/1/${this.y}`)
} getMonthArr(today) {
let month = []
let m = today.getDate()
let y = today.getUTCFullYear()
for(let i = (this.month.getTime() - 604800000); i < (3628800001 + this.today.getTime()); i += 86400000) {
let d = new Date(i)
if(isNaN(d)) return month
month.push(d)
}
return month
}
}
Basically, we find the first of the month (this.month) then we set our index to the first day of the month minus a week’s worth of milliseconds. Notice isNaN is filtering out dates that don't exist! For the dates that DO exist, we push them to our monthArr.
Clean Up
Now we have a pretty nice array of Date instances. We have the month we want and the days leading and trailing. Now we need to clean it up.
Since calendars start on Sundays we’ll find the first in our array and splice off the days leading up to it.
class Calendar {
constructor(m, d, y) {
... this.chopOffLeadingDays()
} chopOffLeadingDays() { /* Chops off days leading up to sunday */
for(let i = 0; i < 7; i++) {
let index = this.monthArr.findIndex(d => d.getDay() === 0)
this.monthArr.splice(0, index)
}
}
}
Find our index with a fat arrow then splice off everything leading up to it.
Format
Now we need to turn this 1D array of Dates into an array of Weeks of Days… or as I like to call it… a month.
class Calendar {
constructor(m, d, y) {
... this.formatWeeks()
} formatWeeks() { /* reformat monthArr to array of weeks of Dates */
let month = []
for(let i = 0; i < 6; i++) {
let week = this.monthArr.splice(0, 7)
month.push(week)
}
this.monthArr = month
}
}
I’m using splice again… mostly because I love it. Splice mutates an existing array AND returns what you just sliced off it! So:
let week = this.monthArr.splice(0, 7)
rips 7 days off of our 1D array AND returns to us a week. Pretty cool. And by using let, we’re ensuring the week variable is re-instantiated thus cleaning it out. Not needed, but I like it.
That’s basically it
And that’s really all you need… but why stop there?!?!
For convenience, I added:
class Calendar {
... getMonthOfDays() { /* returns array of weeks of Ints */
return this.monthArr.map(w => w.map(d => d.getDate()))
} getSelectedDay(d) { /* set date format required */
return new Date(`${this.m}/${d}/${this.y}`)
} getNextMonth() {
if(this.m == 12) {
return new Calendar(1, 1, ++this.y)
}
return new Calendar(++this.m, 1, this.y)
} getPreviousMonth() {
if(this.m == 1) {
return new Calendar(12, 1, --this.y)
}
return new Calendar(--this.m, 1, this.y)
}
}
What’s a modern calendar without the ability to navigate back and forth throughout the months?
Full Code
class Calendar {
constructor(m, d, y) {
if(typeof m === 'string') {
let temp = m.split("/")
this.m = parseInt(temp[0], 10)
this.d = parseInt(temp[1], 10)
this.y = parseInt(temp[2], 10)
} else {
this.m = m
this.d = d
this.y = y
}
this.today = new Date(`${this.m}/${this.d}/${this.y}`)
this.month = this.createMonth()
this.monthArr = this.getMonthArr(this.today) this.chopOffLeadingDays()
this.formatWeeks()
} getMonthArr(today) {
let month = []
let m = today.getDate()
let y = today.getUTCFullYear()
for(let i = (this.month.getTime() - 604800000); i < (3628800001 + this.today.getTime()); i += 86400000) {
let d = new Date(i)
if(isNaN(d)) return month
month.push(d)
}
return month
} createMonth() { /* Creates day 1 of the month */
return new Date(`${this.m}/1/${this.y}`)
} chopOffLeadingDays() { /* Chops off days leading up to sunday */
for(let i = 0; i < 7; i++) {
let index = this.monthArr.findIndex(d => d.getDay() === 0)
this.monthArr.splice(0, index)
}
} formatWeeks() { /* reformat monthArr to array of weeks of Dates */
let month = []
for(let i = 0; i < 6; i++) {
let week = this.monthArr.splice(0, 7)
month.push(week)
}
this.monthArr = month
} getMonthOfDays() { /* returns array of weeks of Ints */
return this.monthArr.map(w => w.map(d => d.getDate()))
} getSelectedDay(d) { /* set date format required */
return new Date(`${this.m}/${d}/${this.y}`)
} getNextMonth() {
if(this.m == 12) {
return new Calendar(1, 1, ++this.y)
}
return new Calendar(++this.m, 1, this.y)
} getPreviousMonth() {
if(this.m == 1) {
return new Calendar(12, 1, --this.y)
}
return new Calendar(--this.m, 1, this.y)
}
}// let m = new Calendar(2, 4, 2020)
/* OR */
let m = new Calendar("2/2/2018")
console.table(m.getMonthOfDays())
I added the option in our constructor to pass in either a date string or three params. Why not?!