实现一个简版的el-form
结合源码,实现form
组件的核心内容,达到学习源码的目的。
组件分为两部分,一部分是form
组件,一部分是form-item
组件。
文章源码
代码路径
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--inline
的class
。
使label
和content
以inline-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组件,有写的不对地方,欢迎大家在评论区提出问题。