1

Creating Datepicker in RemixJS

 1 year ago
source link: https://thewantara.com/posts/creating-datepicker-in-remixjs
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Creating Datepicker in RemixJS

5 days ago

It is true that dozens of similar things have been written by other developers, kewl stuph, fancy features huh! But here is the problem, for a beginner like me, it is never easy to integrate someone else's fancy blocks of code to your project without breaking your hardwork. And I believe it also happens to a lot of beginners out there. So this time, I'd like to create my own simple, a really simple, datepicker.

We'll create this in RemixJS, and we'll also be using tailwindcss for the styling.

Let's get started!

Set Things up

Initialize a New Remix Project

npx create-remix@latest ./datepicker

And these are our configuration.

What type of app do you want to create? 
    Just the basics
Where do you want to deploy? Choose Remix App Server if you're unsure; it's easy to change deployment targets. 
    Remix App Server
TypeScript or JavaScript? 
    JavaScript
Do you want me to run `npm install`? 
    Yes

Add Tailwindcss

cd ./datepicker
npm install -D tailwindcss postcss autoprefixer concurrently
npx tailwindcss init

add this line to /tailwind.config.js

/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [
    "./app/**/*.{js,ts,jsx,tsx}",
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}

then we want to copy this block into /package.json

{
  "scripts": {
    "build": "npm run build:css && remix build",
    "build:css": "tailwindcss -m -i ./styles/app.css -o app/styles/app.css",
    "dev": "concurrently \"npm run dev:css\" \"remix dev\"",
    "dev:css": "tailwindcss -w -i ./styles/app.css -o app/styles/app.css"
  }
}
mkdir /styles
touch /styles/app.css

These would go into /styles/app.css

/* /styles/app.css */
@tailwind base;
@tailwind components;
@tailwind utilities;

Then we would add that css into our /app/root.jsx

// /app/root.jsx
import styles from "./styles/app.css"

export function links() {
  return [{ rel: "stylesheet", href: styles }]
}

Add React-icons

We're also gonna need icons to make things look a little nicer

npm install react-icons --save

Finally, let's run it!

npm run dev

Let's see if we really have tailwindcss applied, we'll remove everything inside /app/routes/index.jsx and paste this block instead.

// /app.routes/index.jsx
export default function Index() {
  return (
    <h1 className="text-3xl font-bold underline">
      Hello world!
    </h1>
  )
}

Do you see your browser showing black huge underlined Hello World on it? Awesome!
Alright, let's get going!

Plans things out!

First of all let's create a clear roadmap that would enable us to easily tackle one step at the time.

At the end of the article we'll have something like this

Datepicker

Let's see what we actually need here.
We need information of a particular month in a particular year, and they are:

  • the name of the month, obviously
  • all dates of that month,
  • few days of the last week(s) of the previous month
  • few days of the first week(s) of the following month

Define functions and methods

Based on that little list above, let's write our first function

getCalendar function

touch app/utils.js
// app/utils.js


export const monthMap = [
    'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec',
];


export const getCalendar = (year, month) => {
    const getLocale = (date, increment) =>
        new Date(year, month + increment, date).toLocaleDateString();
    const getDates = (start, end, index, increment) => {
        const dates = [];
        let idxCounter = index;
        for (let i = start; i <= end; i++) {
            dates.push({
                date: i,
                index: idxCounter,
                dateStr: getLocale(i, increment),
            });
            if (idxCounter === 6) idxCounter = 0;
            else idxCounter++;
        }
        return dates;
    };
    const date = new Date(year, month);
    const monthIndex = date.getDay();
    // returns 0-6, tells us on what day the month starts
    const monthLastIndex = new Date(year, month + 1, 0).getDay();
    // returns 0-6, adding it by one would tell us where the next month begins
    const dayCount = new Date(year, month + 1, 0).getDate();
    // returns 28-31, depends on the month
    const prevMonthCount = new Date(year, month, 0).getDate();
    const prevMonthStart = prevMonthCount - (!monthIndex ? 6 : monthIndex - 1);
    const nextMonthEnd = 41 - dayCount - (prevMonthCount - prevMonthStart);
    /**
     * Most people don't like it when the layout changes adjusting the content
     * so we keep the weeks displayed on the calendar fixed into 6
     */
    const prevMonth = getDates(prevMonthStart, prevMonthCount, 0, -1);
    const currMonth = getDates(1, dayCount, monthIndex, 0);
    const nextMonth = getDates(1, nextMonthEnd, monthLastIndex + 1, 1);
    const dates = prevMonth.concat(currMonth, nextMonth);

    return {
        month,
        year,
        monthName: monthMap[month],
        dates,
    };
};

with this input

const obj = getCalendar(2020, 8);
console.log(obj);

We expect this kind of output

obj = {
    month: 8,
    year: 2020,
    monthName: "Sep",
    dates: [
        {"date":30,"index":0,"dateStr":"8/30/2020"},
        {"date":31,"index":1,"dateStr":"8/31/2020"},
        {"date":1,"index":2,"dateStr":"9/1/2020"},
        // etc
    ],
}

Define states of the component

We'll create another file called DatePicker.jsx inside app/components/ directory. On the top of the file, we bring useReducer hook from React then followed by getCalendar function we wrote before.

mkdir app/components
touch app/components/DatePicker.jsx
import {useReducer} from 'react'; 
import {getCalendar} from '~/utils';

Then we create our main function of the component,

// app/components/DatePicker.jsx
import {useReducer} from 'react'; 
import {getCalendar} from '~/utils';

export default function DatePicker({id, label}) {
    const DATE = new Date();

    // These are our states by default
    const initialState = {
        calendar: getCalendar(DATE.getFullYear(), DATE.getMonth()),
        currentTime: {
            year: DATE.getFullYear(),
            month: DATE.getMonth(),
        },
        isCalendarShown: false,
        isInputSet: false,
        inputValue: '',
        isYearListShown: false,
        highlightedDate: DATE.getDate(),
    };
}

Below that initialState we write functions to alter values it holds.

// app/components/DatePicker.jsx
import {useReducer} from 'react'; 
import {getCalendar} from '~/utils';

export default function DatePicker({id, label}) {
    const DATE = new Date();

    // These are our states by default
    const initialState = { /* same as before */ };

    const reducer = (state, action) => {
        const obj = Object.assign({}, state);

        switch (action.type) {
            case 'showCalendar': {
                // Showing the calendar, that's all
                obj.isCalendarShown = true;
                return obj;
            }
            case 'hideCalendar': {
                // Hiding the calendar, obviously
                obj.isCalendarShown = false;
                return obj;
            }
            case 'setCalendar': {
                // Here we change the month and year that calendar displays
                const {data} = action;
                const d = new Date(data.year, data.month);
                obj.calendar = getCalendar(d.getFullYear(), d.getMonth());
                obj.isYearListShown = false;
                return obj;
            }
            case 'highlightDate': {
                /**
                 * This gets triggered as user clicks the date of the calendar
                 * There are several things we do here.
                 * Basically we only want to grab value of the date and set it
                 * into the input field
                 *
                 * Beside that, we also want to change the month of the calendar 
                 * if the date clicked  is whether at the beginning of
                 * the calendar (previous month) or near the end of the
                 * calendar (means first week of the next month)
                 */

                const toHighlight = new Date(action.data);
                const {
                    calendar: {year: calYear, month: calMonth},
                } = state;
                const isSameMonth =
                    new Date(calYear, calMonth).getMonth() ===
                    new Date(toHighlight).getMonth();

                if (!isSameMonth)
                    obj.calendar = getCalendar(
                        toHighlight.getFullYear(),
                        toHighlight.getMonth(),
                    );
                obj.isInputSet = true;
                obj.inputValue = toHighlight.toLocaleDateString();
                obj.highlightedDate = new Date(toHighlight).getDate();
                return obj;
            }

            case 'clearInput': {
                obj.isInputSet = false;
                return obj;
            }
            case 'showYearList': {
                obj.isYearListShown = true;
                return obj;
            }
            case 'hideYearList': {
                obj.isYearListShown = false;
                return obj;
            }
        }
    };
    const [state, dispatch] = useReducer(reducer, initialState);

    const {
        calendar: {monthName, dates, month, year},
        currentTime: {month: curMonth, year: curYear},
        highlightedDate,
        isCalendarShown,
        isInputSet,
        isYearListShown,
        inputValue,
    } = state;
   
    return (
        <>
           
        </>
    );
}

Let's actually show it, shall we?

We'll know want to see something we've done so far.
We bring these four icons from heroicons,

import {HiArrowSmLeft, HiArrowSmRight, HiCalendar, HiX} from 'react-icons/hi'; // <== here
import {useReducer} from 'react';

Within the same file, in our DatePicker function, in the return statement precisely. We throw this

// app/components/DatePicker.jsx
export default function DatePicker({id, label}){
    const { calendar /* same as before */ } = state;

    return (
        <>
            <label htmlFor={id}>{label}</label>
            <div className='relative w-full mb-3'>
                <input
                    type='text'
                    id={id}
                    placeholder='10/22/2021'
                    className='block p-2.5 w-full z-20 text-sm text-gray-900 bg-gray-50 rounded-md border border-gray-300 focus:ring-blue-500 focus:border-blue-500'
                />
               
                <button
                    type='button'
                    className='absolute top-0 right-0 p-2.5 text-sm font-medium text-white bg-slate-400 rounded-r-md border border-slate-500 hover:bg-slate-600 focus:ring-4 focus:outline-none focus:ring-blue-300'
                >
                    <HiCalendar size={20} />
                </button>
               
               {/* Calendar container*/}
                <div
                    className='z-10 p-2 bg-white rounded-lg border border-gray-200 shadow-md absolute left-0 top-[50px] w-full'
                >
                    {/* Calendar Header */}
                    <div className='flex justify-between'>
                        <button type='button' >
                            <HiArrowSmLeft size={22} />
                        </button>
                        <button type='button'>
                            {`${monthName + ' ' + year}`}
                        </button>
                        <button type='button'>
                            <HiArrowSmRight size={22} />
                        </button>
                    </div>
                    {/* Calendar Content */}
                    <div className='grid grid-cols-7 gap-2 text-center'>
                        <div className='text-red-400'>Su</div>
                        <div>Mo</div>
                        <div>Tu</div>
                        <div>We</div>
                        <div>Th</div>
                        <div>Fr</div>
                        <div>Sa</div>
                        
                    </div>
                    {/* Calendar Footer */}
                    <div className='flex justify-between mt-2 text-sm px-2 text-blue-400'>
                        <button title='Clear input' type='button'>
                            Clear
                        </button>
                        <button title='Close' type='button'>
                            <HiX size={20} />
                        </button>
                        <button type='button' title="Show today's date">
                            Today
                        </button>
                    </div>
                </div>
            </div>
        </>
    )
}

We'll get rid of everything inside app/routes/index.jsx, and paste this instead.

// app/routes/index.jsx
import {Form} from '@remix-run/react';
import DatePicker from '~/components/DatePicker';
export default function Index() {
    return (
        <Form>
            <div className='flex pt-14 md:pt-24 justify-center md:justify-end md:pr-20 '>
                <div className='p-4 w-80 border rounded'>
                    <h1 className='text-[25px]'>
                        Create an account to continue!
                    </h1>
                    <label htmlFor='name'>Username</label>
                    <div className='relative w-full mb-3'>
                        <input
                            type='text'
                            id='name'
                            className='block p-2.5 w-full z-20 text-sm text-gray-900 bg-gray-50 rounded-md border border-gray-300 focus:ring-blue-500 focus:border-blue-500'
                            placeholder='yourname'
                        />
                    </div>
                    <DatePicker id='dateOfBirth' label='Birthday' />
                    <label htmlFor='password'>Password</label>
                    <div className='relative w-full mb-3'>
                        <input
                            type='password'
                            id='password'
                            className='block p-2.5 w-full z-20 text-sm text-gray-900 bg-gray-50 rounded-md border border-gray-300 focus:ring-blue-500 focus:border-blue-500'
                            placeholder='******'
                        />
                    </div>
                    <div className='text-center'>
                        <button
                            className='rounded bg-blue-400 border border-blue-700 text-white px-2 py-0.5 hover:bg-blue-500'
                            type='submit'
                        >
                            Sign up
                        </button>
                    </div>
                </div>
            </div>
        </Form>
    );
}

The next thing we want to do is to show the dates from getCalendar function which now stored in state.calendar variable

We'll create another file for that, and let's call it CalendarDate.jsx which will reside inside the app/components/ directory

touch app/components/CalendarDate.jsx
// app/components/CalendarDate.jsx
export default function CalendarDate(props) {
    const {
        calendar: {dates, month, year},
        highlightedDate,
    } = props;

    
    const getDateButton = (date) => {
        const {dateStr, date: d, index: i} = date;

        const addZero = (n) => (parseInt(n) < 10 ? `0${n}` : n);

        const style = () => {
            const isToday =
                new Date().toDateString() === new Date(dateStr).toDateString();
            const isCurrenMonth =
                new Date(year, month).getMonth() ===
                new Date(dateStr).getMonth();

            const isHighlighted = parseInt(d) === parseInt(highlightedDate);

            // I put these colors classes on variables that it helps me to organize the ideas better in taking the desicions . You can move these classes inside the ifs elses blocks below if you want to
            const defStyle = 'hover:bg-slate-300 rounded-full';
            const sunday = 'text-red-400';
            const sundayInactive = 'text-red-200';
            const colorInactive = 'text-slate-400';
            const bgToday = 'bg-orange-400';
            const colorToday = 'text-white drop-shadow-xl shadow-black';
            const bgHighlight = 'bg-slate-500';

            let bgColor = '';
            let color = '';

            // this applies to the main month of the calendar, while the previous and the following month would go into else statement
            if (isCurrenMonth) {
                if (!i) color = sunday;

                if (isHighlighted) bgColor = bgHighlight;

                if (isToday) {
                    color = colorToday;
                    bgColor = bgToday;
                }
            } else {
                if (!i) color = sundayInactive;
                else color = colorInactive;
            }
            return `${defStyle} ${color} ${bgColor}`;
        };

        return (
            <button
                type='button'
                className={style()}
                key={dateStr}
            >
                {addZero(d)}
            </button>
        );
    };

    return <>{dates.map((date) => getDateButton(date))}</>;
}

Then we'll import that component into our DatePicker,

// app/components/DatePicker.jsx
import {HiArrowSmLeft, HiArrowSmRight, HiCalendar, HiX} from 'react-icons/hi';
import {useReducer} from 'react';
import {getCalendar} from '~/utils.js'
import {CalendarDate} from './CalendarDate';

still in the same file, we go into "Calendar content" section, right below names of days, we write this

<div className='grid grid-cols-7 gap-2 text-center'>
    <div className='text-red-400'>Su</div>
    <div>Mo</div>
    <div>Tu</div>
    <div>We</div>
    <div>Th</div>
    <div>Fr</div>
    <div>Sa</div>
    {/* change below*/}
    <CalendarDate calendar={state.calendar} highlightedDate={highlightedDate} />
</div>

Make it interactive

Up to this point, we have our calendar shown. This is the time to make it alive.
This is the list of things we'd accomplish:

  • Show/hide the calendar
  • Navigate through different months
  • Highlight a particular date, and set that date as a value of the input field as the user clicks on it.
  • Clear the input field, and
  • Add a list of years and month
    As we've already defined the functions earlier, it's now just the matter of attaching those functions to the elements.

Show/hide calendar

By default we want that calendar to be hidden, and only visible when the user clicks on the button. We'll trigger these two functions this time

const reducer = (state, action) => {
    const obj = Object.assign({}, state);

    switch (action.type) {                  
        case 'showCalendar': {
            obj.isCalendarShown = true;     // <== this function
            return obj;
        }
        case 'hideCalendar': {              // <== and this one
            obj.isCalendarShown = false;
            return obj;
        }

        // same as before
    }
}

Now find a button element with calendar icon inside the return statement of DatePicker function,

  • insert an onClick attribute to it.
  • we want it to also hides itself as it shows the calendar,
  • And of course, to make it work, We'll also need to wrap entire calendar container element into an expression.
// app/components/DatePicker.jsx
{!isCalendarShown && (
    <button
        onClick={() => dispatch({type: 'showCalendar'}) /* <== here */ }   
        type='button'
        className='absolute top-0 right-0 p-2.5 text-sm font-medium text-white bg-slate-400 rounded-r-md border border-slate-500 hover:bg-slate-600 focus:ring-4 focus:outline-none focus:ring-blue-300'
    >
        <HiCalendar size={20} />
    </button>
)}

{/* Calendar container*/}
{isCalendarShown && (
    <div className='z-10 p-2 bg-white rounded-lg border border-gray-200 shadow-md absolute left-0 top-[50px] w-full' >
        {/* Calendar Header */}
        <div className='flex justify-between'> 
            {/* same as before */}
        </div>
        {/* Calendar Content */}
        <div className='grid grid-cols-7 gap-2 text-center'> 
            {/* same as before */}
        </div>
        {/* Calendar Footer */}
        <div className='flex justify-between mt-2 text-sm px-2 text-blue-400'>
            {/* same as before */}
         </div>
    </div>
)}

Next, within that "Calendar footer" we need to find a button with an x icon on it, which is responsible to hide the calendar.

// app/components/DatePicker.jsx
{/* Calendar Footer */}
<div className='flex justify-between mt-2 text-sm px-2 text-blue-400'>
    <button title='Clear input' type='button'>
        Clear
    </button>
    <button 
        onClick={() => dispatch({type: "hideCalendar"}) /* <== here */}
        title='Close' type='button'>
        <HiX size={20} />
    </button>
    <button type='button' title="Show today's date">
        Today
    </button>
</div>

Move to the next/previous month

Early on, we have that state.calendar variable that holds an object from getCalendar function based on arguments that are passed on it, which by default are set to current month and current year at the moment we load the page.

What we actually need to do here is to change the value of that year and month arguments that we pass to the getCalendar function, which happens here.

const reducer = (state, action) => {
    const obj = Object.assign({}, state);

    switch (action.type) {
        case 'showCalendar': { /* same as before */ }
        case 'hideCalendar': { /* same as before */ } 
        case 'setCalendar': { // <== this one
            // Here we change the month and year that calendar displays
            const {data} = action;
            const d = new Date(data.year, data.month);
            obj.calendar = getCalendar(d.getFullYear(), d.getMonth());
            obj.isYearListShown = false;
            return obj;
        }
        // same as before
    }
}

In the return statement, There's a block that says "Calendar header", we need to modify two of three buttons we have inside that block.

// app/components/DatePicker.jsx
{/* Calendar Header */}
<div className='flex justify-between'>
    <button 
        onClick={() => // <== here 
            dispatch({
                type: 'setCalendar',
                data: {month: month - 1, year}
            })
        }
        type='button' >
        <HiArrowSmLeft size={22} />
    </button>
    <button type='button'>
        {`${monthName + ' ' + year}`}
    </button>
    <button
        onClick={() => // <== here 
            dispatch({
                type: 'setCalendar',
                data: {month: month - 1, year}
            })
        }
        type='button'>
        <HiArrowSmRight size={22} />
    </button>
</div>

By passing this month = month + 1 or month = month - 1, we don't need to worry in cases where month = 11 (December), for example, then adding one into it would make it twelve and causes JavaScript throws us an error. No it wouldn't. It would instead becomes the thirteenth month of the year we pass.

So in case we write 2020 and 12, it goes thirteen months started from January of 2020, which in this case, returns January 2021. The same thing, 2019 and -2 would fall to November of 2018.

Highlight date and set the input value

We want to pass that dispatch function into CalendarDate component

// app/components/DatePicker.jsx

 <CalendarDate 
    fn={dispatch /* this */} 
    calendar={state.calendar} 
    highlightedDate={highlightedDate} />

In the CalendarDate.jsx, we do this

// app/components/CalendarDate.jsx
export default function CalendarDate(props){
    const {
        calendar: {dates, month, year},
        highlightedDate,
        fn, // <== this
    }
}

and this

// app/components/CalendarDate.jsx
return (
    <button
        onClick={() => fn({type: 'highlightDate', data: dateStr}) /* this */}
        type='button'
        className={style()}
        key={dateStr}
    >
        {addZero(d)}
    </button>
);

Clear input

This happens in the Calendar footer, the first children of the flex justify-between div, the button that says "Clear"

// app/components/DatePicker.jsx
{/* Calendar Footer */}
<div className='flex justify-between mt-2 text-sm px-2 text-blue-400'>
    <button 
        onClick={() => dispatch({type: "clearInput"}) /* <== here */} 
        title='Clear input' type='button'>
        Clear
    </button>
    <button onClick={() => dispatch({type: "hideCalendar"})} title='Close' type='button'>
        <HiX size={20} />
    </button>
    <button type='button' title="Show today's date">
        Today
    </button>
</div>

Go home

// app/components/DatePicker.jsx
{/* Calendar Footer */}
<div className='flex justify-between mt-2 text-sm px-2 text-blue-400'>
    <button onClick={() => dispatch({type: "clearInput"})} title='Clear input' type='button'>
        Clear
    </button>
    <button onClick={() => dispatch({type: "hideCalendar"})} title='Close' type='button'>
        <HiX size={20} />
    </button>
    <button 
        onClick={() => dispatch({
            type: "setCalendar", 
            data: {year: curYear, month: curMonth} 
        })}
        type='button' title="Show today's date">
        Today
    </button>
</div>

In case we travel too far to the world of future imagination, or even trapped in the ocean of the past painful memory, the button on the right side of the calendar footer would bring us back to the present

Add year option

Now, things have been fine if we only need to move back and forth within a couple of months before/after the current time.
But if we need a date of, let's say, fourteen years back, or twenty one years. It's not convenient that we need to hit those arrows button several hundred times to go several hundreds months, is it?.
Now let's add that,

touch app/components/YearList.jsx
// app/components/YearList.jsx
import {HiChevronUp, HiChevronDown} from 'react-icons/hi';
import {useState, useEffect} from 'react';
export default function YearList(props) {
    const {months, curYear, fn} = props;
    const getYears = () => {
        const years = [];
        for (let i = 1901; i <= 2200; i++) {
            years.push(i);
        }
        return years;
    };

    const [selectedYear, selectYear] = useState(curYear);

    // by setting it to zero, we remove children component that has list of months inside of it.
    const handleYearSelect = (year) => {
        if (selectedYear !== year) selectYear(year);
        else selectYear(0);
    };

    const years = getYears();

    useEffect(() => {
        // We don't want the user to scroll too far, do we?
        const yearListContainer = document.getElementById('yearListContainer');
        const yearIndex = years.indexOf(selectedYear);

        // I got this magic number just by trying and failing, not perfect, but still works fine
        const yearHeight = 25.041322314049587;
        if (selectedYear)
            yearListContainer.scroll({
                top: yearHeight * yearIndex,
                behavior: 'smooth',
            });
    }, [selectedYear]);
    return (
        <div
            id='yearListContainer'
            className='z-10 p-2 bg-white rounded-lg border border-gray-200 shadow-md absolute left-0 top-[50px] w-full h-64 overflow-y-auto'
        >
            {years.map((year) => (
                <div key={year}>
                    <button
                        onClick={() => handleYearSelect(year)}
                        className='w-full flex justify-between border-b-[1px] border-blue-400 '
                    >
                        <div>{year}</div>
                        {selectedYear === year ? (
                            <HiChevronUp />
                        ) : (
                            <HiChevronDown />
                        )}
                    </button>
                    <div className='grid grid-cols-3 gap-2'>
                        {selectedYear === year &&
                            months.map((month, i) => (
                                <button
                                    key={month}
                                    onClick={() =>
                                        fn({
                                            type: 'setCalendar',
                                            data: {year, month: i},
                                        })
                                    }
                                >
                                    {month}
                                </button>
                            ))}
                    </div>
                </div>
            ))}
        </div>
    );
}

Bring it on to the table!

// app/components/DatePicker.jsx
import {HiArrowSmLeft, HiArrowSmRight, HiCalendar, HiX} from 'react-icons/hi';
import {useReducer} from 'react';
import {getCalendar} from '~/utils.js'
import {CalendarDate} from './CalendarDate';
import {YearList} from './YearList';

We want that either calendar or the list of year that is shown.
Now that we previously have already wrapped that calendar container into an expression, we'd just change that a little bit.

What we have earlier

{/* Calendar container */}
{isCalendarShown && (
    <div className='z-10 p-2 bg-white rounded-lg border border-gray-200 shadow-md absolute left-0 top-[50px] w-full' >
        {/* and so on */} 
    </div>
)}

need to to be modified into this.

{/* Calendar container */}

{isCalendarShown && 
    (isYearListShown ? 
        (
            <YearList months={monthMap} curYear={DATE.getFullYear} fn={dispatch}>
        ) :
        (
            <div className='z-10 p-2 bg-white rounded-lg border border-gray-200 shadow-md absolute left-0 top-[50px] w-full' >
                {/*and so on */}
            </div>
        )
    
    )
}

Then to control the YearList component, we do it in the "Calendar header" section, and we add onClick attribute to the button in the middle

{/* Calendar Header */}
<button 
    type='button' 
    onClick={() => dispatch({type: "showYearList"})}>
    {`${monthName + ' ' + year}`}
</button>
  

ClickOutsideHide

Alright, things have been good so far, but if you notice. Even if the calendar element goes away when we click the x button, it remains there if we click outside of it. Well, we normally want it to be hidden as well in cases like this.
Well, let's do it!

We want to import two more things from react, useRef and useEffect

// app/components/DatePicker.jsx
import {HiArrowSmLeft, HiArrowSmRight, HiCalendar, HiX} from 'react-icons/hi';
import {useReducer, useRef, useEffect} from 'react'; // <== here
import {getCalendar} from '~/utils.js'
import {CalendarDate} from './CalendarDate';

We need to refer to those elements,

export default function DatePicker({id, label}){
    const DATE = new Date();
    const calRef = useRef(null); // <== this
    const yearListRef = useRef(null); // <== and this one
    // same as before
}
// app/components/DatePicker.jsx
 {isCalendarShown &&
    (isYearListShown ? (
        <YearList
            months={monthMap}
            curYear={DATE.getFullYear()}
            fn={dispatch}
            Ref={yearListRef /* this*/}
        />
    ) : (
        <div
            ref={calRef /* and this */}
            className='z-10 p-2 bg-white rounded-lg border border-gray-200 shadow-md absolute left-0 top-[50px] w-full'
        >
            {/* same as before */}
        </div>
    ))
 }

go to our YearList.jsx, then add this,

// app/components/YearList.jsx
export default function YearList(props) {
   const {months, curYear, fn, Ref} = props; // <== here
   const getYears = () => {
       // same as before
   }
   // same as before
}

Now we go back to DatePicker, where we'd define functions to hande that above the DatePicker function declaration.

// app/components/DatePicker.jsx

const clickOutsideHide = (el, fn, fnPar) => {
    const listener = (event) => {
        if(!el.current || el.current.contains(event.target)) return;

        fn(fnPar);
    }
    document.addEventListener('mousedown', listener);
    document.addEventListener('touchstart', listener);

    return () => {
        document.removeEventListener('mousedown', listener);
        document.removeEventListener('touchstart', listener);
    };
};

export default function DatePicker({id, label}){
    // same as before
}

Still in the same file, we now put this just before the return statement,

// app/components/DatePicker.jsx

useEffect(() => {
    if (state.isCalendarShown) {
        const fnPar = {type: 'hideCalendar'};
        clickOutsideHide(calRef, dispatch, fnPar);
    }
    if (state.isYearListShown) {
        const fnPar = {type: 'hideYearList'};
        clickOutsideHide(yearListRef, dispatch, fnPar);
    }
}, [state]);

return (
    <>
        {/* same as before */}
    </>
)

Wrap up

That's everything for this article, hope you find it useful. And of course, there's a lot of things can be improved in my code. To mention some, we didn't really talk a lot about styling,

The full project can be found here.

Resources


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK