Vue2 + Elementui 表单嵌套结构校验问题

此文章记录解决Vue2+Elementui中Form组件嵌套Table结构校验问题,主要是分享一下发现问题到解决问题的思路。

问题来源

由于上文Formily IE兼容性」的原因,我需要使用Vue2+Elementui来实现一个比较复杂的表单。在处理到某个部分的时候,遇到一个需求:

页面中的出现的表单控件都需要填写,Table「动态增加行」中的控件支持「远程搜索」,全部控件支持「校验」。

其中Table中控件支持校验的需求就是此次文章的核心。

解决过程

以下模拟数据的键值用a~z字母命名进行演示,未使用项目中的真实字段。

  1. 尝试一:

我初次看这个结构的时候,首先想到的是上面两个表单控件为一部分,下面的动态Table为另一部分,即上下结构。如下代码:

html 复制代码
<template>
  <div>
    <el-form
      :model="outpatientMesForm"
      ref="outpatientMesForm"
      :inline="true"
      :rules="rules"
    >
      ...
    </el-form>
    <el-table :data="tableData" style="width: 100%">
      ...
    </el-table>
  </div>
</template>
  • 问题:Table部分不好做「控件校验」
  • 思考:Table放在外面,整体校验要考虑两部分,就不能使用Form的校验方法,应该让From来校验Table里面的控件,就不用再去另外校验Table中的控件了。
  1. 尝试二:

为了解决校验的问题,我把Table放到了Form中,并且加上了字段校验;布局也没有被影响,还是正常的。

html 复制代码
<template>
  <div>
    <el-form
      :model="outpatientMesForm"
      ref="outpatientMesForm"
      :inline="true"
      :rules="rules"
    >
    ...
      <el-form-item prop="b" label="出生日期:">
        <el-date-picker
          v-model="outpatientMesForm.b"
          type="date"
          value-format="yyyy-MM-dd"
          :picker-options="pickerOptions"
          placeholder="选择日期"
        >
        </el-date-picker>
      </el-form-item>
      ...
      <el-table :data="outpatientMesForm.tableData" style="width: 100%">
        <el-table-column label="病种名称">
          <template slot-scope="scope">
            <el-form-item prop="c">
              <el-autocomplete
                class="input-width-200"
                v-model="scope.row.c"
                placeholder="请输入"
                :fetch-suggestions="getData"
                @select="
                  e => {
                    scope.row.c = e;
                  }
                "
              >
                <template slot-scope="{ item }">
                  <div class="name">{{ item }}</div>
                </template>
              </el-autocomplete>
            </el-form-item>
          </template>
        </el-table-column>
        ...
      </el-table>
    </el-form>
  </div>
</template>

<script>
export default {
  data() {
    return {
      outpatientMesForm: {
        a: "",
        b: "",
        tableData: [
          {
            c: "",
            d: "",
            e: "",
            f: ""
          },
          {
            c: "",
            d: "",
            e: "",
            f: ""
          }
        ]
      },
      rules: {
        a: [{ required: true, message: "请选择诊断科别", trigger: "change" }],
        b: [
          {
            required: true,
            message: "请选择出生日期",
            trigger: "change"
          }
        ],
        c: [{ required: true, message: "请选择病种名称", trigger: "change" }],
        d: [{ required: true, message: "请选择病种代码", trigger: "change" }],
        e: [{ required: true, message: "请选择手术及操作名称", trigger: "change" }],
        f: [{ required: true, message: "请选择手术及操作代码", trigger: "change" }],
      },
      ...
    };
  },
  ...
};
</script>
  • 问题:Table中控件的值未填时,校验能够提示。填写了值之后,校验没有消失。
  • 思考:目前这个校验没有起到作用,上面代码中与校验相关的proprules属性的写法在没有嵌套结构的From中能正常的工作。与这两个属性相关的是el-form-item组件,它决定了校验的规则和现实效果,所以我把问题聚焦到了这个组件中。
  1. 尝试三

我首先看了官方文档的相关部分:

文档中的校验部分没有详细的解释,也就得不到嵌套结构中如何处理的方式了。所以我就转而去研究el-form-item源码部分,应该这里面会有详细的用法。

这里我挂了一个线上的el-form-item完整代码,就不全部展示了。

  • 问题:在研究代码之前,还有一个问题需要解决,那就是:研究什么?在这样的一段具有校验功能的代码中,哪一个是关键?
html 复制代码
<el-form-item label="诊断科别:" prop="a">
    <el-select
      v-model="outpatientMesForm.a"
      placeholder="请输入"
      filterable
      clearable
      collapse-tags
    >
      <el-option
        v-for="item in formOptions.aOptions"
        :key="item.value"
        :label="item.label"
        :value="item.value"
      >
      </el-option>
    </el-select>
</el-form-item>
  • 思考:校验的核心肯定是el-form-item组件的prop属性,这个属性的值来匹配el-formrules属性,从而达成校验的目的。

所以我去研究了源码中和prop属性相关的部分,发现有两个地方比较关键:

js 复制代码
computed: {
    //...
    // 取得prop对应的值
    fieldValue() {
        const model = this.form.model;
        if (!model || !this.prop) { return; }

        let path = this.prop;
        if (path.indexOf(':') !== -1) {
          path = path.replace(/:/, '.');
        }

        return getPropByPath(model, path, true).v;
    },
    //... 
},
methods: {
      //...
      // 校验方法
      validate(trigger, callback = noop) {
        this.validateDisabled = false;
        const rules = this.getFilteredRule(trigger);
        if ((!rules || rules.length === 0) && this.required === undefined) {
          callback();
          return true;
        }

        this.validateState = 'validating';

        const descriptor = {};
        if (rules && rules.length > 0) {
          rules.forEach(rule => {
            delete rule.trigger;
          });
        }
        descriptor[this.prop] = rules;

        const validator = new AsyncValidator(descriptor);
        const model = {};

        model[this.prop] = this.fieldValue;

        validator.validate(model, { firstFields: true }, (errors, invalidFields) => {
          this.validateState = !errors ? 'success' : 'error';
          this.validateMessage = errors ? errors[0].message : '';

          callback(this.validateMessage, invalidFields);
          this.elForm && this.elForm.$emit('validate', this.prop, !errors, this.validateMessage || null);
        });
      },
}

总体读下来的意思是prop属性可以是一个路径,需要根据这个值来进行取值,取到el-form-item对应的值然后和对应的规则进行判断。

  1. 尝试四

我把Table里面的prop改成了这样:prop="tableData[{scope.index}].c"

  • 问题:校验不显示
  • 思考:proprules是对应关系,我这里改了prop,应该是能取到嵌套的值的,有没有可能是没有匹配到对应的rule呢?所以下一步尝试从rules解决。
  1. 尝试五

由于prop改成了:prop="tableData[{scope.index}].c",所以我想把rules改成

js 复制代码
rules: {
        ...
        `tableData[${scope.$index}].c`: ...
      }

但是scope在外面取不到,所以这种方式不行。就在我一筹莫展的时候,我又去看el-form-item组件源码,想尝试在里面找到问题的解决办法,发现组件的props中有一个rules属性。

js 复制代码
props: {
  ...
  rules: [Object, Array],
  ...
},

我就准备尝试一下不用取el-form中的rules,给每一个el-form-item单独设置rules

  1. 尝试六

将Table中表单控件的proprules改为以下的写法:

html 复制代码
<el-table-column label="病种名称">
  <template slot-scope="scope">
  <!-- ** 关键点在prop和rules的写法 ** -->
    <el-form-item
      :prop="`tableData[${scope.$index}].c`"
      :rules="[
        { required: true, message: '请输入活动名称', trigger: 'change' }
      ]"
    >
      <el-autocomplete
        class="input-width-200"
        v-model="scope.row.c"
        placeholder="请输入"
        :fetch-suggestions="getData"
        @select="
          e => {
            scope.row.c = e;
          }
        "
      >
        <template slot-scope="{ item }">
          <div class="name">{{ item }}</div>
        </template>
      </el-autocomplete>
    </el-form-item>
  </template>
</el-table-column>

改为这种方式之后,解决了这个问题,也就说明我的思路是对的。以下是暗色系中的效果。

完整代码

提示:代码中暂未添加Table的动态增加行功能,如有需要可自行处理

html 复制代码
<template>
  <div>
    <el-form
      :model="outpatientMesForm"
      ref="outpatientMesForm"
      :inline="true"
      :rules="rules"
    >
      <el-form-item label="诊断科别:" prop="a">
        <el-select
          v-model="outpatientMesForm.a"
          placeholder="请输入"
          filterable
          clearable
          collapse-tags
        >
          <el-option
            v-for="item in formOptions.aOptions"
            :key="item.value"
            :label="item.label"
            :value="item.value"
          >
          </el-option>
        </el-select>
      </el-form-item>
      <el-form-item prop="b" label="出生日期:">
        <el-date-picker
          v-model="outpatientMesForm.b"
          type="date"
          value-format="yyyy-MM-dd"
          :picker-options="pickerOptions"
          placeholder="选择日期"
        >
        </el-date-picker>
      </el-form-item>
      <el-table :data="outpatientMesForm.tableData" style="width: 100%">
        <el-table-column label="病种名称">
          <template slot-scope="scope">
          <!-- ** 关键点在prop和rules的写法上 ** -->
            <el-form-item
              :prop="`tableData[${scope.$index}].c`"
              :rules="[
                { required: true, message: '请输入活动名称', trigger: 'change' }
              ]"
            >
              <el-autocomplete
                class="input-width-200"
                v-model="scope.row.c"
                placeholder="请输入"
                :fetch-suggestions="getData"
                @select="
                  e => {
                    scope.row.c = e;
                  }
                "
              >
                <template slot-scope="{ item }">
                  <div class="name">{{ item }}</div>
                </template>
              </el-autocomplete>
            </el-form-item>
          </template>
        </el-table-column>
        <el-table-column label="病种代码">
          <template slot-scope="scope">
            <el-form-item
              :prop="`tableData[${scope.$index}].d`"
              :rules="[
                { required: true, message: '请选择病种代码', trigger: 'change' }
              ]"
            >
              <el-autocomplete
                class="input-width-200"
                v-model="scope.row.d"
                placeholder="请输入"
                :fetch-suggestions="getData"
                @select="
                  e => {
                    scope.row.d = e;
                  }
                "
              >
                <template slot-scope="{ item }">
                  <div class="name">{{ item }}</div>
                </template>
              </el-autocomplete>
            </el-form-item>
          </template>
        </el-table-column>
        <el-table-column label="手术及操作名称">
          <template slot-scope="scope">
            <el-form-item
              :prop="`tableData[${scope.$index}].e`"
              :rules="[
                {
                  required: true,
                  message: '请选择手术及操作名称',
                  trigger: 'change'
                }
              ]"
            >
              <el-autocomplete
                class="input-width-200"
                v-model="scope.row.e"
                placeholder="请输入"
                :fetch-suggestions="getData"
                @select="
                  e => {
                    scope.row.e = e;
                  }
                "
              >
                <template slot-scope="{ item }">
                  <div class="name">{{ item }}</div>
                </template>
              </el-autocomplete>
            </el-form-item>
          </template>
        </el-table-column>
        <el-table-column label="手术及操作代码">
          <template slot-scope="scope">
            <el-form-item
              :prop="`tableData[${scope.$index}].f`"
              :rules="[
                {
                  required: true,
                  message: '请选择手术及操作代码',
                  trigger: 'change'
                }
              ]"
            >
              <el-autocomplete
                class="input-width-200"
                v-model="scope.row.f"
                placeholder="请输入"
                :fetch-suggestions="getData"
                @select="
                  e => {
                    scope.row.f = e;
                  }
                "
              >
                <template slot-scope="{ item }">
                  <div class="name">{{ item }}</div>
                </template>
              </el-autocomplete>
            </el-form-item>
          </template>
        </el-table-column>
      </el-table>
    </el-form>
    <el-button @click="submitForm" style="margin-top: 15px;">点击校验</el-button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      outpatientMesForm: {
        a: "",
        b: "",
        tableData: [
          {
            c: "",
            d: "",
            e: "",
            f: ""
          },
          {
            c: "",
            d: "",
            e: "",
            f: ""
          },
        ]
      },
      formOptions: {
        aOptions: [{ label: "11", value: 11 }]
      },
      pickerOptions: {
        disabledDate(time) {
          return time.getTime() > Date.now();
        }
      },
      rules: {
        a: [{ required: true, message: "请选择诊断科别", trigger: "change" }],
        b: [
          {
            required: true,
            message: "请选择出生日期",
            trigger: "change"
          }
        ]
      }
    };
  },
  methods: {
    submitForm() {
      this.$refs.outpatientMesForm.validate(valid => {
        if (valid) {
          alert("submit!");
        } else {
          console.log("error submit!!");
          return false;
        }
      });
    },
    async getData(str, cb) {
      if (!str) {
        cb && cb([]);
        return;
      }
      const p = new Promise((res, rej) => {
        res(["1", "2", "3", "4", "5", "6"]);
      });
      const data = await p;
      cb && cb(data);
    }
  }
};
</script>

总结

以上就是我在解决Form中Table校验的思考路径,经历了问题的出现,到逐步地探索、分析、尝试,最后成功的过程。

之所以想分享出来,是因为,咱们在处理需求的过程中,总会遇到网上搜不出答案 的情况,这种时候咱们需要耐心 一点根据已有的条件一点点找下一个突破口,过程中保持思考 ,大多数问题都能解决。别忘了事后总结和记录 ,对于养成解决问题的习惯很有帮助。

以上就是全部内容,下一篇文章再见

相关推荐
passerby60612 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了2 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅2 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅3 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅3 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment3 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅3 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊4 小时前
jwt介绍
前端
爱敲代码的小鱼4 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax
吹牛不交税4 小时前
admin.net-v2 框架使用笔记-netcore8.0/10.0版
vue.js·.netcore