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

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

相关推荐
M_emory_20 分钟前
解决 git clone 出现:Failed to connect to 127.0.0.1 port 1080: Connection refused 错误
前端·vue.js·git
Ciito23 分钟前
vue项目使用eslint+prettier管理项目格式化
前端·javascript·vue.js
成都被卷死的程序员1 小时前
响应式网页设计--html
前端·html
mon_star°1 小时前
将答题成绩排行榜数据通过前端生成excel的方式实现导出下载功能
前端·excel
Zrf21913184551 小时前
前端笔试中oj算法题的解法模版
前端·readline·oj算法
文军的烹饪实验室2 小时前
ValueError: Circular reference detected
开发语言·前端·javascript
Martin -Tang4 小时前
vite和webpack的区别
前端·webpack·node.js·vite
迷途小码农零零发4 小时前
解锁微前端的优秀库
前端
王解4 小时前
webpack loader全解析,从入门到精通(10)
前端·webpack·node.js
我不当帕鲁谁当帕鲁4 小时前
arcgis for js实现FeatureLayer图层弹窗展示所有field字段
前端·javascript·arcgis