DEV Community

Cover image for Building a simple Date Picker with React and Day.js
Oluwadahunsi A.
Oluwadahunsi A.

Posted on

Building a simple Date Picker with React and Day.js

Welcome!

This is the second part of our three-part tutorial on creating a simple calendar using React and Day.js. In the first part, we built a custom calendar with React and Day.js.

You can check the first and the last parts here:

The first part:

The last part:

In this part, we'll enhance our calendar from the first part by adding a date picker functionality. Our goal is to build the date picker below.

A custom date picker

Starter files.

If you have not gone through the first part where we built a basic calendar, don't worry. I'm providing all the necessary files so we can start together from the same point.



//style

.calendar__container {
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 25px;
  width: max-content;
  background: #ffffff;
  box-shadow: 5px 10px 10px #dedfe2;
}

.month-year__layout {
  display: flex;
  margin: 0 auto;
  width: 100%;
  flex-direction: row;
  align-items: center;
  justify-content: space-around;
}

.year__layout,
.month__layout {
  width: 150px;
  display: flex;
  padding: 10px;
  font-weight: 600;
  align-items: center;
  text-transform: capitalize;
  justify-content: space-between;
}

.back__arrow,
.forward__arrow {
  cursor: pointer;
  background: transparent;
  border: none;
}

.back__arrow:hover,
.forward__arrow:hover {
  scale: 1.1;
  transition: scale 0.3s;
}

.days {
  display: grid;
  grid-gap: 0;
  width: 100%;
  grid-template-columns: repeat(7, 1fr);
}

.day {
  flex: 1;
  font-size: 16px;
  padding: 5px 7px;
  text-align: center;
}

.calendar__content {
  position: relative;
  background-color: transparent;
}

.calendar__items-list {
  text-align: center;
  width: 100%;
  height: max-content;
  overflow: hidden;
  display: grid;
  grid-gap: 0;
  list-style-type: none;
  grid-template-columns: repeat(7, 1fr);
}

.calendar__items-list:focus {
  outline: none;
}

.calendar__day {
  position: relative;
  display: flex;
  justify-content: center;
  align-items: center;
}

.calendar__item {
  position: relative;
  width: 50px;
  height: 50px;
  cursor: pointer;
  background: transparent;
  border-collapse: collapse;
  background-color: white;

  display: flex;
  justify-content: center;
  align-items: center;
  text-align: center;
  border: 1px solid transparent;
  z-index: 200;
}

button {
  margin: 0;
  display: inline;
  box-sizing: border-box;
}

.calendar__item:focus {
  outline: none;
}

.calendar__item.selected {
  font-weight: 700;
  border-radius: 50%;
  background: #1a73e8;
  color: white;
  outline: none;
  border: none;
}

.calendar__item.selectDay {
  position: relative;
  background: #1a73e8;
  color: white;
  border-radius: 50%;
  border: none;
  z-index: 200;
}

.calendar__item.gray,
.calendar__item.gray:hover {
  color: #c4cee5;
  display: flex;
  justify-content: center;
  align-items: center;
}

.input__container {
  display: flex;
  justify-content: space-around;
}

.input {
  height: 30px;
  border-radius: 8px;
  text-align: center;
  align-self: center;
  border: 1px solid #1a73e8;
}

.shadow {
  position: absolute;
  display: inline-block;
  z-index: 10;
  top: 0;
  background-color: #f4f6fa;
  height: 50px;
  width: 50px;
}

.shadow.right {
  left: 50%;
}

.shadow.left {
  right: 50%;
}




Enter fullscreen mode Exit fullscreen mode



//Calendar.tsx

import dayjs, { Dayjs } from 'dayjs';
import backArrow from '../assets/images/back.svg';
import forwardArrow from '../assets/images/forward.svg';
import './style.css';
import { useState } from 'react';
import { calendarObjectGenerator } from '../helper/calendarObjectGenerator';

const weekDays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thur', 'Fri', 'Sat'];

export const Calendar = () => {
  const [currentDate, setCurrentDate] = useState<Dayjs>(dayjs(Date.now()));

  const daysListGenerator = calendarObjectGenerator(currentDate);

  const dateArrowHandler = (date: Dayjs) => {
    setCurrentDate(date);
  };

  const handlePreviousMonthClick = (day: number) => {
    const dayInPreviousMonth = currentDate.subtract(1, 'month').date(day);
    setCurrentDate(dayInPreviousMonth);
  };

  const handleCurrentMonthClick = (day: number) => {
    const dayInCurrentMonth = currentDate.date(day);
    setCurrentDate(dayInCurrentMonth);
  };

  const handleNextMonthClick = (day: number) => {
    const dayInNextMonth = currentDate.add(1, 'month').date(day);
    setCurrentDate(dayInNextMonth);
  };

  return (
    <div className='calendar__container'>
      <div className='control__layer'>
        <div className='month-year__layout'>
          <div className='year__layout'>
            <button
              className='back__arrow'
              onClick={() => dateArrowHandler(currentDate.subtract(1, 'year'))}
            >
              <img src={backArrow} alt='back arrow' />
            </button>
            <div className='title'>{currentDate.year()}</div>
            <button
              className='forward__arrow'
              onClick={() => dateArrowHandler(currentDate.add(1, 'year'))}
            >
              <img src={forwardArrow} alt='forward arrow' />
            </button>
          </div>
          <div className='month__layout'>
            <button
              className='back__arrow'
              onClick={() => dateArrowHandler(currentDate.subtract(1, 'month'))}
            >
              <img src={backArrow} alt='back arrow' />
            </button>
            <div className='new-title'>
              {daysListGenerator.months[currentDate.month()]}
            </div>
            <button
              className='forward__arrow'
              onClick={() => dateArrowHandler(currentDate.add(1, 'month'))}
            >
              <img src={forwardArrow} alt='forward arrow' />
            </button>
          </div>
        </div>
        <div className='days'>
          {weekDays.map((el, index) => (
            <div key={`${el}-${index}`} className='day'>
              {el}
            </div>
          ))}
        </div>
        <div className='calendar__content'>
          <div className={'calendar__items-list'}>
            {daysListGenerator.prevMonthDays.map((el, index) => {
              return (
                <button
                  key={`${el}/${index}`}
                  className='calendar__item gray'
                  onClick={() => handlePreviousMonthClick(el)}
                >
                  {el}
                </button>
              );
            })}
            {daysListGenerator.days.map((el, index) => {
              return (
                <div
                  key={`${index}-/-${el}`}
                  className='calendar__day'
                  onClick={() => handleCurrentMonthClick(el)}
                >
                  <button
                    className={`calendar__item 
                      ${+el === +daysListGenerator.day ? 'selected' : ''}`}
                  >
                    <div className='day__layout'>
                      <div className='text'>{el.toString()}</div>
                    </div>
                  </button>
                </div>
              );
            })}

            {daysListGenerator.remainingDays.map((el, idx) => {
              return (
                <button
                  className='calendar__item gray'
                  key={`${idx}----${el}`}
                  onClick={() => handleNextMonthClick(el)}
                >
                  {el}
                </button>
              );
            })}
          </div>
        </div>
      </div>
    </div>
  );
};




Enter fullscreen mode Exit fullscreen mode


//back.svg
<?xml version="1.0" encoding="utf-8"?>
<svg width="20px" height="20px" viewBox="0 0 1000 1000" class="icon"  version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M768 903.232l-50.432 56.768L256 512l461.568-448 50.432 56.768L364.928 512z" fill="#000000" /></svg>


Enter fullscreen mode Exit fullscreen mode


//forward.svg
<?xml version="1.0" encoding="utf-8"?>
<svg width="20px" height="20px" viewBox="0 0 1000 1000" class="icon"  version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M256 120.768L306.432 64 768 512l-461.568 448L256 903.232 659.072 512z" fill="#000000" /></svg>


Enter fullscreen mode Exit fullscreen mode

The helper function for generating the days in our calendar.




//calendarObjectGenerator.tsx

import dayjs, { Dayjs } from 'dayjs';

import LocaleData from 'dayjs/plugin/localeData';

dayjs.extend(LocaleData);

type GeneratedObjectType = {
  prevMonthDays: number[];
  days: number[];
  remainingDays: number[];
  day: number;
  months: string[];
};

export const calendarObjectGenerator = (
  currentDate: Dayjs
): GeneratedObjectType => {
  const numOfDaysInPrevMonth = currentDate.subtract(1, 'month').daysInMonth();
  const firstDayOfCurrentMonth = currentDate.startOf('month').day();
  return {
    days: Array.from(
      { length: currentDate.daysInMonth() },
      (_, index) => index + 1
    ),
    day: Number(currentDate.format('DD')),
    months: currentDate.localeData().months(),

    prevMonthDays: Array.from(
      { length: firstDayOfCurrentMonth },
      (_, index) => numOfDaysInPrevMonth - index
    ).reverse(),

    remainingDays: Array.from(
      { length: 6 - currentDate.endOf('month').day() },
      (_, index) => index + 1
    ),
  };
};




Enter fullscreen mode Exit fullscreen mode



//App.tsx

import { Calendar } from './Calendar/Calendar';

function App() {
  return (
    <>
      <Calendar />
    </>
  );
}

export default App;




Enter fullscreen mode Exit fullscreen mode

Adding an input field

To create a single date picker, we need to add an input field to our calender.

Ideally, we should use a masked input to ensure that only a valid date format is entered. Although we're currently checking if the entry is at least 10 characters to satisfy the DD.MM.YYYY format, this alone isn't enough to ensure validity. Implementing a masked input will enforce the exact format we require.

Also, because we will be changing date formats often, let us add the customParseFormat plugin from dayjs. Here is the updated Calendar.tsx file.




//Calendar.tsx

import dayjs, { Dayjs } from 'dayjs';
import customParseFormat from 'dayjs/plugin/customParseFormat'; // add line
import backArrow from '../assets/images/back.svg';
import forwardArrow from '../assets/images/forward.svg';
import { useState } from 'react';
import { calendarObjectGenerator } from '../helper/calendarObjectGenerator';
import './style.css';

dayjs.extend(customParseFormat); // add line

const weekDays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thur', 'Fri', 'Sat'];

export const Calendar = () => {
  const [currentDate, setCurrentDate] = useState<Dayjs>(dayjs(Date.now()));
  const [inputValue, setInputValue] = useState<string>('');

  const daysListGenerator = calendarObjectGenerator(currentDate);

  const dateArrowHandler = (date: Dayjs) => {
    setCurrentDate(date);
  };

  const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    const date = event.target.value;

    setInputValue(date);

    // check if the entered date is valid.
    const isValidDate = dayjs(date, 'DD.MM.YYYY').isValid();

    if (!isValidDate || date.length < 10) return;

    //if you pass date without specifying the format ('DD.MM.YYYY'), 
    // you might get an error when you decide to edit a selected date in the input field.

    setCurrentDate(dayjs(date, 'DD.MM.YYYY'));
  };



  const handlePreviousMonthClick = (day: number) => {

    const dayInPreviousMonth = currentDate.subtract(1, 'month').date(day);

    setCurrentDate(dayInPreviousMonth);

    setInputValue(dayInPreviousMonth.format('DD.MM.YYYY')); // add line
  };


   const handleCurrentMonthClick = (day: number) => {

    const dayInCurrentMonth = currentDate.date(day);

    setCurrentDate(dayInCurrentMonth);

    setInputValue(dayInCurrentMonth.format('DD.MM.YYYY')); // add line
  };


  const handleNextMonthClick = (day: number) => {

    const dayInNextMonth = currentDate.add(1, 'month').date(day);

    setCurrentDate(dayInNextMonth);

    setInputValue(dayInNextMonth.format('DD.MM.YYYY'));  // add line
  };



  return (
    <div className='calendar__container'>
      <input
        className='input'
        value={inputValue}
        onChange={handleInputChange}
      />
      <div className='control__layer'>
        <div className='month-year__layout'>
          <div className='year__layout'>
            <button
              className='back__arrow'
              onClick={() => dateArrowHandler(currentDate.subtract(1, 'year'))}
            >
              <img src={backArrow} alt='back arrow' />
            </button>
            <div className='title'>{currentDate.year()}</div>
            <button
              className='forward__arrow'
              onClick={() => dateArrowHandler(currentDate.add(1, 'year'))}
            >
              <img src={forwardArrow} alt='forward arrow' />
            </button>
          </div>
          <div className='month__layout'>
            <button
              className='back__arrow'
              onClick={() => dateArrowHandler(currentDate.subtract(1, 'month'))}
            >
              <img src={backArrow} alt='back arrow' />
            </button>
            <div className='new-title'>
              {daysListGenerator.months[currentDate.month()]}
            </div>
            <button
              className='forward__arrow'
              onClick={() => dateArrowHandler(currentDate.add(1, 'month'))}
            >
              <img src={forwardArrow} alt='forward arrow' />
            </button>
          </div>
        </div>
        <div className='days'>
          {weekDays.map((el, index) => (
            <div key={`${el}-${index}`} className='day'>
              {el}
            </div>
          ))}
        </div>
        <div className='calendar__content'>
          <div className={'calendar__items-list'}>
            {daysListGenerator.prevMonthDays.map((el, index) => {
              return (
               <div 
                key={`${el}/${index}`}
                className='calendar__day'
                //add this line
                onClick={() => handlePreviousMonthClick(el)}
                > 
                <button
                  className='calendar__item gray'
                                    >
                  {el}
                </button>
               </div>
              );
            })}
            {daysListGenerator.days.map((el, index) => {
              return (
                <div 
                key={`${index}-/-${el}`} 
                className='calendar__day' 
                //add this line
                onClick={() => handleCurrentMonthClick(el)}
                                    >
                  <button
                    className={`calendar__item 
                      ${+el === +daysListGenerator.day ? 'selected' : ''}`}
                  >
                    <div className='day__layout'>
                      <div className='text'>{el.toString()}</div>
                    </div>
                  </button>
                </div>
              );
            })}

            {daysListGenerator.remainingDays.map((el, idx) => {
              return (
                <div 
                 key={`${idx}----${el}`}
                className='calendar__day'
                //add this line 
                onClick={() => handleNextMonthClick(el)}
                > 
                <button 
                className='calendar__item gray' 
                >
                  {el}
                </button>
               </div>
              );
            })}
          </div>
        </div>
      </div>
    </div>
  );
};



Enter fullscreen mode Exit fullscreen mode

At this point, you should have something similar to this:

date picker

There you have it, a simple date picker built upon our Calender. In the concluding part, we are going to be building a date range picker on this date picker.

See you in the next one.

Top comments (0)