多个 el-form 共用同一 ref 导致表单校验部分失效

Vue3 + Element Plus:多个 el-form 共用同一 ref 导致表单校验部分失效

关键词:Vue3、Element Plus、el-form、模板 ref、表单校验失效、validate

一、问题背景

一个表单类组件内部按业务模块拆成了多个 el-form(模块一、模块二、模块三),每个模块包含若干必填项,组件通过 defineExpose 暴露统一的 validate() 方法给父组件在提交前调用。

实际现象:点击提交时,只有其中一个模块的必填校验生效,其余模块的校验被静默跳过,数据不填也能提交成功,且控制台无任何报错。

二、错误示例代码

vue 复制代码
<template>
  <div class="survey-form">
    <!-- 模块一 -->
    <el-form ref="formRef" :model="formModel" label-width="120px">
      <el-form-item
        label="问题1"
        prop="answerA"
        :rules="[{ required: true, message: '请填写问题1', trigger: 'blur' }]"
      >
        <el-input v-model="formModel.answerA" />
      </el-form-item>
    </el-form>

    <!-- 模块二:注意!这里重复使用了同一个 ref 名称 -->
    <el-form ref="formRef" :model="formModel" label-width="120px">
      <el-form-item
        label="问题2"
        prop="answerB"
        :rules="[{ required: true, message: '请选择问题2', trigger: 'change' }]"
      >
        <el-radio-group v-model="formModel.answerB">
          <el-radio label="是" />
          <el-radio label="否" />
        </el-radio-group>
      </el-form-item>
    </el-form>

    <!-- 模块三:同样重复使用了同一个 ref 名称 -->
    <el-form ref="formRef" :model="formModel" label-width="120px">
      <el-form-item
        label="问题3"
        prop="answerC"
        :rules="[{ required: true, message: '请选择问题3', trigger: 'change' }]"
      >
        <el-select v-model="formModel.answerC">
          <el-option label="选项一" value="1" />
          <el-option label="选项二" value="2" />
        </el-select>
      </el-form-item>
    </el-form>
  </div>
</template>

<script setup lang="ts">
import { reactive, ref } from 'vue'
import type { FormInstance } from 'element-plus'

const formRef = ref<FormInstance>()

const formModel = reactive({
  answerA: '',
  answerB: '',
  answerC: '',
})

// 暴露给父组件的校验方法
const validate = () =>
  new Promise((resolve, reject) => {
    if (!formRef.value) return resolve(true)
    formRef.value.validate((valid: boolean) =>
      valid ? resolve(true) : reject(new Error('表单校验失败'))
    )
  })

defineExpose({ validate })
</script>

三、错误原因

同一个模板中,多个元素使用了相同的静态 ref 名称。

Vue 3 中,非 v-for 场景下的同名 ref 不会自动收集为数组,而是按挂载顺序依次赋值、互相覆盖 ------formRef.value 最终只指向最后挂载的那个 el-form 实例(本例中为模块三)。

因此 validate() 实际只调用了一个表单的 validate:

  • 模块三(最后挂载):校验正常生效;
  • 模块一、模块二:所有必填校验被静默跳过,不填也能通过。

由于整个过程没有任何报错和警告,问题非常隐蔽,往往到线上脏数据出现后才被发现。

关键认知:el-form 的校验是按表单实例隔离 的。el-form-item 只会注册到离它最近的祖先 el-form 上,form.validate() 只能校验自己名下的字段,管不到其他表单。

四、正确写法

方案一:合并为单个 el-form(推荐)

如果各模块的数据同属一个 model、提交时整体校验一次即可,直接用一个 el-form 包裹所有模块:

vue 复制代码
<template>
  <div class="survey-form">
    <el-form ref="formRef" :model="formModel" label-width="120px">
      <!-- 模块一 -->
      <el-form-item
        label="问题1"
        prop="answerA"
        :rules="[{ required: true, message: '请填写问题1', trigger: 'blur' }]"
      >
        <el-input v-model="formModel.answerA" />
      </el-form-item>

      <!-- 模块二 -->
      <el-form-item
        label="问题2"
        prop="answerB"
        :rules="[{ required: true, message: '请选择问题2', trigger: 'change' }]"
      >
        <el-radio-group v-model="formModel.answerB">
          <el-radio label="是" />
          <el-radio label="否" />
        </el-radio-group>
      </el-form-item>

      <!-- 模块三 -->
      <el-form-item
        label="问题3"
        prop="answerC"
        :rules="[{ required: true, message: '请选择问题3', trigger: 'change' }]"
      >
        <el-select v-model="formModel.answerC">
          <el-option label="选项一" value="1" />
          <el-option label="选项二" value="2" />
        </el-select>
      </el-form-item>
    </el-form>
  </div>
</template>

<script setup lang="ts">
// validate 保持不变,一次调用即可校验全部字段
const validate = () =>
  new Promise((resolve, reject) => {
    if (!formRef.value) return resolve(true)
    formRef.value.validate((valid: boolean) =>
      valid ? resolve(true) : reject(new Error('表单校验失败'))
    )
  })

defineExpose({ validate })
</script>

方案二:多个 el-form 使用不同的 ref,并聚合校验

如果确实需要保留多个表单(例如要按模块单独校验 / 单独清除校验状态),给每个表单独立的 ref 名称,validate 内部用 Promise.all 聚合:

vue 复制代码
<template>
  <div class="survey-form">
    <el-form ref="formRefSectionA" :model="formModel" label-width="120px">
      <!-- 模块一表单项 -->
    </el-form>
    <el-form ref="formRefSectionB" :model="formModel" label-width="120px">
      <!-- 模块二表单项 -->
    </el-form>
    <el-form ref="formRefSectionC" :model="formModel" label-width="120px">
      <!-- 模块三表单项 -->
    </el-form>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import type { FormInstance } from 'element-plus'

const formRefSectionA = ref<FormInstance>()
const formRefSectionB = ref<FormInstance>()
const formRefSectionC = ref<FormInstance>()

// 聚合校验:任何一个表单校验失败则整体失败
const validate = () =>
  Promise.all(
    [formRefSectionA, formRefSectionB, formRefSectionC].map(
      (f) => f.value?.validate() ?? Promise.resolve(true)
    )
  ).then(() => true)

defineExpose({ validate })
</script>

注意:el-form 的 validate() 在校验通过时 resolve、失败时 reject,因此 Promise.all 任一失败即整体 reject,符合"全部通过才算通过"的语义。

五、两种方案优缺点对比

维度 方案一:单一 el-form 方案二:多 el-form + 多 ref
校验完整性 天然完整,不可能漏 依赖人工聚合,漏写一个 ref 就会静默跳过(原 bug 的同形态风险)
模板简洁度 少嵌套、无重复配置 :model、label-width 等配置重复多份
prop 冲突检测 所有 prop 在同一命名空间,重名立即暴露 各表单 prop 互相隔离,重名不易发现
细粒度控制 validate/clearValidate/resetFields/scrollToField 只能整体调用 可按模块独立调用,灵活
可拆分性 模块与表单耦合在一个组件里 每个模块天然可拆成独立子组件

选型建议:

  • 字段同属一个数据对象、提交时整体校验 → 用方案一,简单且不会出错。
  • 有"按模块分别校验 / 分别清除校验状态 / 分别滚动到错误项"的需求 → 用方案二,但务必把"聚合所有表单 ref"写成显式列表,新增表单时同步维护。

六、经验总结

  1. 同一组件模板中,禁止给多个元素使用相同的静态 ref 名称 (v-for 除外,v-for 中同名 ref 会收集为数组,是另一套语义)。需要多个实例时,要么用不同名称,要么用函数 ref 收集。
  2. 多个 el-form 的校验互相隔离 ,共用 ref 时 validate 容易"只校验了最后一个挂载的表单",且无任何报错提示,属于静默 Bug,Code Review 时应重点关注。
  3. 暴露给父组件的 validate() 若聚合多个表单,推荐显式列出 ref 数组 + Promise.all,并在新增表单时同步维护,避免再次遗漏。
相关推荐
liangshanbo121512 小时前
JavaScript 面试题:Object.keys() 的返回顺序是无序的吗?
开发语言·javascript·ecmascript
geovindu12 小时前
css: Timeline scribble
前端·css·iphone
沙漠之主21 小时前
C++编程教学设计资料:从入门到实战的完整课程方案
java·前端·c++
hasty21 小时前
不上传新包,也能改变用户拿到的版本:npm dist-tag 的 OIDC 权限治理
前端·npm·node.js
Csvn21 小时前
diff 算法(虚拟 DOM Reconciliation)
前端
前端snow1 天前
ai agent --- 文件存储
前端
Frag0ut1 天前
Chrome四大版本获取及共存指南:Stable/Beta/Dev/Canary
前端·chrome·浏览器·dev·beta·canary·共存版
IT_陈寒1 天前
SpringBoot自动配置坑了我一把,原来是这样绕过去的
前端·人工智能·后端
广州华水科技1 天前
单北斗GNSS变形监测在大坝安全监测中的应用与优势
前端
zeng不错1 天前
B站 173 个投稿活动看到眼瞎?我写了个扩展,主打一个精准打击
前端