react實現個有特殊需求的級聯組件(附帶思路和優化點)

需求


ant design有個組件cascader組件,但是不同時支持模糊搜索和遠程數據加載,那麼自己實現個。
需要實現的需求

  • 遠程加載所有一級類目錄,點擊不同的一級類目錄,遠程請求加載不同的二級類目錄。
  • 同時二級類目錄還需要一個搜索功能,需要按照用戶輸入的內容實時獲取對應的二級目錄
    具體可以粗略的見下圖
    在這裏插入圖片描述

需求分析


  • 最上面的那個選擇框,只能讓用戶點擊,不能輸入,實現方法太多,我們使用 改改樣式即可
  • 下拉框無論是一級還是二級,其實就是個list,這裏我們用ul>li實現
  • 二級下拉框上的input輸入框,我們使用input即可
  • 這裏只需要二級,我們也只實現二級,如果像其他框架的無限制層級,只需要把list做成組件,傳值即可,這裏就不展開了,有興趣的童鞋可以自己實現!
  • 二級下拉框因爲要按照用戶輸入的實時搜索,所有我們使用節流,暫定1s搜索一次,如果搜索太頻繁,很容易導致發送請求過多,增加服務端壓力。在這裏我們可以增加個模式,用戶可以控制是否使用遠程實時加載,如果不使用遠程實時加載,那麼使用本地模糊匹配。
  • 二級下拉框,如果數據量過大,我們也需要一個滾動加載,所有在二級下拉框中,我們需要來個防抖,不要頻繁觸發scroll事件。
  • 爲了方便用戶,我們暫定如果二級數據少,用戶下拉框使用本地模糊搜索,也不存在滾動加載!如果數據多,我們就採用滾動加載和遠程實時加載相結合!

注意點


  • ant design的組件很多都是需要你觸發後,點擊組件相關的,組件繼續展開,點擊組件以爲其他地方,組件自動關閉,詳見下圖:組件展開後,點擊紅色區域,組件一直處於展開狀態,點擊其他區域,組件關閉!
    在這裏插入圖片描述
    這裏實現的思路很簡單,組件內部只需要狀態判斷,即可自己控制組件展開和收縮!問題在於如何控制組件以外的地方,如果通過組件獲得用戶網頁的body,再綁定,顯然不現實,因爲這個需要用戶把最外層的元素的this傳入,所有我們的實現方案是給組件增加個全局透明的遮罩層,當組件展開後,遮罩層自動出現,這個時候只要點擊在紅色方框之外,其實就是點擊在遮罩層,方便我們收縮組件。說白了,遮罩層也是組件一部分!
  • 第二個問題就是組件展開內容的定位,我們需要讓效果真實,所以,我們每次展開組件,都重新計算組件展開內容的位置!

初步實現


代碼都是簡化的,如果要使用到實際中,請自己加以改造

class CascaderSelf extends React.Component {
constructor(props) {
        super(props);
        this.throttleData = Throttle(this.getChildData);
        this.scrollDataDeb= Debounce(this.scrollData)
    }
	state = {
        show1: false,
        parentValue: '',
        parentText: '',
        childNode: [],
        childText: '',
        childValue: '',
        ParentNode: [],
        loading: false,
        filterValue: '',
        mode: false, 
        page: 1,
        pageSize:20,
        total: 0,
    };
    componentDidMount() {
        this.setState({
            ParentNode: this.props.ParentNode,
            parentText: this.props.parentText,
            mode: this.props.mode
        })
    }

    componentWillReceiveProps(props) {
        const {
            ParentNode,
            childText,
            mode = false,
        } = props;

        this.setState({
            ParentNode,
            childText,
            mode,
        })
    }
    showLabel = (e) => {
        this.setState({
            show1: true,
            left1: e.currentTarget.getBoundingClientRect().left,
            top1: e.currentTarget.getBoundingClientRect().top + e.currentTarget.clientHeight + 2,
            width: e.currentTarget.clientWidth
        })
    }
    showLabel2 = (e) => {
        e.stopPropagation();
        const parentValue = e.target.getAttribute('value');
        const {
            mode,
            pageSize
        } = this.state;
        const params = {
            value: parentValue,
        };
        if (mode) {
            params['page'] = 1;
            params['pageSize'] = pageSize;
        }
        setTimeout(() => {
            this.getChildData({
                ...params,
            });
        }, 2)
        this.setState({
            parentValue,
            show2: true,
            parentText: e.target.innerHTML,
            loading: true,
            childValue: '',
            childText: '',
            childNode: [],
            page: 1,
        })
    }
    closeAll = (flag, e) => {
        if (e.target.nodeName == 'LI') {
            if (flag) {
                const {
                    childNode
                } = this.state;
                const childValue = e.target.getAttribute('value');
                const childText = e.target.innerHTML;
                const chlidOpts = childNode.filter(v => v.value == childValue)
                const {
                    parentValue
                } = this.state;
                this.setState({
                    childValue,
                    childText
                })
                this.props.onChange([parentValue, childValue], chlidOpts[0] || {})
            }
        }
        if (e.target.nodeName != 'Input') {
            this.setState({
                show1: false,
                show2: false,
                loading: false,
                childNode: []
            })
        }
    }
    getChildData = (params) => {
        let {
            page,
            childNode,
        } = this.state;
        this.props.loadData(params, (newNode, total = 0) => {
            this.setState({
                childNode: [...childNode, ...newNode],
                loading: false,
                page: ++page,
                total,
            })
        })
    }
    render(){
		const {
            show1,
            parentValue,
            show2,
            left1,
            top1,
            width,
            childNode,
            childValue,
            childText,
            ParentNode,
            loading,
            filterValue,
            mode
        } = this.state;
        const {
            showLabel,
            closeAll,
            searchChildData,
            scrollDataDeb,
        } = this;
	}
	scrollData = () => {
        let scrollTop = this.listRef.scrollTop;
        let clientHeight = this.listRef.clientHeight;
        let scrollHeight = this.listRef.scrollHeight;
        const {
            loading, 
            page, 
            mode,
            parentValue,
            filterValue,
            pageSize,
            total,
        } = this.state;
        if (total <= (page - 1) * pageSize) return;
        if ((scrollTop + clientHeight >= scrollHeight - 20) && !loading) {
            this.setState({
                loading: true
            });
            setTimeout(() => {
                this.getChildData({
                    value: parentValue,
                    name: filterValue,
                    page,
                    pageSize
                });
            }, 2)
        }
    }
	searchChildData = (e) => {
        const {
            mode,
            parentValue,
            pageSize,
            filterValue: oldfilterValue,
            childNode
        } = this.state;
        const filterValue = e.target.value.trim('');
        if (filterValue == oldfilterValue) return;
        this.setState({
            filterValue,
            page: 1,
            childNode: mode ? [] : childNode
        });
        if (!mode) return;
        setTimeout(() => {
            this.throttleData({
                value: parentValue,
                name: filterValue,
                page:1,
                pageSize
            });
        }, 2)
    }
	return (
            <div style={{ width: '100%' }}>
                <Input
                    type='button'
                    onBlur={e => {

                    }}
                    onFocus={e => {
                        showLabel(e)
                    }}
                    value={childText}
                    className={'onlyButton'}
                />
                {show1 && <div style={{ position: 'fixed', left: 0, right: 0, top: 0, bottom: 0, zIndex: 10000 }} onClick={e => closeAll(false, e)}>
                    <div style={{ position: 'absolute', left: left1 + 'px', top: top1 + 'px', zIndex: 100, display: 'flex' }}>
                        <Spin spinning={loading}>
                            <ul className={styles.box} style={{ minWidth: width + 'px', maxHeight: '300px', overflow: 'auto' }} onClick={this.showLabel2}>
                                {ParentNode.map(v => <li className={classnames({ [styles.common]: true, [styles.active]: parentValue == v.value })} value={v.value}>{v.label}</li>)}
                            </ul>
                        </Spin>
                        {show2 && <Spin spinning={loading}>
                            <div>
                                <Input
                                    onClick={e => {
                                        e.stopPropagation()
                                    }}
                                    onChange={
                                        e => {
                                            searchChildData(e)
                                        }
                                    }
                                    value={filterValue}
                                />
                            </div>
                            {mode&&<ul ref={el => this.listRef = el} onScroll={e => { scrollDataDeb() }} style={{ minWidth: width + 'px', background: '#fff', height: '300px', overflow: 'auto' }} onClick={e => this.closeAll(true, e)}>
                                {childNode.map(v => <li className={classnames({ [styles.common]: true, [styles.active]: childValue == v.value })} value={v.value}>{v.label}</li>)}
                            </ul>}
                            {!mode&&<ul style={{ minWidth: width + 'px', background: '#fff',minHeight: '90px', maxHeight: '300px', overflow: 'auto' }} onClick={e => this.closeAll(true, e)}>
                                {filterValue ?
                                    childNode.filter(v => v.label.indexOf(filterValue) > -1).map(v => <li className={classnames({ [styles.common]: true, [styles.active]: childValue == v.value })} value={v.value}>{v.label}</li>) :
                                    childNode.map(v => <li className={classnames({ [styles.common]: true, [styles.active]: childValue == v.value })} value={v.value}>{v.label}</li>)
                                }
                            </ul>}
                        </Spin>
                        }
                    </div>
                </div>}
            </div>
        );
}

上文中的節流和防抖大家自行實現,主要講講下面幾點

  • mode,當mode是true的時候,是數據比較多的情況,所以匹配了二級滾動加載和遠程模糊搜索,mode爲false的時候,則是數據比較少的,可以本地模糊匹配
  • 文中出現了this.props.loadData是客戶自己用於請求數據的,所以有很多的回調方法,修改組件的state
  • 用戶loadData的實現,可以用promise,在then中調用回調函數!這個和ant design的思路有點不同,ant design是修改了所有的數據源,也就是options,然後繼續重新渲染cascader組件,我們的方法是把重新獲取的數據通過組件回調函數處理,更新組件的state,再重新渲染!
  • 用戶onChange實現沒啥要求,唯一的要求是更新props或者state,觸發組件的渲染
  • ant design是統一了數據渲染,外界傳入的數據修改了,就重新渲染。我們這個組件其實也是同樣的方法,只是我們用了回調函數,傳新增給組件,讓組件重新渲染。大家可以對比下ant design 的loadData方法,發現多了個cb回調給客戶使用,雖然耦合有點強,但是卻減少了組件數據的解析!有興趣的童鞋可以修改成類似cascader那種,直接修改options的方式處理!

下面是部分樣式

.common {
    background: #fff;
    min-height: 30px;
    line-height: 30px;
    padding: 0 8px;
    cursor: pointer;
    &:hover {
        background: #ecf6fd;
    }
}

.active {
    background-color: #f7f7f7;
    font-weight: 600;
    color: rgba(0,0,0,.65);
    &:hover {
        background: #f7f7f7;
    }
}

.box {
    background-color: #fff;
    box-shadow: 0 1px 6px rgba(0,0,0,.2);
    border-radius: 4px;
    box-sizing: border-box;
}

後記


  • 組件的思路其實很多,寫代碼理順思路大概要花70%的時間,真的實現起來,也就只有30%
  • 上述組件其實還有幾個地方可以優化
    1.通用性,就像一開始提到的,把組件ul>li這段修改成組件,同時loadData不再使用cb更新數據,而是像ant design一樣,每次傳入全部數據,然後解析
    2.用戶定製化,用戶可以自己傳入valueKey和labelKey,來定製化獲得的值,而不是像現在組件這裏都使用固定value和label
    3.契合ant design的form表單,思路很簡單,看下form表單對組件做了哪些處理,按照form表單接受的形式,改造下組件即可
  • 這個組件其實很多子組件是基於ant Design,大家可以看到組件中的Input,Spin等等,這也契合了react 的積木思想,大家可以把一個個組件看成樂高積木,自由搭配成你想要的組件即可!
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章