Vue中一个复杂表单的处理(子表单嵌套)
本次需求是一个比较复杂的数据录入表单(如下图所示),分成5个步骤,其中1,2,4,5步骤的子表单是比较简单的输入和选择,最复杂的是步骤3:分为6种组织类型,每种类型可以增(上限10个)删改其下的组织(公司),每个组织(公司)下可以增删改联系人(上限20个),同时需要对表单做即时验证,包括必填字段,组织(公司)名称判重,联系人判重...
本次需求是一个比较复杂的数据录入表单(如下图所示),分成5个步骤,其中1,2,4,5步骤的子表单是比较简单的输入和选择,最复杂的是步骤3:分为6种组织类型,每种类型可以增(上限10个)删改其下的组织(公司),每个组织(公司)下可以增删改联系人(上限20个),同时需要对表单做即时验证,包括必填字段,组织(公司)名称判重,联系人判重…
开发之前,思考一下几个问题:
-
父子组件传值props是单向数据流吗?
-
浅拷贝和深拷贝的区别?
-
多级子组件的验证如何往父级回归
-
多级子组件的数据如何往父级回归
-
promise.all 的作用是什么?
具体实现
首先,对整个表单进行拆分,按上图,可以拆成多个子组件:5个子表单组件 + 组织类型组件 + 组织(公司)组件 + 联系人组件 + 其它功能模块组件。
组织(公司)这一块,组织类型中循环嵌套公司组件,公司中再循环嵌套联系人组件… 听起来比较复杂,其实不然~ 想象我们每次只用考虑一个简单的表单组件,无论是增删改,还是获取表单数据,表单验证,都是很普通的代码。此处要考虑的就是两点:
- 父组件如何拿到子组件最新的值(子组件如何把值回传给父组件)
- 父组件如果获得子组件的验证结果
在下方贴出的代码中展示了这两点的实现方式。
我们可以这样认为,这一个大表单中层层嵌套的组件都差不多,除去针对各个表单自己的业务逻辑模块代码,剩下得就是值的传递和验证结果的传递。
回答开头抛出的几个问题
-
父子组件传值props是单向数据流吗?
Vue官方明确说明props的传递是单向数据流,
所有的 prop 都使得其父子 prop 之间形成了一个单向下行绑定:父级 prop 的更新会向下流动到子组件中,但是反过来则不行。这样会防止从子组件意外变更父级组件的状态,从而导致你的应用的数据流向难以理解。
往下阅读代码,你会发现,每个子组件都在改变父组件form中的值,但是并么有报错,这是不是违反了Vue关于props单项数据流的规定?继续阅读官方文档
注意在 JavaScript 中对象和数组是通过引用传入的,所以对于一个数组或对象类型的 prop 来说,在子组件中改变变更这个对象或数组本身将会影响到父组件的状态。
利用prop是对象类型,我们在此模仿实现出“双向数据流”,这为本次复杂表单的开发提供了很大的便利。虽然我们可以完全遵循单向数据量的规定,不直接在子组件修改prop的值,但这样就会增加一些复杂度。说到这里,可以继续看下一个问题。
-
浅拷贝和深拷贝的区别?
子组件直接获取prop是浅拷贝,所以我们修改子组件form的值,父组件会同时变更。如果我们想让prop不被子组件修改,就需要用深拷贝。在此我们会根据实际需要,灵活使用浅拷贝或者深拷贝。
-
多级子组件的验证如何往父级回归?
每个组件的验证单独写在组件中,如果需要在提交时回归到父组件,子组件中的验证方法应返回promise,这样在this.$refs.step1.validateForm() 调用子组件中的方法,一层一层往上返回即可。
在此可以使用async,await简化我们的代码。
-
多级子组件的数据如何往父级回归?
如果我们使用深拷贝prop的值,就要额外的写一个方法,让父组件调用后获取子组件的数据,原理和验证的回归类似。
-
promise.all 的作用是什么?
Promise.all可以将多个Promise实例包装成一个新的Promise实例。同时,成功和失败的返回值是不同的,成功的时候返回的是一个结果数组,而失败的时候则返回最先被reject失败状态的值。
父组件会同时调用多个子组件的异步验证方法,我们需要保证所有子组件验证都完成才能交给父组件,这是就需要用promise.all。
顺表提一下promise.race
顾名思义,Promse.race就是赛跑的意思,意思就是说,Promise.race([p1, p2, p3])里面哪个结果获得的快,就返回那个结果,不管结果本身是成功状态还是失败状态。
整个模块代码太多,有不少是业务逻辑代码,故此处只贴出部分代码演示组件间传值
父表单 form.vue
<template>
<div>
<!--此处有省略-->
<step-form1 v-if="form1.id" v-show="active==0" ref="step1" :options="options" :form="form1" />
<step-form2 v-if="form2.id" v-show="active==1" ref="step2" :form="form2" />
<step-form3 v-if="form3.id" v-show="active==2" ref="step3" :options="options" :form="form3" />
<step-form4 v-if="form4.id" v-show="active==3" ref="step4" :options="options" :form="form4" />
<step-form5 v-if="form5.id" v-show="active==4" ref="step5" :options="options" :form="form5" />
<!--此处有省略-->
<el-button type="primary" :loading="btnLoading" @click="submitForm">{{ btnLoading ?'正在提交':'直接提审' }} </el-button>
</div>
</template>
// ...
data(){
return {
// ...
postForm: {},
form: { webid: '', tracking: '', arraigned: 0, pid: '', exStatus: '' },
form1: { id: '', title:'',postTime:'', description:'' },
form2: { id: '', content: '', progress: '' },
form3: { id: '', companys: [] },
form4: { id: '', tag: '', type:'' },
form5: { id: '', editor:'', status:'' },
// res1,res2。。。用于接收子表单数据
res1: {}, res2: {}, res3: {}, res4: {}, res5: {},
validResult: [], // 整个表单的验证结果
// ...
}
},
created(){
// ... 初始化表单的值,把回显的数据分给子表单的postForm
this.setFormData(this.postForm, this.form1)
this.setFormData(this.postForm, this.form2)
this.setFormData(this.postForm, this.form3)
this.setFormData(this.postForm, this.form4)
this.setFormData(this.postForm, this.form5)
// ...
},
methods:{
// ...
setFormData(res, form) {
for (const key in form) {
form[key] = res[key]
}
},
// ...
// 获取子表单的数据
async getPostData() {
const res1 = this.$refs.step1.postForm
const res2 = this.$refs.step2.postForm
const res4 = this.$refs.step4.postForm
const res5 = this.$refs.step5.postForm
const companys = await this.$refs.step3.getData()
await this.$nextTick(() => {
this.postForm = Object.assign(res1, res2, companys, res4, res5)
})
},
// 表单提交
submitForm() {
const v0 = this.$refs.form.validate()
const v1 = this.$refs.step1.validateForm() // 调用子表单中的validateForm方法
const v2 = this.$refs.step2.validateForm()
const v3 = this.$refs.step3.validateForm()
const v4 = this.$refs.step4.validateForm()
const v5 = this.$refs.step5.validateForm()
// 使用子表单的验证结果
Promise.all([v1, v2, v3, v4, v5, v0]).then((res) => {
this.validResult = res
if (res.every(val => val)) {
this.btnLoading = true
this.getPostData().then(res => {
updateProject({ ...this.postForm, ...this.form }).then(res => {
this.btnLoading = false
if (res.code === 1) {
this.$message({
message: '操作成功',
type: 'success'
})
}
}).catch(e => {
this.btnLoading = false
})
})
} else {
this.$message({
message: '请填写完整后再提交!',
type: 'error'
})
}
})
}
// ...
}
表单1,2,4,5部分类似,此处展示form2.vue完整代码
<template>
<el-form ref="postForm" :model="postForm" :rules="rules" size="small" label-width="120px" class="demo-ruleForm">
<el-form-item label="详情" prop="content">
<el-input v-model="postForm.content" type="textarea" :rows="5" show-word-limit maxlength="1000" />
</el-form-item>
<el-form-item class="item-notice" label="进展情况" prop="progress">
<el-input v-model="postForm.progress" type="textarea" :rows="5" show-word-limit maxlength="1000" />
</el-form>
</template>
<script>
export default {
name: 'StepForm2',
props: {
form: {
type: Object,
default: () => {}
}
},
data() {
return {
postForm: { },
rules: {
content: [
{ required: true, message: '请填写', trigger: 'blur' }
],
progress: [
{ required: true, message: '请填写', trigger: 'blur' }
],
}
}
},
created() {
this.postForm = this.form
},
methods: {
// form2的验证,通过 this.$refs.step2.validateForm() 被父组件调用
async validateForm() {
return this.$refs['postForm'].validate().then(valid => {
return valid
}).catch(e => {
return false
})
}
}
}
</script>
form3.vue
<template>
<!--此处有省略-->
<div v-for="group in postForm.companyGroups" :key="group.key">
<contact-unit v-if="group.units.length" ref="group" :data="group" :options="options" />
</div>
<!--此处有省略-->
</template>
// ...
import ContactUnit from './ContactUnit'
// ...
data(){
return {
postForm: {
companyGroups: [
{ key: 1, label: '组织类型1', units: [] },
{ key: 2, label: '组织类型2', units: [] },
{ key: 3, label: '组织类型3', units: [] },
{ key: 4, label: '组织类型4', units: [] },
{ key: 5, label: '组织类型5', units: [] },
{ key: 6, label: '组织类型6', units: [] }
]
},
}
},
methods:{
// 因为要传回符合后端接口的数据,这里对数据做一些处理
getData() {
const temp = this.postForm.companyGroups
const res = []
temp.forEach((val, key) => {
if (val.units && val.units.length) {
val.units.forEach(company => {
res.push({
type: key,
...company
})
})
}
})
return { companys: res }
},
// 收集子组件的验证结果
async validateForm() {
if (!(this.postForm.companyGroups[0].units && this.postForm.companyGroups[0].units.length)) {
this.errMsg = '请至少添加一个单位'
this.$message({
message: '请至少添加一个主单位',
type: 'danger'
})
return false
} else {
this.errMsg = ''
const result = []
this.postForm.companyGroups.forEach((val, key) => {
const el = this.$refs['group'][key]
el && result.push(el.validate())
})
return Promise.all(result).then((res) => {
return !res.some(val => !val)
})
}
},
}
// ...
ContactUnit.vue
<template>
<div>
<el-collapse-item :title="`${item.label} (${item.units.length})`" :name="item.key">
<contact-company
v-for="(unit, uIndex) in item.units"
:key="uIndex"
ref="company"
:data="unit"
:type="item.type"
:index="uIndex"
:options="options"
@remove="removeCompany"
/>
</el-collapse-item>
</div>
</template>
<script>
import ContactCompany from './ContactCompany'
export default {
name: 'ContactUnit',
components: { ContactCompany },
props: {
data: {
type: Object,
default: () => {}
},
options: {
type: Object,
default: () => []
}
},
data() {
return {
item: null
}
},
created() {
this.item = this.data
},
methods: {
removeCompany(index) {
this.item.units.splice(index, 1)
},
async validate() {
const result = []
this.item.units.forEach((val, key) => {
const el = this.$refs['company'][key]
el && result.push(el.validate())
})
return Promise.all(result).then((res) => {
return !res.some(val => !val)
})
}
}
}
</script>
ContactCompany.vue, ContactLinkman.vue和ContactUnit.vue内容差不多,就不再贴代码了。
更多推荐
所有评论(0)