Comments

Build a React Calendar Component from scratch

This post aims to explain, from scratch, to create a beautiful calendar with a date selector. You’ll be able to use it in any of your react application.

If you have gone through other tutorials for building a react calendar and struggled to follow along, you’re going to succeed here. Congratulations on getting to the right resource.

The beautiful calendar that you’ll create in next few minutes will look like the one embedded below. Of course, with your modifications.

Breakdown of the calendar tutorial

  1. Create a simple calendar component
  2. Create a table that uses a calendar
  3. Change month and year
  4. Get date value from React calendar on click

Use codesandbox for the quick environment, no setup required on your machine.

Draw the Date view with Table

Here’s the directory structure of this project.

The code for Calendar component:

import React from "react";

export default class Calendar extends React.Component {
  render() {
    return (
      <div>
        <h2>Calendar</h2>
      </div>
    );
  }
}

Include Calendar component to index.js

import React from "react";
import ReactDOM from "react-dom";

import Calendar from "./components/calendar";

function App() {
  return (
    <div className="App">
      <Calendar />
    </div>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

It must show Calendar heading if you included the component properly in the react calendar. Verify the output before continuing from here.

Display day abbreviation (Sun to Sat)

To display days, install moment.

Add moment to dependencies

Importmoment to calendar component.

import moment from 'moment'

Get short week day withmoment.weekdaysShort();.

weekdayshort = moment.weekdaysShort();

Map the day to a function that returns short day wrapped in <th> tag. The tag will contain a date short name.

let weekdayshortname = this.weekdayshort.map(day => {
   return (
     <th key={day} className="week-day">
      {day}
     </th>
   );
});

Grab the beautiful calendar CSS from tail.Datetimetail.Datetime can be used as an npm package or can be embedded using a CDN link (jsDelivr).

Finally, display in a table.

Locating the first weekday of a month

The month starts with 1, but which day does the first of this month fall on?

While creating a table style calendar, we need to know where to start with the number 1.

Is it a Monday or a Wednesday or a Friday?

Create a moment state variable.

state = {
        dateObject: moment()
    }

We need to store moment object to as the first day of the month. The state will be modified using an external function.

Create a getter function to retrieve the first weekday in a month.

firstDayOfMonth = () => {
    let dateObject = this.state.dateObject;
    let firstDay = moment(dateObject)
                 .startOf("month")
                 .format("d"); 
   return firstDay;
};

First, we get current moment object from state and store as a dateObject variable.

Then, we use moment function to get the first weekday of a month. The function returns this day.

Create a blank area before filling the first date of the month

Create a blank cell and start filling with the first date of the month in a render function

    let blanks = [];
    for (let i = 0; i < this.firstDayOfMonth(); i++) {
      blanks.push(
        <td className="calendar-day empty">{""}</td>
      );
    }

It generates an empty </td> with class empty if a counter is less than firstDayOfMonth.

Next, generate </td> of date in the month.

    let daysInMonth = [];
    for (let d = 1; d <= this.daysInMonth(); d++) {
      daysInMonth.push(
        <td key={d} className="calendar-day">
          {d}
        </td>
      );
    }

Now, there’s <td> with or without epmty class.

Define some more variables

  1. totalSlots contains blanks and daysInMonth using the spread operator. These variables have dynamic values.
  2. rows hold  </td>while going to a new row
  3. cells contain each </td> to assign to each row
    var totalSlots = [...blanks, ...daysInMonth];
    let rows = [];
    let cells = [];

Loop through totalSlots to get a calendar structure of a week.

    totalSlots.forEach((row, i) => {
      if (i % 7 !== 0) {
        cells.push(row); // if index not equal 7 that means not go to next week
      } else {
        rows.push(cells); // when reach next week we contain all td in last week to rows 
        cells = []; // empty container 
        cells.push(row); // in current loop we still push current row to new container
      }
      if (i === totalSlots.length - 1) { // when end loop we add remain date
        rows.push(cells);
      }
    });

Wrap all rows in a </td>

    let daysinmonth = rows.map((d, i) => {
      return <tr>{d}</tr>;
    });

and</td> in <tbody>

         <table className="calendar-day">
            <thead>
              <tr>{weekdayshortname}</tr>
            </thead>
            <tbody>{daysinmonth}</tbody>
          </table>

The result shows a valid month, valid as in first of the month falls on a correct day.

Highlighting the current day

Some a little detail, such as highlighting the current day, can make things convenient.

Find the current day.

currentDay = () => {  
     return this.state.dateObject.format("D");
};

Add a conditional operator to check if the day is a current day. If it is a current day, add a classtoday while pushing to the daysInMonth.

let daysInMonth = [];
    for (let d = 1; d <= this.daysInMonth(); d++){ 
        let currentDay = d == this.currentDay() ? "today" : "";   
            daysInMonth.push(  
               <td key={d} className={`calendar-day ${currentDay}`}>   
                 {d} 
               </td>);
     }
}

The result highlights the current day.

Create a month picker

The month shows, but which month it is? We’re missing the month name.

Show the current month name on top of the calendar.

After calendar table div insert month picker div.

return (
      <div className="tail-datetime-calendar">
         <div className="calendar-navi">
      </div>
)

Insert the current month name with  momentObject.

Create a getter function to do it.

  month = () => {
    return this.state.dateObject.format("MMMM");
  };

Show the month in the div.

return (
      <div className="tail-datetime-calendar">
        <div className="calendar-navi">
           {this.month()}
        </div>
)

You will see the name of a current month in the calendar.

It’s too ugly, so add CSS class from tail.DateTime.

<span data-tail-navi="switch" class="calendar-label">
       {this.month()}
 </span>

There’s no month picker, the same month name displays there forever.

Create a function to render the month table

Get all months from an momentobject and assign it to allMonths state

state = {
  dateObject: moment(),
  allmonths :moment.months()
};

Create a function to handle this month table

MonthList = props => {}

props are the selected month object (Jan-Dec).

    let months = [];
    props.data.map(data => {
      months.push(
        <td>
          <span>{data}</span>
        </td>
      );
    });

Create a list of cells that contain a month name. Define rows to store td while going through rows.

    let rows = [];
    let cells = [];

For table, grab condition from calendar table

months.forEach((row, i) => { 
   if (i % 3 !== 0 || i == 0) { // except zero index 
       cells.push(row); 
   } else { 
       rows.push(cells); 
       cells = [];
       cells.push(row); 
   }
});
rows.push(cells); // add last row

Map over the rows and wrap in<tr>.

let monthlist = rows.map((d, i) => {
   return <tr>{d}</tr>;
});

There’s a table, put it in a <tbody>. Return a proper table in HTML, styled with some tail.Datetime css.

return (
      <table className="calendar-month">
        <thead>
          <tr>
            <th colSpan="4">Select a Month</th>
          </tr>
        </thead>
        <tbody>{monthlist}</tbody>
      </table>
    );

Just render it in the parent.

<div className="calendar-date">
   <this.MonthList data={moment.months()} />
</div>

The result is a month selector.

Changing the Month

When clicked on month selector, the selector bar in month selector should change the month name to the selected month.

Create a setMonth function.

setMonth = month => {
    let monthNo = this.months.indexOf(month);// get month number 
    let dateObject = Object.assign({}, this.state.dateObject);
    dateObject = moment(dateObject).set("month", monthNo); // change month value
    this.setState({
      dateObject: dateObject // add to state
    });
  };

Add it to <td>

props.data.map(data => {
      months.push(
        <td
          key={data}
          className="calendar-month"
          onClick={e => {
            this.setMonth(data);
          }}
        >
          <span>{data}</span>
        </td>
      );
    });

Click on any month, and it should appear as in the picture below.

Hide the month picker after selecting a month

Once a different month is selected, its job is done. It should hide a month table.

To handle displaying month picker, add a showMonthTable state.

state = {
   showMonthTable:false,  
   dateObject: moment(),  
   allmonths: moment.months()
};

To hide on first t load, add simple check state in the calendar-date div.

<div className="calendar-date">  
   {this.state.showMonthTable &&  
   < this.MonthList data = {moment.months()} />}
</div>

The month table will be hidden.

Create a function showTable to display month selector table when clicked on month name.

<div className="calendar-navi" 
     onClick={e => {
        this.showMonth();
     }}
>

Define action for toggle the state.

showMonth = (e, month) => {   
   this.setState({  
      showMonthTable: !this.state.showMonthTable   
   });
};

Every click will toggle the state, so it will be used to show and hide.

A calendar is not hiding when month-selector is displayed. Only one should be visible at a time, this is expected behavior in a calendar.

{ !this.state.showMonthTable && (
   <div className="calendar-date">
     <table className="calendar-day">
        <thead>
         <tr>{weekdayshortname}</tr>
        </thead>
        <tbody>{daysinmonth}</tbody>
      </table>
</div>)}

Wrap calendar view with !showMonthTable, this will show calendar when showMonthTable is false.

 

The calendar shows the same data for any month. Set appropriate dateObject while changing the month.

setMonth = month => {
    let monthNo = this.months.indexOf(month);// get month number 
    let dateObject = Object.assign({}, this.state.dateObject);
    dateObject = moment(dateObject).set("month", monthNo); // change month value
    this.setState({
      dateObject: dateObject // add to state
      showMonthTable: !this.state.showMonthTable
    });
  };

The result displays a different calendar for each month.

 Year Picker

Using the same pattern of months, create a year picker and select month.

Show the current year on side of the month name.

Create a getter function to get a year from dateObject.

year = () => {    
   return this.state.dateObject.format("Y");
};

Display it as

<span className="calendar-label">
   {this.year()}
</span>

The result displays the year on the right of the month.

Create a year table

After clicking on the year, it should show a year table to select from.

  YearTable = props => {
    let months = [];
    let nextten = moment()
      .set("year", props)
      .add("year", 12)
      .format("Y");

Receive current year and create next 12 to create year range.

To create a year range, create a function named getDates.

getDates(startDate, stopDate) {
    var dateArray = [];
    var currentDate = moment(startDate);
    var stopDate = moment(stopDate);
    while (currentDate <= stopDate) {
      dateArray.push(moment(currentDate).format("YYYY"));
      currentDate = moment(currentDate).add(1, "year");
    }
    return dateArray;
  }

Send start and end range.

let twelveyears = this.getDates(props, nextten);

Get the next twelve years’ object and use it to create year table.

twelveyears.map(data => {
      months.push(
        <td
          key={data}
          className="calendar-month"
          onClick={e => {
            this.setYear(data);
          }}
        >
          <span>{data}</span>
        </td>
      );
    });
    let rows = [];
    let cells = [];

    months.forEach((row, i) => {
      if (i % 3 !== 0 || i == 0) {
        cells.push(row);
      } else {
        rows.push(cells);
        cells = [];
        cells.push(row);
      }
    });
    rows.push(cells);
    let yearlist = rows.map((d, i) => {
      return <tr>{d}</tr>;
    });

    return (
      <table className="calendar-month">
        <thead>
          <tr>
            <th colSpan="4">Select a Yeah</th>
          </tr>
        </thead>
        <tbody>{yearlist}</tbody>
      </table>
    );
  };

It’s the same function used in the month table, setMonth changed to setYear to trigger the year selected.

Now, display the year in calendar view.

<div className="calendar-date">
         <this.YearTable props={this.year()} />
          {this.state.showMonthTable && (
            <this.MonthList data={moment.months()} />
          )}
</div>

This results in  year selector.

Change state when the selected year changes.

setYear = year => {
    // alert(year)
    let dateObject = Object.assign({}, this.state.dateObject);
    dateObject = moment(dateObject).set("year", year);
    this.setState({
      dateObject: dateObject
    });
  };

It’s same moment object in month, parameter being year this time.

This selects the year.

It works but hides year selector after it’s selected. Use showYearTable state to display and hide the year table.

state = {
  showYearTable: false,
  showMonthTable: false,
  showDateTable: true,
  dateObject: moment(),
   allmonths: moment.months(),
};

Apply this state to the view calendar table only, hiding the year table.

  <div className="calendar-date"> 
   {this.state.showYearTable && (
      <this.YearTable props={this.year()} /> 
    )}
  { this.state.showMonthTable && (
    <this.MonthList data={moment.months()}
   /> 
  )}</div>
  {this.state.showDateTable && (
   <div className="calendar-date">
    <table className="calendar-day">
      <thead>
       <tr>{weekdayshortname}</tr>
      </thead>
     <tbody>{daysinmonth}</tbody>
    </table>
  </div>
)}

Year table is now hidden in this react calendar.

While clicking on a year, date table should hide and year table should be shown.

Create a function named showYearTable and toggle state in year and date view.

showYearTable = (e) => {
  this.setState({
      showYearTable: !this.state.showYearTable,
      showDateTable: !this.state.showDateTable
  });
};

Add onClick event to year label

<span className="calendar-label" onClick={(e)=>this.showYearTable()} >

This results in proper year selector.

Show month table after selecting a year

Toggle the state from setYear function.

setYear = year => {
    let dateObject = Object.assign({}, this.state.dateObject);
    dateObject = moment(dateObject).set("year", year);
    this.setState({
      dateObject: dateObject,
      showMonthTable: !this.state.showMonthTable,
      showYearTable: !this.state.showYearTable });
};

It results in this react calendar displaying month selector after selecting a year.

Set date after changing the month

After selecting year and month, we need to change date view.

setMonth = month => {
    let monthNo = this.state.allmonths.indexOf(month);
    let dateObject = Object.assign({}, this.state.dateObject);
    dateObject = moment(dateObject).set("month", monthNo);
    this.setState({
      dateObject: dateObject,
      showMonthTable: !this.state.showMonthTable,
      showDateTable: !this.state.showDateTable
    });
  };

Toggle the state variable and the result is below.

Adding Next and Previous buttons

Add a previous button

<span  onClick={e => {
           this.onPrev();
        }}
   class="calendar-button button-prev"
 />

Also, add the next button

<span onClick={e => {
         this.onNext();
      }}
       className="calendar-button button-next"
 />

Handle it with an empty function to prevent any error.

onPrev = () => {};
onNext = () => {};

will show the result

The code below handles next and previous month and year

onPrev = () => {
    let curr = "";
    if (this.state.showYeahTable == true) {
      curr = "year";
    } else {
      curr = "month";
    }
    this.setState({
      dateObject: this.state.dateObject.subtract(1, curr)
    });
  };
onNext = () => {
    let curr = "";
    if (this.state.showYeahTable == true) {
      curr = "year";
    } else {
      curr = "month";
    }
    this.setState({
      dateObject: this.state.dateObject.add(1, curr)
    });
  };

that result

Grab the selected cell

for (let d = 1; d <= this.daysInMonth(); d++) {
    let currentDay = d == this.currentDay() ? "today" : "";
      daysInMonth.push(
        <td key={d} className={`calendar-day ${currentDay}`}>
           <span onClick={e => { 
             this.onDayClick(e, d);
           }} >
              {d}
           </span>
        </td>
     );
   }

Pass the current d value upon onDayClick.

onDayClick = (e, d) => { 
    this.setState({
      selectedDay: d
    },
    () => {
       console.log("SELECTED DAY: ", this.state.selectedDay);
    }
   );
};

The selectedDay is a state of a selected day on the date picker. Use this state to get the selected date.

React Calendar is done, but it’s not a typical tutorial, so here’s some homework for you.

You have seen the components, there are things to do with it.

  • Separate each picker to a component
  • Add a decade picker
  • Make previous and next work on year table
  • Transform it to a Datepicker
  • Make year picker dynamic
  • Improve the UX
  • Make it translatable

Send your pull requests here.

Want to learn React through fast-track route and in a much entertaining way? Check out Mosh’s Complete React Course.

 

Krissanawat is Nomad Web developer live in Chiangmai passionate on React and Laravel
Tags: ,

Leave a Reply

Connect with Me
  • Categories
  • Popular Posts