实现一个简版的el-form

实现一个简版的el-form

结合源码,实现form组件的核心内容,达到学习源码的目的。

组件分为两部分,一部分是form组件,一部分是form-item组件。

文章源码

gitee/element-ui-learn

代码路径

src/components/Form

注意:文章并不会完整实现el-form的所有功能,对其他属性感兴趣的小伙伴可以自行查看源码。

基础的结构实现

form组件

首先先实现form组件,form组件使用原生的form元素构成,并包含一个default slot

包含基本的props如下所示。

html 复制代码
<!-- Form -->
<template>
  <form class="el-form">
    <slot></slot>
  </form>
</template>

<script>
export default {
  name: "Form",
  // provide 提供form对象自身 方便其他组件访问
  provide() {
    return {
      form: this,
    };
  },
  props: {
    // 表单数据对象
    model: Object,
    // label位置
    labelPosition: String,
    // label宽度
    labelWidth: String,
    // size 大小
    size: String,
    // 是否禁用
    disabled: Boolean,
  },
};
</script>

form-item组件

form-item组件用于包含其他表单组件,并可以设置label标签,基本结构如下。

label标签默认使用传递的label属性,也可以通过label slot自由设置。

html 复制代码
<!-- FormItem -->
<template>
  <div class="el-form-item">
    <label class="el-form-item__label">
      <slot name="label">{{ label }}</slot>
    </label>
    <div class="el-form-item__content">
      <slot></slot>
    </div>
  </div>
</template>

<script>
export default {
  name: "FormItem",
  inject: ["formComponent"],
  props: {
    label: String,
    prop: String,
    labelWidth: String,
  },
  data() {
    return {
      isNested: false,
    };
  },
  computed: {
    // 查找距离最近的form组件
    form() {
      let parent = this.$parent;
      let parentName = parent.$options.componentName;
      while (parentName !== "FormComponent") {
        if (parentName === "FormItem") {
          this.isNested = true;
        }
        parent = parent.$parent;
        parentName = parent.$options.componentName;
      }

      return parent;
    },
  },
};
</script>

<style>
@import "./index.scss";
</style>

使用后的效果

html 复制代码
<template>
  <div class="home">
    <Form :model="formModel">
      <FormItem label="姓名" style="width: 30%">
        <el-input v-model="formModel.name" placeholder="请输入姓名" />
      </FormItem>
    </Form>
  </div>
</template>

<script>
import Form from "@/components/Form/Form.vue";
import FormItem from "@/components/Form/FormItem.vue";

export default {
  name: "Home",
  components: { Form, FormItem },
  data() {
    return {
      formModel: { name: "" },
    };
  },
};
</script>

基础用法

labelWidth

通过labelWidth来设置label的宽度,相对应的form-content需要设置margin-left

html 复制代码
<!-- FormItem -->
<template>
  <div class="el-form-item">
    <label class="el-form-item__label" :style="labelStyle">
      <slot name="label">{{ label }}</slot>
    </label>
    <div class="el-form-item__content" :style="contentStyle">
      <slot></slot>
    </div>
  </div>
</template>

<script>
export default {
  name: "FormItem",
  inject: ["formComponent"],
  props: {
    label: String,
    prop: String,
    labelWidth: String,
  },
  data() {
    return {
      isNested: false,
    };
  },
  computed: {
   ...
    // 设置labelWidth,如果labelPosition为top,则返回空{}
    labelStyle() {
      const ret = {};
      if (this.form.labelPosition === "top") return ret;
      const labelWidth = this.labelWidth || this.form.labelWidth;
      if (labelWidth) {
        ret.width = labelWidth;
      }
      return ret;
    },
    // 设置表单内容的style
    contentStyle() {
      const labelWidth = this.labelWidth || this.form.labelWidth;

      return { marginLeft: labelWidth };
    },
  },
};
</script>

<style>
@import "./index.scss";
</style>

labelPosition

labelPostition用来改变label的位置,是通过在form组件中设置不同的class来实现。

默认文字靠右显示。

设置了top时,label取消浮动,content就会在下一行显示,并且取消了margin-left

html 复制代码
<!-- Form -->
<template>
  <form
    class="el-form"
    :class="[labelPosition ? `el-form--label-${labelPosition}` : '']"
  >
    <slot></slot>
  </form>
</template>

行内表单

让表单元素可以一行内显示,主要是添加了el-form--inlineclass

使labelcontentinline-block 显示。

html 复制代码
<!-- Form -->
<template>
  <form
    class="el-form"
    :class="[
      labelPosition ? `el-form--label-${labelPosition}` : '',
      inline ? 'el-form--inline' : '',
    ]"
  >
    <slot></slot>
  </form>
</template>
css 复制代码
.el-form--inline .el-form-item__label {
  float: none;
  display: inline-block;
}
css 复制代码
.el-form--inline .el-form-item__content {
  display: inline-block;
  vertical-align: top;
}

表单验证

在使用时,首先传递给form组件rules属性,表示要添加的校验规则。

form-item组件中需要传递prop属性,属性要和form组件中传递的model属性对应。

form-item组件的校验

form-item的校验首先在组件的mounted钩子中添加对应的表单事件监听。

js 复制代码
mounted() {
  if (this.prop) {
    // 获取当前表单field的初始值
    let initialValue = this.fieldValue;
    if (Array.isArray(initialValue)) {
      initialValue = [].concat(initialValue);
    }
    // 在this上添加一个initialValue属性
    Object.defineProperty(this, "initialValue", {
      value: initialValue,
    });

    // 添加校验事件监听
    this.addValidateEvents();
  }
},
js 复制代码
addValidateEvents() {
  // 获取校验规则
  const rules = this.getRules();

  // 如果校验规则不为空,则添加不同触发事件的校验监听
  // 触发el.form.blur或者el.form.change事件,要在不同的form组件中触发
  if (rules.length || this.required !== undefined) {
    this.$on("el.form.blur", this.onFieldBlur);
    this.$on("el.form.change", this.onFieldChange);
  }
},

例如当el-input元素在触发了blur事件时,会触发el-form-item组件上的el.form.blur事件。

其他表单组件类似。

js 复制代码
// el-input
handleBlur(event) {
  this.focused = false;
  this.$emit('blur', event);
  if (this.validateEvent) {
      // 触发el-form-item组件上的 'el.form.blur'事件
    this.dispatch('ElFormItem', 'el.form.blur', [this.value]);
  }
},

然后通过async-validator库对表单进行校验,并设置校验状态(validateState)和校验后的信息(validateMessage )。

js 复制代码
onFieldBlur() {
  this.validate("blur");
},
js 复制代码
validate(trigger, callback = noop) {
  this.validateDisabled = false;
  // 获取对应trigger的rules数组
  const rules = this.getFilteredRule(trigger);
  // 如果rules为空 并且required为false 则返回true
  if ((!rules || rules.length === 0) && this.required === undefined) {
    callback();
    return true;
  }

  this.validateState = "validating";

  const descriptor = {};
  // 将rules中的trigger属性删除
  if (rules && rules.length > 0) {
    rules.forEach((rule) => {
      delete rule.trigger;
    });
  }
  descriptor[this.prop] = rules;

  // 初始化async-validator
  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
      callback(this.validateMessage, invalidFields);

      // 在form组件中触发validate事件
      this.elForm &&
        this.elForm.$emit(
          "validate",
          this.prop,
          !errors,
          this.validateMessage || null
        );
    }
  );
},

如果校验状态为error就会显示错误信息。

html 复制代码
<div class="el-form-item__content" :style="contentStyle">
  <slot></slot>
  <transition name="el-zoom-in-top">
    <slot
      v-if="validateState === 'error' && showMessage && form.showMessage"
      name="error"
      :error="validateMessage"
    >
      <div
        class="el-form-item__error"
        :class="{
          'el-form-item__error--inline':
            typeof inlineMessage === 'boolean'
              ? inlineMessage
              : (elForm && elForm.inlineMessage) || false,
        }"
      >
        {{ validateMessage }}
      </div>
    </slot>
  </transition>
</div>

form组件的校验

form组件的校验首先要在form组件中添加要校验的表单项。

需要在form-item组件完成挂载时,即触发mounted钩子时,通过触发el.form.addField事件来添加。

js 复制代码
// form-item
mounted() {
  if (this.prop) {
    // 添加要校验的表单项
    this.dispatch("ElForm", "el.form.addField", [this]);
    ...
  }
},
js 复制代码
// form
created() {
  // 添加form-item
  this.$on("el.form.addField", (field) => {
    if (field) {
      this.fields.push(field);
    }
  });
},

form的校验是通过调用组件上的validate函数。

js 复制代码
validate(callback) {
  // 选用传model选项
  if (!this.model) {
    console.warn(
      "[Element Warn][Form]model is required for validate to work!"
    );
    return;
  }

  let promise;
  // 如果没有传递callback,或者callback不是函数,则返回promise
  if (typeof callback !== "function" && window.Promise) {
    promise = new window.Promise((resolve, reject) => {
      callback = function (valid, invalidFields) {
        valid ? resolve(valid) : reject(invalidFields);
      };
    });
  }

  let valid = true;
  let count = 0;
  // 如果需要验证的fields为空,调用验证时立刻返回callback
  if (this.fields.length === 0 && callback) {
    callback(true);
  }
  let invalidFields = {};
  this.fields.forEach((field) => {
    // 调用form-item组件中的validate方法
    // message对应的是校验信息
    // field对应的是校验错误的表单项
    field.validate("", (message, field) => {
      if (message) {
        valid = false;
      }
      invalidFields = objectAssign({}, invalidFields, field);
      // 如果callback是函数并且校验的次数和表单项的数量相同
      // 则调用callback
      if (
        typeof callback === "function" &&
        ++count === this.fields.length
      ) {
        callback(valid, invalidFields);
      }
    });
  });

  // 如果promise存在,则返回promise
  if (promise) {
    return promise;
  }
},

重置

js 复制代码
// form
resetFields() {
  if (!this.model) {
    console.warn(
      "[Element Warn][Form]model is required for resetFields to work."
    );
    return;
  }
  // 调用form-item的重置方法
  this.fields.forEach((field) => {
    field.resetField();
  });
},
js 复制代码
// form-item
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;
  // 重置this.prop为初始值
  if (Array.isArray(value)) {
    prop.o[prop.k] = [].concat(this.initialValue);
  } else {
    prop.o[prop.k] = this.initialValue;
  }

  // 重置validateDisabled为false
  this.$nextTick(() => {
    this.validateDisabled = false;
  });

  this.broadcast("ElTimeSelect", "fieldReset", this.initialValue);
},

单个属性的校验和清除校验

js 复制代码
// form 单个属性的校验
validateField(props, cb) {
  props = [].concat(props);
  // 获取需要校验的fields
  const fields = this.fields.filter(
    (field) => props.indexOf(field.prop) !== -1
  );
  if (!fields.length) {
    console.warn("[Element Warn]please pass correct props!");
    return;
  }

  // 使用form-item组件中的validate方法实现校验
  fields.forEach((field) => {
    field.validate("", cb);
  });
},
js 复制代码
// form 清除校验结果
// 如果传递的是数组并且为空,则清除所有的表单校验
// 如果传递的是字符串则找到要清楚的fields
// 调用form-item上的clearValidate方法
clearValidate(props = []) {
  const fields = props.length
    ? typeof props === "string"
      ? this.fields.filter((field) => props === field.prop)
      : this.fields.filter((field) => props.indexOf(field.prop) > -1)
    : this.fields;
  fields.forEach((field) => {
    field.clearValidate();
  });
},
js 复制代码
// form-item
clearValidate() {
  this.validateState = "";
  this.validateMessage = "";
  this.validateDisabled = false;
},

自定义校验规则

自定义校验规则就是传入自定义的校验函数,符合async-validator的规则即可。

表单内组件尺寸控制

form组件或者form-item组件中设置size属性,添加对应的class

优先使用form-item上面的size属性。

通过设置line-height修改内部组件的大小。

html 复制代码
<!-- FormItem -->
<template>
  <div
    class="el-form-item"
    :class="[
      {
        'is-required': isRequired || required,
        'is-error': validateState === 'error',
      },
      sizeClass ? 'el-form-item--' + sizeClass : '',
    ]"
  >
js 复制代码
// form-item computed 
_formSize() {
  return this.elForm.size;
},
elFormItemSize() {
  return this.size || this._formSize;
},
// 确定使用的form size
sizeClass() {
  return this.elFormItemSize || (this.$ELEMENT || {}).size;
},

总结

以上实现了一个基础版本的form组件,有写的不对地方,欢迎大家在评论区提出问题。

相关推荐
小远yyds11 分钟前
前端Web用户 token 持久化
开发语言·前端·javascript·vue.js
吕彬-前端1 小时前
使用vite+react+ts+Ant Design开发后台管理项目(五)
前端·javascript·react.js
学前端的小朱1 小时前
Redux的简介及其在React中的应用
前端·javascript·react.js·redux·store
guai_guai_guai1 小时前
uniapp
前端·javascript·vue.js·uni-app
bysking2 小时前
【前端-组件】定义行分组的表格表单实现-bysking
前端·react.js
王哲晓2 小时前
第三十章 章节练习商品列表组件封装
前端·javascript·vue.js
fg_4113 小时前
无网络安装ionic和运行
前端·npm
理想不理想v3 小时前
‌Vue 3相比Vue 2的主要改进‌?
前端·javascript·vue.js·面试
酷酷的阿云3 小时前
不用ECharts!从0到1徒手撸一个Vue3柱状图
前端·javascript·vue.js
微信:137971205873 小时前
web端手机录音
前端