动手打造React组件库-autoComplete

800 阅读3分钟

本文正在参加「金石计划」

一:引言

本篇文章仿写antd组件库的autoComplete组件,文章重点是模拟autoComplete组件的内部功能实现,样式比较丑希望大家包涵。

二:组件分析

观察下图,我们设计一个可配置的autoComplete组件,它主要有包含以下配置。

1.gif

  • className: 类名
  • placeholder:初始输入提示
  • onSearch:自定义搜索查询

三:问题拆解

在设计组件前,我们可以先考子问题,最后组装起来就实现了目标组件。

**问题1:输入框输入时,数据防抖处理? **

考虑搜索过程中频繁的向后端发送请求,因此我们对搜索的处理添加防抖

  1. 导入防抖lodash

     import {debounce} from "lodash";
    
  2. 定义防抖函数

     const debunleInput = useCallback(debounce(onInputChange,500),[])
     
    
  3. 添加输入框内

     <input 
         type="text"
         onInput={(e:any)=>{
           setInputVal(e.target.value);
           debunleInput(e)
         }} 
       />
    

问题2:如何将用户自定义搜索结果展示?(搜索过程有可能是请求后端,也有可能是前端处理)

由于我们不确定用户处理根据输入框内容如何处理搜索结果,可能是同步代码处理,也有可能是后端异步请求的处理方式,因此我们必须使用Promise.resolve(...)将所有的可能统一变为prmosise操作,然后就可以便捷的获取数据

  //输入搜索返回结果处理,onSearch是用户自定义搜索返回的结果
  const onInputChange = (e:any) => {
    Promise.resolve(onSearch(e.target.value)).then((msg:any)=>{
      setList(msg);
      setActivedIndex(0);
      setIsDownShow(true);
    })
  }

问题3:如何通过键盘指令控制下拉列表选择?

  1. 首先我们给输入框添加键盘监听事件

  2. 根据键盘输入的字符可以获取其唯一的code值,判断是否是上,下,enter,esc指令

     const handleKeyDown = (e:any) => {
         console.log(e.keyCode);
         switch (e.keyCode) {
           case 13://enter
             //选择
             setInputVal(list[activedIndex]);
             setIsDownShow(false);
             break
           case 38://↑
             if(activedIndex!==0) {
               setActivedIndex(activedIndex-1);
             }
             break
           case 40://↓
             if(activedIndex!==list.length-1) {
               setActivedIndex(activedIndex+1);
             } 
             break
           case 27://esc
             //关闭并退出
             setActivedIndex(0);
             setIsDownShow(false);
             break
           default:
             break
         }
       }
    

五:autoComplete代码实现

    import React,{useState,useCallback, useEffect, useRef} from "react";
    import {debounce} from "lodash";
    import classNames from "classnames";
    interface AutoComplete {
      className?:string
      options?:any,
      placeholder?:string,
      onSearch:(e:any)=>void //用户自定义搜索结果
    }
    /**
     * 1.输入框防抖,避免多次请求
     * 2.非输入框点击关闭下拉框
     */
    const AutoComplate = (props:AutoComplete) => {
      const {
        options,
        className,
        placeholder,
        onSearch
      } = props;
      const [list,setList] = useState([]);
      const [inputVal,setInputVal] = useState('');
      const [isDownShow,setIsDownShow] = useState(false);
      const [activedIndex,setActivedIndex] = useState(0);//当前选中的列表
      const divRef = useRef<HTMLDivElement>(null);
      useEffect(()=>{
        const handle = (e:any) => {
          const isOutside = !divRef.current?.contains(e.target as Node);
          if(isOutside) setIsDownShow(false);
        }
        document.addEventListener('click',handle);
        return ()=>{
          document.removeEventListener('click',handle);
        }
      },[])
      //input change 
      const onInputChange = (e:any) => {
        Promise.resolve(onSearch(e.target.value)).then((msg:any)=>{
          setList(msg);
          setActivedIndex(0);
          setIsDownShow(true);
        })
      }
      //防抖处理
      const debunleInput = useCallback(debounce(onInputChange,500),[])

      //classname
      const classes = classNames('auto-complete',className);

      //选择下拉列表某一项
      const choseItem = (e:any) => {
        setInputVal(e);
      }

      //键盘事件监听
      const handleKeyDown = (e:any) => {
        console.log(e.keyCode);
        switch (e.keyCode) {
          case 13://enter
            //选择
            setInputVal(list[activedIndex]);
            setIsDownShow(false);
            break
          case 38://↑
            if(activedIndex!==0) {
              setActivedIndex(activedIndex-1);
            }
            break
          case 40://↓
            if(activedIndex!==list.length-1) {
              setActivedIndex(activedIndex+1);
            } 
            break
          case 27://esc
            //关闭并退出
            setActivedIndex(0);
            setIsDownShow(false);
            break
          default:
            break
        }
      }
      return (
        <div className={classes} ref={divRef}>
          <input 
            type="text" 
            placeholder={placeholder} 
            onKeyDown={handleKeyDown}
            onInput={(e:any)=>{
              setInputVal(e.target.value);
              debunleInput(e)
            }} 
            value={inputVal}
          />
          <div className="auto-complete-lists">
            {isDownShow && list.length>0 && list.map((item,index)=>(
              <div 
                key={item+''+Math.random()} 
                className={activedIndex===index?'auto-complete-lists-item auto-complete-lists-item-actived':'auto-complete-lists-item'}
                onClick={()=>{
                  setIsDownShow(false);
                  choseItem(item);
                }}
              >
                {item}
              </div>
            ))}
          </div>
        </div>
      )
    }

    export default AutoComplate;

六:功能演示

演示1:基础功能演示

<AutoComplate onSearch={search}/>

const search = (value:any) => {
    //模拟后后端请求
    return new Promise((resolve,reject)=>{
      setTimeout(() => {
        let arr = Array.from(new Array(7),item=>Math.floor(Math.random()*10));
        resolve(arr);
      }, 500);
    })
  }
  
  
  
  
  

2.gif

演示2:键盘事件演示(纯键盘操作,无鼠标介入)

3.gif

总结

今天autoComplete组件到此结束,希望大家多多支持,我们下一个组件见。