Glad Chinda Full-stack web developer learning new hacks one day at a time. Web technology enthusiast. Hacking stuffs @theflutterwave.

How to build a custom date picker with React

16 min read 4571

Original Photo by rawpixel on Unsplash

It is very common to see forms on the web that include one or more date fields. Whether it is a person’s date of birth that is required or a flight schedule date, you always want to be sure that the user supplies a valid date.

In HTML5, a new date input type was introduced to ensure better ways of getting valid date values in forms. The default behavior of the date input type is to present a date picker to the user. However, the appearance of this date picker is not consistent across browsers.

You can find out more about the date input type and browser support here.

In this tutorial, you will learn how to build a custom date picker from scratch with React, using native JavaScript Date objects. Here is a short demo showing what the date picker will look like.

You can also get a live demo on Code Sandbox.

react-datepicker

The online code editor tailored for web applications

Prerequisites

This tutorial assumes that you are pretty comfortable working with JavaScript and are already familiar with the React framework. If that’s not the case, you can checkout the React documentation to learn more about React.

Before getting started, you need to ensure that you have Node already installed on your machine. It is recommended that you install the Yarn package manager on your machine, since it will be used instead of npm that ships with Node. You can follow this Yarn installation guide to install Yarn on your machine.

The boilerplate code for the React app will be created using the create-react-app package. You also need to ensure that it is installed globally on your machine. If you are using npm >= 5.2 then you don’t need to install create-react-app as a global dependency — you can use the npx command instead.

Getting started

Create new Application

Start a new React application using the following command. You can name the application whatever you like.

create-react-app react-datepicker

npm >= 5.2

If you are using npm version 5.2 or higher, it ships with an additional npx binary. Using the npx binary, you don’t need to install create-react-app globally on your machine. You can start a new React application with this simple command:

npx create-react-app react-datepicker

Install Dependencies

The dependencies for this application are kept as lean as possible. Run the following command to install the required dependencies.

yarn add bootstrap reactstrap styled-components prop-types

Include Bootstrap CSS

The bootstrap package has been installed as a dependency for the application to provide some default styling. To include the Bootstrap 4 styles, edit the src/index.js file and add the following line before every other import statement.

import "bootstrap/dist/css/bootstrap.min.css";

Directory setup

For this application, two major components are required.

  1. The Calendar component, which renders the custom calendar with date selection functionality.
  2. The Datepicker component, which renders a date input and presents the calendar for picking date.

Each of these components will be contained in its own directory with two files — index.js and styles.js. The index.js file exports the component while the styles.js file exports the styled components required by the component to add some styling.

Go ahead and run the following commands from your project root to create the component directories and files:

# Create directories
mkdir -p src/components/Calendar src/components/Datepicker

# Create files
(cd src/components/Calendar && touch index.js styles.js)
(cd src/components/Datepicker && touch index.js styles.js)

Since no external dependency will be required for handling dates in this application, there is need for date handling helper functions. Run the following commands to create a file for the calendar helper module.

mkdir -p src/helpers
touch src/helpers/calendar.js

Start the application

Start the application by running the following command on your terminal with yarn:

yarn start

The application is now started and development can begin. Notice that a browser tab has been opened for you with live reloading functionality to keep in sync with changes in the application as you develop.

Calendar helpers module

Basic constants and helpers

First off, define some calendar helpers and constants that will be needed to build the calendar. These helpers and constants will be defined in the calendar helper module you created earlier and will be exported as named exports.

Add the following content to the src/helpers/calendar.js file.

// (int) The current year
export const THIS_YEAR = +(new Date().getFullYear());

// (int) The current month starting from 1 - 12
// 1 => January, 12 => December
export const THIS_MONTH = +(new Date().getMonth()) + 1;

// Week days names and shortnames
export const WEEK_DAYS = {
  Sunday: "Sun",
  Monday: "Mon",
  Tuesday: "Tue",
  Wednesday: "Wed",
  Thursday: "Thu",
  Friday: "Fri",
  Saturday: "Sat"
}

// Calendar months names and shortnames
export const CALENDAR_MONTHS = {
  January: "Jan",
  February: "Feb",
  March: "Mar",
  April: "Apr",
  May: "May",
  June: "Jun",
  July: "Jul",
  August: "Aug",
  September: "Sep",
  October: "Oct",
  November: "Nov",
  December: "Dec"
}

// Weeks displayed on calendar
export const CALENDAR_WEEKS = 6;

// Pads a string value with leading zeroes(0) until length is reached
// For example: zeroPad(5, 2) => "05"
export const zeroPad = (value, length) => {
  return `${value}`.padStart(length, '0');
}

// (int) Number days in a month for a given year from 28 - 31
export const getMonthDays = (month = THIS_MONTH, year = THIS_YEAR) => {
  const months30 = [4, 6, 9, 11];
  const leapYear = year % 4 === 0;

  return month === 2
    ? leapYear
      ? 29
      : 28
    : months30.includes(month)
      ? 30
      : 31;
}

// (int) First day of the month for a given year from 1 - 7
// 1 => Sunday, 7 => Saturday
export const getMonthFirstDay = (month = THIS_MONTH, year = THIS_YEAR) => {
  return +(new Date(`${year}-${zeroPad(month, 2)}-01`).getDay()) + 1;
}

This code snippet contains comments to explain what each helper function is doing. However, there are a few things worth pointing out.

First, methods like getDay() and getMonth() in Date.prototype usually return a zero-based value. Hence, the first month of the year (January) is 0 where as December is 11, while the first day of the week (Sunday) is 0 where as Saturday is 7.

In the previous code snippet, you’ll see that 1 was always added to these zero-based values, so that Sunday becomes 1 for week days, and December becomes 12 for months.

Also, notice that CALENDAR_WEEKS is set to 6. Since a month typically spans through 4 weeks, this allows the calendar to accommodate at least the last week from the previous month, and the first week from the next month. You will see the effect of this constant soon, as it will be used in the calendar builder function.

Additional helpers

Append the following content to the src/helpers/calendar.js file to add some additional helper functions to the calendar module.

// (bool) Checks if a value is a date - this is just a simple check
export const isDate = date => {
  const isDate = Object.prototype.toString.call(date) === '[object Date]';
  const isValidDate = date && !Number.isNaN(date.valueOf());
  
  return isDate && isValidDate;
}

// (bool) Checks if two date values are of the same month and year
export const isSameMonth = (date, basedate = new Date()) => {
  
  if (!(isDate(date) && isDate(basedate))) return false;

  const basedateMonth = +(basedate.getMonth()) + 1;
  const basedateYear = basedate.getFullYear();

  const dateMonth = +(date.getMonth()) + 1;
  const dateYear = date.getFullYear();

  return (+basedateMonth === +dateMonth) && (+basedateYear === +dateYear);
  
}

// (bool) Checks if two date values are the same day
export const isSameDay = (date, basedate = new Date()) => {
  
  if (!(isDate(date) && isDate(basedate))) return false;

  const basedateDate = basedate.getDate();
  const basedateMonth = +(basedate.getMonth()) + 1;
  const basedateYear = basedate.getFullYear();

  const dateDate = date.getDate();
  const dateMonth = +(date.getMonth()) + 1;
  const dateYear = date.getFullYear();

  return (+basedateDate === +dateDate) && (+basedateMonth === +dateMonth) && (+basedateYear === +dateYear);
  
}

// (string) Formats the given date as YYYY-MM-DD
// Months and Days are zero padded
export const getDateISO = (date = new Date) => {
  
  if (!isDate(date)) return null;

  return [
    date.getFullYear(),
    zeroPad(+date.getMonth() + 1, 2),
    zeroPad(+date.getDate(), 2)
  ].join('-');
  
}

// ({month, year}) Gets the month and year before the given month and year
// For example: getPreviousMonth(1, 2000) => {month: 12, year: 1999}
// while: getPreviousMonth(12, 2000) => {month: 11, year: 2000}
export const getPreviousMonth = (month, year) => {
  const prevMonth = (month > 1) ? month - 1 : 12;
  const prevMonthYear = (month > 1) ? year : year - 1;

  return { month: prevMonth, year: prevMonthYear };
}

// ({month, year}) Gets the month and year after the given month and year
// For example: getNextMonth(1, 2000) => {month: 2, year: 2000}
// while: getNextMonth(12, 2000) => {month: 1, year: 2001}
export const getNextMonth = (month, year) => {
  const nextMonth = (month < 12) ? month + 1 : 1;
  const nextMonthYear = (month < 12) ? year : year + 1;

  return { month: nextMonth, year: nextMonthYear };
}

Default export

Finally, here comes the default export of the calendar helper module — the calendar builder function itself. This function takes a month and year as arguments and returns an array of 42 elements, each element representing a calendar date in the format [YYYY, MM, DD].

Here is the calendar builder function. Append this code snippet to the src/helpers/calendar.js file.

// Calendar builder for a month in the specified year
// Returns an array of the calendar dates.
// Each calendar date is represented as an array => [YYYY, MM, DD]

export default (month = THIS_MONTH, year = THIS_YEAR) => {
  
  // Get number of days in the month and the month's first day
  
  const monthDays = getMonthDays(month, year);
  const monthFirstDay = getMonthFirstDay(month, year);

  // Get number of days to be displayed from previous and next months
  // These ensure a total of 42 days (6 weeks) displayed on the calendar
  
  const daysFromPrevMonth = monthFirstDay - 1;
  const daysFromNextMonth = (CALENDAR_WEEKS * 7) - (daysFromPrevMonth + monthDays);

  // Get the previous and next months and years
  
  const { month: prevMonth, year: prevMonthYear } = getPreviousMonth(month, year);
  const { month: nextMonth, year: nextMonthYear } = getNextMonth(month, year);

  // Get number of days in previous month
  const prevMonthDays = getMonthDays(prevMonth, prevMonthYear);

  // Builds dates to be displayed from previous month
  
  const prevMonthDates = [...new Array(daysFromPrevMonth)].map((n, index) => {
    const day = index + 1 + (prevMonthDays - daysFromPrevMonth);
    return [ prevMonthYear, zeroPad(prevMonth, 2), zeroPad(day, 2) ];
  });

  // Builds dates to be displayed from current month
  
  const thisMonthDates = [...new Array(monthDays)].map((n, index) => {
    const day = index + 1;
    return [year, zeroPad(month, 2), zeroPad(day, 2)];
  });

  // Builds dates to be displayed from next month
  
  const nextMonthDates = [...new Array(daysFromNextMonth)].map((n, index) => {
    const day = index + 1;
    return [nextMonthYear, zeroPad(nextMonth, 2), zeroPad(day, 2)];
  });

  // Combines all dates from previous, current and next months
  return [ ...prevMonthDates, ...thisMonthDates, ...nextMonthDates ];
  
}

Notice that the calendar dates returned in the array from the builder span from the dates in the last week of the previous month, through the dates in given month, to the dates in the first week of the next month.

Calendar component

Building the calendar component

Now you have the calendar helper module, it’s time to build the Calendar React component. Add the following code snippet to the src/components/Calendar/index.js file.

import React, { Component, Fragment } from "react";
import PropTypes from "prop-types";
import * as Styled from "./styles";
import calendar, {
  isDate,
  isSameDay,
  isSameMonth,
  getDateISO,
  getNextMonth,
  getPreviousMonth,
  WEEK_DAYS,
  CALENDAR_MONTHS
} from "../../helpers/calendar";

class Calendar extends Component {
  
  state = { ...this.resolveStateFromProp(), today: new Date() };

  resolveStateFromDate(date) {
    const isDateObject = isDate(date);
    const _date = isDateObject ? date : new Date();

    return {
      current: isDateObject ? date : null,
      month: +_date.getMonth() + 1,
      year: _date.getFullYear()
    };
  }

  resolveStateFromProp() {
    return this.resolveStateFromDate(this.props.date);
  }

  getCalendarDates = () => {
    const { current, month, year } = this.state;
    const calendarMonth = month || +current.getMonth() + 1;
    const calendarYear = year || current.getFullYear();

    return calendar(calendarMonth, calendarYear);
  };

  render() {
    return (
      <Styled.CalendarContainer>
        
        { this.renderMonthAndYear() }

        <Styled.CalendarGrid>
          <Fragment>
            { Object.keys(WEEK_DAYS).map(this.renderDayLabel) }
          </Fragment>

          <Fragment>
            { this.getCalendarDates().map(this.renderCalendarDate) }
          </Fragment>
        </Styled.CalendarGrid>
        
      </Styled.CalendarContainer>
    );
  }
}

Calendar.propTypes = {
  date: PropTypes.instanceOf(Date),
  onDateChanged: PropTypes.func
}

export default Calendar;

Notice in this code snippet that the default calendar export as well as other helper functions and constants have been imported from the calendar helper module. Also, all the exports from the calendar styles module have been imported with the Styled namespace.

Although, the styles have not been created at the moment, they will be created soon using the styled-components package.

The component state is partly resolved from props using the resolveStateFromProp() method which returns an object containing:

  • current — which is a Date object for the currently selected date or null.
  • month — which is the month of the currently selected date if it is set, otherwise it is the month of the current date (today).
  • year — which is the year of the currently selected date if it is set, otherwise it is the year of the current date (today).

The month and year state properties are required to properly render the calendar as shown in the getCalendarDates() method, which uses the calendar builder function to build the calendar for the month and year.

Finally, the state is augmented with the today property which is a Date object for the current date.

Rendering parts of the calendar component

From the previous Calendar component code snippet, the render() method made reference to some other methods for rendering the month and year, week days and calendar dates.

Add these methods to the Calendar component as shown in the following code snippet.

class Calendar extends Component {
  
  // Render the month and year header with arrow controls
  // for navigating through months and years
  renderMonthAndYear = () => {
    const { month, year } = this.state;
    
    // Resolve the month name from the CALENDAR_MONTHS object map
    const monthname = Object.keys(CALENDAR_MONTHS)[
      Math.max(0, Math.min(month - 1, 11))
    ];

    return (
      <Styled.CalendarHeader>
        
        <Styled.ArrowLeft
          onMouseDown={this.handlePrevious}
          onMouseUp={this.clearPressureTimer}
          title="Previous Month"
        />
        
        <Styled.CalendarMonth>
          {monthname} {year}
        </Styled.CalendarMonth>
        
        <Styled.ArrowRight
          onMouseDown={this.handleNext}
          onMouseUp={this.clearPressureTimer}
          title="Next Month"
        />
        
      </Styled.CalendarHeader>
    );
  }

  // Render the label for day of the week
  // This method is used as a map callback as seen in render()
  renderDayLabel = (day, index) => {
    // Resolve the day of the week label from the WEEK_DAYS object map
    const daylabel = WEEK_DAYS[day].toUpperCase();
    
    return (
      <Styled.CalendarDay key={daylabel} index={index}>
        {daylabel}
      </Styled.CalendarDay>
    );
  }

  // Render a calendar date as returned from the calendar builder function
  // This method is used as a map callback as seen in render()
  renderCalendarDate = (date, index) => {
    const { current, month, year, today } = this.state;
    const _date = new Date(date.join("-"));

    // Check if calendar date is same day as today
    const isToday = isSameDay(_date, today);
    
    // Check if calendar date is same day as currently selected date
    const isCurrent = current && isSameDay(_date, current);
    
    // Check if calendar date is in the same month as the state month and year
    const inMonth = month && year && isSameMonth(_date, new Date([year, month, 1].join("-")));

    // The click handler
    const onClick = this.gotoDate(_date);

    const props = { index, inMonth, onClick, title: _date.toDateString() };

    // Conditionally render a styled date component
    const DateComponent = isCurrent
      ? Styled.HighlightedCalendarDate
      : isToday
        ? Styled.TodayCalendarDate
        : Styled.CalendarDate;

    return (
      <DateComponent key={getDateISO(_date)} {...props}>
        {_date.getDate()}
      </DateComponent>
    );
  }
  
}

In the renderMonthAndYear() method, the month name is first resolved from the CALENDAR_MONTHS object. Then it is rendered alongside the year and two arrow controls on the left side and on the right side for navigating through months and years.

The arrow controls each has event handlers for the mousedown and mouseup events, which will be defined later — handlePrevious(), handleNext() and clearPressureTimer().

The rendered DOM from the renderMonthAndYear() method looks like the following screenshot (with some styling):

The renderDayLabel() method renders a label for a day of the week. It resolves the label from the WEEK_DAYS object. Notice that it takes two arguments — day and index, since it is used as a callback function to .map() as seen in the render() method.

After the mapping, here is what the rendered DOM looks like for the days of the week.

The renderCalendarDate() method is also used as a .map() callback function and renders a calendar date. The date it receives as its first argument is in the format [YYYY, MM, DD].

It checks if the date is same as today, same as currently selected date, and in the same month as the current state’s month and year. With these checks, it conditionally renders one of the variants of the calendar date cell — HiglightedCalendarDate, TodayCalendarDate or CalendarDate.

Also notice that an onClick handler is set for each rendered calendar date to jump to that particular date using the gotoDate() method that will be defined in the next section.

The event handlers

A couple of references have been made to some event handlers in previous sections. Go ahead and update the Calendar component to include the following code snippet for the event handlers.

class Calendar extends Component {
  
  gotoDate = date => evt => {
    evt && evt.preventDefault();
    const { current } = this.state;
    const { onDateChanged } = this.props;

    !(current && isSameDay(date, current)) &&
      this.setState(this.resolveStateFromDate(date), () => {
        typeof onDateChanged === "function" && onDateChanged(date);
      });
  }

  gotoPreviousMonth = () => {
    const { month, year } = this.state;
    this.setState(getPreviousMonth(month, year));
  }

  gotoNextMonth = () => {
    const { month, year } = this.state;
    this.setState(getNextMonth(month, year));
  }

  gotoPreviousYear = () => {
    const { year } = this.state;
    this.setState({ year: year - 1 });
  }

  gotoNextYear = () => {
    const { year } = this.state;
    this.setState({ year: year + 1 });
  }

  handlePressure = fn => {
    if (typeof fn === "function") {
      fn();
      this.pressureTimeout = setTimeout(() => {
        this.pressureTimer = setInterval(fn, 100);
      }, 500);
    }
  }

  clearPressureTimer = () => {
    this.pressureTimer && clearInterval(this.pressureTimer);
    this.pressureTimeout && clearTimeout(this.pressureTimeout);
  }

  handlePrevious = evt => {
    evt && evt.preventDefault();
    const fn = evt.shiftKey ? this.gotoPreviousYear : this.gotoPreviousMonth;
    this.handlePressure(fn);
  }

  handleNext = evt => {
    evt && evt.preventDefault();
    const fn = evt.shiftKey ? this.gotoNextYear : this.gotoNextMonth;
    this.handlePressure(fn);
  }
  
}

The gotoDate() method is a higher-order function that takes a Date object as it’s argument and returns an event handler that can be triggered to update the currently selected date in the state. Notice that the resolveStateFromDate() method is used to resolve the month and year from the date and update the state.

If a callback function is passed to the onDateChanged prop of the Calendar component, then that function will be called with the updated date. This is very useful for cases where you want to propagate the date change to a parent component.

The handlePrevious() and handleNext() event handlers share a similar behavior. By default, they cycle through the months. However if the shift key is pressed, then they cycle through years instead. Finally, they handover control to the handlePressure() method.

The handlePressure() method simply uses timers to simulate pressure clicking for rapidly cycling through months or years, while the clearPressureTimer() method clears these timers.

Component lifecycle methods

The Calendar component is just some lifecycle methods away from being complete. Here are the lifecycle methods for the Calendar component.

class Calendar extends Component {
  
  // ... other methods here
  
  componentDidMount() {
    const now = new Date();
    const tomorrow = new Date().setHours(0, 0, 0, 0) + 24 * 60 * 60 * 1000;
    const ms = tomorrow - now;

    this.dayTimeout = setTimeout(() => {
      this.setState({ today: new Date() }, this.clearDayTimeout);
    }, ms);
  }

  componentDidUpdate(prevProps) {
    const { date, onDateChanged } = this.props;
    const { date: prevDate } = prevProps;
    const dateMatch = date == prevDate || isSameDay(date, prevDate);

    !dateMatch &&
      this.setState(this.resolveStateFromDate(date), () => {
        typeof onDateChanged === "function" && onDateChanged(date);
      });
  }

  clearDayTimeout = () => {
    this.dayTimeout && clearTimeout(this.dayTimeout);
  }

  componentWillUnmount() {
    this.clearPressureTimer();
    this.clearDayTimeout();
  }
  
}

In the componentDidMount() method, there is a day timer that is set to automatically update today state property to the next day when the current day is over.

Before the component is unmounted, all the timers are cleared as seen in the componentWillUnmount() method.

Styling the calendar

Now you have completed Calendar component, you will go ahead and create the styled components required for giving the calendar some styling.

Add the following code snippet to the src/components/Calendar/styles.js file.

import styled from 'styled-components';

export const Arrow = styled.button`
  appearance: none;
  user-select: none;
  outline: none !important;
  display: inline-block;
  position: relative;
  cursor: pointer;
  padding: 0;
  border: none;
  border-top: 1.6em solid transparent;
  border-bottom: 1.6em solid transparent;
  transition: all .25s ease-out;
`;

export const ArrowLeft = styled(Arrow)`
  border-right: 2.4em solid #ccc;
  left: 1.5rem;
  :hover {
    border-right-color: #06c;
  }
`;

export const ArrowRight = styled(Arrow)`
  border-left: 2.4em solid #ccc;
  right: 1.5rem;
  :hover {
    border-left-color: #06c;
  }
`;

export const CalendarContainer = styled.div`
  font-size: 5px;
  border: 2px solid #06c;
  border-radius: 5px;
  overflow: hidden;
`;

export const CalendarHeader = styled.div`
  display: flex;
  align-items: center;
  justify-content: space-between;
`;

export const CalendarGrid = styled.div`
  display: grid;
  grid-template: repeat(7, auto) / repeat(7, auto);
`;

export const CalendarMonth = styled.div`
  font-weight: 500;
  font-size: 5em;
  color: #06c;
  text-align: center;
  padding: 0.5em 0.25em;
  word-spacing: 5px;
  user-select: none;
`;

export const CalendarCell = styled.div`
  text-align: center;
  align-self: center;
  letter-spacing: 0.1rem;
  padding: 0.6em 0.25em;
  user-select: none;
  grid-column: ${props => (props.index % 7) + 1} / span 1;
`;

export const CalendarDay = styled(CalendarCell)`
  font-weight: 600;
  font-size: 2.25em;
  color: #06c;
  border-top: 2px solid #06c;
  border-bottom: 2px solid #06c;
  border-right: ${props => (props.index % 7) + 1 === 7 ? `none` : `2px solid #06c`};
`;

export const CalendarDate = styled(CalendarCell)`
  font-weight: ${props => props.inMonth ? 500 : 300};
  font-size: 4em;
  cursor: pointer;
  border-bottom: ${props => ((props.index + 1) / 7) <= 5 ? `1px solid #ddd` : `none`};
  border-right: ${props => (props.index % 7) + 1 === 7 ? `none` : `1px solid #ddd`};
  color: ${props => props.inMonth ? `#333` : `#ddd`};
  grid-row: ${props => Math.floor(props.index / 7) + 2} / span 1;
  transition: all .4s ease-out;
  :hover {
    color: #06c;
    background: rgba(0, 102, 204, 0.075);
  }
`;

export const HighlightedCalendarDate = styled(CalendarDate)`
  color: #fff !important;
  background: #06c !important;
  position: relative;
  ::before {
    content: '';
    position: absolute;
    top: -1px;
    left: -1px;
    width: calc(100% + 2px);
    height: calc(100% + 2px);
    border: 2px solid #06c;
  }
`;

export const TodayCalendarDate = styled(HighlightedCalendarDate)`
  color: #06c !important;
  background: transparent !important;
  ::after {
    content: '';
    position: absolute;
    right: 0;
    bottom: 0;
    border-bottom: 0.75em solid #06c;
    border-left: 0.75em solid transparent;
    border-top: 0.75em solid transparent;
  }
  :hover {
    color: #06c !important;
    background: rgba(0, 102, 204, 0.075) !important;
  }
`;

And that’s all for the components and styles required to properly render the calendar. If you render the Calendar component in the app at this time, it should look like this screenshot.

Datepicker component

Building the datepicker

To begin building the Datepicker component, add the following code snippet to the src/components/Datepicker/index.js file.

import React from "react";
import PropTypes from "prop-types";
import Calendar from "../Calendar";
import * as Styled from "./styles";
import { isDate, getDateISO } from "../../helpers/calendar";

class Datepicker extends React.Component {
  
  state = { date: null, calendarOpen: false }

  toggleCalendar = () => this.setState({ calendarOpen: !this.state.calendarOpen })

  handleChange = evt => evt.preventDefault()

  handleDateChange = date => {
    const { onDateChanged } = this.props;
    const { date: currentDate } = this.state;
    const newDate = date ? getDateISO(date) : null;

    currentDate !== newDate &&
      this.setState({ date: newDate, calendarOpen: false }, () => {
        typeof onDateChanged === "function" && onDateChanged(this.state.date);
      });
  }

  componentDidMount() {
    const { value: date } = this.props;
    const newDate = date && new Date(date);

    isDate(newDate) && this.setState({ date: getDateISO(newDate) });
  }

  componentDidUpdate(prevProps) {
    const { value: date } = this.props;
    const { value: prevDate } = prevProps;
    const dateISO = getDateISO(new Date(date));
    const prevDateISO = getDateISO(new Date(prevDate));

    dateISO !== prevDateISO && this.setState({ date: dateISO });
  }

}

Datepicker.propTypes = {
  label: PropTypes.string,
  value: PropTypes.string,
  onDateChanged: PropTypes.func
}

export default Datepicker;

Here, the component state is initialized with two properties:

  • date — an ISO string representation for the current date on the date picker. The format is “YYYY-MM-DD”.
  • calendarOpen — a boolean flag that indicates if the date picker calendar is visible or not.

When the component mounts, the Date object is resolved from the value prop passed to the component and is updated on the state as seen in the componentDidMount() method.

The handleDateChange() method takes a Date object as its argument and updates the date in the state. If a callback function is passed to the onDateChanged prop of the Datepicker component, then that function will be called with the updated ISO date string.

Rendering the datepicker

At this point, it is worth mentioning that the Bootstrap Dropdown component will be used to simulate the dropdown effect for the custom date picker. This is the reason why the Reactstrap package was added as a dependency for this project.

As you will soon notice, the styled components that are rendered in the date picker are styled extensions of the dropdown components from Reactstrap.

Update the Datepicker component to include the render() method as shown in the following code snippet.

class Datepicker extends React.Component {
  
  // ... other methods here
  
  render() {
    const { label } = this.props;
    const { date, calendarOpen } = this.state;

    return (
      <Styled.DatePickerContainer>
        
        <Styled.DatePickerFormGroup>
          
          <Styled.DatePickerLabel>{label || 'Enter Date'}</Styled.DatePickerLabel>
          
          <Styled.DatePickerInput
            type="text"
            value={date ? date.split("-").join(" / ") : ""}
            onChange={this.handleChange}
            readOnly="readonly"
            placeholder="YYYY / MM / DD"
          />
          
        </Styled.DatePickerFormGroup>

        <Styled.DatePickerDropdown isOpen={calendarOpen} toggle={this.toggleCalendar}>
          
          <Styled.DatePickerDropdownToggle color="transparent" />

          <Styled.DatePickerDropdownMenu>
            { calendarOpen && (
              <Calendar date={date && new Date(date)} onDateChanged={this.handleDateChange} />
            )}
          </Styled.DatePickerDropdownMenu>
          
        </Styled.DatePickerDropdown>
        
      </Styled.DatePickerContainer>
    );
  }
  
}

Here, the Styled.DatePickerFormGroup component is a Bootstrap .form-group that wraps the date picker label and input field. It is important to note that the input field is of type “text” and also marked as readonly so that it cannot be edited directly. Also notice that the default behavior for the change event on the input element has been prevented.

The Styled.DatePickerDropdown component and its descendants are styled extensions of the Dropdown component from the Reactstrap package. You can learn more about dropdowns in Reactstrap here.

Finally, the Calendar component is rendered in the dropdown menu passing the date from the state and the handleDateChange() method as callback function for the onDateChanged prop.

The final rendered DOM for the Datepicker component should look like the following screenshot (with some styling):

Styling the datepicker

Add the following code snippet to the src/components/Datepicker/styles.js file to create the styled components required for the date picker.

import styled from 'styled-components';
import { FormGroup, Label, Input, Dropdown, DropdownToggle, DropdownMenu } from 'reactstrap';

export const DatePickerContainer = styled.div`
  position: relative;
`;

export const DatePickerFormGroup = styled(FormGroup)`
  display: flex;
  justify-content: space-between;
  position: relative;
  width: 100%;
  border: 2px solid #06c;
  border-radius: 5px;
  overflow: hidden;
`;

export const DatePickerLabel = styled(Label)`
  margin: 0;
  padding: 0 2rem;
  font-weight: 600;
  font-size: 0.7rem;
  letter-spacing: 2px;
  text-transform: uppercase;
  color: #06c;
  border-right: 2px solid #06c;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(0, 102, 204, 0.05);
`;

export const DatePickerInput = styled(Input)`
  padding: 1rem 2rem;
  font-weight: 500;
  font-size: 1rem;
  color: #333;
  box-shadow: none;
  border: none;
  text-align: center;
  letter-spacing: 1px;
  background: transparent !important;
  display: flex;
  align-items: center;

  ::placeholder {
    color: #999;
    font-size: 0.9rem;
  }
`;

export const DatePickerDropdown = styled(Dropdown)`
  position: absolute;
  width: 100%;
  height: 100%;
  top: 0;
  left: 0;
`;

export const DatePickerDropdownToggle = styled(DropdownToggle)`
  position: relative;
  width: 100%;
  height: 100%;
  background: transparent;
  opacity: 0;
  filter: alpha(opacity=0);
`;

export const DatePickerDropdownMenu = styled(DropdownMenu)`
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  border: none;
  padding: 0;
  margin: 0;
  transform: none !important;
`;

The app component

Finally, update the src/App.js file to look like the following code snippet.

import React, { Component } from "react";
import Datepicker from "./components/Datepicker";

class App extends Component {
  render() {
    return (
      
        
      
    );
  }
}

export default App;

If you followed this article and the code snippets through, you should have a working custom date picker rendered in your React application.

Conclusion

In this tutorial, you’ve been able to follow through a step-by-step guide on how you can build a custom React date picker component that can be used as a replacement for the native HTML5 date picker input element.

Although the custom date picker created in this tutorial works as expected, it does not completely satisfy all the requirements for a date picker element. Further improvements can be made such as:

  • Implementation of max and min dates via props
  • Switching the input type from “text” to “date”
  • Better accessibility improvements

You can get the full source code for a more improved version of this custom date picker on GitHub at react-datepicker-demo. You can also get the live demo here on Code Sandbox.

Clap & Follow

If you found this article insightful, feel free to give some rounds of applause if you don’t mind.

You can also follow me on Medium (Glad Chinda) for more insightful articles you may find helpful. You can also follow me on Twitter (@gladchinda).

Enjoy coding…

 

 

Full visibility into production React apps

Debugging React applications can be difficult, especially when users experience issues that are difficult to reproduce. If you’re interested in monitoring and tracking Redux state, automatically surfacing JavaScript errors, tracking slow network requests and component load time, try LogRocket. https://logrocket.com/signup/

LogRocket is like a DVR for web apps, recording literally everything that happens on your React app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors performance of your app with metrics like client CPU load, client memory usage, and more.

The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.

Modernize how you debug your React apps - .

Glad Chinda Full-stack web developer learning new hacks one day at a time. Web technology enthusiast. Hacking stuffs @theflutterwave.

Leave a Reply