vue3 实现一个简单的输入框

实现一个简单的输入框

需求背景

需要一个输入框,可以输入文字,添加表情,一开始用了富文本编辑器,有点大材小用,所以自己封装一个输入框组件。支持输入文字,选择表情/插入表情,支持组合键换行,使用enter 进行提交

效果图

技术实现

  1. 通过原生textarea实现
  2. 通过 v-model 来实现 父子组件的数据传递,子组件监听数据的变化,向外emit('update:modelValue', inputValue),保证父组件能更新绑定的值
  3. 每次插入时,需要重新聚焦更新光标位置
  4. 通过向外 暴露的 (__insertText),来实现插入表情/文字
  5. 通过向外暴露的(_clear) 输入完毕发送后,需要clear 掉输入框的内容
  6. 通过向外暴露的(__isEmpty) 来判断是否有内容,如果没内容,做按钮的禁用状态(当然也可以直接用父组件绑定的值)
  7. 父组件通过ref 就可以调用以上方法,来做操作。比如发送完数据,调clear清空内容,如果输入框没有内容,则调用isEmpty,做按钮的一些状态

代码实现(子组件)

js 复制代码
/**
 * 自定义文本输入框组件
 */

import { ref, watch } from 'vue'
import { ElInput } from 'element-plus'

export default defineComponent({
  props: {
    modelValue: {
      type: String,
      default: ''
    }
  },
  emits: ['update:modelValue', 'chatSend'],
  setup(props, { emit, expose }) {
    const { t } = useI18n()
    const editorRef = ref()
    const disabled = ref(false)
    const valueHtml = ref(props.modelValue)
    const currentEvent = ref()
    watch(
      () => valueHtml.value,
      () => {
        emit('update:modelValue', valueHtml.value)
      }
    )

    // 插入元素
    const _insertText = async (msg: string) => {
      const msgLength = msg.length
      const editor = currentEvent.value
      if (editor) {
        const startPos = editor.selectionStart
        valueHtml.value = `${valueHtml.value.substring(0, startPos)}${'' + msg}${valueHtml.value.substring(startPos)}`
        _focus()
        nextTick(() => {
        // 此处是 根据元素的长度,来设置光标位置
          editor.setSelectionRange(startPos + msgLength, startPos + msgLength)
        })
      }
    }

    // focus
    const _focus = () => {
      editorRef.value && editorRef.value.focus()
    }

    // 清空编辑器
    const _clear = () => {
      editorRef.value && editorRef.value.clear()
    }

    // 是否内容为空
    const _isEmpty = () => {
      const str = valueHtml.value.trim().replace(/\n/g, '')
      return str === '' || str.length == 0 || typeof str === 'undefined'
    }

    // 禁用编辑器
    const _disable = () => {
      disabled.value = true
    }

    // 解除禁用编辑器
    const _enable = () => {
      disabled.value = false
    }

    const handleKeyDown = (event: KeyboardEvent) => {
      currentEvent.value = event.target as HTMLInputElement
      // 定义组合键 Map
      const shortCutKeys: (keyof KeyboardEvent)[] = ['metaKey', 'altKey', 'ctrlKey', 'shiftKey']
      const isEnterKey = event.code === 'Enter'
      const isShortcutKeys = shortCutKeys.some((item) => event[item])
      if (isEnterKey && isShortcutKeys) {
        // 获取光标位置
        const cursorPosition = currentEvent.value.selectionStart

        // 拆分成两段文本
        const textBeforeCursor = valueHtml.value.slice(0, cursorPosition)
        const textAfterCursor = valueHtml.value.slice(cursorPosition)

        // 合并为带有换行符的新文本
        const newText = textBeforeCursor + '\n' + textAfterCursor

        // 更新输入框的值
        valueHtml.value = newText
        // 文本编辑器的高度发生变化后
        nextTick(() => {
          // 高度变化 自动滚动到底部
          const editor = editorRef.value.textarea
          editorRef.value.textarea.scrollTop = editor.scrollHeight
          // 设置光标位置为: start 和 end 相同,光标会移动到换行符后面的新行首
          currentEvent.value.setSelectionRange(cursorPosition + 1, cursorPosition + 1)
        })
      } else if (event.code === 'Enter') {
        // 阻止掉 Enter 的默认换行行为
        event.preventDefault()
        emit('chatSend')
      }
    }
    // 向外暴露方法
    expose({
      _insertText,
      _clear,
      _disable,
      _isEmpty,
      _enable,
      _focus
    })
    return () => (
      <div class="chatEditor">
        <ElInput
          ref={editorRef}
          v-model={valueHtml.value}
          type="textarea"
          disabled={disabled.value}
          onKeydown={handleKeyDown}
        />
      </div>
    )
  }
})

代码实现(父组件调用)

  • 输入框组件
js 复制代码
<base-editor
        ref="chatEditorRef"
        v-model="inputText"
        @chat-send="sendText"
 ></base-editor>
  • 工具栏组件
js 复制代码
<ChatTools :chat-tools-list="newChatToolsList" @get-emoji="getToolsMsg" />

当我们点击工具栏组件,就会获取到工具栏的文字/表情/或者插入的xxx ,此时根据引用,调用输入框的暴露出的_insertText方法,直接就插入进去

js 复制代码
const getToolsMsg = async (msg: string) => {
  chatEditorRef.value._insertText(msg)
}

其他方法调用

  • _isEmpty(): 未输入,按钮是禁用状态
js 复制代码
 <el-button :disabled="chatEditorRef?._isEmpty()" type="primary" @click="sendText">

当然你也可以直接使用绑定的输入框的变量去判断

js 复制代码
<el-button :disabled="!inputText" @click="sendText">
  • _clear(): 提交完请求,清空输入框
js 复制代码
chatEditorRef.value._clear()

关于插入的问题

光标位置的获取,根据元素插入位置,光标换行,支持快捷键等。可以参考上一篇文章 juejin.cn/post/732680...

总结

  1. 原生其实也可以实现,没必要用一个很重的富文本编辑器
  2. 做表情插入,或者其他插入,都是工具栏,和输入框组件的关系是兄弟组件的关系,兄弟组件之间,怎么做数据传递,事件传递,怎么设计

(1) 父组件作为中介

(2)事件总线通过订阅/发布做消息通知

(3)仓库vuex/pina

事件总线还是能不用就不用,因为全局性的东西,用着爽,后面复杂了,就乱了。而且 事件总线是发布订阅,你还得注意销毁,存仓库 又不太合适,子组件自己暴露出方法,让其他组件调用,感觉目前是最简单的方式了

相关推荐
糕冷小美n6 小时前
elementuivue2表格不覆盖整个表格添加固定属性
前端·javascript·elementui
小哥不太逍遥6 小时前
Technical Report 2024
java·服务器·前端
沐墨染6 小时前
黑词分析与可疑对话挖掘组件的设计与实现
前端·elementui·数据挖掘·数据分析·vue·visual studio code
anOnion6 小时前
构建无障碍组件之Disclosure Pattern
前端·html·交互设计
threerocks6 小时前
前端将死,Agent 永生
前端·人工智能·ai编程
问道飞鱼7 小时前
【前端知识】Vite用法从入门到实战
前端·vite·项目构建
爱上妖精的尾巴7 小时前
8-10 WPS JSA 正则表达式:贪婪匹配
服务器·前端·javascript·正则表达式·wps·jsa
Zhencode7 小时前
Vue3响应式原理之ref篇
vue.js
小疙瘩8 小时前
element-ui 中 el-upload 多文件一次性上传的实现
javascript·vue.js·ui
Aliex_git9 小时前
浏览器 API 兼容性解决方案
前端·笔记·学习