DEV Community

Daveyon Mayne 😻
Daveyon Mayne 😻

Posted on

Refactoring: Build a Date Picker in 15mins Using Javascript/React from Scratch

I was in search of a tutorial on how to roll my own datepicker and I've found this.

Since the original example is using a React Class, I thought I could refactor to use Hooks and replace let with const where possible.

We start off by moving some of the functions into its separate shared file, into a file called ./shared/dates.js.

/** Your style of below or React.use-what-ever */ import React, { useState, useEffect, useRef, createRef, useReducer } from 'react' import { getDateStringFromTimestamp, getMonthDetails, monthMap } from './shared/dates' import PropTypes from 'prop-types' /** * We wont show stylesheet in this refactor example. * @see https://medium.com/swlh/build-a-date-picker-in-15mins-using-javascript-react-from-scratch-f6932c77db09 */ import './DatePicker.scss' /** We'll use useReducer to manage our state **/ const date = new Date() const oneDay = 60 * 60 * 24 * 1000 const todayTimestamp = date.getTime() - (date.getTime() % oneDay) + (date.getTimezoneOffset() * 1000 * 60) const initialState = { todayTimestamp: todayTimestamp, // or todayTimestamp, for short year: date.getFullYear(), month: date.getMonth(), selectedDay: todayTimestamp, monthDetails: getMonthDetails(date.getFullYear(), date.getMonth()) } export function DatePicker(props) { const el = useRef(null) const inputRef = createRef() const [state, dispatch] = useReducer(reducer, initialState) /** Maybe you could add this to initialState 🤷🏽‍♂️ */ const [showDatePicker, setShowDatePicker] = useState(false) const addBackDrop = e => { if(showDatePicker && (el && !el.current.contains(e.target))) { setShowDatePicker(false); } } const setDateToInput = (timestamp) => { const dateString = getDateStringFromTimestamp(timestamp) inputRef.current.value = dateString } useEffect(() => { /** * Only needed when using SSR ie Next.js * Uncomment if you're using SSR: * if (!process.browser) { return } */ window.addEventListener('click', addBackDrop) setDateToInput(state.selectedDay) // returned function will be called on component unmount  return () => { window.removeEventListener('click', addBackDrop) } }, [showDatePicker]) const isCurrentDay = day => day.timestamp === todayTimestamp const isSelectedDay = day => day.timestamp === state.selectedDay const getMonthStr = month => monthMap[Math.max(Math.min(11, month), 0)] || 'Month' const onDateClick = day => { dispatch({type: 'selectedDay', value: day.timestamp}) setDateToInput(day.timestamp) /** Pass data to parent */ props.onChange(day.timestamp) } const setYear = offset => { const year = state.year + offset dispatch({type: 'year', value: year}) dispatch({type: 'monthDetails', value: getMonthDetails(year, state.month)}) } const setMonth = offset => { let year = state.year let month = state.month + offset if(month === -1) { month = 11 year-- } else if(month === 12) { month = 0 year++ } dispatch({type: 'year', value: year}) dispatch({type: 'month', value: month}) dispatch({type: 'monthDetails', value: getMonthDetails(year, month)}) } const setDate =dateData=> { const selectedDay = new Date(dateData.year, dateData.month - 1, dateData.date).getTime() dispatch({type: 'selectedDay', value: selectedDay}) /** Pass data to parent */ props.onChange(selectedDay) } const getDateFromDateString = dateValue => { const dateData = dateValue.split('-').map(d => parseInt(d, 10)) if (dateData.length < 3) { return null } const year = dateData[0] const month = dateData[1] const date = dateData[2] return {year, month, date} } const updateDateFromInput =()=> { const dateValue = inputRef.current.value const dateData = getDateFromDateString(dateValue) if (dateData !== null) { setDate(dateData) dispatch({type: 'year', value: dateData.year}) dispatch({type: 'month', value: dateData.month - 1}) dispatch({type: 'monthDetails', value: getMonthDetails(dateData.year, dateData.month -1)}) } } const daysMarkup = ( state.monthDetails.map((day, index) => ( <div className={'c-day-container ' + (day.month !== 0 ? ' disabled' : '') + (isCurrentDay(day) ? ' highlight' : '') + (isSelectedDay(day) ? ' highlight-green' : '')} key={index}> <div className='cdc-day'> <span onClick={() => onDateClick(day)}> {day.date} </span>  </div>  </div>  )) ) const calendarMarkup = ( <div className='c-container'> <div className='cc-head'> {['SUN', 'MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT'].map((d,i)=><div key={i} className='cch-name'>{d}</div>)}  </div>  <div className='cc-body'> {daysMarkup} </div>  </div>  ) return ( <div ref={el} className='MyDatePicker'> <div className='mdp-input' onClick={()=> setShowDatePicker(true)}> <input type='date' ref={inputRef} onChange={updateDateFromInput} />  </div>  {showDatePicker ? ( <div className='mdp-container'> <div className='mdpc-head'> <div className='mdpch-button'> <div className='mdpchb-inner' onClick={()=> setYear(-1)}> <span className='mdpchbi-left-arrows'></span>  </div>  </div>  <div className='mdpch-button'> <div className='mdpchb-inner' onClick={()=> setMonth(-1)}> <span className='mdpchbi-left-arrow'></span>  </div>  </div>  <div className='mdpch-container'> <div className='mdpchc-year'>{state.year}</div>  <div className='mdpchc-month'>{getMonthStr(state.month)}</div>  </div>  <div className='mdpch-button'> <div className='mdpchb-inner' onClick={()=> setMonth(1)}> <span className='mdpchbi-right-arrow'></span>  </div>  </div>  <div className='mdpch-button' onClick={()=> setYear(1)}> <div className='mdpchb-inner'> <span className='mdpchbi-right-arrows'></span>  </div>  </div>  </div>  <div className='mdpc-body'> {calendarMarkup} </div>  </div>  ) : ''} </div>  ) } /** Fancy using switch statement? Go ahead */ function reducer(state, action) { if (state.hasOwnProperty(action.type)) { return { ...state, [`${action.type}`]: action.value } } console.log(`Unknown key in state: ${action.type}`) } DatePicker.propTypes = { onChange: PropTypes.func.isRequired } 

I had to make some changes to the css but nothing major. The original example seems to be using a version 2 as it's showing reminders, left/right arrows are positioned differently but that could be easily wired in. Now you should be able to add dark mode 🌓

Credit goes to @thestartup_. I only refactored to use React Hooks.

Top comments (2)

Collapse
 
himanshupal0001 profile image
Himanshupal0001

Thanks for the refactoring. I followed your code and it worked. But there are some bugs in css.

Collapse
 
mirmayne profile image
Daveyon Mayne 😻

👋🏼

There's a possibility for bugs but if it's only for css, then that should be easily fixed 😉