本文有以下几个内容
- 为什么要封装一个form组件
- 封装的实现原理及参照
- 如何发布到npm
开发之前需要掌握的知识
- 一点点 vue组件通讯方式 provide/inject 和
$dispatch/$broadcase(自己实现)方法 - 一点点 递归的知识
- 一点点 事件绑定机制
- 一点点 $on 和 $emit 知识
- 一点点 $attr和$listen 知识 怀疑我在为一点点代言🤣🤣🤣🤣
为什么要封装一个form组件
因为在某个老项目中,用到了mintui这个ui框架,看着小伙伴写的表单校验异常痛苦,到了我这里,觉得不能忍了,就去研究了一下并写了组件。研究对象为 element-ui,对开发element-ui的大佬深深佩服,其中大部分代码是element-ui的源码搬运过来的。如果你们项目用的框架有form组件,本文可以不看呀。
封装的实现原理及参照
我们需要封装 form、form-item组件,而且form组件传入model、rules等字段。form-item组件传入 prop、required、label等字段,支持 v-model,然后 rules中支持async-validator库的方法。同时我们需要写一些校验不通过的样式。ok,以上就是我们这次需要实现的目标。后面的代码都是mintui框架来编写。
编写mixins,实现组件通讯,自定义dispatch和broadcase方法,
function broadcast(componentName, eventName, params) {
this.$children.forEach(child => {
var name = child.$options.componentName;
if (name === componentName) {
child.$emit.apply(child, [eventName].concat(params));
} else {
broadcast.apply(child, [componentName, eventName].concat([params]));
}
});
}
export default {
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));
}
},
broadcast(componentName, eventName, params) {
broadcast.call(this, componentName, eventName, params);
}
}
};
form组件
- props
props: {
model: {
type: Object,
},
rules: {
type: Object,
},
validateOnRuleChange: {
type: Boolean,
default: true,
},
},
- 注入 form自身,方便
slot中使用
provide() {
return {
MintForm: this,
};
},
data() {
return {
fields: [],
};
},
- 声明
fields变量来收集要校验的field,初始化的时候监听添加删除操作
data() {
return {
fields: [],
};
},
created() {
this.$on("mint.form.addField", (field) => {
if (field) {
this.fields.push(field);
}
});
this.$on("mint.form.removeField", (field) => {
if (field.prop) {
this.fields.splice(this.fields.indexOf(field), 1);
}
});
},
- methods中定义
validate方法,提交时校验表单
validate(callback) {
if (!this.model) {
console.warn(
"[Element Warn][Form]model is required for validate to work!"
);
return;
}
let promise;
// if no callback, 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) => {
field.validate("", (message, field) => {
if (message) {
valid = false;
}
invalidFields = objectAssign({}, invalidFields, field);
if (
typeof callback === "function" &&
++count === this.fields.length
) {
callback(valid, invalidFields);
}
});
});
if (promise) {
return promise;
}
},
form-item 组件
- html
<div class="mint-form-item" :class="[{'is-error': validateState === 'error'}]">
<div class="mint-form-item__label" v-if="label">{{label}}</div>
<div class="mint-form-item-content">
<slot></slot>
<transition name="mint-zoom-in-top">
<slot v-if="validateState === 'error'" name="error" :error="validateMessage">
<div class="mint-form-item__error">{{validateMessage}}</div>
</slot>
</transition>
</div>
</div>
- props
props: {
prop: String,
error: String,
required: {
type: Boolean,
default: undefined,
},
label: String,
},
data中声明校验信息validateMessage和校验状态validateState。
data() {
return {
validateMessage: "",
validateState: "",
};
},
mixins,混入 dispath 方法,通知 form 组件增加/删除监听
mixins: [emitter],
methods中编写form-item的校验方法validate,使用async-validator。监听事件触发的类型的方法addValidateEvents,并在mounted中去调用。
methods: {
validate(trigger, callback = () => {}) {
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
);
}
);
},
addValidateEvents() {
const rules = this.getRules();
if (rules.length || this.required !== undefined) {
this.$on("mint.form.blur", this.onFieldBlur);
this.$on("mint.form.change", this.onFieldChange);
}
},
// ...
},
mounted(){
if (this.prop) {
this.dispatch("mintForm", "mint.form.addField", [this]);
let initialValue = this.fieldValue;
if (Array.isArray(initialValue)) {
initialValue = [].concat(initialValue);
}
Object.defineProperty(this, "initialValue", {
value: initialValue,
});
this.addValidateEvents();
}
}
ok,大功告成,我们已经初步实现了form和form-item,来试一下。
<mt-form ref="form" :model="formModel" :rules="formRules">
<mt-form-item label="测试" prop="testProp">
<mt-field v-mode="formModel.name"></mt-field>
</mt-form-item>
</mt-form>
<mt-button @click="submit">default</mt-button>
submit() {
this.$refs.form.validate((res) => {
console.log(res);
// 这里可以得出是否校验通过
});
}
已经可以通过提交来判断表单是否校验通过了,但是我们还无法解决blur事件和chage事件,从代码中来看,我们监听了mint.form.blur 和 mint.form.change这两个事件,
// form-item 中监听的
this.$on("mint.form.blur", this.onFieldBlur);
this.$on("mint.form.change", this.onFieldChange);
需要在对应的组件中去触发他们。那我们开始吧
blur事件,一般是input,这里我们就对mintui的field进行改造。当你以为只是一个简单的改造时,你发现field的组件,竟然没有注册blur事件!!!这里用了一个原生的写法,手动的给它增加了一个blur事件。
<div>
<mt-field ref="input" v-bind="$attrs" v-on="$listeners" @change="handChange" />
</div>
export default {
name: "mint-input",
mixins: [emitter],
methods: {
injectionInputFn() {
this.$nextTick(() => {
const inputInstance = this.$refs.input;
const inputInstanceDom = inputInstance.$refs.input;
const that = this;
inputInstanceDom.onblur = function (event) {
inputInstance.$emit("blur", event);
that.dispatch("mintFormItem", "mint.form.blur", [this.value]);
};
});
},
handChange(val) {
this.dispatch("mintFormItem", "mint.form.change", [val]);
},
},
mounted() {
this.injectionInputFn();
},
};
change事件,常规操作,目前封装radio和checklist了。
// radio
<template>
<div>
<mt-radio v-bind="$attrs" v-on="$listeners" @change="handChange"></mt-radio>
</div>
</template>
<script>
import emitter from "../../mixins/emitter";
export default {
name: "mt-form-radio",
mixins: [emitter],
methods: {
handChange(val) {
this.$emit("change", val);
this.dispatch("mintFormItem", "mint.form.change", [val]);
},
},
};
</script>
// checklist
<template>
<div>
<mt-checklist v-bind="$attrs" v-on="$listeners" @change="handChange"></mt-checklist>
</div>
</template>
<script>
import emitter from "../../mixins/emitter";
export default {
name: "mt-form-checklist",
mixins: [emitter],
methods: {
handChange(val) {
this.$emit("change", val);
this.dispatch("mintFormItem", "mint.form.change", [val]);
},
},
};
</script>
ok,搞完之后,我们已经可以正常使用一个form和form-item来做业务了。快速迭代,持续修复!
如何发布到npm
我是看了这个文章来实现的
基于vue-cli3创建libs库。按照步骤来即可。唯一需要注意的地方是,打包出来后,css文件和js文件是分开的。可以通过阅读文档 vue构建lib来进行设置不分开。不过一般不推荐这种做法,我们可以在使用的时候进行引入。
import mintuiform from 'mintuiform'
import 'mintuiform/lib/common.css'
Vue.use(mintuiform)
注意事项
- 一定要注入
mintui,然后再使用form。 form-item的label和mintui组件的title有些冲突,最好用label。mintui的样式有点怪,建议使用方重写样式,覆盖。form组件在组件内覆盖了mintui的样式,不影响全局,只针对于本身组件。 写的丑陋,就不放github地址了😂😂😂😂