基于React-hooks和Typescript封装的AutoComplete组件。
组件介绍
1. 基本功能
1.1 一个文本输入框,能够输入文字并根据所输入的文字给出对应提示
1.2 所提示的文字以列表形式呈现,并能选择然后填充至输入框
1.3 提示的文本列表中,需要将输入框中的文字高亮显示(或者醒目的 css 样式)
1.4 当输入匹配结果为空时,需要显示特定的内容
1.5 输入搜索时需要 debounce
1.6 能够传递style参数,方便修改input样式
2. 需要实现的apis
| field | description | type |
|---|---|---|
| options | 自动完成的数据源 | Array |
| notFoundContent | 当匹配结果为空时的内容 | React.ReactNode |
| debounceDelay | Debounce 延迟时间,以毫秒记 | number |
| placeholder | 输入框的 placeholder | string |
| dropdownClassname | 提示下拉栏的类名 | string |
| style | 给input框添加样式 | CSSProperties |
实现过程
创建组件
// props 接口
interface IProps {
placeholder?: string,
options: Array<any>,
notFoundContent?: React.ReactNode,
style?: CSSProperties,
debounceDelay?: number,
dropdownClassname?: string
}
// 声明并暴露组件
export const AutoComplete: React.FC<IProps> = (props) => {
return (
<div>
<input
type="text" className="auto-complete" value={value}
placeholder={placeholder} onFocus={onFocus}
onBlur={onBlur} onChange={onChange} style={style}
/>
</div>
);
}
下拉列表
下拉列表根据
isShow决定是否显示:let [isShow, setIsShow] = useState(false)
jsx部分
<div>
<input
type="text" className="auto-complete" value={value}
placeholder={placeholder} onFocus={onFocus}
onBlur={onBlur} onChange={onChange} style={style}
/>
{
isShow ?
(filterOptions.length>0 ? (<ul className={`dropdown ${dropdownClassname}`}>
{
filterOptions.map((item,index) => {
return <li className='li' key={index}>
{item}
</li>
})
}
</ul>) : notFoundContent) : null
}
</div>
输入框focus时(highlight是高亮,我们后面再讲)
const onFocus = (): void => {
// value是输入框的value值:const [value, setValue] = useState('')
if(value){
// 先展示出下拉列表,再高亮
setIsShow(true)
setTimeout(() => {
highlight()
});
}
}
输入框blur时
const onBlur = (): void => {
setTimeout(() => {
setIsShow(false)
}, 100);
}
这里设置定时器是为了当点击下拉列表的时候也会触发blur事件,如果立马隐藏下拉列表就无法处理点击下拉列表的事件,所以这里设置100ms是让程序去处理完点击下拉列表的事件,再隐藏下拉列表。我不确定这样做是不是最优解,但是出来效果达到预期,所以暂时先这样 ┑( ̄Д  ̄)┍(摊手)
输入框change时
const delayEvent = useDebounce(event, debounceDelay)
const onChange = (e: React.FormEvent<HTMLInputElement>): void => {
setValue(e.currentTarget.value)
delayEvent()
}
delayEvent是实现输入防抖效果的,这里算是一个重难点,跟上哦~useDebounce文件:
import { useRef } from 'react'
export default function useDebounce(fn:()=>void,delay:number) {
const { current } = useRef({timer:0})
return () => {
if(current.timer){
clearTimeout(current.timer)
}
current.timer = window.setTimeout(() => {
fn()
}, delay)
}
}
防抖咱们都会写,但是这里为什么要用到useRef呢?
useRef保留了timer的唯一性,也就是说组件render前后timer都是这一个。
那有人就要说了(其实是之前的我自己。。):多此一举!直接这样不香吗:
const debounce: IDebounce = (delay, event) => {
setValue(e.currentTarget.value)
let timer:number | null = null
return function () {
if(timer) {
window.clearTimeout(timer as number);
}
timer = window.setTimeout(() => {
event()
}, delay);
}
}
我们之前防抖都是这样写的对吧,但是在react中不得行,因为每次render的时候都会重新声明一个timer(跟之前的timer没关系),所以最后效果就是没有防抖。
插一嘴,React搭配TS时定时器的相关写法,也许可以帮助到你。
说完了防抖的注意事项,我们再来看防抖中的事件event:
const event = (): void => {
console.log('event');
setTimeout(() => {
// 获取最新的value值
let _value = valueRef.current
let filterArr: Array<string> = []
if(_value){
setFilter([])
options.forEach(str => {
// 如果str以input里的值开头
if(str.startsWith(_value)){
filterArr.push(str)
}
})
setFilter(filterArr)
setIsShow(true)
} else {
setIsShow(false)
}
});
}
我们主要做的就是从传入的option中筛选出用于提示的filterOption。我的判断依据是:是否是以当前输入框中的值开头。如果是,那么压入filterOption,供下拉列表显示。
关键字高亮(重难点)
useEffect(()=>{
setTimeout(() => {
if(filterOptions.length){
// 高亮关键词
highlight()
}
});
}, [filterOptions])
highlight函数:
const highlight = (): void => {
let transformString = value.replace(/[.[*?+^$|()/]|\]|\\/g, '\\$&');
let pattern = new RegExp(transformString, 'i'); // 不区分大小写
const liList = document.querySelectorAll('.li')
for(let i=0; i<liList.length; i++) {
highlightKeyword(liList[i], pattern)
}
}
highlightKeyword函数(重头戏):
const highlightKeyword = (node:any, pattern:RegExp): void => {
// nodeType等于3表示是文本节点
if (node.nodeType === 3) {
// node.data为文本节点的文本内容
var matchResult = node.data.match(pattern);
// 有匹配上的话
if (matchResult) {
// 创建一个span节点,用来包裹住匹配到的关键词内容
let highlightEl = document.createElement('span');
// 不用类名来控制高亮,用自定义属性data-*来标识,
// 比用类名更减少概率与原本内容重名,避免样式覆盖
highlightEl.dataset.highlight = 'yes';
// 从匹配到的初始位置开始截断到原本节点末尾,产生新的文本节点
let matchNode = node.splitText(matchResult.index);
// 从新的文本节点中再次截断,按照匹配到的关键词的长度开始截断,
// 此时0-length之间的文本作为matchNode的文本内容
matchNode.splitText(matchResult[0].length);
// 对matchNode这个文本节点的内容(即匹配到的关键词内容)创建出一个新的文本节点出来
let highlightTextNode = document.createTextNode(matchNode.data);
// 插入到创建的span节点中
highlightEl.appendChild(highlightTextNode);
// 把原本matchNode这个节点替换成用于标记高亮的span节点
matchNode.parentNode.replaceChild(highlightEl, matchNode);
}
}
// 如果是元素节点 且 不是script、style元素 且 不是已经标记过高亮的元素
// 不是已经标记过高亮的元素作为条件之一的理由是,避免进入死循环,一直往里套span标签
else if ((node.nodeType === 1) && !(/script|style/.test(node.tagName.toLowerCase())) && (node.dataset.highlight !== 'yes')) {
// 遍历该节点的所有子孙节点,找出文本节点进行高亮标记
var childNodes = node.childNodes;
for (var i = 0; i < childNodes.length; i++) {
highlightKeyword(childNodes[i], pattern);
}
}
}
通过文本节点和splitText方法实现,具体实现参考这里。
github
其他的需求是如何完成的直接看我的代码就好了,应该是简单易懂的,github走起 (✿◡‿◡)