howenhuo

2018-12-09 17:42

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

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

接着上一篇:如何使用 React 构建自定义日期选择器(1)

Calendar 组件

构建 Calendar 组件

现在您已经有了 calendar helper 模块,是时候构建 React Calendar 组件了。

将以下代码片段添加到 src/components/Calendar/index.js 文件。

import React, { Component, Fragment } from "react";
import PropTypes from "prop-types";
import * as Styled from "./styles";
import calendar, {
  isDate,
  isSameDay,
  isSameMonth,
  getDateISO,
  getNextMonth,
  getPreviousMonth,
  WEEK_DAYS,
  CALENDAR_MONTHS
} from "../../helpers/calendar";

class Calendar extends Component {

  state = { ...this.resolveStateFromProp(), today: new Date() };

  resolveStateFromDate(date) {
    const isDateObject = isDate(date);
    const _date = isDateObject ? date : new Date();

    return {
      current: isDateObject ? date : null,
      month: +_date.getMonth() + 1,
      year: _date.getFullYear()
    };
  }

  resolveStateFromProp() {
    return this.resolveStateFromDate(this.props.date);
  }

  getCalendarDates = () => {
    const { current, month, year } = this.state;
    const calendarMonth = month || +current.getMonth() + 1;
    const calendarYear = year || current.getFullYear();

    return calendar(calendarMonth, calendarYear);
  };

  render() {
    return (
      <Styled.CalendarContainer>

        { this.renderMonthAndYear() }

        <Styled.CalendarGrid>
          <Fragment>
            { Object.keys(WEEK_DAYS).map(this.renderDayLabel) }
          </Fragment>

          <Fragment>
            { this.getCalendarDates().map(this.renderCalendarDate) }
          </Fragment>
        </Styled.CalendarGrid>

      </Styled.CalendarContainer>
    );
  }
}

Calendar.propTypes = {
  date: PropTypes.instanceOf(Date),
  onDateChanged: PropTypes.func
}

export default Calendar;

请注意,在此代码片段中,已经从 calendar helper 模块导入了 calendar builder 函数以及其他 helper 函数和常量。此外,calendar styles 模块的所有导出都已使用 Styled 命名空间导入。

虽然目前还没有创建样式,但是很快就会使用 styled-components 包创建样式。

组件 state 部分通过使用 resolveStateFromProp() 方法从 props 解析,该方法返回一个对象,该对象包含:

  • current:当前所选日期的 Date 对象或 null。
  • month:如果已设定,则为当前选定日期的月份,否则为当前日期(今天)的月份。
  • year:如果已设定,则为当前选定日期的年份,否则为当前日期(今天)的年份。

monthyear 状态属性是正常渲染日历所必需的,如 getCalendarDates() 方法所示,该方法使用 calendar builder 函数构建月份和年份的日历。

最后,使用 today 属性对 state 进行扩展,该属性是当前日期的 Date 对象。

渲染 Calendar 组件的各个部分

在前面的 Calendar 组件代码片段中,render() 方法引用了其他一些用于渲染月份、年份、星期和日历日期的方法。

将这些方法添加到 Calendar 组件,如下面的代码片段所示。

class Calendar extends Component {

  // Render the month and year header with arrow controls
  // for navigating through months and years
  renderMonthAndYear = () => {
    const { month, year } = this.state;

    // Resolve the month name from the CALENDAR_MONTHS object map
    const monthname = Object.keys(CALENDAR_MONTHS)[
      Math.max(0, Math.min(month - 1, 11))
    ];

    return (
      <Styled.CalendarHeader>

        <Styled.ArrowLeft
          onMouseDown={this.handlePrevious}
          onMouseUp={this.clearPressureTimer}
          title="Previous Month"
        />

        <Styled.CalendarMonth>
          {monthname} {year}
        </Styled.CalendarMonth>

        <Styled.ArrowRight
          onMouseDown={this.handleNext}
          onMouseUp={this.clearPressureTimer}
          title="Next Month"
        />

      </Styled.CalendarHeader>
    );
  }

  // Render the label for day of the week
  // This method is used as a map callback as seen in render()
  renderDayLabel = (day, index) => {
    // Resolve the day of the week label from the WEEK_DAYS object map
    const daylabel = WEEK_DAYS[day].toUpperCase();

    return (
      <Styled.CalendarDay key={daylabel} index={index}>
        {daylabel}
      </Styled.CalendarDay>
    );
  }

  // Render a calendar date as returned from the calendar builder function
  // This method is used as a map callback as seen in render()
  renderCalendarDate = (date, index) => {
    const { current, month, year, today } = this.state;
    const _date = new Date(date.join("-"));

    // Check if calendar date is same day as today
    const isToday = isSameDay(_date, today);

    // Check if calendar date is same day as currently selected date
    const isCurrent = current && isSameDay(_date, current);

    // Check if calendar date is in the same month as the state month and year
    const inMonth = month && year && isSameMonth(_date, new Date([year, month, 1].join("-")));

    // The click handler
    const onClick = this.gotoDate(_date);

    const props = { index, inMonth, onClick, title: _date.toDateString() };

    // Conditionally render a styled date component
    const DateComponent = isCurrent
      ? Styled.HighlightedCalendarDate
      : isToday
        ? Styled.TodayCalendarDate
        : Styled.CalendarDate;

    return (
      <DateComponent key={getDateISO(_date)} {...props}>
        {_date.getDate()}
      </DateComponent>
    );
  }

}

renderMonthAndYear() 方法中,首先从 CALENDAR_MONTHS 对象解析月份名称。然后它与年份及左侧和右侧两个箭头控件一起渲染,用于导航月和年。

箭头控件每个都有 mousedownmouseup 事件处理,稍后将定义这些事件处理——handlePrevious()handleNext()clearPressureTimer()

renderMonthAndYear() 方法渲染的 DOM 看起来像下面的截图(带有一些样式):

renderDayLabel() 方法渲染一周中某一天的标签。 它解析 WEEK_DAYS 对象中的标签。注意,它有两个参数——dayindex,因为它用作 .map() 的回调函数,如 render() 方法所示。

映射之后,一周中日期的渲染 DOM 看起来像下面的截图 。

renderCalendarDate() 方法也用作 .map() 回调函数并渲染日历日期。它接收到的第一个参数 date 的格式是 [YYYY, MM, DD]

它检查 date 是否与今天相同,是否与当前选择的日期相同,是否与当前 state 的月份和年份相同。通过这些检查,它有条件地渲染日历日期单元格的不同形态——HiglightedCalendarDateTodayCalendarDateCalendarDate

还要注意,使用 gotoDate() 方法(将在下一节中定义)为每个日历日期设置 onClick 处理,以跳转到特定日期。

事件处理

在前面几节中已经对一些事件处理进行了一些引用。继续并更新 Calendar 组件,以包含事件处理的以下代码片段。

class Calendar extends Component {

  gotoDate = date => evt => {
    evt && evt.preventDefault();
    const { current } = this.state;
    const { onDateChanged } = this.props;

    !(current && isSameDay(date, current)) &&
      this.setState(this.resolveStateFromDate(date), () => {
        typeof onDateChanged === "function" && onDateChanged(date);
      });
  }

  gotoPreviousMonth = () => {
    const { month, year } = this.state;
    this.setState(getPreviousMonth(month, year));
  }

  gotoNextMonth = () => {
    const { month, year } = this.state;
    this.setState(getNextMonth(month, year));
  }

  gotoPreviousYear = () => {
    const { year } = this.state;
    this.setState({ year: year - 1 });
  }

  gotoNextYear = () => {
    const { year } = this.state;
    this.setState({ year: year + 1 });
  }

  handlePressure = fn => {
    if (typeof fn === "function") {
      fn();
      this.pressureTimeout = setTimeout(() => {
        this.pressureTimer = setInterval(fn, 100);
      }, 500);
    }
  }

  clearPressureTimer = () => {
    this.pressureTimer && clearInterval(this.pressureTimer);
    this.pressureTimeout && clearTimeout(this.pressureTimeout);
  }

  handlePrevious = evt => {
    evt && evt.preventDefault();
    const fn = evt.shiftKey ? this.gotoPreviousYear : this.gotoPreviousMonth;
    this.handlePressure(fn);
  }

  handleNext = evt => {
    evt && evt.preventDefault();
    const fn = evt.shiftKey ? this.gotoNextYear : this.gotoNextMonth;
    this.handlePressure(fn);
  }

}

gotoDate() 方法是一个高阶函数,它接受一个 Date 对象作为参数,并返回一个事件处理函数,该事件处理函数可以被触发以更新 state 中当前选定的日期。注意,resolveStateFromDate() 方法用于从日期中解析 monthyear 并更新 state。

如果 Calendar 组件的 props 传递了 onDateChanged 回调函数,则将使用更新的日期调用该函数。 这对于您希望将日期更改传播到父组件的情况非常有用。

handlePrevious()handleNext() 事件处理共享类似的行为。默认情况下,它们会按月循环。然而,如果按下 shift 键,它们就会以年为单位循环。最后,他们将控制权交给 handlePressure() 方法。

handlePressure() 方法简单地使用计时器模拟压力单击,以快速循环数月或数年,而clearPressureTimer() 方法清除这些计时器。

组件生命周期方法

Calendar 组件离完成还差一些生命周期方法。下面是 Calendar 组件的生命周期方法。

class Calendar extends Component {

  // ... other methods here

  componentDidMount() {
    const now = new Date();
    const tomorrow = new Date().setHours(0, 0, 0, 0) + 24 * 60 * 60 * 1000;
    const ms = tomorrow - now;

    this.dayTimeout = setTimeout(() => {
      this.setState({ today: new Date() }, this.clearDayTimeout);
    }, ms);
  }

  componentDidUpdate(prevProps) {
    const { date, onDateChanged } = this.props;
    const { date: prevDate } = prevProps;
    const dateMatch = date == prevDate || isSameDay(date, prevDate);

    !dateMatch &&
      this.setState(this.resolveStateFromDate(date), () => {
        typeof onDateChanged === "function" && onDateChanged(date);
      });
  }

  clearDayTimeout = () => {
    this.dayTimeout && clearTimeout(this.dayTimeout);
  }

  componentWillUnmount() {
    this.clearPressureTimer();
    this.clearDayTimeout();
  }

}

componentDidMount() 方法中,有一个日期计时器,它被设置为在当前日期结束时自动将 state 中的 today 属性更新到第二天。

在卸载组件之前,清除所有计时器,如 componentWillUnmount() 方法中所示。

设置日历样式

现在您已经完成了 Calendar 组件,接下来您将创建为日历提供样式的样式化组件。

将以下代码片段添加到 src/components/Calendar/styles.js 文件。

import styled from 'styled-components';

export const Arrow = styled.button`
  appearance: none;
  user-select: none;
  outline: none !important;
  display: inline-block;
  position: relative;
  cursor: pointer;
  padding: 0;
  border: none;
  border-top: 1.6em solid transparent;
  border-bottom: 1.6em solid transparent;
  transition: all .25s ease-out;
`;

export const ArrowLeft = styled(Arrow)`
  border-right: 2.4em solid #ccc;
  left: 1.5rem;
  :hover {
    border-right-color: #06c;
  }
`;

export const ArrowRight = styled(Arrow)`
  border-left: 2.4em solid #ccc;
  right: 1.5rem;
  :hover {
    border-left-color: #06c;
  }
`;

export const CalendarContainer = styled.div`
  font-size: 5px;
  border: 2px solid #06c;
  border-radius: 5px;
  overflow: hidden;
`;

export const CalendarHeader = styled.div`
  display: flex;
  align-items: center;
  justify-content: space-between;
`;

export const CalendarGrid = styled.div`
  display: grid;
  grid-template: repeat(7, auto) / repeat(7, auto);
`;

export const CalendarMonth = styled.div`
  font-weight: 500;
  font-size: 5em;
  color: #06c;
  text-align: center;
  padding: 0.5em 0.25em;
  word-spacing: 5px;
  user-select: none;
`;

export const CalendarCell = styled.div`
  text-align: center;
  align-self: center;
  letter-spacing: 0.1rem;
  padding: 0.6em 0.25em;
  user-select: none;
  grid-column: ${props => (props.index % 7) + 1} / span 1;
`;

export const CalendarDay = styled(CalendarCell)`
  font-weight: 600;
  font-size: 2.25em;
  color: #06c;
  border-top: 2px solid #06c;
  border-bottom: 2px solid #06c;
  border-right: ${props => (props.index % 7) + 1 === 7 ? `none` : `2px solid #06c`};
`;

export const CalendarDate = styled(CalendarCell)`
  font-weight: ${props => props.inMonth ? 500 : 300};
  font-size: 4em;
  cursor: pointer;
  border-bottom: ${props => ((props.index + 1) / 7) <= 5 ? `1px solid #ddd` : `none`};
  border-right: ${props => (props.index % 7) + 1 === 7 ? `none` : `1px solid #ddd`};
  color: ${props => props.inMonth ? `#333` : `#ddd`};
  grid-row: ${props => Math.floor(props.index / 7) + 2} / span 1;
  transition: all .4s ease-out;
  :hover {
    color: #06c;
    background: rgba(0, 102, 204, 0.075);
  }
`;

export const HighlightedCalendarDate = styled(CalendarDate)`
  color: #fff !important;
  background: #06c !important;
  position: relative;
  ::before {
    content: '';
    position: absolute;
    top: -1px;
    left: -1px;
    width: calc(100% + 2px);
    height: calc(100% + 2px);
    border: 2px solid #06c;
  }
`;

export const TodayCalendarDate = styled(HighlightedCalendarDate)`
  color: #06c !important;
  background: transparent !important;
  ::after {
    content: '';
    position: absolute;
    right: 0;
    bottom: 0;
    border-bottom: 0.75em solid #06c;
    border-left: 0.75em solid transparent;
    border-top: 0.75em solid transparent;
  }
  :hover {
    color: #06c !important;
    background: rgba(0, 102, 204, 0.075) !important;
  }
`;

以上就是正常渲染日历所需的组件和样式。如果此时在应用程序中渲染 Calendar 组件,它应该看起来像这个截图。

0条评论

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