如何为自定义React TagsInput组件添加自动联想补全功能
不需要引入第三方依赖,直接在现有组件基础上扩展即可,实现步骤如下:
核心实现逻辑
- 预置模拟数据源做匹配基准,实际项目可替换为接口返回的品牌列表
- 新增候选项过滤逻辑:输入内容非空时,实时筛选出未被选中、和输入内容大小写不敏感匹配的条目
- 扩展键盘交互:支持上下箭头切换候选项、回车直接选中高亮项,不破坏原有回删、逗号确认标签的逻辑
- 新增鼠标交互:点击候选项直接添加为标签,点击组件外部自动隐藏下拉列表
- 过滤逻辑默认排除已添加的标签,避免重复推荐
实现代码
1. 定义模拟数据源
可单独抽为常量文件,也可直接放在组件内:
// 模拟汽车品牌数据库 const CAR_BRANDS = [ "Alfa Romeo", "Aston Martin", "BMW", "Bentley", "Bugatti", "Cadillac", "Chevrolet", "Ferrari", "Fiat", "Ford", "Honda", "Jaguar", "Jeep", "Lamborghini", "Land Rover", "Lexus", "Maserati", "Mazda", "Mercedes-Benz", "Porsche", "Rolls-Royce", "Tesla", "Toyota", "Volkswagen", "Volvo" ];
2. 修改后TagsInput组件代码
原有标签增删逻辑完全保留,仅新增自动补全相关逻辑:
import React, { useState, useRef, useEffect } from 'react'; import ClearIcon from '@mui/icons-material/Clear'; // 原有classes样式可保留,下面补下拉相关样式即可 const classes = { container: 'relative flex flex-wrap gap-2 p-2 border rounded', tag: 'flex items-center gap-1 px-2 py-1 bg-gray-100 rounded text-sm', del: 'cursor-pointer text-gray-500 hover:text-gray-700', input: 'flex-1 min-w-[120px] outline-none border-none p-1', // 新增下拉候选项样式 dropdown: 'absolute top-full left-0 right-0 z-10 mt-1 bg-white border rounded shadow max-h-[200px] overflow-y-auto', option: 'px-3 py-2 cursor-pointer hover:bg-gray-100 text-sm', optionActive: 'px-3 py-2 cursor-pointer bg-blue-50 text-blue-700 text-sm' } export default function TagsInput(props) { const tags = props.tags const setTags = props.setTags const [input, setInput] = useState(''); const [isKeyReleased, setIsKeyReleased] = useState(false); // 新增自动补全相关状态 const [suggestions, setSuggestions] = useState([]); const [highlightIndex, setHighlightIndex] = useState(0); const containerRef = useRef(null); // 点击组件外部时隐藏下拉 useEffect(() => { const handleClickOutside = (e) => { if (containerRef.current && !containerRef.current.contains(e.target)) { setSuggestions([]); } } document.addEventListener('mousedown', handleClickOutside); return () => document.removeEventListener('mousedown', handleClickOutside); }, []) // 选中候选项通用逻辑 const addTag = (value) => { const trimmedValue = value.trim(); if (trimmedValue.length && !tags.includes(trimmedValue)) { setTags(prev => [...prev, trimmedValue]); setInput(''); setSuggestions([]); setHighlightIndex(0); } } const onChange = (e) => { const { value } = e.target; setInput(value); // 输入变化时实时过滤候选项 if (value.trim()) { const filtered = CAR_BRANDS.filter(brand => { // 大小写不敏感匹配,已选中的标签不展示 return brand.toLowerCase().includes(value.toLowerCase().trim()) && !tags.includes(brand); }); setSuggestions(filtered); setHighlightIndex(0); } else { setSuggestions([]); } }; const onKeyDown = (e) => { const { key } = e; const trimmedInput = input.trim(); // 处理上下箭头选择候选项 if (key === 'ArrowDown' && suggestions.length) { e.preventDefault(); setHighlightIndex(prev => prev < suggestions.length - 1 ? prev + 1 : 0); return; } if (key === 'ArrowUp' && suggestions.length) { e.preventDefault(); setHighlightIndex(prev => prev > 0 ? prev - 1 : suggestions.length - 1); return; } // 回车/逗号确认:有高亮候选项时优先选候选项 if ((key === ',' || key === 'Enter')) { e.preventDefault(); if (suggestions.length) { addTag(suggestions[highlightIndex]); } else { addTag(trimmedInput); } return; } if (key === "Backspace" && !input.length && tags.length && isKeyReleased) { e.preventDefault(); const tagsCopy = [...tags]; const poppedTag = tagsCopy.pop(); setTags(tagsCopy); setInput(poppedTag); setIsKeyReleased(false); setSuggestions([]); } }; const onKeyUp = () => { setIsKeyReleased(true); } const deleteTag = (index) => { setTags(prevState => prevState.filter((tag, i) => i !== index)) } return ( <div className={classes.container} ref={containerRef}> {tags.map((tag, index) => <div className={classes.tag} key={index}> <ClearIcon className={classes.del} fontSize="small" onClick={() => deleteTag(index)} /> {tag} </div> )} <input className={classes.input} value={input} placeholder={props.inputPlaceholder} onKeyDown={onKeyDown} onKeyUp={onKeyUp} onChange={onChange} /> {/* 新增自动补全下拉 */} {suggestions.length > 0 && ( <div className={classes.dropdown}> {suggestions.map((item, index) => ( <div key={item} className={index === highlightIndex ? classes.optionActive : classes.option} onClick={() => addTag(item)} > {item} </div> ))} </div> )} </div>); }
3. FilterMarkAuto组件无需修改
原有逻辑可直接复用,不需要调整。
可自定义调整点
- 匹配规则:当前是任意位置包含匹配,如需前缀匹配把
includes改成startsWith即可 - 数据源对接:如果需要从接口拉取候选项,在
onChange里加300ms防抖,把过滤逻辑换成接口请求即可 - 样式:下拉、高亮项的样式可完全对齐MUI主题色,和现有设计风格保持一致
- 数量限制:可在
addTag逻辑里加标签数量上限判断,避免选太多
内容的提问来源于stack exchange,提问作者Paul
相关产品推荐
相关产品推荐

