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>
)
|}
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>
)
|}
import React from 'react';
import DatePicker from '../lib/DatePicker';
export default function App () {
return (
<div>
** Date Picker App **
<DatePicker />
</div>
)
|}
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>
)
}
...
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);
};
...
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)));
}
};
Usage
Component can be used as below.
...
return (
<DatePicker
defaultValue={dates}
onChange={setDates}
maxDate={maxDate}
minDate={minDate}
maxSelections={samples}
/>
);
)
...
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'] : []
}
},
...
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.