react封装可输入选择器
前言
用React15写的,选项是静态数据,为了适应原来的代码,加了很多复杂的东西 - 。-,不过也算学了点新东西,记录一下。
效果展示
结构分析
数据结构
constructor(props) {
super(props)
const { value, dataList } = props // 获取传过来的当前选中值和选项列表---[[value:label],[value:label]]
const label=dataList.find((item) => item[0] === value)[1] // 根据映射找到显示在输入框的值
this.state = {
oldLabel: label, // 用于过滤选项数据
isBlur: false, // 控制选项容器的显示
label, // 显示在输入框的值
top:undefined // (针对特殊环境,可不用)
}
}
Dom结构
- selector-container
- selector-input-container // 输入框容器
- input
-selector-options-container // 选项容器
- option
具体结构:
<div
style={{width:`${this.props.width}px`}} // 自定义选择器宽度,无需求的话可不写
className="selector-container"
onMouseDown={this.handleMouseDown.bind(this)} // 代理事件
>
<div
className="selector-input-container"
data-id="selector-input-container"
>
<input
ref="myinput" // 获取dom,用于获取位置,一般情况下可不用
data-id="selector-input"
onBlur={(e) => { // 失去焦点时,隐藏选项容器
this.setState({ isBlur: false })
}}
type="text"
value={this.state.label}
onChange={this.inputChange.bind(this)}
/>
</div>
<div
className={`selector-options-container ${
this.state.isBlur ? "show" : "hide"
}`}
data-id="options-container"
style={{top:`${this.state.top}px`,minWidth:`${this.props.width+2}px`}} // (top针对特殊环境,可不用)
>
{this.filterdList.length !== 0 ? (
this.filterdList.map((item, idx) => (
<SelectorOption
key={item[0]}
value={item[0]}
label={item[1]}
/>
))
) : (
// 当过滤数据为空时,添加两个选项,优化交互。
// 比如该选择是新建名称的,那么选项容器就会出现:新增名称:`label` 和 no result
<div className="no-result">
{noResultText && (
<option>
{noResultText}:{this.state.label}
</option>
)}
<div data-id="no-result">no result</div>
</div>
)}
</div>
</div>
选项组件
class SelectorOption extends React.Component {
render() {
const { value, label } = this.props
return (
<option
value={value}
data-label={label}
data-id="selector-item"
>
{label}
</option>
)
}
}
解释几点:
为什么在最外层使用onMouseDown?
------------------
由于很多地方需要监听点击事件,
比如点击输入框,点击输入框右侧的为元素“x”,点击选项,
所以与其在它们每个地方注册事件,不如交给父元素代理
为什么不使用onClick?
------------------
input监听了blur事件,而click事件是在blur之后触发的,这样就会出现问题。
比如当我点击选项的时候,会先触发blur,这时选项容器就会被关闭,
导致我无法获取对应的选项dom,也就不知道自己选择了什么。
data-id是什么,为什么使用dataset?
-----------------
data-id是自定义的标签属性,
由于每个标签固有的属性不同,管理起来很不方便,
使用dataset方便管理,易于辨别自己点击了哪个dom
为什么使用ref?
-----------------
这里使用ref是为了控制选项容器的位置。
在一个简单的环境里,使用ref是完全么必要的,直接使用“子元素绝对位置,父元素相对位置”就可以。
但是我这边的环境比较复杂,
第一,我是在一个表格组件内里面使用该选择器的,由于它原本的样式,那个表格组件会将我的选项容器覆盖,
并且它已经在其他地方被使用,我无法去更改这个table样式来适应我的组件。
源码
样式
.selector-container {
.selector-input-container {
position: relative;
width: 100%;
// 加一个 x 用于清空输入框
&::after {
position: absolute;
content: "\D7";
right: 2px;
top: 4px;
font-size: 14px;
background: white;
border-radius: 7px;
width: 14px;
height: 14px;
cursor: pointer;
}
input {
padding-right: 30px;
}
}
.selector-options-container {
text-align: left;
min-width: 200px;
max-width: 276px;
max-height: 320px;
min-height: 16px;
position: absolute;
background-color: white;
border: 1px solid rgb(118, 118, 118);
height: fit-content;
font-size: 12px;
overflow-y: auto;
overflow-x: hidden;
white-space: nowrap;
box-shadow: 0 0 2px rgb(118, 118, 118);
z-index: 99;
border-radius: 2px;
.no-result {
padding: 2px 1px;
div {
padding: 1px 2px;
line-height: 14px;
cursor: default;
}
}
}
option {
padding: 1px 2px;
line-height: 14px;
}
option:hover {
background-color: #1e90ff;
color: white;
cursor: default;
}
.show {
display: block;
}
.hide {
display: none;
}
}
jsx
import React from "react"
import "./style.less"
class SelectorOption extends React.Component {
render() {
const { key, value, label } = this.props
return (
<option
key={key}
value={value}
data-label={label}
data-id="selector-item"
>
{label}
</option>
)
}
}
export default class Selector extends React.Component {
constructor(props) {
super(props)
const { value, dataList } = props
const label=dataList.find((item) => item[0] === value)[1]
this.state = {
oldLabel: label,
isBlur: false,
label,
top:undefined
}
}
// 当输入框为空或者为旧数据的时候 返回全部选项。这里的过滤使用的是完全匹配
get filterdList() {
if (this.state.label === ""|| this.state.label===this.state.oldLabel) {
return this.props.dataList
}
return this.props.dataList.filter(
(item) =>item[1]===this.state.label
)
}
// 监听输入框,将数据交给父组件处理,并且更新自身状态。(我这样写完全是为了适应父组件,可根据情况调整)
inputChange(event) {
const value = event.target.value
this.props.handleChange(value)
this.setState({
label: value,
})
}
// 代理点击事件,根据不同情形执行不同操作
handleMouseDown(e) {
const strategies = {
// 点击选项时:将选项value传给父组件,且更新自身状态
"selector-item": () => {
this.props.handleChange(e.target.value)
this.setState({
label: e.target.dataset.label,
})
},
// 点击输入框时:显示选项容器。
"selector-input": () => {
this.setState({
top:this.refs.myinput.getBoundingClientRect().top+20, //(top针对特殊环境,可不用)
isBlur: true
})
},
// 点击滚动条时:不做变化
"options-container": () => {
return
},
"no-result": () => {
return
},
// 点击伪元素x时:
// 1.一开始是focus时,阻止默认事件(失去焦点,即防止选项容器消失),清空数据;
// 2.一开始没有focus时,判断选项容器是否显示,没有显示的话,让input进行focus,同时显示选项容器
"selector-input-container": () => {
e.preventDefault()
this.props.handleChange("")
this.setState({
label: "",
})
if(!this.state.isBlur){
this.refs.myinput.focus()
this.setState({
isBlur:true
})
}
},
}
const id = e.target.dataset.id
if (id) {
strategies[id]()
}
}
render() {
// 如果想给没数据的选项容器添加一条自定义选项,可传入这个属性
const noResultText=this.props.noResultText
return (
<div
style={{width:`${this.props.width}px`}}
className="selector-container"
onMouseDown={this.handleMouseDown.bind(this)}
>
<div
className="selector-input-container"
data-id="selector-input-container"
>
<input
ref="myinput"
data-id="selector-input"
onBlur={(e) => {
this.setState({ isBlur: false })
}}
type="text"
value={this.state.label}
onChange={this.inputChange.bind(this)}
/>
</div>
<div
className={`selector-options-container ${
this.state.isBlur ? "show" : "hide"
}`}
data-id="options-container"
style={{top:`${this.state.top}px`,minWidth:`${this.props.width+2}px`}}
>
{this.filterdList.length !== 0 ? (
this.filterdList.map((item, idx) => (
<SelectorOption
key={idx}
value={item[0]}
label={item[1]}
/>
))
) : (
<div className="no-result">
{noResultText && (
<option>
{noResultText}:{this.state.label}
</option>
)}
<div data-id="no-result">no result</div>
</div>
)}
</div>
</div>
)
}
}
使用
import Selector from "./Selector"
export default function App() {
return (
<Selector
dataList={serviceTypeList}
value={1}
handleChange={(value) => {
console.log("value", value)
}}
width={200}
/>
)
}