为什么不? — 事实证明,这很容易。

在本文中,我们将从头开始创建一个简单的反应日期选择器。 作为我们的日期操作助手,我们将使用一个名为“Day.js”的库。 请不要太在意造型。 我们的目标是创建一个基本组件,您可以随意修改它。
那么什么是 Day.js?
Day.js 是一个极简的 JavaScript 库,用于解析、验证、操作和显示日期。 它非常轻量级(只有 2 kB!)并且公开了非常简单的 API。
为什么我们不能只使用“react-datepicker”之类的库?
首先,这篇文章一点都不有趣,如果我只是告诉你如何安装一个库)。 此外,我认为在某些情况下,无需任何 3rd 方包(通常很难定制)即可轻松实现。
我们的最终结果是什么?

我们这里有什么功能?
- 显示当前月份日期的日历,带有工作日的名称。 此外,它可以向前或向后翻转。
- 选择特定日期的功能。 默认情况下,将选择当前日期。
让我们开始吧!
依赖关系。
首先,我们需要安装我们的依赖项。 正如我之前提到的,我们将使用 Day.js 和名为“clsx”的库,它允许我们轻松组合类名(它完全是可选的,我只是更喜欢使用它)

应用程序组件。
一切都很简单。 我们将为我们的应用程序创建一个框架并为我们的日历添加一个状态。 请注意,我们在这里调用 dayjs 作为我们的初始值。 它返回包含当前日期信息的 dayjs 实例。 此外,我们格式化所选日期并将其放在标题中。 不用担心 DatePicker 的导入,我们将在下面稍微实现它。
import React, { useState } from "react";
import dayjs from "dayjs";
import { DatePicker } from "./components/DatePicker/DatePicker";
export interface IAppProps {}
export const App: React.FC<IAppProps> = () => {
const [date, setDate] = useState(dayjs());
return (
<div className="app__container">
<div className="app">
<h4 className="app__title">
Picked Date: {date.format("DD - MMMM - YYYY")}
</h4>
<DatePicker selectedDate={date} onChange={setDate} />
</div>
</div>
);
};
日期选择器组件
接下来,我们应该创建我们的 datepicker 根组件。 我会把它分成两个独立的部分。
第一部分是 DatePickerSelector,这是包含我们翻转逻辑的地方。
第二部分称为 DatePickerCalendar 负责显示日历并选择特定日期。
我们的组件接收两个道具:
- selectedDate 是 dayjs 实例,代表选择的日期。
- onChange 是在单击日期单元格时调用的处理程序,并将新日期作为其第一个参数。
import React, { useState } from "react";
import type { Dayjs } from "dayjs";
import { DatePickerCalendar } from "./DatePickerCalendar/DatePickerCalendar";
import { DatePickerSelector } from "./DatePickerSelector/DatePickerSelector";
import "./DatePicker.css";
export interface IDatePickerProps {
selectedDate: Dayjs;
onChange: (newDate: Dayjs) => void;
}
export const DatePicker: React.FC<IDatePickerProps> = ({
selectedDate,
onChange
}) => {
const [shownDate, setShownDate] = useState(selectedDate);
return (
<div className={"datePicker"}>
<DatePickerSelector
shownDate={shownDate}
setShownDate={setShownDate}
/>
<DatePickerCalendar
selectedDate={selectedDate}
shownDate={shownDate}
onChange={onChange}
/>
</div>
);
};
另外,请注意我们还有另一个本地状态,称为显示日期。 它将负责在我们的日历中显示正确的月份和年份。
DatePickerSelector 组件
正如我之前提到的,这个组件将包含我们的翻转逻辑。 这里没什么特别的,我们只是处理我们的点击事件并按月增加/减少我们的显示日期。
import React from "react";
import { Dayjs } from "dayjs";
import { ChevronDownIcon } from "../../icons/ChevronDownIcon";
import { changeDateMonth } from "../utils";
import "./DatePickerSelector.css";
import clsx from "clsx";
export interface IDatePickerSelectorProps {
shownDate: Dayjs;
setShownDate: React.Dispatch<React.SetStateAction<Dayjs>>;
}
export const DatePickerSelector: React.FC<IDatePickerSelectorProps> = ({
shownDate,
setShownDate
}) => {
const handleIconClick = (isNextMonth: boolean) => {
return () => {
setShownDate(changeDateMonth(shownDate, isNextMonth));
};
};
return (
<div className={"DatePickerSelector"}>
<div
className={clsx(
"DatePickerSelector__icon",
"DatePickerSelector__iconLeft"
)}
onClick={handleIconClick(false)}
>
<ChevronDownIcon />
</div>
<div className={"DatePickerSelector__date"}>
{shownDate.format("MMMM YYYY")}
</div>
<div
className={clsx(
"DatePickerSelector__icon",
"DatePickerSelector__iconRight"
)}
onClick={handleIconClick(true)}
>
<ChevronDownIcon />
</div>
</div>
);
};
changeDateMonth 是一个函数,它决定我们是否还需要增加或减少一年。
import { Dayjs } from "dayjs";
export function changeDateMonth(date: Dayjs, isNextMonth: boolean): Dayjs {
// should decrease year
if (date.month() === 0 && !isNextMonth) {
return date.set("year", date.year() - 1).set("month", 11);
}
// should increase year
if (date.month() === 11 && isNextMonth) {
return date.set("year", date.year() + 1).set("month", 0);
}
// add or substract
return date.add(isNextMonth ? 1 : -1, "month");
}
DatePickerCalendar 组件
为了实现这部分,我们需要在前面创建一些助手。 首先,让我们创建一个表示日历中单个单元格的接口,它将被称为 ICalendarCell。 此外,我们将编写另外两个函数:
- getCalendarCells 将获取当前显示的日期并返回一个单元格数组。 此外,它将添加上个月和下个月的天数,以完全填满我们的日历行。
- getCalendarRows 将为我们的日历返回一个行数组。
// our interface for a single cell
export interface ICalendarCell {
text: string;
value: Dayjs;
}
function getCalendarCells(date: Dayjs): ICalendarCell[] {
const daysInMonth = date.daysInMonth();
const calendarCells: ICalendarCell[] = [];
const prepareCell = (date: Dayjs, dayNumber: number) => {
return {
text: String(dayNumber),
value: date.clone().set("date", dayNumber)
};
};
// push current month day cells
for (let i = 0; i < daysInMonth; i++) {
calendarCells.push(prepareCell(date, i + 1));
}
// how much more we need to add?
const cellsToAdd = 35 - daysInMonth;
// add to start from prev month
const lastMonth = date.subtract(1, "month");
for (let i = 0; i < Math.floor(cellsToAdd / 2); i++) {
calendarCells.unshift(prepareCell(lastMonth, lastMonth.daysInMonth() - i));
}
// add to end from next month
const nextMonth = date.add(1, "month");
for (let i = 0; i < Math.round(cellsToAdd / 2); i++) {
calendarCells.push(prepareCell(nextMonth, i + 1));
}
return calendarCells;
}
export function getCalendarRows(date: Dayjs): Array<ICalendarCell[]> {
const cells = getCalendarCells(date);
const rows: Array<ICalendarCell[]> = [];
// split one array into chunks
for (let i = 0; i < cells.length; i += 7) {
rows.push(cells.slice(i, i + 7));
}
return rows;
}
在此之后,我们可以转到我们的 UI。
import React, { useMemo } from "react";
import { Dayjs } from "dayjs";
import clsx from "clsx";
import { getCalendarRows } from "../utils";
import "./DatePickerCalendar.css";
export interface IDatePickerCalendarProps {
shownDate: Dayjs;
selectedDate: Dayjs;
onChange: (newDate: Dayjs) => void;
}
export const DatePickerCalendar: React.FC<IDatePickerCalendarProps> = ({
shownDate,
selectedDate,
onChange
}) => {
const handleSelectDate = (value: Dayjs) => {
return () => onChange(value);
};
const rows = useMemo(() => getCalendarRows(shownDate), [shownDate]);
return (
<>
<div className={"DatePickerCalendar__header"}>
{rows[0].map(({ value }, i) => (
<div key={i} className={"DatePickerCalendar__cell"}>
{value.format("dd")}
</div>
))}
</div>
{rows.map((cells, rowIndex) => (
<div key={rowIndex} className={"DatePickerCalendar__row"}>
{cells.map(({ text, value }, i) => (
<div
key={`${text} - ${i}`}
className={clsx(
"DatePickerCalendar__cell",
"DatePickerCalendar__dayCell",
{
DatePickerCalendar__dayCell_selected:
value.toString() === selectedDate.toString()
}
)}
onClick={handleSelectDate(value)}
>
{text}
</div>
))}
</div>
))}
</>
);
};
这里的一切都很简单:
- handleSelectDate 是一个选择日期处理程序,它利用闭包并使用所需日期调用 onChange。
- rows 是我们的 getCalendarRows 的记忆返回值。 所以它只会在显示的日期发生变化时重新计算。
- 在 29-35 行中,我们渲染工作日。
- 在 37-56 行中,我们渲染日历日行。 此外,我们会根据所选日期附加适当的班级名称。
这就是本文的内容。 现在我们的 Datepicker 组件已经可以使用了。