本文作者:IMWeb howenhuo 原文出处:IMWeb社区 未经同意,禁止转载
接着上一篇:如何使用 React 构建自定义日期选择器(1)
现在您已经有了 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
:如果已设定,则为当前选定日期的年份,否则为当前日期(今天)的年份。month
和 year
状态属性是正常渲染日历所必需的,如 getCalendarDates()
方法所示,该方法使用 calendar builder
函数构建月份和年份的日历。
最后,使用 today
属性对 state 进行扩展,该属性是当前日期的 Date
对象。
在前面的 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
对象解析月份名称。然后它与年份及左侧和右侧两个箭头控件一起渲染,用于导航月和年。
箭头控件每个都有 mousedown
和 mouseup
事件处理,稍后将定义这些事件处理——handlePrevious()
、handleNext()
和 clearPressureTimer()
。
renderMonthAndYear()
方法渲染的 DOM 看起来像下面的截图(带有一些样式):
renderDayLabel()
方法渲染一周中某一天的标签。 它解析 WEEK_DAYS
对象中的标签。注意,它有两个参数——day
和 index
,因为它用作 .map()
的回调函数,如 render()
方法所示。
映射之后,一周中日期的渲染 DOM 看起来像下面的截图 。
renderCalendarDate()
方法也用作 .map()
回调函数并渲染日历日期。它接收到的第一个参数 date
的格式是 [YYYY, MM, DD]
。
它检查 date
是否与今天相同,是否与当前选择的日期相同,是否与当前 state 的月份和年份相同。通过这些检查,它有条件地渲染日历日期单元格的不同形态——HiglightedCalendarDate
、TodayCalendarDate
或 CalendarDate
。
还要注意,使用 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()
方法用于从日期中解析 month
和 year
并更新 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
组件,它应该看起来像这个截图。