深入瞭解Element Form表單動態驗證問題

在上一篇《vue elementUI組件表單動態驗證失效的問題與解決辦法》中,講到直接修改prop屬性,未觸發form-item的重新渲染,所以雖然有校驗*的標誌,實際上並不會校驗。這是表面現象,最近有了空餘時間,去看看了element form組件的源碼,找到了根本原因。

源碼分析

  1. form組件的created鉤子函數中添加了el.form.addFieldel.form.removeField事件監聽,往fields中添加或刪除field,校驗的時候會遍歷fields數組,而field就是form-item的實例。接着查看form-item組件的源碼,可以看到在組件掛載後mounted,如果prop屬性有值就會觸發el.form.addField事件,在組件銷燬前beforeDestroy觸發el.form.removeField事件。由此可知如果掛載時form-item組件prop屬性無值,不會觸發el.form.addField
// form.vue
created() {
  this.$on('el.form.addField', (field) => {
    if (field) {
      this.fields.push(field);
    }
  });
  /* istanbul ignore next */
  this.$on('el.form.removeField', (field) => {
    if (field.prop) {
      this.fields.splice(this.fields.indexOf(field), 1);
    }
  });
}

// form-item.vue
mounted() {
  // 重點掛載前有prop屬性,纔會觸發'el.form.addField'
  if (this.prop) {
    this.dispatch('ElForm', 'el.form.addField', [this]);
    // ...
    this.addValidateEvents();
  }
},
beforeDestroy() {
  this.dispatch('ElForm', 'el.form.removeField', [this]);
}
  1. form表單校驗是調用表單實例的validate方法,去掉邊界、合法性判斷一類的代碼,其核心源碼如下,從代碼註釋中可以知道,form表單的校驗方法核心是調用form-item組件實例的validate方法。
// form.vue
methods: {
  validate(callback) {
    // ...
    this.fields.forEach(field => {
      // 調用form-item實例的validate方法
      field.validate('', (message, field) => {
        if (message) {
          valid = false;
        }
        invalidFields = objectAssign({}, invalidFields, field);
        if (typeof callback === 'function' && ++count === this.fields.length) {
          callback(valid, invalidFields);
        }
      });
    });
  }
},

watch: {
  // 如果rules有變化,強制更新form-item的校驗事件,並觸發一次form實例validate方法
  rules() {
    // remove then add event listeners on form-item after form rules change
    this.fields.forEach(field => {
      field.removeValidateEvents();
      field.addValidateEvents();
    });

    if (this.validateOnRuleChange) {
      this.validate(() => {});
    }
  }
}
  1. 最後看form-itemvalidate方法,省略一些邏輯判斷和優化代碼,其核心是獲取form組件的對應屬性的rulesform-item自身的rules,生成AsyncValidator校驗器的校驗規則描述,並進行數據校驗。
// form-item.vue
validate(trigger, callback = noop) {
  this.validateDisabled = false;
  // 獲取form組件的對應屬性的rules及form-item自身的rules,並根據觸發器trigger過濾
  const rules = this.getFilteredRule(trigger);
  // 判斷校驗規則,假值或者rules數組爲空,則跳過。值得注意的是空對象並不是假值,所以不會跳過
  if ((!rules || rules.length === 0) && this.required === undefined) {
    callback();
    return true;
  }

  this.validateState = 'validating';

  // 設置AsyncValidator的校驗規則描述
  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初始值爲空字符串'',所以form表單的fileds不會持有該form-item實例,故即使後面修改prop屬性,也不會去校驗該表單項,導致校驗失效。

解決辦法

  1. 《vue elementUI組件表單動態驗證失效的問題與解決辦法》中講的強制重新渲染form-item,當時不知道根本原因採用的笨辦法。
  2. form-item保留prop屬性,根據條件動態修改form組件的rules對象。比如下面這樣:
watch: {
  'formData.age': {
    immediate: true,
    handler (newVal) {
      if (newVal >= 18) {
        this.addRule('bankCardNo', [{ required: true, message: '請輸入銀行卡號', trigger: 'blur' }])
      } else {
        this.addRule('bankCardNo', [])
      }
    }
  }
}

data () {
  return {
    rules: {}
  }
},

methods: {
  addRule (prop, rule) {
    this.$set(this.rules, prop, rule)
  }
}
  1. form-item保留prop屬性,根據條件動態修改form-item組件的rules屬性。比如下面兩種方式,因爲rules屬性接受數據和對象。
<el-form-item label="銀行卡號" prop="bankCardNo" :rules="formData.age >= 18 ? [{ required: true, message: '請輸入銀行卡號', trigger: 'blur' }] : []">
  <el-input v-model="formData.bankCardNo"></el-input>
</el-form-item>

<!-- 或者 -->
<el-form-item label="銀行卡號" prop="bankCardNo" :rules="formData.age >= 18 ? { required: true, message: '請輸入銀行卡號', trigger: 'blur' } : []">
  <el-input v-model="formData.bankCardNo"></el-input>
</el-form-item>

總結

終須由淺入深,浮於表面便是管中窺豹,自身還需靜下心認真研究

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章