element-ui[2.15.x]源码从开始到放弃(四)-表单

344 阅读2分钟

上一章我们讲了element的图标,按钮,链接,这章咱讲表单

表单

表单的代码量就稍微多了一点,我会一步步解析它

Form

首先是Form组件的created方法,它会绑定两个事件,一个是添加field,一个是删除field

  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);
    }
  });

那么这个field是什么呢,其实它是通过插槽嵌入的子组件FormItem,当FormItem执行mounted回调时,会将FormItem实例也就是this通过触发事件进行保存起来

Form组件主要的四个方法validate校验规则,validateField校验部分规则,resetFields重置校验和数据,clearValidate重置校验

四个方法都是遍历子组件FormItem(也就是field)并触发它的逻辑进行处理,validateField和clearValidate通过fields.filter(field => ~props.indexOf(field.prop))过滤出相关的FormItem选项

FormItem

mounted() {
  if (this.prop) {
    this.dispatch('ElForm', 'el.form.addField', [this]);
    let initialValue = this.fieldValue;
    if (Array.isArray(initialValue)) {
      initialValue = [].concat(initialValue);
    }
    Object.defineProperty(this, 'initialValue', {
      value: initialValue
    });
    this.addValidateEvents();
  }
},
beforeDestroy() {
  this.dispatch('ElForm', 'el.form.removeField', [this]);
}

FormItem执行dispatch方法,这个方法是通过mixin混入进去的,也是通过比较有意思的方法去触发父组件Form

 methods: {
    dispatch(componentName, eventName, params) {
      var parent = this.$parent || this.$root;
      var name = parent.$options.componentName;

      while (parent && (!name || name !== componentName)) {
        parent = parent.$parent;

        if (parent) {
          name = parent.$options.componentName;
        }
      }
      if (parent) {
        parent.$emit.apply(parent, [eventName].concat(params));
      }
    }
  }

通过while向上查找,如果存在name值为ElForm的组件也就是Form组件,就执行它绑定的添加删除field的事件

把当前FormItem实例存入Form中后,fieldValue会以initialValue为键添加到实例上,因为是通过Object.defineProperty定义的默认不可更改,通过prop属性获取到Form的model属性所对应的prop,再通过计算属性监听数据的变化,对于prop的写法可以是xxx或xxx:xxx或xxx.xxx

fieldValue的值通过getPropByPath进行了处理,它对path进行了转换

/\[(\w+)\]/g => '.$1'
/^\./ => ''
// getPropByPath返回值
return {
    o: tempObj, // prop指向的值所处的对象
    k: keyArr[i], // prop指向的键
    v: tempObj ? tempObj[keyArr[i]] : null // prop指向的值
  };

当处理完之后通过split('.')将path分割,获取model的子元素,像path='input.el.text'对应的就是model.input.el.text,所以fieldValue返回的就是相应的值

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;
  }

mounted继续执行this.addValidateEvents(),这个函数会判断是否传入了rules/required或者父组件是否有rules,优先级:FormItem的rules > Form的rules

如果满足其中一个条件,就会添加两个事件来监听输入框,单选框等表单元素的变化

addValidateEvents() {
        const rules = this.getRules(); // 获取表单规则
        if (rules.length || this.required !== undefined) {
          this.$on('el.form.blur', this.onFieldBlur);
          this.$on('el.form.change', this.onFieldChange);
        }
      }

当子组件如Input,Radio失去焦点或者改变时,触发这些事件

触发这些事件之后,会执行this.validate('blur' || 'change'),validate根据第一个参数过滤rules,getFilteredRule函数根据trigger也就是'blur' || 'change'过滤

可以看出,trigger支持字符串数组两种写法,过滤后的rules在进行浅拷贝,objectAssign是Object.assign的polyfill

getFilteredRule(trigger) {
        const rules = this.getRules();
        return rules.filter(rule => {
          if (!rule.trigger || trigger === '') return true;
          if (Array.isArray(rule.trigger)) {
            return rule.trigger.indexOf(trigger) > -1;
          } else {
            return rule.trigger === trigger;
          }
        }).map(rule => objectAssign({}, rule));
      }

将过滤后的rules存入到一个对象中,descriptor = { [this.prop]: rules },element使用了async-validator这个库,他接受一个规则对象,也就是rules,通过它生成校验器去校验数据,调用validate函数,如果不符合规则,会返回报错信息,如果上层组件有Form就执行Form绑定的validate方法,可惜我没看到这个方法,如果想触发单个FormItem进行校验可以使用这个validate方法来实现

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);
        });
      }

clearValidate用来清理状态,也非常简单

clearValidate() {
        this.validateState = '';
        this.validateMessage = '';
        this.validateDisabled = false;
      }

resetField通过getPropByPath函数获取相关对象和键,通过obj[key] = this.initialValue还原初始值

其中TimeSelect组件比较特殊,需要触发该组件的fieldReset事件,这个以后有机会再说

LabelWrap

这个组件是FormItem的label,它是通过render实现的,获取父组件Form的autoLabelWidth,也就是左外边距,通过getComputedStyle(this.$el.firstElementChild).不断去计算文本宽度,对label进行自适应

  render() {
    const slots = this.$slots.default;
    if (!slots) return null;
    if (this.isAutoWidth) {
      const autoLabelWidth = this.elForm.autoLabelWidth;
      const style = {};
      if (autoLabelWidth && autoLabelWidth !== 'auto') {
        const marginLeft = parseInt(autoLabelWidth, 10) - this.computedWidth;
        if (marginLeft) {
          style.marginLeft = marginLeft + 'px';
        }
      }
      return (<div class="el-form-item__label-wrap" style={style}>
        { slots }
      </div>);
    } else {
      return slots[0];
    }
  }

累了,就写到这里吧