Sep 22, 20192 min read

Create Calendar Matrix for Datepicker Backbone with Javascript, React, React Hooks, and date-fns library

When building webapp, I often need a datepicker functionality. And I find myself too often reaching for library like jQuery UI. But It’s hard to tweak those library to suits your use case. Sometimes you just need to add a really specific functionality to your datepicker. Also, even if it’s just to tweak the styles and interface of the datepicker, I ended up with overriding a lots of CSS and forced to use !important to make it works. That’s spooky!

Wouldn’t it be super convenient if there’s JUST a backbone for a datepicker component that you can use to create your own datepicker component, using your own CSS or design sytem, and add functionality as you need it?

Well, currenly I need one and I’ve never learned to make one myself. So for the sake of learning, let’s give it a try then.

Updates

  1. If you’re only interested in the final result, you can check the repository here https://github.com/vasilenka/use-calendar-matrix.
  2. I’ve transformed into a React Hook and published it as an NPM package, you can install the package by doing
$_
npm install use-calendar-matrix
# or if you prefer yarn
yarn add use-calendar-matrix
npm install use-calendar-matrix

# or if you prefer yarn
yarn add use-calendar-matrix

The Goals

What we want to achieve is something like, given the month and year, we want to generate the matrix of days in that specific month and year. And later we can use that matrix to create a datepicker component, showing a calendar, etc.

We’ll try to achieve that with the help of date-fns. Some kind of lodash for manipulating JavaScript dates in a browser & Node.js.

The Code

JS
import {
addDays,
startOfWeek,
differenceInCalendarWeeks,
endOfMonth,
startOfMonth,
format
} from "date-fns";
function generateCalendarMatrix(year, month, weekStartsOn = 1) {
// 1. Generate the date from params, then get the firstDay and lastDay in the month
let date = new Date(year, month);
let firstDay = startOfMonth(date);
let lastDay = endOfMonth(date);
// 2. Get the start date for our matrix
const startDate = startOfWeek(date, { weekStartsOn });
// 3. Get the differences in weeks from lastDay to firstDay
// Add (+1) to get total row we need for the matrix to cover all the days in the month
// It'll be used as total rows needed for our matrix
const matrixRows =
differenceInCalendarWeeks(lastDay, firstDay, { weekStartsOn }) + 1;
// 4. Set the number of days in a week.
// It'll be used as total columns needed for our matrix
const matrixColumns = 7;
// 5. Get the total days that we are going to generate.
const totalDays = matrixRows * matrixColumns;
// Preparations complete! Let's generate the calendar matrix
let calendar =
// 6. Generate an empty Array from the totalDays
Array.from({ length: totalDays });
// 7. Assign a Date value to each value of the array
// We'll get an array with each value is a Date value
.map((_, index) => addDays(startDate, index))
// 8. use Array.reduce to transform our array for each week
// We want to cut the array at the beginning of each week
.reduce((matrix, current, index, days) =>
index % matrixColumns === 0
? [...matrix, days.slice(index, index + matrixColumns)]
: matrix,
[]
);
return calendar
};
console.log(generateCalendarMatrix(2019, 8))
import {
  addDays,
  startOfWeek,
  differenceInCalendarWeeks,
  endOfMonth,
  startOfMonth,
  format
} from "date-fns";

function generateCalendarMatrix(year, month, weekStartsOn = 1) {
  //  1. Generate the date from params, then get the firstDay and lastDay in the month
  let date = new Date(year, month);
  let firstDay = startOfMonth(date);
  let lastDay = endOfMonth(date);

  //  2. Get the start date for our matrix
  const startDate = startOfWeek(date, { weekStartsOn });

  //  3. Get the differences in weeks from lastDay to firstDay
  //  Add (+1) to get total row we need for the matrix to cover all the days in the month
  //  It'll be used as total rows needed for our matrix
  const matrixRows =
    differenceInCalendarWeeks(lastDay, firstDay, { weekStartsOn }) + 1;

  //  4. Set the number of days in a week.
  //  It'll be used as total columns needed for our matrix
  const matrixColumns = 7;

  //  5. Get the total days that we are going to generate.
  const totalDays = matrixRows * matrixColumns;

  //  Preparations complete! Let's generate the calendar matrix

  let calendar =
    //  6. Generate an empty Array from the totalDays
    Array.from({ length: totalDays });
    //  7. Assign a Date value to each value of the array
    //  We'll get an array with each value is a Date value
    .map((_, index) => addDays(startDate, index))
    //  8. use Array.reduce to transform our array for each week
    //  We want to cut the array at the beginning of each week
    .reduce((matrix, current, index, days) =>
      index % matrixColumns === 0
        ? [...matrix, days.slice(index, index + matrixColumns)]
        : matrix,
      []
    );

  return calendar

};

console.log(generateCalendarMatrix(2019, 8))

Note

  1. Please note that Javascript month start from 0 to 11, so for example if you want to generate for September, you’ll assign 8 as the month value.
  2. Also date-fns weekStartsOn start from 0 to 6, with 0 for sunday and 6 is saturday.

Make it into a React Hook

To make it even better, we can transform the code into a React Hook. So you can just supply the year and month value, and it will return a calendar matrix that will automatically updated when the year and month value changed.

Here’s the code for the React Hook

JS
import { useEffect, useState } from "react"
import {
addDays,
startOfWeek,
differenceInCalendarWeeks,
endOfMonth,
startOfMonth,
} from "date-fns"
function generateCalendarMatrix(year, month, weekStartsOn = 1) {
let date = new Date(year, month);
let firstDay = startOfMonth(date);
let lastDay = endOfMonth(date);
const startDate = startOfWeek(date, { weekStartsOn });
const matrixRows =
differenceInCalendarWeeks(lastDay, firstDay, { weekStartsOn }) + 1;
const matrixColumns = 7;
const totalDays = matrixRows * matrixColumns;
let calendar =
Array.from({ length: totalDays });
.map((_, index) => addDays(startDate, index))
.reduce((matrix, current, index, days) =>
index % matrixColumns === 0
? [...matrix, days.slice(index, index + matrixColumns)]
: matrix,
[]
);
return calendar
};
export const useCalendarMatrix = (year, month, weekStartsOn = 1) => {
let [matrix, setMatrix] = useState(
generateCalendarMatrix(year, month, weekStartsOn)
)
useEffect(() => {
setMatrix(generateCalendarMatrix(year, month, weekStartsOn))
}, [year, month])
return [ matrix ]
}
import { useEffect, useState } from "react"
import {
  addDays,
  startOfWeek,
  differenceInCalendarWeeks,
  endOfMonth,
  startOfMonth,
} from "date-fns"

function generateCalendarMatrix(year, month, weekStartsOn = 1) {
  let date = new Date(year, month);
  let firstDay = startOfMonth(date);
  let lastDay = endOfMonth(date);

  const startDate = startOfWeek(date, { weekStartsOn });
  const matrixRows =
    differenceInCalendarWeeks(lastDay, firstDay, { weekStartsOn }) + 1;
  const matrixColumns = 7;
  const totalDays = matrixRows * matrixColumns;

  let calendar =
    Array.from({ length: totalDays });
    .map((_, index) => addDays(startDate, index))
    .reduce((matrix, current, index, days) =>
      index % matrixColumns === 0
        ? [...matrix, days.slice(index, index + matrixColumns)]
        : matrix,
      []
    );

  return calendar

};

export const useCalendarMatrix = (year, month, weekStartsOn = 1) => {
  let [matrix, setMatrix] = useState(
    generateCalendarMatrix(year, month, weekStartsOn)
  )

  useEffect(() => {
    setMatrix(generateCalendarMatrix(year, month, weekStartsOn))
  }, [year, month])

  return [ matrix ]
}

and then used it your component like this:

JS
import React from "react"
import useCalendarMatrix from "use-calendar-matrix"
const MyCalendar = () => {
let [matrix] = useCalendarMatrix(2019, 8)
return (
<ul>
{matrix.map((week, index) => (
<ul>
{week.map((day, i) => (
<li>{day}</li>
))}
</ul>
))}
</ul>
)
}
import React from "react"
import useCalendarMatrix from "use-calendar-matrix"

const MyCalendar = () => {
  let [matrix] = useCalendarMatrix(2019, 8)

  return (
    <ul>
      {matrix.map((week, index) => (
        <ul>
          {week.map((day, i) => (
            <li>{day}</li>
          ))}
        </ul>
      ))}
    </ul>
  )
}

In the component above, we’re trying to generate a calendar matrix for September 2019 and after applying some styling to the value, you’ll get yourself a calendar matrix like this:

September 2019

  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

With the calendar matrix, you can do whatever you want and add any functionality you need for your use case. For example, add a next and prev month button, or return a value whenever the day in calendar matrix receive a click event.

Good luck!

References

  1. Published package ready to use
  2. Gist as the base for this matrix
  3. More about Array.reduce
  4. More about Array.slice
  5. date-fns