howenhuo

2018-12-09 16:18

如何使用 React 构建自定义日期选择器(1)

本文作者:IMWeb howenhuo 原文出处:IMWeb社区 未经同意,禁止转载

在 web 上经常看到包含一个或多个日期的表单。无论是出生日期还是航班时间表日期,您总希望用户能够提供了有效的日期。

在 HTML5 中,引入了新的 date 输入类型,来确保获取表单中的有效日期值。 date 输入类型的默认行为是向用户显示日期选择器。但是,这个日期选择器的外观在不同浏览器之间并不一致。

您可以在 这里 找到更多关于 date 输入类型和浏览器支持的信息。

在本教程中,您将学习如何使用 React 和原生 JavaScript日期对象从头构建自定义日期选择器。下面是一个简短的演示,展示了日期选择器的外观。

您还可以在 Code Sandbox 上查看演示

先决条件

本教程假设您非常熟悉 JavaScript,并且已经熟悉 React 框架。如果不是这样,您可以查看 React文档 来了解有关React的更多信息。

在开始之前,您需要确保您的计算机上已经安装了 Node。建议您在机器上安装 Yarn 包管理器,因为它将代替 Node 附带的 npm。您可以按照此 Yarn 安装指南 在您的机器上安装 Yarn。

React 应用程序的样板代码将使用 create-react-app 包创建。您还需要确保它在您的机器上是全局安装的。如果您使用 npm >= 5.2,那么您不需要将 create-react-app 作为一个全局依赖项安装——您可以使用 npx 命令。

开始

创建新的应用程序

使用以下命令创建新的 React 应用程序。您可以随意命名应用程序。

create-react-app react-datepicker

npm> = 5.2 如果您使用的是 npm 5.2 或更高版本,它会附带一个额外的 npx 二进制文件。 使用 npx 二进制文件,您无需在计算机上全局安装 create-react-app。 您可以使用以下简单命令创建新的 React 应用程序:

npx create-react-app react-datepicker

安装依赖

这个应用程序的依赖尽可能地保持精简。运行以下命令安装所需的依赖项。

yarn add bootstrap reactstrap styled-components prop-types

引入 Bootstrap CSS

本教材为了方便,直接使用 bootstrap 来提供一些默认样式。请编辑 src/index 并在其他 import 语句之前添加以下行。

import 'bootstrap/dist/css/bootstrap.min.css';

目录设置

对于这个应用程序,需要两个主要组件。

  • Calendar组件:它渲染带有日期选择功能的自定义日历。
  • Datepicker组件:它渲染日期输入并显示选择日期的日历。

每个组件都将包含在自己的目录中,其中包含两个文件——index.jsstyles.jsindex.js 导出组件,而 styles.js 导出组件所需样式的样式化组件。

从项目根目录运行以下命令来创建组件目录和文件:

# Create directories
mkdir -p src/components/Calendar src/components/Datepicker

# Create files
(cd src/components/Calendar && touch index.js styles.js)
(cd src/components/Datepicker && touch index.js styles.js)

由于在这个应用程序中不需要外部依赖来处理日期,因此需要自己编写日期处理的 helper 函数。运行以下命令来创建 calendar helper 模块。

mkdir -p src/helpers
touch src/helpers/calendar.js

启动应用程序

通过在终端上使用 yarn 运行以下命令来启动应用程序:

yarn start

应用程序现在已经启动,可以开始开发了。请注意,已经为您打开了一个浏览器选项卡,该选项卡具有实时重新加载功能,以便在开发时与应用程序中的更改保持同步。

Calendar helper 模块

基本常量和 helper 函数

首先,定义一些构建日历所需的日历常量和 helper 函数。它们将在前面创建的 calendar helper 模块中定义并导出。

将以下内容添加到 src/helpers/calendar.js 文件中。

// (int) The current year
export const THIS_YEAR = +(new Date().getFullYear());

// (int) The current month starting from 1 - 12
// 1 => January, 12 => December
export const THIS_MONTH = +(new Date().getMonth()) + 1;

// Week days names and shortnames
export const WEEK_DAYS = {
  Sunday: "Sun",
  Monday: "Mon",
  Tuesday: "Tue",
  Wednesday: "Wed",
  Thursday: "Thu",
  Friday: "Fri",
  Saturday: "Sat"
}

// Calendar months names and shortnames
export const CALENDAR_MONTHS = {
  January: "Jan",
  February: "Feb",
  March: "Mar",
  April: "Apr",
  May: "May",
  June: "Jun",
  July: "Jul",
  August: "Aug",
  September: "Sep",
  October: "Oct",
  November: "Nov",
  December: "Dec"
}

// Weeks displayed on calendar
export const CALENDAR_WEEKS = 6;

// Pads a string value with leading zeroes(0) until length is reached
// For example: zeroPad(5, 2) => "05"
export const zeroPad = (value, length) => {
  return `${value}`.padStart(length, '0');
}

// (int) Number days in a month for a given year from 28 - 31
export const getMonthDays = (month = THIS_MONTH, year = THIS_YEAR) => {
  const months30 = [4, 6, 9, 11];
  const leapYear = year % 4 === 0;

  return month === 2
    ? leapYear
      ? 29
      : 28
    : months30.includes(month)
      ? 30
      : 31;
}

// (int) First day of the month for a given year from 1 - 7
// 1 => Sunday, 7 => Saturday
export const getMonthFirstDay = (month = THIS_MONTH, year = THIS_YEAR) => {
  return +(new Date(`${year}-${zeroPad(month, 2)}-01`).getDay()) + 1;
}

这个代码片段包含注释,解释每个 helper 函数都做了什么。然而,有几件事值得指出。

首先,Date.prototype 中的 getDay()getMonth() 方法通常会返回从零开始的值。因此,一年的第一个月(January)是 0,December 是 11,而一周的第一天(Sunday)是 0,Saturday 是 7

在前面的代码片段中,您会看到 1 总是被添加到这些从零开始的值中,因此 Sunday 为 1 ,December 为 12

还要注意,CALENDAR_WEEKS 被设置为 6。由于一个月通常跨越 4 周,因此日历至少可以容纳上个月的最后一周和下个月的第一周。您很快就会看到这个常量的效果,因为它将在 calendar builder 函数中使用。

额外的 helper 函数

将以下内容附加到 src/helper/calendar.js 文件中,添加一些额外的辅助功能到 calendar helper 模块。

// (bool) Checks if a value is a date - this is just a simple check
export const isDate = date => {
  const isDate = Object.prototype.toString.call(date) === '[object Date]';
  const isValidDate = date && !Number.isNaN(date.valueOf());

  return isDate && isValidDate;
}

// (bool) Checks if two date values are of the same month and year
export const isSameMonth = (date, basedate = new Date()) => {

  if (!(isDate(date) && isDate(basedate))) return false;

  const basedateMonth = +(basedate.getMonth()) + 1;
  const basedateYear = basedate.getFullYear();

  const dateMonth = +(date.getMonth()) + 1;
  const dateYear = date.getFullYear();

  return (+basedateMonth === +dateMonth) && (+basedateYear === +dateYear);

}

// (bool) Checks if two date values are the same day
export const isSameDay = (date, basedate = new Date()) => {

  if (!(isDate(date) && isDate(basedate))) return false;

  const basedateDate = basedate.getDate();
  const basedateMonth = +(basedate.getMonth()) + 1;
  const basedateYear = basedate.getFullYear();

  const dateDate = date.getDate();
  const dateMonth = +(date.getMonth()) + 1;
  const dateYear = date.getFullYear();

  return (+basedateDate === +dateDate) && (+basedateMonth === +dateMonth) && (+basedateYear === +dateYear);

}

// (string) Formats the given date as YYYY-MM-DD
// Months and Days are zero padded
export const getDateISO = (date = new Date) => {

  if (!isDate(date)) return null;

  return [
    date.getFullYear(),
    zeroPad(+date.getMonth() + 1, 2),
    zeroPad(+date.getDate(), 2)
  ].join('-');

}

// ({month, year}) Gets the month and year before the given month and year
// For example: getPreviousMonth(1, 2000) => {month: 12, year: 1999}
// while: getPreviousMonth(12, 2000) => {month: 11, year: 2000}
export const getPreviousMonth = (month, year) => {
  const prevMonth = (month > 1) ? month - 1 : 12;
  const prevMonthYear = (month > 1) ? year : year - 1;

  return { month: prevMonth, year: prevMonthYear };
}

// ({month, year}) Gets the month and year after the given month and year
// For example: getNextMonth(1, 2000) => {month: 2, year: 2000}
// while: getNextMonth(12, 2000) => {month: 1, year: 2001}
export const getNextMonth = (month, year) => {
  const nextMonth = (month < 12) ? month + 1 : 1;
  const nextMonthYear = (month < 12) ? year : year + 1;

  return { month: nextMonth, year: nextMonthYear };
}

Default export

最后,这里是 calendar helper 模块的默认导出——calendar builder 函数。该函数以 monthyear 作为参数,并返回一个包含 42 个元素的数组,每个元素以 [YYYY, MM, DD] 的格式表示日历日期。

下面是 calendar builder 函数。将此代码段追加到 src/helper/calendar.js 文件。

// Calendar builder for a month in the specified year
// Returns an array of the calendar dates.
// Each calendar date is represented as an array => [YYYY, MM, DD]

export default (month = THIS_MONTH, year = THIS_YEAR) => {

  // Get number of days in the month and the month's first day

  const monthDays = getMonthDays(month, year);
  const monthFirstDay = getMonthFirstDay(month, year);

  // Get number of days to be displayed from previous and next months
  // These ensure a total of 42 days (6 weeks) displayed on the calendar

  const daysFromPrevMonth = monthFirstDay - 1;
  const daysFromNextMonth = (CALENDAR_WEEKS * 7) - (daysFromPrevMonth + monthDays);

  // Get the previous and next months and years

  const { month: prevMonth, year: prevMonthYear } = getPreviousMonth(month, year);
  const { month: nextMonth, year: nextMonthYear } = getNextMonth(month, year);

  // Get number of days in previous month
  const prevMonthDays = getMonthDays(prevMonth, prevMonthYear);

  // Builds dates to be displayed from previous month

  const prevMonthDates = [...new Array(daysFromPrevMonth)].map((n, index) => {
    const day = index + 1 + (prevMonthDays - daysFromPrevMonth);
    return [ prevMonthYear, zeroPad(prevMonth, 2), zeroPad(day, 2) ];
  });

  // Builds dates to be displayed from current month

  const thisMonthDates = [...new Array(monthDays)].map((n, index) => {
    const day = index + 1;
    return [year, zeroPad(month, 2), zeroPad(day, 2)];
  });

  // Builds dates to be displayed from next month

  const nextMonthDates = [...new Array(daysFromNextMonth)].map((n, index) => {
    const day = index + 1;
    return [nextMonthYear, zeroPad(nextMonth, 2), zeroPad(day, 2)];
  });

  // Combines all dates from previous, current and next months
  return [ ...prevMonthDates, ...thisMonthDates, ...nextMonthDates ];

}

请注意,calendar builder 在数组中返回的日历日期从上一个月最后一周的日期到给定月份的日期,再到下一个月第一周的日期。

0条评论

    您需要 注册 一个IMWeb账号或者 才能进行评论。