如何使用 React 構建自定義日期選擇器(2)
接著上一篇: ofollow,noindex">如何使用 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 month year
month
和 year
狀態屬性是正常渲染日曆所必需的,如 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
物件解析月份名稱。然後它與年份及左側和右側兩個箭頭控制元件一起渲染,用於導航月和年。
箭頭控制元件每個都有 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
元件,它應該看起來像這個截圖。