DEV Community

Daveyon Mayne 😻
Daveyon Mayne 😻

Posted on

4

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.

AWS Security LIVE!

Join us for AWS Security LIVE!

Discover the future of cloud security. Tune in live for trends, tips, and solutions from AWS and AWS Partners.

Learn More

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 πŸ˜‰

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs