当前位置: 首页 > news >正文

Vue3:根据el-input封装全局v-focus指令

问:

/*** v-focus* 按钮权限指令*/
import type { Directive } from 'vue'
import { nextTick } from 'vue'const vFocus: Directive = {async mounted(el: HTMLElement) {setTimeout(() => {const input = el.querySelector<HTMLInputElement>('.el-input__inner') || el.querySelector('input') || el// 微任务延迟确保渲染完成if (input instanceof HTMLInputElement || input instanceof HTMLTextAreaElement) {input.focus() // 直接聚焦找到的元素// 更新ElementPlus输入框样式const wrapper = input.closest('.el-input')wrapper?.classList.add('is-focus')}}, 200)},
}export default vFocus
请问这段代码是我封装的自定义v-focus方法永爱自动触发el-input的输入框的光标的,但是现在进入页面只能触发一次,关闭弹框后再次打开弹框el-input的光标就不会自动触发了,请问怎么回事怎么解决

回答:

问题复现

  1. 页面第一次打开弹框v-focus 能正常把 el-input(或普通 <input>)聚焦并给外层 .el-input 加上 is-focus 样式。
  2. 关闭弹框(弹框的 DOM 被 v-if 或 v-show 隐藏/销毁)后再次打开,光标不再自动出现

这通常是因为 指令只在元素第一次挂载 (mounted) 时执行,而弹框关闭后 Vue 并没有重新触发 mounted(如果是 v-show 只切换 display,DOM 并未销毁),或者弹框是通过 v-if 销毁后又重新创建,但 指令的 mounted 回调中的 setTimeout 已经在第一次挂载后就被清理掉,导致后续实例没有再执行聚焦逻辑。


1. 了解指令的生命周期钩子

钩子何时调用
created指令第一次绑定到元素上(还未挂载到 DOM)
beforeMount元素即将挂载到 DOM(mounted 之前)
mounted元素挂载到 DOM,el 已经可用
beforeUpdateVNode 更新前(属性/子节点可能变化)
updatedVNode 更新后,DOM 已经重新渲染
beforeUnmount指令即将被卸载(元素即将从 DOM 移除)
unmounted指令已经被卸载(元素已从 DOM 中移除)

关键点:如果弹框是通过 v-show 隐藏/显示,元素始终保持挂载状态,只会触发 updated(因为 show/hide 会导致属性变化)。如果是 v-if,每次打开都会重新创建元素,mounted 会再次执行。


2. 为什么只能聚焦一次?

场景 A:使用 v-show 隐藏弹框

  • v-show 只改动 display不会触发 mounted
  • 你的指令只在 mounted 时聚焦一次,弹框再次显示时指令不再运行,导致光标不出现。

场景 B:使用 v-if,但指令内部的 setTimeout 被缓存

  • v-if 销毁后再次创建,mounted 会再次执行。
  • 但是 setTimeout 的延迟(200 ms)可能在弹框内部的动画或 nextTick 完成前就执行,导致聚焦的目标元素仍未渲染完毕,从而失效。
  • 另外,如果弹框内部还有 el-input 的 v-model 异步更新,nextTick 需要再等一次微任务。

3. 推荐的解决方案

下面提供 三种可行的实现,任选其一即可。
(1)把聚焦逻辑放到 updated/beforeUpdate,配合 nextTick 确保 DOM 已经渲染完;
(2)在指令里监听弹框的 打开事件(如 @open@after-enter);
(3)使用 v-focus + v-if 的组合,确保每次弹框创建时都走 mounted

3.1 最简版:在 mounted + updated 中都执行一次

import type { Directive } from 'vue'
import { nextTick } from 'vue'function focusInput(el: HTMLElement) {// 1. 先找最可能的输入框const input =el.querySelector<HTMLInputElement>('.el-input__inner') ||el.querySelector('input') ||el// 2. 必须是可聚焦的表单控件if (!(input instanceof HTMLInputElement || input instanceof HTMLTextAreaElement)) return// 3. 使用 nextTick 确保所有子组件已渲染nextTick(() => {input.focus()const wrapper = input.closest('.el-input')wrapper?.classList.add('is-focus')})
}const vFocus: Directive = {// 第一次挂载(适用于 v-if 重建的情况)mounted(el) {// 加一点延迟防止弹框动画未完成setTimeout(() => focusInput(el), 200)},// 当父组件重新渲染(v-show 切换 display)时再次尝试聚焦// updated 会在每次 VNode 更新后触发updated(el) {// 这里不需要再加 setTimeout,直接 nextTick 即可focusInput(el)},// 可选:在指令被销毁时清理可能残留的 classunmounted(el) {const input = el.querySelector('input')const wrapper = input?.closest('.el-input')wrapper?.classList.remove('is-focus')},
}export default vFocus
为什么可行?
  • mounted 负责 首次v-if)的聚焦,保留原来的 200 ms 延迟以兼容弹框动画。
  • updated 会在 每次弹框显示/隐藏(v-show)或内部状态变化时触发,内部使用 nextTick 等待子组件渲染完毕,再聚焦。
  • 这样无论是 v-show 还是 v-if,指令都会在合适的时机再次执行聚焦逻辑。

3.2 进阶版:监听弹框的 after-enter(如果使用 Element Plus ElDialog

如果你使用的是 Element Plus 的 ElDialog(或者其他基于 transition 的弹框),可以在指令内部监听弹框的 transitionend/after-enter 事件,确保动画结束后再聚焦:

const vFocus: Directive = {mounted(el) {const dialog = el.closest('.el-dialog')if (dialog) {// 当弹框打开动画结束后触发const onAfterEnter = () => {focusInput(el)dialog?.removeEventListener('after-enter', onAfterEnter)}dialog.addEventListener('after-enter', onAfterEnter)} else {// 非弹框场景,直接聚焦setTimeout(() => focusInput(el), 200)}},// 同样保留 updated 以防 v-show 场景updated(el) {focusInput(el)},
}

注意ElDialog 在内部会把 after-enter 事件挂在根元素(.el-dialog__wrapper),因此需要 closest('.el-dialog') 或者直接在弹框组件上绑定 @after-enter="..." 并在外层调用 focus()


3.3 另一种思路:使用 nextTick + requestAnimationFrame

有时候弹框的渲染顺序非常快,setTimeout(200) 可能仍然早于真实可聚焦的时机。可以把 两层异步组合起来:

function focusWhenReady(el: HTMLElement) {nextTick(() => {requestAnimationFrame(() => {const input = /* 同上 */ if (input instanceof HTMLInputElement || input instanceof HTMLTextAreaElement) {input.focus()input.closest('.el-input')?.classList.add('is-focus')}})})
}

nextTick → 微任务,随后 requestAnimationFrame → 下一帧渲染,基本可以保证输入框已经真正出现在页面上。


4. 小技巧:防止重复添加 is-focus

如果弹框打开多次,is-focus 类可能会一直残留在上一次的 DOM 节点上,导致样式不统一。可以在聚焦前先 移除 再 添加

const wrapper = input.closest('.el-input')
wrapper?.classList.remove('is-focus')
wrapper?.classList.add('is-focus')

或者在 unmounted/beforeUnmount 时统一清理。


5. 完整示例(整合所有建议)

import type { Directive } from 'vue'
import { nextTick } from 'vue'function focusInput(el: HTMLElement) {const input =el.querySelector<HTMLInputElement>('.el-input__inner') ||el.querySelector('input') ||elif (!(input instanceof HTMLInputElement || input instanceof HTMLTextAreaElement)) returnnextTick(() => {// 让浏览器在下一帧真正渲染后再聚焦requestAnimationFrame(() => {input.focus()const wrapper = input.closest('.el-input')wrapper?.classList.remove('is-focus')wrapper?.classList.add('is-focus')})})
}const vFocus: Directive = {mounted(el) {// 如果是弹框组件,等弹框动画结束再聚焦const dialog = el.closest('.el-dialog')if (dialog) {const handler = () => {focusInput(el)dialog.removeEventListener('after-enter', handler)}dialog.addEventListener('after-enter', handler)} else {// 普通场景保持原来的延迟setTimeout(() => focusInput(el), 200)}},// 兼容 v-show 切换或内部数据变化updated(el) {focusInput(el)},// 清理样式,防止残留unmounted(el) {const input = el.querySelector('input')const wrapper = input?.closest('.el-input')wrapper?.classList.remove('is-focus')},
}export default vFocus

使用方式

<template><el-dialog v-model="show" title="编辑"><el-input v-focus v-model="name" placeholder="请输入姓名" /></el-dialog>
</template><script setup lang="ts">
import { ref } from 'vue'
import vFocus from '@/directives/v-focus'const show = ref(false)
const name = ref('')
</script>

6. 小结

关键点说明
指令只在 mounted 执行一次对 v-show 隐藏的弹框不起作用
使用 updated(或 beforeUpdate每次弹框显示/内容变化时重新聚焦
配合 nextTick + requestAnimationFrame确保 DOM 已完成渲染后才调用 .focus()
如果有弹框动画监听弹框的 after-enter/transitionend 再聚焦
清理 is-focus防止类名残留导致样式异常

把上述任意一种实现方式替换掉你原来的 vFocus就可以在弹框每次打开时都自动聚焦 el-input,不会再出现“只能触发一次”的情况。祝开发愉快 🎉!


文章转载自:

http://Db9abekR.jjzrh.cn
http://qSuWt5yV.jjzrh.cn
http://5uRfeiB6.jjzrh.cn
http://Uawm72VI.jjzrh.cn
http://flkG1F9H.jjzrh.cn
http://KBcIT94D.jjzrh.cn
http://xq0S4fFO.jjzrh.cn
http://MydfUkBf.jjzrh.cn
http://XWaNmIJp.jjzrh.cn
http://6sn034QN.jjzrh.cn
http://NaxhqAhD.jjzrh.cn
http://YbvegkBd.jjzrh.cn
http://LabN6lk5.jjzrh.cn
http://dn8Wnhum.jjzrh.cn
http://5A5b4nBx.jjzrh.cn
http://2Oz3yg5o.jjzrh.cn
http://CSJUN3Yz.jjzrh.cn
http://MelNZnEZ.jjzrh.cn
http://VX3Gwxs9.jjzrh.cn
http://mUZofQgZ.jjzrh.cn
http://hub04GrZ.jjzrh.cn
http://z4cfA8PF.jjzrh.cn
http://ADGQpk6f.jjzrh.cn
http://WRHM0eMi.jjzrh.cn
http://t0ZSUISO.jjzrh.cn
http://aFpQoqpg.jjzrh.cn
http://H2KCqucO.jjzrh.cn
http://KOAI9lVD.jjzrh.cn
http://o32NLJEp.jjzrh.cn
http://EjHXfK8g.jjzrh.cn
http://www.dtcms.com/a/380690.html

相关文章:

  • 企业AI战略构建与成品选择指南
  • Semaphore和CountDownLatch
  • 实战ELK与AI MCP:构建高可用的智能化日志可观测体系
  • SAP-MM:SAP MM学习分享:深入浅出解析物料需求计划(MRP)及MRP配置图解
  • 【LLM】使用 Google ADK、Gemini、QDrant 和 MCP 构建深度研究系统
  • 【CSS学习笔记2】-css复合选择器
  • 186. Java 模式匹配 - Java 21 新特性:Record Pattern(记录模式匹配)
  • Electron下载失败
  • Origin绘制双Y轴网格叠加图|科研论文图表教程(附数据排列格式)
  • XXL-JOB框架SRC高频漏洞分析总结
  • 未启用Spring事务管理 执行mapper.xml文件的sql,为什么会自动提交
  • 亚马逊云代理:亚马逊云怎么样进行大规模数据分析与处理?
  • Linux防火墙iptables
  • 基于联邦学习与神经架构搜索的可泛化重建:用于加速磁共振成像|文献速递-最新医学人工智能文献
  • 如何将 Wine 应用包转换成玲珑格式包:完整技术教程
  • 函数库 动静态库
  • EPC企业如何通过数字化管理提高盈利能力?
  • P2678 [NOIP 2015 提高组] 跳石头
  • 旋转位置编码的论文阅读
  • UE5 基础应用 —— 08 - 动画蓝图 简单使用
  • unity pcd 二进制版 简单显示文件对象(单色)
  • 面试题:Redis要点总结(复制、哨兵、集群)
  • Leetcode 18 java
  • Redis集群为何采用16384个槽的设计?
  • 《树与二叉树详解:概念、结构及应用》
  • Certimate SSL证书自动申请部署
  • 《Spring事务的失效》
  • Maya绑定:小球挤压拉伸变形详细绑定(晶格、簇、测量工具、节点编辑器)
  • 【比亚迪璇玑架构深度解析:重新定义智能电动汽车的“整车智能”】
  • jdbc DAO封装及BaseDAO工具类