如何用react封装一款日期选择器组件

1,970 阅读7分钟

这是我参与11月更文挑战的第12天,活动详情查看:2021最后一次更文挑战

TIP 👉 呜呼!楚虽三户能亡秦,岂有堂堂中国空无人!____陆游《金错刀行》

前言

Web Component是前端界一直非常热衷的一个领域,用第三方组件化的框架去实现的话,你需要依赖框架本身很多东西,很多时候我们只是简单的几个组件,不是很大,也不是很多,所以为了保证组件的`轻量,简单`,其实这个时候我们并不想采用第三方的框架。

日期选择器组件

import

import DatePicker from '@/components/DatePicker/DatePicker';

Props

1. onChange
  • 类型:func (必填)
  • 默认值:无
  • 说明:选中日期后的回调函数,入参:
    • {Moment | Date | String | Number} value 选中日期值(与valueType对应)
    • {Moment} momentValue 选中日期的Moment值
2. valueType
  • 类型:DatePicker.VALUE_TYPE 中的一种
  • 默认值:DatePicker.VALUE_TYPE.string
  • 说明:日期值类型
3. value
  • 类型:valueType指定的类型
  • 默认值:无
  • 说明:日期值
<DatePicker value={'2019-12-12'}
            valueType={DatePicker.VALUE_TYPE.string}
            onChange={this.dateChange} />
<DatePicker value={new Date()}
            valueType={DatePicker.VALUE_TYPE.date}
            onChange={this.dateChange} />
<DatePicker value={1576049926055}
            valueType={DatePicker.VALUE_TYPE.millisecond}
            onChange={this.dateChange} />
<DatePicker value={moment()}
            valueType={DatePicker.VALUE_TYPE.moment}
            onChange={this.dateChange} />
4. format
  • 类型:string
  • 默认值:'YYYY-MM-DD'(showTime为true时为'YYYY-MM-DD HH:mm:ss')
  • 说明:日期字符串的格式
5. showTime
  • 类型:bool
  • 默认值:false
  • 说明:是否显示时间(时分秒)
6. defaultTime
  • 类型:string
  • 默认值:无(未设置时,默认为当前时间)
  • 说明:默认时间(时分秒),如:'00:00:00'
7. minValue
  • 类型:DatePicker.VALUE_TYPE 中的任意一种,与valueType无关
  • 默认值:无
  • 说明:可选日期的最小值
8. maxValue
  • 类型:DatePicker.VALUE_TYPE 中的任意一种,与valueType无关
  • 默认值:无
  • 说明:可选日期的最大值
9. placeholder
  • 类型:string
  • 默认值:'请选择日期'(showTime为true时为'请选择日期时间')
  • 说明:输入提示信息
10. showClear
  • 类型:bool
  • 默认值:true
  • 说明:输入框右侧是否显示清空按钮
11. disabled
  • 类型:bool
  • 默认值:false
  • 说明:是否不可用,true表示日期选择器不可用
12. disabledDate
  • 类型:func (必填)
  • 默认值:无
  • 说明:判断日期是否可选
    • 入参:
      • {Moment} current 当前日期
    • 返回:
      • {Boolen} 是否为不可选,true表示当前日期不可选
13. disabledHours
  • 类型:func (必填)
  • 默认值:无
  • 说明:获取不可用小时的数组(showTime为true时有效)
    • 入参:
      • {Moment} current 当前日期
    • 返回:
      • {Array} 不可用的小时数组
14. disabledMinutes
  • 类型:func (必填)
  • 默认值:无
  • 说明:获取不可用分钟的数组(showTime为true时有效)
    • 入参:
      • {Number} selectedHour 当前选中的小时
      • {Moment} current 当前日期
    • 返回:
      • {Array} 不可用的分钟数组
15. disabledSeconds
  • 类型:func (必填)
  • 默认值:无
  • 说明:获取不可用秒的数组(showTime为true时有效)
    • 入参:
      • {Number} selectedHour 当前选中的小时
      • {Number} selectedMinute 当前选中的分钟
      • {Moment} current 当前日期
    • 返回:
      • {Array} 不可用的秒数组

实现DatePicker.js

import React from 'react';
import PropTypes from 'prop-types';
import Calendar from 'rc-calendar';
import Picker from 'rc-calendar/lib/Picker';
import zhCN from 'rc-calendar/lib/locale/zh_CN';
import TimePickerPanel from 'rc-time-picker/lib/Panel';
import moment from 'moment';
import valueTypes from './utils/value-types';
import PickerPropTypes from './utils/picker-prop-types.js';
import valueConvertUtil from './utils/value-convert-util.js';
import 'moment/locale/zh-cn';
import './datePicker.scss';

/**
 * 日期选择器
 */
export default class DatePicker extends React.Component {
    // 值类型常量
    static VALUE_TYPE = valueTypes;

    // 入参类型检查
    static propTypes = {
        /**
         * 选中日期后的回调函数
         * @param {Moment | Date | String | Number} value 选中日期值(与valueType对应)
         * @param {Moment} momentValue 选中日期的Moment值
         */
        onChange: PropTypes.func.isRequired,
        // 日期值类型:DatePicker.VALUE_TYPE 中的一种
        valueType: PropTypes.oneOf(Object.keys(DatePicker.VALUE_TYPE).map( k => DatePicker.VALUE_TYPE[k]) ),
        // 日期值(必须是 valueType 指定类型的数值)
        value: PickerPropTypes.dateValue,
        // 日期字符串的格式
        format: PropTypes.string,
        // 是否显示时间(时分秒)
        showTime: PropTypes.bool,
        // 默认时间(时分秒),showTime为true时有效,如:'00:00:00',为空则会是当前时间
        defaultTime: PropTypes.string,
        // 可选日期的最小值(DatePicker.VALUE_TYPE 中的任意一种,与valueType无关)
        minValue: PickerPropTypes.looseDateValue,
        // 可选日期的最大值(DatePicker.VALUE_TYPE 中的任意一种,与valueType无关)
        maxValue: PickerPropTypes.looseDateValue,
        // 输入提示信息
        placeholder: PropTypes.string,
        // 是否显示清空按钮
        showClear:  PropTypes.bool,
        // 是否不可用
        disabled: PropTypes.bool,
        /**
         * 判断日期是否可选
         * @param {Moment} current 当前日期
         * @return {Boolen} 是否为不可选,true表示当前日期不可选
         */
        disabledDate: PropTypes.func,
        /**
         * 获取不可用小时的数组(showTime为true时有效)
         * @param {Moment} current 当前日期
         * @return {Array<Number>} 不可用的小时数组
         */
        disabledHours: PropTypes.func,
        /**
         * 获取不可用分钟的数组(showTime为true时有效)
         * @param {Number} selectedHour 当前选中的小时
         * @param {Moment} current 当前日期
         * @return {Array<Number>} 不可用的分钟数组
         */
        disabledMinutes: PropTypes.func,
        /**
         * 获取不可用秒的数组(showTime为true时有效)
         * @param {Number} selectedHour 当前选中的小时
         * @param {Number} selectedMinute 当前选中的分钟
         * @param {Moment} current 当前日期
         * @return {Array<Number>} 不可用的秒数组
         */
        disabledSeconds: PropTypes.func
    }

    // 入参默认值
    static defaultProps = {
        // 日期值类型,默认为字符串
        valueType: DatePicker.VALUE_TYPE.string,
        // 是否显示时间(时分秒),默认为不显示
        showTime: false,
        // 是否显示清空按钮
        showClear: true,
        // 是否不可用,默认为可用
        disabled: false
    }

    constructor(props) {
        super(props);

        const format = this.props.format || (this.props.showTime ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD');
        const momentValue = valueConvertUtil.convertToMoment(this.props.value, this.props.valueType, this.props.showTime, format);
        const minMomentValue = valueConvertUtil.convertToMoment(this.props.minValue, null, this.props.showTime, format);
        const maxMomentValue = valueConvertUtil.convertToMoment(this.props.maxValue, null, this.props.showTime, format);
        const defaultTimeValue = this.props.defaultTime ? moment(this.props.defaultTime, 'HH:mm:ss') : moment();
        const placeholder = this.props.placeholder ? this.props.placeholder : (this.props.showTime ? '请选择日期时间' : '请选择日期');

        this.state = {
            // moment类型的值
            momentValue,
            // 时间字符串格式
            format,
            // 可选时间最小值的moment对象
            minMomentValue,
            // 可选时间最大值的moment对象
            maxMomentValue,
            // 默认时分秒
            defaultTimeValue,
            // 输入提示信息
            placeholder
        };
    }

    componentDidUpdate(prevProps, prevState) {
        this.receivePropsResetState(prevProps);
    }

    // 根据 props 的变化重新设置 state
    receivePropsResetState (prevProps) {
        const format = this.props.format || (this.props.showTime ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD');
        if (prevProps.format !== this.props.format
            || prevProps.showTime !== this.props.showTime) {
            const momentValue = valueConvertUtil.convertToMoment(this.props.value, this.props.valueType, this.props.showTime, format);
            const minMomentValue = valueConvertUtil.convertToMoment(this.props.minValue, null, this.props.showTime, format);
            const maxMomentValue = valueConvertUtil.convertToMoment(this.props.maxValue, null, this.props.showTime, format);
            const placeholder = this.props.placeholder ? this.props.placeholder : (this.props.showTime ? '请选择日期时间' : '请选择日期');

            this.setState({
                // moment类型的值
                momentValue,
                // 时间字符串格式
                format,
                // 可选时间最小值的moment对象
                minMomentValue,
                // 可选时间最大值的moment对象
                maxMomentValue,
                // 输入提示信息
                placeholder
            });
        } else {
            if (prevProps.valueType !== this.props.valueType) {
                console.warn('DatePicker组件 props.valueType 的值不能随意改变');
            }
            if (!valueConvertUtil.isSameValue(prevProps.value, this.props.value)) {
                const momentValue = valueConvertUtil.convertToMoment(this.props.value, this.props.valueType, this.props.showTime, format);
                this.setState({ momentValue });
            }
            if (prevProps.minValue !== this.props.minValue) {
                const minMomentValue = valueConvertUtil.convertToMoment(this.props.minValue, null, this.props.showTime, format);
                this.setState({ minMomentValue });
            }
            if (prevProps.maxValue !== this.props.maxValue) {
                const maxMomentValue = valueConvertUtil.convertToMoment(this.props.maxValue, null, this.props.showTime, format);
                this.setState({ maxMomentValue });
            }
            if (prevProps.defaultTime !== this.props.defaultTime) {
                const defaultTimeValue = this.props.defaultTime ? moment(this.props.defaultTime, 'HH:mm:ss') : moment();
                this.setState({ defaultTimeValue });
            }
            if (prevProps.placeholder !== this.props.placeholder) {
                const placeholder = this.props.placeholder ? this.props.placeholder : (this.props.showTime ? '请选择日期时间' : '请选择日期');
                this.setState({ placeholder });
            }
        }
    }

    /**
     * 将其他类型转换为moment对象
     * @param value 其他类型时间值
     */
    convertToMoment (value) {
        return valueConvertUtil.convertToMoment(value, this.props.valueType, this.props.showTime, this.state.format);
    }

    /**
     * 将moment对象转换为其他类型
     * @param obj moment对象
     */
    convertFromMoment (obj) {
        return valueConvertUtil.convertFromMoment(obj, this.props.valueType, this.props.showTime, this.state.format);
    }

    // 日期选择器选择后触发的事件
    onPickerChange = (value) => {
        this.setState({ momentValue: value })

        if (!this.props.showTime || value === null) {
            this.props.onChange && this.props.onChange(this.convertFromMoment(value), value);
        }
    }

    // 日期选择器打开状态改变时触发的事件
    onOpenChange = (open) => {
        if (this.props.showTime && !open) {
            let value = this.state.momentValue;
            this.props.onChange && this.props.onChange(this.convertFromMoment(value), value);
        }
    }

    // 清空
    onClear = (event) => {
        this.props.onChange && this.props.onChange(null, null);
        event.stopPropagation();
    };

    // 创建时间选择器组件(时分秒选择器组件)
    createTimePicker = (value) => {
        if (this.props.showTime) {
            return <TimePickerPanel value={value} defaultValue={this.state.defaultTimeValue}/>;
        }
        return null;
    }

    /**
     * 判断日期是否可选
     * @param {Moment} current moment对象
     * @returns {boolean}
     */
    disabledDate = (current) => {
        if (!this.props.showTime) { // 不显示时间(时分秒)时
            let curValue = moment(current.format('YYYY-MM-DD'), 'YYYY-MM-DD');

            let { minMomentValue, maxMomentValue } = this.state;

            if (minMomentValue && curValue.isBefore(minMomentValue)) return true;
            if (maxMomentValue && curValue.isAfter(maxMomentValue)) return true;
        } else { // 显示时间(时分秒)时
            let curValue = this.state.momentValue;
            if ( curValue ) {
                curValue = moment(`${current.format('YYYY-MM-DD')} ${this.convertToMoment(curValue).format('HH:mm:ss')}`, 'YYYY-MM-DD HH:mm:ss');
            } else {
                curValue = moment(`${current.format('YYYY-MM-DD')} ${this.state.defaultTimeValue.format('HH:mm:ss')}`, 'YYYY-MM-DD HH:mm:ss');
            }

            let { minMomentValue, maxMomentValue } = this.state;
            let minVal, maxVal;
            if (minMomentValue) {
                minVal = moment(minMomentValue.format('YYYY-MM-DD'), 'YYYY-MM-DD')
                if (curValue.valueOf() < minVal.valueOf())  return true;
            }

            if (maxMomentValue) {
                if (maxMomentValue.format('HH:mm:ss') === '00:00:00') {
                    maxVal = maxMomentValue.clone();
                } else {
                    maxVal = moment(maxMomentValue.format('YYYY-MM-DD'), 'YYYY-MM-DD').add(moment.duration({'day' : 1}));
                }
                if (curValue.valueOf() >= maxVal.valueOf()) return true;
            }
        }

        if (this.props.disabledDate) {
            return this.props.disabledDate(current);
        }

        return false;
    }

    /**
     * 判断时间(时、分、秒)是否可选
     * @param {Moment} current moment对象
     * @returns {Object} 返回包含disabledHours、disabledMinutes、disabledSeconds函数的对象
     */
    disabledTime = (current) => {
        // 根据最小值和最大值创建连续数字数组方法
        function createNumArray (minNum, maxNum) {
            let arr = [];
            for (let i = minNum; i <= maxNum; i++) {
                arr.push(i);
            }
            return arr;
        }

        // 可选日期的最大值和最小值
        let { minMomentValue, maxMomentValue } = this.state;

        // 如果已选择日期
        if (current) {
            // 获取不可用小时数组的方法
            let disabledHours = () => {
                let hours = [];
                if (minMomentValue && current.isSame(minMomentValue, 'day')) {
                    hours = createNumArray(0, minMomentValue.hour() - 1);
                }
                if (maxMomentValue && current.isSame(maxMomentValue, 'day')) {
                    hours = hours.concat(createNumArray(maxMomentValue.hour() + 1, 23));
                }
                if (this.props.disabledHours) {
                    hours = hours.concat(this.props.disabledHours(current));
                }
                return hours;
            }

            // 获取不可用分钟数组的方法
            let disabledMinutes = (selectedHour) => {
                let minutes = [];
                if (minMomentValue && current.isSame(minMomentValue, 'day') && selectedHour === minMomentValue.hour()) {
                    minutes = createNumArray(0, minMomentValue.minute() - 1);
                }
                if (maxMomentValue && current.isSame(maxMomentValue, 'day') && selectedHour === maxMomentValue.hour()) {
                    minutes = minutes.concat(createNumArray(maxMomentValue.minute() + 1, 59));
                }
                if (this.props.disabledMinutes) {
                    minutes = minutes.concat(this.props.disabledMinutes(selectedHour, current));
                }
                return minutes;
            }

            // 获取不可用秒数组的方法
            let disabledSeconds = (selectedHour, selectedMinute) => {
                let seconds = [];
                if (minMomentValue && current.isSame(minMomentValue, 'day') && selectedHour === minMomentValue.hour() && selectedMinute === minMomentValue.minute()) {
                    seconds = createNumArray(0, minMomentValue.second() - 1);
                }
                if (maxMomentValue && current.isSame(maxMomentValue, 'day') && selectedHour === maxMomentValue.hour() && selectedMinute === maxMomentValue.minute()) {
                    seconds = seconds.concat(createNumArray(maxMomentValue.second() + 1, 59));
                }
                if (this.props.disabledSeconds) {
                    seconds = seconds.concat(this.props.disabledSeconds(selectedHour, selectedMinute, current));
                }
                return seconds;
            }

            return {
                disabledHours,
                disabledMinutes,
                disabledSeconds
            }
        } else {
            // 如果未选择日期,且限制了日期可选范围,则时分秒不可选
            if (minMomentValue || maxMomentValue) {
                return {
                    disabledHours: () => createNumArray(0, 23),
                    disabledMinutes: () => createNumArray(0, 59),
                    disabledSeconds: () => createNumArray(0, 59)
                };
            } else { // 如果未选择日期,且未限制日期范围,则使用入参的disabledHours、disabledMinutes、disabledSeconds进行过滤
                return {
                    disabledHours: () => {
                        return this.props.disabledHours ? this.props.disabledHours(current) : [];
                    },
                    disabledMinutes: (selectedHour) => {
                        return this.props.disabledMinutes ? this.props.disabledMinutes(selectedHour, current) : [];
                    },
                    disabledSeconds: (selectedHour, selectedMinute) => {
                        return this.props.disabledSeconds ? this.props.disabledSeconds(selectedHour, selectedMinute, current) : [];
                    }
                };
            }
        }
    }

    render() {
        const dateValue = this.state.momentValue;
        const showClear = this.props.showClear && this.props.value;

        const calendar = (<Calendar locale={ zhCN }
                            dateInputPlaceholder={this.state.placeholder}
                            format={this.state.format}
                            timePicker={this.createTimePicker(dateValue)}
                            showDateInput={false}
                            disabledDate={this.disabledDate}
                            disabledTime={this.disabledTime} />);
        return (
            <Picker animation="slide-up"
                    calendar={calendar} 
                    value={dateValue}
                    onChange={this.onPickerChange}
                    onOpenChange={this.onOpenChange}
                    disabled={this.props.disabled}>
                {
                    ({ value }) => {
                        return (
                            <div className={'calendar-picker-box' + (showClear ? ' calendar-picker-box-show-clear' : '')}>
                                <input placeholder={this.state.placeholder}
                                       disabled={this.props.disabled}
                                       className="date-picker-input"
                                       value={value && value.format && value.format(this.state.format) || ''}
                                       readOnly />
                                {showClear ?
                                    <span className="date-picker-clear" onClick={this.onClear} title="清空">
                                        <i className="icon-guanbishibaimianxing clear-icon"></i>
                                    </span>
                                    : null}
                                <i className="icon icon-rili"></i>
                            </div>);
                    }
                }
            </Picker>);
    }
}

样式这块就先不放了

「欢迎在评论区讨论」

希望看完的朋友可以给个赞,鼓励一下