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

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

相关推荐
C澒1 分钟前
多场景多角色前端架构方案:基于页面协议化与模块标准化的通用能力沉淀
前端·架构·系统架构·前端框架
崔庆才丨静觅2 分钟前
稳定好用的 ADSL 拨号代理,就这家了!
前端
江湖有缘4 分钟前
Docker部署music-tag-web音乐标签编辑器
前端·docker·编辑器
恋猫de小郭1 小时前
Flutter Zero 是什么?它的出现有什么意义?为什么你需要了解下?
android·前端·flutter
崔庆才丨静觅8 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60618 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了9 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅9 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅9 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅9 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端