手写一个Input.Password与Mentions功能结合的组件

简介: 手写一个Input.Password与Mentions功能结合的组件

背景

最近接到了一个需求,要求在密码框输入$符号时出现一个列表,点击列表中的某一项,自动填充密码输入框中的value值,并且输入框中需要支持明密文切换

我立马想到了聊天输入框中输入@就会提及某人或某事这个场景,而Antd的Mentions组件就具备这种功能:

网络异常,图片无法展示
|
但是还需要支持明密文切换,纳尼?Mentions组件没这功能啊!这是要我把密码输入框与Mentions组件的功能相结合吗?额...今日无法摸鱼了...

目标

手写一个Input.Password与Mentions功能结合的组件

需要解决的问题

  1. 输入@符号出现的成员列表出现在输入框下面,并且层级更高。点击页面其他地方成员列表需要消失。
  2. 点击目标成员之后,需要动态改变输入框中显示的值,这就涉及到输入框光标处插值以及重新设置光标位置的问题
  3. 支持明密文切换。密文状态下,也可以支持@功能,并且点击成员之后,在输入框里显示的值是密文。

涉及知识点

  1. 明密文切换 -> 直接使用原生input里的type=“password”属性
  2. 光标处插值以及重新设置光标位置 -> 使用dom元素的selectionEndsetSelectionRange方法
  3. 点击页面其他地方成员列表需要消失 -> 直接使用ahooks库里的useClickAway方法
  4. 成员列表出现在输入框下面,并且层级更高 -> 使用rc-trigger
  5. setState更新值的异步问题
  6. 受控组件与非受控组件
const dom = document.getElementById('id');
const idx = dom.selectionEnd; // 获取鼠标光标的位置
dom.setSelectionRange(startIndex, endIndex);  // 自定义选中输入框里的文字范围
复制代码

踩坑记录

1.不能基于Antd的Mentions组件进行实现吗?

在开始手写组件之前,笔者确实有尝试过直接基于Antd的Mentions组件进行二次封装或改造,但是发现还是太天真了。

笔者先是通过ref去获取到mentions这个组件实例:

网络异常,图片无法展示
|

其中,当我们在页面里进行输入时,就会触发mentions组件里的textarea的原生onChange事件,从而触发其内部的triggerChange方法,方法内部去改变textarea组件值,也就是说,内部的textarea是一个受控组件。

内部源码:

网络异常,图片无法展示
|

此时,我如果要实现我的需求,我需要去动态更改textarea组件值,然而mentions组件暴露出来的方法我们能用到的也只是triggerChange事件, 我们在业务代码里,本身是通过监听Mensions组件的onChange事件才需要去动态更改textarea值,这样的话,就会陷入循环调用,进行报错。

2.Antd的Mentions是基于textarea实现的,这里为何要替换成input?

这个其实就是是否要自己实现一个密码输入框的问题。笔者这里因为业务需求比较急,这里就不深入去探讨如何实现了,就直接使用原生的input密码框了。有兴趣或有想法的朋友可以留言评论。

3.setSelectionRange方法失效?造成自定义光标位置无法实现?

笔者一开始是使用state值来控制输入框显示的值,使得输入框变成了受控组件,但是由于React中的useState更新数据会有一定的延迟,当执行到setSelectionRange方法时,其实页面中的dom元素还没有更新,当setSelectionRange方法之后,dom元素进行了更新,使得元素进行重新绘制,也就自然导致方法失效了。

另外需要注意的就是需要先聚焦,setSelectionRange才能生效。

const onSelect = (value: string) => {
    // 光标插值
    const selectionEndIdx = getSelectionEndIdx(); // 光标位置
    const newValue = inputText.slice(0, selectionEndIdx) + value + inputText.slice(selectionEndIdx)
    // setInputText(newValue);  这里弄成了受控组件,使用下一行代码,摆脱这个问题
    inputRef.current.value = newValue;
    // 重新设置鼠标光标位置
    const idx = selectionEndIdx + value.length
    inputRef.current.focus();   // 这里需要先聚焦,setSelectionRange才能生效
    inputRef.current.setSelectionRange(0, 0)
  }
复制代码

4.如何判断一个字符串是否是以某个子字符串开头?

const isTargetStart = strCode.indexOf("ssss"); 
// isTargetStart === 0 表示strCode是以ssss开头
// isTargetStart === -1 表示strCode不是以ssss开头
复制代码

最终代码

import { useClickAway } from "ahooks";
import React from 'react';
import Trigger from 'rc-trigger';
import { EyeInvisibleOutlined, EyeTwoTone } from '@ant-design/icons';
const optionsBase = [
  { name: '1' },
  { name: '2' }
]
export const MentionsWithPassword = (): React.ReactElement => {
  const [showPwd, setShowPwd] = React.useState(true);
  const [popupVisible, setPopupVisible] = React.useState(false);
  const popupRef = React.useRef<any>(null);
  const inputRef = React.useRef<any>(null);
  const [visibleOption, setVisibleOptions] = React.useState(optionsBase);
  useClickAway(() => {
    setPopupVisible(false)
  }, popupRef);
  const onPwdVisibleChange = () => {
    setShowPwd(!showPwd);
  }
  // 判断是否要出现引用参数列表
  const calcPopupVisible = (text: string) => {
    const selectionEndIdx = getSelectionEndIdx(); // 光标位置
    const textBeforeSelectionEnd = text.substring(0, selectionEndIdx);  // 光标前面的文字
    const lastIdx = textBeforeSelectionEnd.lastIndexOf('$');
    // 1.无匹配字符
    if (lastIdx === -1) return;
    // 2.匹配字符在光标位置
    if (lastIdx === textBeforeSelectionEnd.length - 1) {
      setPopupVisible(true);
      setVisibleOptions(optionsBase)
      return;
    }
    // 3.不止输入了匹配字符,还输入了其他字符
    const centerText = textBeforeSelectionEnd.substring(lastIdx + 1);
    if (centerText.includes(' ')) {
      setPopupVisible(false)
      return;
    }
    setPopupVisible(true);
    setVisibleOptions(getFilterOptions(centerText, options));
  }
  // 获取光标位置
  const getSelectionEndIdx = () => {
    return inputRef.current.selectionEnd;
  }
  // 过滤引用参数列表
  const getFilterOptions = (text: string, options: any[]) => {
    return options.filter(item => item.name.indexOf(text) > -1)
  }
  const onChange = (e: any) => {
    const value = e.target.value;
    calcPopupVisible(value);
  }
  const onSelect = (value: string) => {
    // 光标插值
    const selectionEndIdx = getSelectionEndIdx(); // 光标位置
    const oldInputText = inputRef.current.value;
    const newValue = oldInputText.slice(0, selectionEndIdx) + value + oldInputText.slice(selectionEndIdx)
    // setInputText(newValue);
    inputRef.current.value = newValue;
    // 重新设置鼠标光标位置
    const idx = selectionEndIdx + value.length
    inputRef.current.focus();
    inputRef.current.setSelectionRange(idx, idx);
  }
  return (
    <div>
      <Trigger
        popup={(
          <div ref={popupRef} style={{ background: 'red' }}>
            {visibleOption.map(item => <div onClick={() => onSelect(item.name)}>{item.name}</div>)}
          </div>
        )}
        destroyPopupOnHide
        popupVisible={popupVisible}
        popupAlign={{
          points: ['tl', 'bl'],
          offset: [0, 3]
        }}
      >
        <input
          type={showPwd ? 'text' : 'password'}
          onChange={onChange}
          ref={inputRef}
        />
      </Trigger>
      <span onClick={onPwdVisibleChange}>{showPwd ? <EyeInvisibleOutlined /> : <EyeTwoTone />}</span>
    </div>
  )
}
复制代码

参考链接

  1. HTMLInputElement.setSelectionRange() - MDN



相关文章
|
2月前
|
前端开发
antd_使用Input封装实现Form校验效果(最终版)
本文介绍了在Ant Design (antd)框架中如何封装Input组件以实现表单校验效果,并提供了封装代码示例以及如何在提交时进行校验。
63 4
antd_使用Input封装实现Form校验效果(最终版)
|
2月前
|
前端开发
antd_使用Input封装实现Form校验效果
本文介绍了如何在Ant Design (antd) 中使用 Input 组件封装实现表单校验效果,包括必填、数字、IP、邮箱、手机号、身份证号和域名等校验规则的使用,以及如何通过回调函数进行校验。
100 4
|
6月前
|
JavaScript
vue element upload组件配合axios实现用 “Content-Type“: “multipart/form-data“上传方式导入xls文件
vue element upload组件配合axios实现用 “Content-Type“: “multipart/form-data“上传方式导入xls文件
|
6月前
|
前端开发
UniApp 中的 u-input 属性讲解
UniApp 中的 u-input 属性讲解
1192 0
|
6月前
uniapp App端 解决input@input事件动态修改值不生效的问题
uniapp App端 解决input@input事件动态修改值不生效的问题
398 1
|
数据安全/隐私保护
input中常用的type属性与使用场景
input中常用的type属性与使用场景
60 0
|
JavaScript 开发工具 git
手写实现el-form系列组件的核心逻辑 -- 练习组件通信
手写实现el-form系列组件的核心逻辑 -- 练习组件通信
194 0
让el-form更好用,通过配置的方式
让el-form更好用,通过配置的方式
429 0
|
Java 数据安全/隐私保护 Spring
阅读 SpringSeccurity 源码—理解登录表单传参名默认是 username 和 password
阅读 SpringSeccurity 源码—理解登录表单传参名默认是 username 和 password
265 0
阅读 SpringSeccurity 源码—理解登录表单传参名默认是 username 和 password