拜讀及分析Element原始碼-form表單元件篇

hollyDysania發表於2018-09-13

element from表單原始碼分析,涉及到input、select、checkbox、picker、radio等元件的的驗證。表單的驗證用到了async-validator 外掛。一起來看看吧。

拜讀及分析Element原始碼-form表單元件篇
首先form表單元件由兩部分組成

  • form: 統一管理form-item。
  • form-item:負責完成驗證等。

form

結構

  <form class="el-form" :class="[
    labelPosition ? 'el-form--label-' + labelPosition : '',
    { 'el-form--inline': inline }
  ]">
    <slot></slot>
  </form>
複製程式碼

結構很簡單,form元素包裹插槽,也就是form-item

script部分

1.方便與form-item關聯,注入form例項

    // 注入元件例項
    provide() {
      return {
        elForm: this
      };
    }
複製程式碼

2.例項建立後,fields收集form-item的例項

    created() {
      // 監聽el.form.addField事件,觸發:將form-item例項push到fields
      this.$on('el.form.addField', (field) => {
        if (field) {
          this.fields.push(field);
        }
      });
      // 監聽el.form.removeField事件,觸發:form-item例項有prop規則屬性從fields移除form-item例項
      this.$on('el.form.removeField', (field) => {
        if (field.prop) {
          this.fields.splice(this.fields.indexOf(field), 1);
        }
      });
    }
複製程式碼

3.關於表單驗證的一些方法,form元件是做是統一管理具體執行還是在form-item中

  • 對整個表單進行驗證

          // 對整個表單進行驗證
          validate(callback) {
            // 沒有表單資料 拋警告跳出
            if (!this.model) {
              console.warn('[Element Warn][Form]model is required for validate to work!');
              return;
            }
    
            let promise;
            // 沒有callback並且瀏覽器支援Promise return promise
            if (typeof callback !== 'function' && window.Promise) {
              promise = new window.Promise((resolve, reject) => {
                callback = function(valid) {
                  valid ? resolve(valid) : reject(valid);
                };
              });
            }
              
            let valid = true;
            let count = 0;
            // 如果需要驗證的fields為空,呼叫驗證時立刻返回callback
            if (this.fields.length === 0 && callback) {
              callback(true);
            }
            let invalidFields = {};
            // 遍歷所有例項,一個個驗證
            this.fields.forEach(field => {
              // 這裡的validate是form-item的方法
              field.validate('', (message, field) => {
                // 如果有返回資訊, 則說明驗證失敗
                if (message) {
                  valid = false;
                }
                // 將錯誤物件複製到invalidFields
                invalidFields = objectAssign({}, invalidFields, field);
                // 調callback
                if (typeof callback === 'function' && ++count === this.fields.length) {
                  callback(valid, invalidFields);
                }
              });
            });
    
            if (promise) {
              return promise;
            }
          }
    複製程式碼

    objectAssign方法在utils/merge 中,合併物件的方法

  • 對部分表單進行驗證

          // 對部分表單驗證
          validateField(prop, cb) {
            let field = this.fields.filter(field => field.prop === prop)[0];
            if (!field) { throw new Error('must call validateField with valid prop string!'); }
            // 驗證對應表單規則的表單
            field.validate('', cb);
          }
    複製程式碼
  • 移除表單項的校驗結果

    傳入待移除的表單項的 prop 屬性組成的陣列,如不傳則移除整個表單的校驗結果

          clearValidate(props = []) {
            const fields = props.length
              ? this.fields.filter(field => props.indexOf(field.prop) > -1)
              : this.fields;
            fields.forEach(field => {
            // form-item例項的方法clearValidate(清除驗證狀態與提示)
              field.clearValidate();
            });
          }
    複製程式碼
  • 對整個表單進行重置,將所有欄位值重置為初始值並移除校驗結果

          resetFields() {
            // 沒有表單資料 return
            if (!this.model) {
              // 環境變數,非生產環境拋警告再return
              process.env.NODE_ENV !== 'production' &&
              console.warn('[Element Warn][Form]model is required for resetFields to work.');
              return;
            }
            this.fields.forEach(field => {
              field.resetField();
            });
          }
    複製程式碼

4.監聽驗證規則

    // 監聽表單驗證規則
    watch: {
      rules() {
        // validateOnRuleChange未傳入false則立即觸發
        if (this.validateOnRuleChange) {
          // 驗證
          this.validate(() => {});
        }
      }
    }
複製程式碼

form-item

結構

  <div class="el-form-item" :class="[{
      'el-form-item--feedback': elForm && elForm.statusIcon,
      'is-error': validateState === 'error',
      'is-validating': validateState === 'validating',
      'is-success': validateState === 'success',
      'is-required': isRequired || required
    },
    sizeClass ? 'el-form-item--' + sizeClass : ''
  ]">
    <!-- 表單域標籤文字 -->
    <label :for="labelFor" class="el-form-item__label" :style="labelStyle" v-if="label || $slots.label">
      <slot name="label">{{label + form.labelSuffix}}</slot>
    </label>
    <div class="el-form-item__content" :style="contentStyle">
      <!-- 插槽接收表單驗證的元素,input框單選框多選框之類的 -->
      <slot></slot>
      <!-- 驗證不通過時的message -->
      <transition name="el-zoom-in-top">
        <div
          v-if="validateState === 'error' && showMessage && form.showMessage"
          class="el-form-item__error"
          :class="{
            'el-form-item__error--inline': typeof inlineMessage === 'boolean'
              ? inlineMessage
              : (elForm && elForm.inlineMessage || false)
          }"
        >
          {{validateMessage}}
        </div>
      </transition>
    </div>
  </div>
複製程式碼

外層div控制整體樣式,內部分為兩部分

  • 第一部分:可展示label或拼接統一的字尾(from的屬性)
  • 第二部分:slot接收input,select等元件,以及驗證不通過時的message展示

這裡的變數elForm就是form元件provide的from例項

script部分

1.inject接收form例項,並向子孫後代注入form-item例項

    provide() { // 注入form-item例項
      return {
        elFormItem: this
      };
    },
    // 接收form例項
    inject: ['elForm']
複製程式碼

2.元件$el掛載到例項後

初始化,需要驗證的讓form元件收集起來,有驗證規則的el.form.blur ,el.form.change 事件監聽起來,等待觸發驗證。

    mounted() {
      // 有需要驗證的表單
      if (this.prop) {
        // 向上查詢form元件,併發布el.form.addField,暴露form-item例項
        // 即讓form元件收集需要驗證的form-item例項
        this.dispatch('ElForm', 'el.form.addField', [this]);
        // 需要驗證的表單資料
        let initialValue = this.fieldValue;
        // 是陣列
        if (Array.isArray(initialValue)) {
          initialValue = [].concat(initialValue);
        }
        // 響應屬性變成普通屬性
        Object.defineProperty(this, 'initialValue', {
          value: initialValue
        });
        // 該項驗證規則
        let rules = this.getRules();

        if (rules.length || this.required !== undefined) {
          // 監聽el.form.blur,回撥為bluer事件驗證
          // 監聽el.form.change事件,回撥為change事件驗證
          this.$on('el.form.blur', this.onFieldBlur);
          this.$on('el.form.change', this.onFieldChange);
        }
      }
    }
複製程式碼
  • 這裡用到的dispatch方法從mixins中引入:找到指定元件,釋出指定事件。

3.驗證方法

      validate(trigger, callback = noop) {
        this.validateDisabled = false;
        // 符合規則的trigger
        const rules = this.getFilteredRule(trigger);
        // 沒有規則也不是必填 返回true
        if ((!rules || rules.length === 0) && this.required === undefined) {
          // 執行回撥
          callback();
          return true;
        }
        // 驗證中
        this.validateState = 'validating';

        const descriptor = {};
        // 為了匹配AsyncValidator外掛所需要的格式,需要做規則資料做一些操作
        if (rules && rules.length > 0) {
          rules.forEach(rule => {
            delete rule.trigger;
          });
        }
        // AsyncValidator需要的驗證規則
        descriptor[this.prop] = rules;
        // 驗證規則AsyncValidator例項物件
        const validator = new AsyncValidator(descriptor);
        const model = {};
        // AsyncValidator需要的驗證資料
        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);
          // form元件釋出validate事件
          this.elForm && this.elForm.$emit('validate', this.prop, !errors);
        });
      }
複製程式碼

4.清空驗證狀態與重置表單驗證方法

      // 清空驗證狀態及message
      clearValidate() {
        // 驗證狀態
        this.validateState = '';
        // 驗證message
        this.validateMessage = '';
        this.validateDisabled = false;
      },
      // 重置
      resetField() {
        this.validateState = '';
        this.validateMessage = '';

        // 拿到初始資料
        let model = this.form.model;// 所以表單資料
        let value = this.fieldValue; // 該項表單資料
        let path = this.prop; //該項
        if (path.indexOf(':') !== -1) {
          path = path.replace(/:/, '.');
        }
        // 該項表單資料
        let prop = getPropByPath(model, path, true);

        this.validateDisabled = true;
        // 重置為初始表單資料
        if (Array.isArray(value)) {
          prop.o[prop.k] = [].concat(this.initialValue);
        } else {
          prop.o[prop.k] = this.initialValue;
        }
        // 向下尋找select元件,釋出fieldReset事件暴露初始表單資料
        this.broadcast('ElTimeSelect', 'fieldReset', this.initialValue);
      }
複製程式碼

5.監聽error以及驗證狀態

這裡的error是props接收的:若傳入,狀態變為error,並顯示錯誤資訊

    watch: {
      // 監聽error
      error: {
        // 立即執行handler
        immediate: true, 
        handler(value) {
          // 驗證狀態變為error,並顯示錯誤資訊
          this.validateMessage = value;
          this.validateState = value ? 'error' : '';
        }
      },
      // 監聽驗證狀態
      validateStatus(value) {
        this.validateState = value;
      }
    }
複製程式碼
6.例項銷燬之前,釋出form移除收集的該form-item例項
    beforeDestroy() {
      // 向上尋找form元件,釋出el.form.removeField事件,暴露當前例項
      this.dispatch('ElForm', 'el.form.removeField', [this]);
    }

複製程式碼

form表單元件與input等元件的關聯

是怎麼觸發from驗證的呢

form在mounted時監聽了el.form.blur和el.form.change事件,並指定了驗證的回撥函式

this.$on('el.form.blur', this.onFieldBlur);
this.$on('el.form.change', this.onFieldChange);
複製程式碼

以input為例

// form表單釋出el.form.blur事件
this.dispatch('ElFormItem', 'el.form.blur', [this.currentValue]);
複製程式碼

以checkbox多選為例

// 監聽value 向上尋找form元件釋出el.form.change事件暴露value(陣列)
this.dispatch('ElFormItem', 'el.form.change', [value]);
複製程式碼

等... , 這些時機觸發校驗

$on就是監聽指定的事件並且指定回撥函式,$emit就是釋出某個事件並傳遞某些資料(可不傳),當監聽的事件名與釋出的事件名一致就會觸發監聽的回撥函式並且引數就是對應$emit傳遞的引數。

官網直通例項方法$on以及$emit

相關文章