Creating Custom Date Picker

Creating custom components for your projects are hard and they harder to implement successfully when they are used as input methods by your users. Most of the time you can get away by styling a default component such as select, you can change backgroun color, selected color etc and it won't really mess with your design system. Or one can add auxilary fields to default input to create a toggleable password field.

But there are situations where you need to redesign a default input component from ground up. I had that happen to me in my personal project where the date picker will be show in the default user flow. Using the default date picker was an option but in the end it is not customizable in any way, highly varied between system and browsers. Mobile devices has some upper hand in the UX field but they are also not customizable.

There are few third party libraries out there but they are basically the same thing with bells and whistles. Instead of date picker I looked upon full calendar, it is highly customizable, easy to implement after some deliberation it will be a hack that will haunt me whenever I need to manipulate it.

In the end I decided to write a custom date picker for my project and see if it woul be hard to accomplish what I needed.

The stack I decided to use was pretty barebones. I choose to use date-fns, a library of functions to manipulate default JS date objects. This will keep my bundle sizes smaller than packages like moment.js(RIP) or Luxon. To build the package I will be using Vite.js.

You can try the finished component here and check out the final code at abdullahcanakci/date-picker.

Project Foundations

Start by creating a vite project and install required packages.

yarn create vite date-picker --template react
cd date-picker
yarn install
yarn add date-fns prop-types sass

To create a component package with Vite we need to split our component and showcase code. Our folder structure will be like below:

.
├── package.json
├── src/
│   ├── main.jsx
│   ├── App.jsx
│   └── style/
│       ├── globals.scss
│       └── colors.scss
└── lib/
    ├── main.js
    └── DatePicker/
        ├── index.js
        ├── DatePicker.jsx
        ├── DatePickerEntry.jsx
        └── DatePicker.module.scss

We will store our component code in lib folder. ./lib/main.js file will be our entry point when the time comes to bundle it. In ./src/App.jsx we will import our component and test it while developing.

Date Picker

First create a basic components to wire things up.

import React from 'react';

export default function DatePickerEntry () {
    return (
        <div>
        ++ Date picker Entry ++
        </div>
    )
|}
./lib/DatePicker/DatePickerEntry.jsx
import React, {useState} from 'react';
import DatePickerEntry from './DatePickerEntry';

export default function DatePicker () {
    const  [activeMonth, setActiveMonth] = useState(new Date());
    return (
        <div>
        -- Date picker --
        <DatePickerEntry />
        </div>
    )
|}
./lib/DatePicker/DatePicker.jsx
import React from 'react';
import DatePicker from '../lib/DatePicker';

export default function App () {
    return (
        <div>
        ** Date Picker App **
        <DatePicker />
        </div>
    )
|}
./src/App.jsx

The minimal application can be created a above. This app can be run with

yarn dev

In the end I am very happy with how the component turned out and how minimal it is. I can easily extend and manipulate it without fear of breaking something.

To create the date picker I create a new js Date object and create a monthly view first.

import {
	startOfMonth, endOfMonth, startOfWeek, 
	endOfWeek, format
	} 
	from 'date-fns';
...
{
    eachDayOfInterval({
        start: startOfWeek(startOfMonth(activeMonth)),
        end: endOfWeek(endOfMonth(activeMonth))
    }).map( day =>
        <p>{format(day, 'd')}</p>
    )
}
...
./src/App.jsx

This component will render days of the month with leading and following days for now.

30 - Monday
31
1
2
...
30
1
2 - Sunday

Using our DatePickerEntry component we can style this entries.

But we need to add some logic to our DatePicker component to make it usable.

Few feature I want are:

  • Max date limit
  • Min date limit
  • Selection limit
  • Not being able to select date outside of the current month

Limits

To limits days available to select I need to check whether they are outside min or max limits or they are not inside current active month. Date-fns has these helper methods out of the box.

...
  const isDayDisabled = (day) => {
    return !isSameMonth(activeMonth, day) ||
            minDate && isBefore(day, minDate) ||
            maxDate && isAfter(day, maxDate);
  };
...
./lib/DatePicker/DatePicker.jsx

Selection Limit and Duplicate Prevention

After a user selects a day we have to make sure that no duplicate entries are in the state I choose to use basic filter method instead of js sets because performance won't be effected in such limited entry counts.

const onSelect = (date) => {
    if (isDayDisabled(date)) return;
    if (
      selectedDates.filter(e => isSameDay(e, date)).length == 0 && 
      selectedDates.length < maxSelections
    ) {
      setSelectedDates(prevSelected => [...prevSelected, date]);
    } else {
      setSelectedDates(prevSelected => prevSelected.filter(e => !isSameDay(e, date)));
    }
  };
./lib/DatePicker/DatePicker.jsx

Usage

Component can be used as below.

...
return (
	 <DatePicker 
	   defaultValue={dates}
	   onChange={setDates}
	   maxDate={maxDate}
	   minDate={minDate}
	   maxSelections={samples}
	 />
);	
)
...
./src/App.jsx

Packing It Up

I wanted to build 2 different bundles. One for demo purposes where entry point is main.jsx. And show the basic inputs and features. And another one component package one. Luckily Vitejs and Rollup provides an easy way to do both of these things.

We will be editig ./vite.config.js file from now on. By default the project entry will be ./src/main.jsx that entry will build a fully fledged react SPA application that can be served. And I want that behaviour for my demo page but to create a package we have to add a build key and define Rollup options such as entry point and external dependencies.

...
  build: {
    lib: 
    	!process.env.BUILD_APP ? 
    	{
	      entry: path.resolve(__dirname, 'lib/main.js'),
	      name: 'DatePicker',
	      fileName: format => `date-picker.${format}.js`
    	} 
    	:
    	 undefined,
    sourcemap: true,
    rollupOptions: {
      external: !process.env.BUILD_APP ? ['react', 'react-dom'] : []
    }
  },
...
./vite.config.js

Important area is lib value. We provide a entry point, define a name a format the output file names. Env variable is that when I build this app in Cloudflare to showcase I can add an environment variable called BUILD_APP and it will build a regular web app. But other systems it will build a package.

rollupOptions.external defines required packages that need to be provided by the importing application. This needs to be done otherwise our package will bring it's own react copy and they will clash.

TODO

The basic component is working but it has problems. Even though responsivity is working there are few problems with mobile CSS mainly with hover and focus states not losing their state after touch up. And changing limits dynamically won't update the component afterwards.