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校验的思考路径,经历了问题的出现,到逐步地探索、分析、尝试,最后成功的过程。

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

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

相关推荐
凹凸曼打不赢小怪兽6 分钟前
react 受控组件和非受控组件
前端·javascript·react.js
狂奔solar17 分钟前
分享个好玩的,在k8s上部署web版macos
前端·macos·kubernetes
qiyi.sky19 分钟前
JavaWeb——Web入门(8/9)- Tomcat:基本使用(下载与安装、目录结构介绍、启动与关闭、可能出现的问题及解决方案、总结)
java·前端·笔记·学习·tomcat
清云随笔40 分钟前
axios 实现 无感刷新方案
前端
鑫宝Code42 分钟前
【React】状态管理之Redux
前端·react.js·前端框架
忠实米线1 小时前
使用pdf-lib.js实现pdf添加自定义水印功能
前端·javascript·pdf
pink大呲花1 小时前
关于番外篇-CSS3新增特性
前端·css·css3
少年维持着烦恼.1 小时前
第八章习题
前端·css·html
我是哈哈hh1 小时前
HTML5和CSS3的进阶_HTML5和CSS3的新增特性
开发语言·前端·css·html·css3·html5·web
田本初1 小时前
如何修改npm包
前端·npm·node.js