基於react開發的時間選擇組件(TimePicker)

自從學習了react後一直琢磨寫點項目什麼之類的來練練手,剛好公司如今的項目還不是很大,足夠我用react進行重構。最開始的時候是想本身去開發組件,後來發現有更好的antd這個東東就放棄了本身開發組件直接將antd的組件拿過來用,感受挺好用的,直到最近對時間選擇器TimePicker這個組件進行實踐的時候,發現這個組件的api特別難用(貌似還有bug,應該是個人錯覺。。。),因此決定本身開發一個相似的時間選擇器來鍛鍊本身。
言歸正傳,下面講解一下我自定義的時間選擇器TimePicker開發過程當中遇到的坑和目前最終的實現思路(嗯,本人很菜,不吝嗇指點...)。
首先構建靜態的時間結構,我這裏只支持小時和分鐘。結構分爲上下兩層,上層輸入框支持直接輸入時間,下層選擇欄用於給用戶提供點擊選擇,關鍵代碼實現以下:css

<div className="my-TimePicker-wrapper">
    <div className="my-TimePicker-header">
        <span class="ant-time-picker ">
            <input className="ant-time-picker-input" />
        </span>
    </div>
    <div className="my-TimePicker-content">
        <div className="my-TimePicker-content-box">
            <ul className="my-TimePicker-content-menu" >
            {
                // 你的小時循環遍歷
            }
            </ul>
        </div>
        <div className="my-TimePicker-content-box">
            <ul className="my-TimePicker-content-menu">
            {
                // 你的分鐘循環遍歷
            }
            </ul>
        </div>
    </div>
</div>

很簡單的一段靜態結構,css方面抄了antd的一點,本身寫了一點,此處不在過多的去描述,各位童鞋能夠本身去寫適合本身的。嗯,靜態結構搭建完畢後下面就是關鍵的三個部分的描述,也就是輸入框的匹配,點擊小時和分鐘後的事件描述。
第一塊:首先來說解一下小時和分鐘的實現的思路和步驟:react

  1. 當用戶點擊小時或分鐘時,優先須要判斷用戶點擊值是否合法的有效的,好比:你將這個小時和分鐘禁用掉了,這時候用戶點擊確定是無效的;api

  2. 其次就是須要判斷用戶在點擊時是否已經點擊過其餘的小時或着分鐘的元素,若是存在點擊過的元素,咱們須要將上一次存放點擊的元素的變量的選中效果進行清除,而後將這次的點擊設爲選中,而且將這次點擊的元素對象和元素值使用變量進行存放;數組

  3. 而後咱們玩一個簡易的點擊選中的元素速度動畫(我的興趣,你也能夠不用動畫)將選中的元素置頂,
    順手判斷一下小時和分鐘是否同時存在點擊,存在則設置輸入框的值,不推薦使用state設置值,存放的變量也不推薦使用state(結束時解釋);antd

  4. 最後因爲我向用戶提供了一個onchange方法,因此須要判斷一下用戶是否設置了onchange,若設置則執行用戶的onchange。app

關鍵代碼實現以下:dom

// 以小時爲例:

    hourClick = (e) => {
        let target = e.target, obj = this, 
            selectedHour = target.innerHTML, 
            selectedMinute = obj.selectedMinute; 
        if(target.className === "my-TimePicker-time-selected-disabled"){
            // 判斷用戶點擊值是否合法,此處我時判斷是否爲禁用狀態
            return ;
        }
        if(obj.prevHourSelected){
            // 判斷用戶是否已經點擊過其餘元素,有則將選中清空
            obj.prevHourSelected.className = "";
        }
        // 將這次點擊設爲選中
        target.className = "你的選中類名";
    
        // 將這次點擊設置爲上一次選中
        obj.prevHourSelected = target;
        obj.selectedHour = selectedHour;
        obj.animateTop(selectedHour, selectedMinute, obj.time); // 簡易的點擊速度動畫
    
        if(selectedMinute !== ""){
            // 判斷用戶的是否同時點擊過度鍾,若存在則設置輸入框的值
        }
    
        if(obj.props.onChange){
            // 用戶的回調函數執行
        }
    }

第二塊,輸入框輸入時間時的實現思路和步驟:函數

  1. 去除用戶輸入的值的先後空格,而後判斷用戶值的長度是否爲0,若是輸入爲0則用戶執行了刪除操做,此時咱們將用戶以前輸入的值設置的對應的存放變量和選中效果通通清空,而後執行用戶回調(若是有的話);學習

  2. 對用戶值的長度進行判斷是否等於5,由於我是HH:mm的格式,因此長度爲5,爲何要進行長度驗證,由於input的onchange方法當用戶改變值時都會執行方法,我作了正則判斷,不經過就會清空,因此須要在長度等於5時才進行正則驗證;動畫

  3. 經過長度驗證後,進行正則驗證,不經過就清空值,經過後開始進行設置小時和分鐘的選中,同時清掉上一次選中的效果和存放的變量值,接着判斷輸入的值是否合法,好比你不能輸入已經禁用的值,經過後將這次輸入的值設置爲選中,並存放到變量中,又是判斷一下是否有回調;
    關鍵代碼實現以下:

if(value.length !== 0){
        if(value.trim().length === 5){ // 符合長度要求後進行正則驗證
            if(obj.reg.test(value)){
                let hour = +value.split(":")[0], minute = +value.split(":")[1];
                // 獲取輸入值所在的選項的樣式
                    ...
    
                if(obj.prevHourSelected){ // 若是上一次存在選中的時間就清空選中樣式
                    xxx.className = "";
                }
                if(obj.prevMinuteSelected){ // 若是上一次存在選中的時間就清空選中樣式
                    xxx.className = "";
                }
                // 若是所選值不是禁止選中,就添加選中樣式,而且啓用滾動效果
                if(hourClass.indexOf("my-TimePicker-time-selected-disabled") === -1 && minuteClass.indexOf("my-TimePicker-time-selected-disabled") === -1){
                    xxx.className = hourClass + " my-TimePicker-time-selected";
                    xxx.className = minuteClass + " my-TimePicker-time-selected";
                    obj.animateTop(hour, minute, obj.time);
                    //添加這次輸入爲上一次選中
                    obj.prevHourSelected = xxx; 
                    obj.prevMinuteSelected = xxx; 
                    obj.selectedHour = hour; 
                    obj.selectedMinute = minute; 
    
                    if(obj.props.onChange){ // 用戶是否綁定了自定義onChange
                        // 執行回調
                    }
                }else{ // 輸入爲禁用數時清空值
                    input(輸入框的對象).value = "";
                }
            }
        }
    }else{
        // 刪除操做,清空全部值,選中和變量存放到值
    }

第三塊,用戶設置默認值的設置,componentDidMount進行判斷和設置,而後每次用戶更新state是在shouldComponentUpdate判斷是否值是否合法(此處代碼和以前較類似不在貼出關鍵代碼實現):

  1. componentDidMount設置值,並將值設置爲選中和存放到已經爲選中的變量中,而後判斷是否回調;

  2. shouldComponentUpdate中判斷用戶的值是否合法,合法返回true並執行componentWillReceiveProps,不然返回false;

第四塊,componentWillReceiveProps,用戶在外部更新組件到state後的操做,根據我的需求來,我這裏主要描述用戶設置或選擇的值是否爲禁用的值

  1. 拿到用戶設置或選擇的小時和分鐘的值(怎麼拿?以前強調過,用戶每次輸入或者選擇的時候我會將值存放到對應的變量中,此處直接獲取對應變量的值);

  2. 判斷用戶設置的小時和分鐘是否處於禁用狀態(怎麼知道是禁用,我要求用戶傳入要禁用的值的數組),非禁用則執行setState來更新小時和分鐘的列表,不然清空全部的操做狀態和值;

解釋不用state設置值:使用state設置文本框值,會出現沒法刪除文本框的值(不知道大家會不會有這種狀況,我這裏會有,因此我沒有用state),應該是跟react的虛擬dom有關,用戶在界面上的操做在js沒有更新state的狀況下應該會始終保持state值不變。

寫在最後:本人react菜鳥一枚,第一次分享本身作的東西的思路不免出現大量的紕漏或者疏忽,望諒解,因此也是但願和各位相互交流,若有大神指點本人表示熱烈歡迎。本人不吝嗇各位指點,不管水平高低,只要有好的想法或者更好的實現方式均可以和本人交流。。。歡迎回復我。。。

組件代碼示例:

import React from 'react';
import ReactDOM from 'react-dom';

class MyTimePicker extends React.Component {  
    constructor(props){
        super(props);
        this.valueHeight = 28; // 時間值的高度
        this.reg = /^((20|21|22|23|[0-1]\d)\:[0-5][0-9])$/; // 驗證時間格式是否正確
        this.handleHour = null; // 小時滾動定時器
        this.handleMinute = null; // 分鐘滾動定時器
        this.prevHourSelected = null; // 上一級選擇的小時
        this.prevMinuteSelected = null; // 上一次選擇的分鐘
        this.selectedHour = ""; // 選中的小時
        this.selectedMinute = ""; // 選中的分鐘
        this.time = 50; // 滾動時間頻率
        if(props.onChange){ // 若是用戶使用了自定義onChange,就將用戶的onChange賦值給本地的setChange屬性
            this.setChange = props.onChange; 
        }
    }
    state = {
        disabledHours: this.props.disabledHours, // 禁用的小時數組
        disabledMinutes: this.props.disabledMinutes, // 禁用的分鐘數組
        isShow: false, // 是否顯示選擇框
    }
    setChange(hour, minute){} // 構造本地的change方法接受用戶自定義方法
    range(start, end){ // 構造時間
        if(typeof start !== "number" || typeof end !== "number" || start >= end){
            return [];
        }
        let arr = [];
        for(let i = start; i <= end; i++){
            arr.push(i);
        }
        return arr;
    }
    onChange(e){
        e.stopPropagation();
        let obj = this, value = e.target.value.trim();
        if(value.length !== 0){
            if(value.trim().length === 5){ // 符合長度要求後進行正則驗證
                if(obj.reg.test(value)){
                    let hour = +value.split(":")[0], minute = +value.split(":")[1];
                    // 獲取輸入值所在的選項的樣式
                    let hourClass = obj.refs['my-Timepicker-hour'].children[0].children[hour].className;
                    let minuteClass = obj.refs['my-Timepicker-minute'].children[0].children[minute].className;

                    if(obj.prevHourSelected){ // 若是上一次存在選中的時間就清空選中樣式
                        obj.refs['my-Timepicker-hour'].children[0].children[+obj.prevHourSelected.innerHTML].className = "";
                    }
                    if(obj.prevMinuteSelected){ // 若是上一次存在選中的時間就清空選中樣式
                        obj.refs['my-Timepicker-minute'].children[0].children[+obj.prevMinuteSelected.innerHTML].className = "";
                    }

                    // 若是所選值不是禁止選中,就添加選中樣式,而且啓用滾動效果
                    if(hourClass.indexOf("my-TimePicker-time-selected-disabled") === -1 && 
                       minuteClass.indexOf("my-TimePicker-time-selected-disabled") === -1){
                           obj.refs['my-Timepicker-hour'].children[0].children[hour].className = hourClass + " my-TimePicker-time-selected";
                           obj.refs['my-Timepicker-minute'].children[0].children[minute].className = minuteClass + " my-TimePicker-time-selected";
                        obj.animateTop(hour, minute, obj.time);

                        //添加這次輸入爲上一次選中
                        obj.prevHourSelected = obj.refs['my-Timepicker-hour'].children[0].children[hour]; 
                        obj.prevMinuteSelected = obj.refs['my-Timepicker-minute'].children[0].children[minute]; 
                        obj.selectedHour = hour; 
                        obj.selectedMinute = minute; 

                        if(obj.props.onChange){ // 用戶是否綁定了自定義onChange
                            obj.setChange(hour, minute);
                        }
                    }else{ // 輸入爲禁用數時清空值
                        obj.refs['my-Timepicker-text'].value = "";
                    }
                }else{ // 置空
                    e.target.value = "";
                }
            }
        }else{
            obj.prevHourSelected = null; // 上一級選擇的小時
            obj.prevMinuteSelected = null; // 上一次選擇的分鐘
            obj.selectedHour = ""; // 選中的小時
            obj.selectedMinute = ""; // 選中的分鐘
            let hourList = obj.refs['my-Timepicker-hour'].children[0].children, 
                minuteList = obj.refs['my-Timepicker-minute'].children[0].children; 
            for(let i = 0, len = hourList.length; i < len; i++){
                if(hourList[i].className === "my-TimePicker-time-selected"){
                    hourList[i].className = "";
                    break;
                }
            }
            for(let i = 0, len = minuteList.length; i < len; i++){
                if(minuteList[i].className === "my-TimePicker-time-selected"){
                    minuteList[i].className = "";
                    break;
                }
            }
            obj.animateTop(0, 0, obj.time);
            if(obj.props.onChange){ // 用戶是否綁定了自定義onChange
                obj.setChange("", "");
            }
        }
    }
    /*
     * 小時/分鐘點擊效果
     * 參數&變量說明:
     *    參數:e -> 小時選中對象(當前或者上一次)
     *    變量:selectedHour -> 當前選中小時,selectedMinute -> 當前選中分鐘
     * 一、若是是禁止選擇的時間,return
     * 二、若是上一次存在選中的時間就清空選中樣式
     * 三、經過前兩次判斷後添加當前選擇爲選中,設置當前選中小時
     * 四、添加本次選中爲上次選中
     * 五、添加選中動畫
     * 六、若是小時和分鐘同時選中,則設置文本框值
     * 七、若是小時和分鐘同時選中則啓用回調
     */
    hourClick = (e) => {
        let target = e.target, obj = this, 
            selectedHour = target.innerHTML, 
            selectedMinute = obj.selectedMinute; 
        if(target.className === "my-TimePicker-time-selected-disabled"){
            return ;
        }
        if(obj.prevHourSelected){
            obj.prevHourSelected.className = "";
        }
        target.className = "my-TimePicker-time-selected";

        obj.prevHourSelected = target;
        obj.selectedHour = selectedHour;
        obj.animateTop(selectedHour, selectedMinute, obj.time);

        if(selectedMinute !== ""){
            obj.refs['my-Timepicker-text'].value = selectedHour+":"+selectedMinute;
        }

        if(obj.props.onChange){
            obj.setChange(+selectedHour, selectedMinute);
        }
    }
    minuteClick = (e) => {
        let target = e.target, obj = this, 
            selectedHour = obj.selectedHour, 
            selectedMinute = target.innerHTML; 
        if(target.className === "my-TimePicker-time-selected-disabled"){
            return ;
        }
        if(obj.prevMinuteSelected){
            obj.prevMinuteSelected.className = "";
        }
        target.className = "my-TimePicker-time-selected";

        obj.prevMinuteSelected = target;
        obj.selectedMinute = selectedMinute;
        obj.animateTop(selectedHour, selectedMinute, obj.time);

        if(selectedHour !== ""){
            obj.refs['my-Timepicker-text'].value = selectedHour+":"+selectedMinute;
        }

        if(obj.props.onChange){
            obj.setChange(selectedHour, +selectedMinute);
        }
    }
    animateTop(hour, minute, time){ // 時間滾動效果
        let obj = this, 
            curHourTop = obj.refs['my-Timepicker-hour'].scrollTop, // 當前小時的滾動高度
            curMinuteTop = obj.refs['my-Timepicker-minute'].scrollTop; // 當前分鐘的滾動高度
        clearInterval(obj.handleHour);
        clearInterval(obj.handleMinute);

        // 不爲空就轉成數字
        hour = (hour === "")?"":+hour;
        minute = (minute === "")?"":+minute;

        // 判斷是否進行動畫
        if(curHourTop !== obj.valueHeight*hour && hour !== ""){
            obj.handleHour = setInterval(() => {
                let getTop = obj.refs['my-Timepicker-hour'].scrollTop, // 實時獲取滾動高度
                    result = getTop - obj.valueHeight*hour, // 實時計算滾動高度差
                    speed = Math.floor(result/3); // 實時計算滾動的正負速度
                if(speed !== 0){ // 若是滾動高度差絕對值大於0,始終滾動
                    obj.refs['my-Timepicker-hour'].scrollTop = getTop - speed;
                }else{ // 反之,中止滾動
                    obj.refs['my-Timepicker-hour'].scrollTop = obj.valueHeight*hour; // 速度等於0時,手動修正動畫偏差
                    clearInterval(obj.handleHour);
                }
            }, time);
        }
        if(curMinuteTop !== obj.valueHeight*minute && minute !== ""){
            obj.handleMinute = setInterval(() => {
                let getTop = obj.refs['my-Timepicker-minute'].scrollTop, // 實時獲取滾動高度
                    result = getTop - obj.valueHeight*minute, // 實時計算滾動高度差
                    speed = Math.floor(result/2); // 實時計算滾動的正負速度
                if(speed !== 0){ // 若是速度不等於0,始終滾動
                    obj.refs['my-Timepicker-minute'].scrollTop = getTop - speed;
                }else{ // 反之,中止滾動
                    obj.refs['my-Timepicker-minute'].scrollTop = obj.valueHeight*minute; // 速度等於0時,手動修正動畫偏差
                    clearInterval(obj.handleMinute);
                }
            }, time);
        }
    }
    componentDidMount(){
        let obj = this, value = obj.props.defaultValue;
        if(value !== "" && obj.reg.test(value)){
            let hour = +value.split(":")[0], 
                minute = +value.split(":")[1], 
                hourObj = obj.refs['my-Timepicker-hour'].children[0].children[hour],
                minuteObj = obj.refs['my-Timepicker-minute'].children[0].children[minute], 
                hourClass = hourObj.className, 
                minuteClass = minuteObj.className; 

            obj.refs['my-Timepicker-text'].value = obj.props.defaultValue;

            obj.prevHourSelected = hourObj; // 上一級選擇的小時
            obj.prevMinuteSelected = minuteObj; // 上一次選擇的分鐘
            obj.selectedHour = hour; // 選中的小時
            obj.selectedMinute = minute; // 選中的分鐘

            obj.animateTop(hour, minute, obj.time);

            if(obj.props.onChange){
                obj.setChange(hour, minute);
            }
        }
    }
    // 已加載組件首次渲染完畢後,父組件更新state自動調用次方法從新傳入props,接受值後刷新禁用值, 判斷選中的值是否在禁用中存在
    componentWillReceiveProps(nextProps){
        let obj = this, getHour = obj.selectedHour, getMinute = obj.selectedMinute;
        if(nextProps.disabledHours.indexOf(getHour) === -1 && nextProps.disabledMinutes.indexOf(getMinute) === -1){
            this.setState({ 
                disabledHours: nextProps.disabledHours,
                disabledMinutes: nextProps.disabledMinutes, 
            });
        }else{
            obj.reset();
        }
    }
    reset = () => { // 重置全部值和選擇
        let obj = this;
        obj.refs['my-Timepicker-text'].value = "";
        obj.prevHourSelected = null; // 上一級選擇的小時
        obj.prevMinuteSelected = null; // 上一次選擇的分鐘
        obj.selectedHour = ""; // 選中的小時
        obj.selectedMinute = ""; // 選中的分鐘
        let hourList = obj.refs['my-Timepicker-hour'].children[0].children, 
            minuteList = obj.refs['my-Timepicker-minute'].children[0].children;
        obj.setState({ 
            disabledHours: [],
            disabledMinutes: [], 
        }); 
        obj.animateTop(0, 0, obj.time);
    }
    onMouseMove = (e) => {
        this.setState({ isShow: true });
    }
    onMouseOut = (e) => {
        this.setState({ isShow: false });
    }
    render(){
        let obj = this;
        return(
            <div className="my-TimePicker-wrapper" onMouseMove={obj.onMouseMove} onMouseOut={obj.onMouseOut}>
                <div className="my-TimePicker-header">
                    <span class="ant-time-picker ">
                        <input id={obj.props.id} placeholder={obj.props.placeholder}
                            ref="my-Timepicker-text" className="ant-time-picker-input"
                            onChange={obj.onChange.bind(this)} />
                    </span>
                </div>
                <div style={{ height:(obj.state.isShow)?"138px":"0px" }} ref="my-Timepicker-content" className="my-TimePicker-content">
                    <div ref="my-Timepicker-hour" className="my-TimePicker-content-box">
                        <ul className="my-TimePicker-content-menu" onClick={obj.hourClick} >
                        {
                            obj.range(0, 23).map((cur, index) => {
                                let getHour = cur, setClass = "";
                                obj.state.disabledHours.map((cur1, index1) => {
                                    if(getHour === cur1){
                                        setClass = "my-TimePicker-time-selected-disabled"
                                    }
                                })
                                return <li className={ setClass } key={index} >{(cur < 10)?"0"+cur:cur}</li>
                            })
                        }
                        </ul>
                    </div>
                    <div ref="my-Timepicker-minute" className="my-TimePicker-content-box">
                        <ul className="my-TimePicker-content-menu" onClick={obj.minuteClick} >
                        {
                            obj.range(0, 59).map((cur, index) => {
                                let getHour = cur, setClass = "";
                                obj.state.disabledMinutes.map((cur1, index1) => {
                                    if(getHour === cur1){
                                        setClass = "my-TimePicker-time-selected-disabled"
                                    }
                                })
                                return <li className={ setClass } key={index} >{(cur < 10)?"0"+cur:cur}</li>
                            })
                        }
                        </ul>
                    </div>
                </div>
            </div>
        )
    }
}

MyTimePicker.propTypes = {
     id: React.PropTypes.string,
     defaultValue: React.PropTypes.string, // 默認值
     placeholder: React.PropTypes.string,
     disabledHours: React.PropTypes.array, // 禁用的小時數組
     disabledMinutes: React.PropTypes.array // 禁用的分鐘數組
};
MyTimePicker.defaultProps = {
    id: "",
    defaultValue: "",
    placeholder: "請選擇時間",
    disabledHours: [],
    disabledMinutes: []
}

export default MyTimePicker;

使用實例:

/*
     * 參數說明:hour -> 回調小時,minute -> 回調分鐘
     * 變量說明:obj -> 當前上下文, endHourArr -> 禁用結束小時數組,endMinuteArr -> 禁用結束分鐘數組,
     *           startMinuteArr -> 禁用開始分鐘數組,preEndHour -> 上一次選中的結束小時
     *           preEndMinute -> 上一次選中的結束分鐘
     * 方法說明:選中時間後的回調方法,start和end是開始與結束,兩個方法功能相同
     *      一、只選中時間時,返回禁用的小時(不包括選中的小時)
     *      二、判斷時間和分鐘是否同時選中,返回禁用的小時數組,根據分鐘的位置決定小時是否進退
     *      三、在時間和分鐘同時選中時,繼續判斷是否存在已經選中的開始/結束小時並判斷是否和當前選中小時相等,返回禁用的分鐘數組
     *      四、判斷開始/結束小時和分鐘是否選中,並判斷開始/結束小時否和當前小時相等,返回當前元素須要禁用的分鐘數組
     */
    changeStartTime(hour, minute){ 
        let obj = this, 
            endHourArr = [], 
            endMinuteArr = [], 
            startMinuteArr = [], 
            preEndHour = obj.endHour, 
            preEndMinute = obj.endMinute; 

        if(hour !== ""){
            endHourArr = obj.range(0, +hour, "<");
        }
        if(hour !== "" && minute !== ""){
            endHourArr = (+minute === 59)?obj.range(0, +hour):obj.range(0, +hour, "<");
            if(preEndHour !== "" && parseInt(preEndHour) === parseInt(hour)){
                endMinuteArr = obj.range(0, +minute);
            }
        }
        if(hour !== "" && preEndHour !== "" && preEndMinute !== "" && parseInt(preEndHour) === parseInt(hour)){
            startMinuteArr = obj.range(+preEndMinute, 59);
        }

        obj.startHour = hour;
        obj.startMinute = minute;
        obj.setState({ 
            disabledStartMinutes: startMinuteArr, 
            disabledEndHours: endHourArr,
            disabledEndMinutes: endMinuteArr, 
        });
    }
相關文章
相關標籤/搜索