专栏首页IMWeb前端团队如何使用 React 构建自定义日期选择器(2)

如何使用 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 条评论
登录 后参与评论

相关文章

  • React性能优化 -- 利用React-Redux

    注意这里面,如果可以在渲染virtual DOM之前就可以判断渲染结果不会有变化,那么可以直接不进行virtual DOM的渲染和比较,速度会更快。

    IMWeb前端团队
  • Promise的简单实现

    本篇文章通过构建一个简单的Promise对象来了解如何做到异步获得数据。 使用方法 const fetch = function(url) { return...

    IMWeb前端团队
  • Promise的简单实现

    这个fetch()的方法返回了一个Promise对象,接着我们就可以用then来对获得的数据进行处理,catch来捕获可能的错误。

    IMWeb前端团队
  • 三千字讲清TypeScript与React的实战技巧

    很多时候虽然我们了解了TypeScript相关的基础知识,但是这不足以保证我们在实际项目中可以灵活运用,比如现在绝大部分前端开发者的项目都是依赖于框架的,因此我...

    桃翁
  • 前端赋能业务 - Node实现自动化部署平台

    是否有很多人跟我一样有这样的一个烦恼,每天有写不完的需求、改不完的BUG,每天撸着重复、繁琐的业务代码,担心着自己的技术成长。

    coder_koala
  • 使用 Cocos Creator 开发动感音乐游戏!

    在浏览器端 AudioContext 是一个专门用于音频处理的接口,工作原理是将 AudioContext 创建出来的各种节点相互连接,音频数据流经这些节点,我...

    张晓衡
  • GMGC北京2018|倒计时10天:第七届全球游戏大会议程公布

    VRPinea
  • swiper库入门介绍

    一进入界面就可以看到Swiper 4的标题,直接点击开始使用Swiper,开始入门使用。

    Devops海洋的渔夫
  • 字节面试官:请你实现一个大文件上传和断点续传

    原 作 者:yeyan1996原文链接:https://url.cn/5h66afn

    Nealyang
  • 最近有程序员自降80%薪水转行做游戏,那些跨行的游戏作者真的有出路吗?

    从事软件开发多年,开发游戏能加班加到人吐血,虽然普通大众都喜欢玩游戏但对于普通的游戏开发者来讲加班加点已经是家常便饭,特别是做国外优秀山寨这块基本上一周至少工作...

    程序员互动联盟

扫码关注云+社区

领取腾讯云代金券