思路
- 分析主要功能
- 分析所有功能点
- 拆分组件(单一原则)
- 设计组件API接口(props、event、slot)
- 将各个功能散发到各个组件(解耦)
- 构建关系(组件通信)
具体(通过Form表单来验证)
一、分析主要功能
用以收集、校验、提交数据。
二、分析所有功能点
- 单个表单数据的校验
- 所有表单数据的校验
- 清空所有表单数据
- 清空单个表单数据
三、拆分组件(单一原则)
- Form 作为表单控件,用来管理整体数据的收集提交。按照单一原则来领取 Form 组件功能。(所有表单数据的校验、清空所有表单数据)。
- 输入框、选择器、单选框、多选框等控件用来绑定数据,当数据改变之后需要去校验数据。按照单一原则不应该由表单内部来认领校验功能,如果校验给到表单,那么其他地方只是想用表单控件不需要校验数据。
- FormItem 作为表单控件的包裹层,用来分担校验功能。认领单个表单数据的校验、清空单个表单数据。
四、设计组件API
Form:作为数据收集和数据校验数据
一、props
- 数据:model
- 校验:rules
二、event:
- 不需要,是由用户主动调动,而不是触发事件返回给用户。
三、slot
- 作为 FormItem 的分发地
FormItem:作为校验单项表单
一、props
- 需要校验值:prop
二、event:
- 不需要,由 Form 组件主动调起。
三、slot
- 需要,作为最后一层,一方面是承载表单控件。
- 另一方面完成80%的功能,剩下20%留给用户。
五、设计用例
// 用例 <i-form :model="info" :rules="rules"> <i-form-item prop="username"> <i-input v-model="info.username"></i-input> </i-form-item> <i-form-item prop="password"> <i-input v-model="info.password"></i-input> </i-form-item> </i-form> data() { return { info: { username: '', password: '' }, rules: { username: [ {required: true, message: '用户名不能为空', trigger: 'blur'}, ], password: [ {required: true, message: '密码不能为空', trigger: 'blur'}, ] } } }
五、将各个功能散发到各个组件(解耦)
Form
- 整体校验:validates() {}
- 整体重置:resetFields() {}
FormItem
- 单个表单校验:validate() {}
- 单个表单重置:resetField() {}
六、构建关系(组件通信)
一、问题:
- 问题:FormItem 保存单前表单的校验,那么 Form 如何能够主动通知 FormItem 去校验当前表单数据。
- 解决:通过在组件渲染过程中,Form 去收集每一个 FormItem 的实例。
- 问题:FormItem 在校验数据过程中,如何获取到当前表单传入的数据和规则。
- 解决:通过依赖注入,FormItem 去获取 Form 的实例,来获取数据和规则。
- 问题:FormItem 如何知道自己什么时候进行校验数据。
- 解决:表单控件(Input)在数据改变之后去通知 FormItem 去进行校验一次数据。
实践
带着上面的思路来分析下面的 form 表单
// App.js
<i-form ref="form" :model="info" :rules="rules">
<i-form-item prop="username">
<i-input v-model="info.username"></i-input>
</i-form-item>
<i-form-item prop="password">
<i-input v-model="info.password"></i-input>
</i-form-item>
</i-form>
data() {
return {
info: {
username: '',
password: ''
},
rules: {
username: [
{required: true, message: '用户名不能为空', trigger: 'blur'},
],
password: [
{required: true, message: '密码不能为空', trigger: 'blur'},
]
}
}
},
methods: {
handleSubmit() {
this.$refs.form.validate((valid) => {
if (valid) {
window.alert('成功');
} else {
window.alert('表单校验失败');
}
})
}
}
// Form.vue
<template>
<form>
<slot></slot>
</form>
</template>
<script>
export default {
name: 'iForm',
provide() {
return {
form: this
}
},
props: {
model: {
type: Object
},
rules: {
type: Object
}
},
beforeCreate() {
console.log('form-beforeCreate');
},
created() {
this.$on('on-form-item-add', (field) => {
if (field) this.fields.push(field);
})
this.$on('on-form-item-remove', (field) => {
if (field.prop) {
this.fields.splice(this.fields.indexOf(field), 1);
}
})
},
methods: {
resetFields() {
this.fields.forEach(field => {
field.resetFields();
})
},
validate(callback) {
return new Promise(resolve => {
let valid = true;
let count = 0;
this.fields.forEach(field => {
field.validate('', errors => {
if (errors) {
valid = false;
}
if (++count === this.fields.length) {
// 全部完成
resolve(valid);
if (typeof callback === 'function') {
callback(valid);
}
}
})
})
})
}
},
data() {
return {
fields: []
}
},
mounted() {
}
}
</script>
<style>
</style>
// formItem.vue
<template>
<div>
<label v-if="label" :class="{ 'i-form-item-label-required': isRequired }">{{label}}</label>
<div>
<slot></slot>
<div v-if="validateState === 'error'" class="i-form-item-message">{{ validateMessage }}</div>
</div>
</div>
</template>
<script>
import AsyncValidator from 'async-validator';
import Emitter from '../../mixins/emitter';
export default {
name: 'iFormItem',
mixins: [ Emitter ],
inject: [
'form'
],
props: {
label: {
type: String,
default: ''
},
// username 还需要拿到对应的值
prop: {
type: String
}
},
// 组件渲染时,将实例缓存在 Form 中
mounted() {
if (this.prop) {
this.dispatch('iForm', 'on-form-item-add', this);
this.initialValue = this.fieldValue;
this.setRules;
}
},
beforeDestroy() {
this.dispatch('iForm', 'on-form-item-remove', this);
},
data() {
return {
validateState: '',
validateMessage: '',
isRequired: false
}
},
computed: {
// 从 Form 的 model 中动态得到当前表单组件的数据
fieldValue() {
return this.form.model[this.prop];
}
},
methods: {
setRules() {
let rules = this.getRules();
if (rules.length) {
rules.every((rule) => {
// 如果当前校验规则中有必填项,则标记出来
this.isRequired = rule.required;
})
}
this.$on('on-form-blur', this.onFieldBlur);
this.$on('on-form-change', this.onFieldChange);
},
onFieldBlur() {
console.log('onFieldBlur');
// 接收到改变之后进行校验
this.validate('blur');
},
onFieldChange() {
console.log('onFieldChange');
this.validate('change');
},
// 从 Form 的 rules 属性中,获取当前 FormItem 的校验规则
getRules() {
let formRules = this.form.rules;
formRules = formRules ? formRules[this.prop] : [];
return [].concat(formRules || []);
},
// 只支持 blur 和 change, 过滤出符合要求的 rule 规则
getFilteredRule (trigger) {
const rules = this.getRules();
return rules.filter(rule => !rule.trigger || rule.trigger.indexOf(trigger) !== -1);
},
// 重置数据
resetField() {
this.validateState = '';
this.validateMessage = '';
this.form.model[this.prop] = this.initialValue;
},
validate(trigger, callback = function() {}) {
// 交给数据校验 看看是不是符合
// 查看类型
let rules = this.getFilteredRule(trigger);
if (!rules || rules.length === 0) {
return true;
}
// 设置状态
this.validateState = 'validating';
// 调用别人库来进行校验
let descriptor = {};
descriptor[this.prop] = rules;
const validator = new AsyncValidator(descriptor);
let model = {};
model[this.prop] = this.fieldValue;
console.log(model);
validator.validate(model, { firstFields: true }, errors => {
this.validateState = !errors ? 'success' : 'error';
this.validateMessage = errors ? errors[0].message : '';
callback(this.validateMessage);
})
}
}
}
</script>
<style>
.i-form-item-label-required:before {
content: '*';
color: red;
}
.i-form-item-message {
color: red;
}
</style>
// input.vue
<template>
<input
type="text"
:value="currentValue"
@input="handleInput"
@blur="handleBlur">
</template>
<script>
import Emitter from '../../mixins/emitter';
export default {
name: 'iInput',
mixins: [ Emitter ],
props: {
value: {
Type: String,
default: ''
}
},
data() {
return {
currentValue: this.value
}
},
watch: {
value(newVal) {
this.currentValue = newVal;
}
},
methods: {
handleInput(event) {
const value = event.target.value;
this.currentValue = value;
this.$emit('input', value);
this.dispatch('iFormItem', 'on-form-change', value);
},
handleBlur() {
this.dispatch('iFormItem', 'on-form-blur', this.currentValue);
}
}
}
</script>
<style>
</style>
// emitter.js
function broadcast(componentName, eventName, params) {
this.$children.forEach(child => {
const name = child.$options.name;
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) {
let parent = this.$parent || this.$root;
let name = parent.$options.name;
while (parent && (!name || name !== componentName)) {
parent = parent.$parent;
if (parent) {
name = parent.$options.name;
}
}
if (parent) {
parent.$emit.apply(parent, [eventName].concat(params));
}
},
broadcast(componentName, eventName, params) {
broadcast.call(this, componentName, eventName, params);
}
}
};