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}
      />
   )
}

posted @ 2022-08-25 10:12  sanhuamao  阅读(323)  评论(0编辑  收藏  举报