前言
这一篇我们继续用Vue2实现一个经典需求:动态表单。动态表单的核心逻辑就是使用component
ini
<component :is="cutomComponent" />
它将根据customComponent
的值动态地渲染组件。
需求分析
一个表单通常由两部分组成:
- 表单组件:输入框、单选框、下拉框等;
- 表单信息:输入框的名称、单选框的名称和选项等
基础组件
目录结构
这些是我们需要提前在项目中实现的基础组件:
bash
├── src/
│ ├── components/
│ │ ├── FieldTypes/
│ │ │ ├── SelectInput.vue # 下拉框组件
│ │ │ ├── SelectInputSetting.vue # 下拉框设置组件
│ │ │ ├── TextInput.vue # 输入框组件
│ │ │ └── TextInputSetting.vue # 输入框设置组件
│ │ └── DynamicForm.vue # 动态表单
数据
表单组件通常是以一种通用的形式存在,而自定义的表单信息则需要通过其它方式混合到表单组件中以达成我们的目的。同时考虑到信息的可读性(通用的结构)、易维护(前后端交互),我们采用json来设计并存储相关信息:
组件数据
csharp
// 组件加载时所需数据
// TextInputJson
{
name: '文本框', // 名称
placeholder: '',
value: ''
}
// SelectInputJson
{
name: '下拉框',
placeholder: '',
value: '',
options: [ // 下拉框特有,下拉选项
{ 'value': 'option-1', 'name': '选项-1' },
]
}
go
// 使用<component>调用组件时,需要添加一个字段来标记组件类型
// TextInputJson
{
type: 'TextInput', // 组件类型
name: '',
placeholder: '',
value: ''
}
// SelectInputJson
createField: {
type: 'SelectInput',
name: '下拉框',
placeholder: '',
value: '',
options: [
{ value: 'option-1', name: '选项-1' },
]
}
组件设置数据
go
// TextInputSettingJson
{
id: '', // 控件id,唯一标识,用于正确diff
editor: 'TextInputSetting', // 对应的组件编辑器名称
type: 'TextInput',
name: '',
value: '', // 默认值
}
// SelectInputSettingJson
{
id: '',
editor: 'SelectInputSetting',
type: 'SelectInput',
name: '',
value: '',
options:[
{
name: '选项-1',
value: 'option-1',
key: '', // 通过optionCount得到,选项标识。添加选项时进行标记;删除时也需要通过它触发正确的diff
}
],
}
如上所示,通过这样大同小异的json数据,可以描述不同的组件数据、组件设置数据。
页面设计
- 表单预览,用于预览动态生成的表单
- 表单组件,逐个通过基础组件创建所需的表单控件
- 交互,将创建好的表单控件拖拽到表单预览区域,生成动态的预览表单
代码实现
结合以上两点,我们可以开始写代码了:
输入框
展示组件TextInput
ini
<template>
<div class="text-container">
<label :for="field.name" class="text-input-label">{{ field.name }}:</label>
<input
class="text-input"
type="text"
:placeholder="field.placeholder"
:value="field.value"
@input="$emit('input', $event.target.value)"
/>
</div>
</template>
<script>
export default {
props: ['field'],
}
</script>
设置组件TextInputSetting
xml
<template>
<div>
<TextInput :field="labelField" v-model="setting.name" />
</div>
</template>
<script>
import TextInput from './TextInput.vue';
export default {
components: {
TextInput
},
data: () => ({
labelField: {
name: '输入框名称',
placeholder: '请输入输入框名称',
value: '',
},
setting: {
id: '',
editor: 'TextInputSetting',
type: 'TextInput',
name: '',
},
})
}
</script>
下拉框
展示组件SelectInput
xml
<template>
<div class="select-container">
<label :for="field.name">{{ field.name }}:</label>
<select :value="field.value" @change="$emit('input', $event.target.value)">
<option v-for="option in options" :key="option.value" :value="option.value">
{{ option.name || option.value }}
</option>
</select>
</div>
</template>
<script>
export default {
props: ['field'],
computed: {
options() {
return this.field.options || [];
}
}
}
</script>
设置组件SelectInputSetting
xml
<template>
<div>
<TextInput :field="labelField" v-model="setting.name" />
<div class="option" v-for="(item, index) in setting.options" :key="item.key">
<TextInput class="option-content" :field="item" v-model="setting.options[index].value" />
<button @click="deleteOption(index)">删除</button>
</div>
<button @click="addOption">添加选项</button>
</div>
</template>
<script>
import TextInput from './TextInput.vue';
export default {
components: {
TextInput
},
data: () => ({
labelField: {
name: '选项名称',
placeholder: '请输入选项名称',
value: '', // 通过之前的源码文档,我们得知初始的object其中的属性是响应式的
},
setting: {
id: '',
editor: 'SelectInputSetting',
type: 'SelectInput',
name: '',
value: '',
options:[],
optionCount: 0, // 内部自增标识
},
}),
methods: {
addOption() {
this.setting.options.push({
name: '选项内容',
placeholder: '请输入选项名称',
value: '',
key: this.setting.optionCount++
})
},
deleteOption(index) {
this.setting.options.splice(index, 1); // 通过之前的源码文档,我们得知vue通过劫持数组原型方法实现数组响应式,splice就是其中之一
},
}
}
</script>
DynamicForm
基础实现
xml
<template>
<div class="container">
<div class="main-area">
<!-- 表单预览域 -->
<div class="form-title">
<TextInput :field="titleField" />
</div>
<div class="form-content" v-for="(item) in fields" :key="item.id">
<component class="form-component" :is="item.type" :field="item" />
</div>
</div>
<div class="sidebar">
<!-- 表单组件域 -->
<SelectInput v-model="componentValue" :field="createField" />
<div>
<component class="form-component" :is="componentValue" />
</div>
</div>
</div>
</template>
<script>
import TextInput from './FieldTypes/TextInput.vue';
import TextInputSetting from './FieldTypes/TextInputSetting.vue';
import SelectInput from './FieldTypes/SelectInput.vue';
import SelectInputSetting from './FieldTypes/SelectInputSetting.vue';
export default {
components: {
TextInput,
TextInputSetting,
SelectInput,
SelectInputSetting
},
data: () => ({
titleField: {
name: '表单名称',
placeholder: '请输入表单名称',
value: ''
},
componentValue: '',
createField: {
name: '选择要创建的组件',
placeholder: '',
value: '',
options: [
{ 'value': 'TextInputSetting', 'name': '文本框' },
{ 'value': 'SelectInputSetting', 'name': '下拉单选框' },
]
},
fields: [],
})
}
</script>
<style lang="scss" scoped>
.container {
display: flex;
border: 2px solid #000;
padding: 10px;
}
.main-area {
flex-grow: 4;
margin-right: 10px;
padding: 0 10px;
border: 2px solid #000;
border-radius: 10px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: flex-start;
.form-title {
width: auto;
text-align: center;
margin-bottom: 8px;
}
.form-content {
border-radius: 10px;
padding: 8px;
width: 90%;
border: 1px solid #ccc; // 默认边框
.form-component {
width: 300px;
}
&:hover {
border: 1px solid #ccc;
cursor: all-scroll;
}
}
}
.sidebar {
display: flex;
flex-direction: column;
border: 2px solid #000;
border-radius: 10px;
padding: 10px;
flex-grow: 1;
.form-component {
border: 1px solid #555;
border-radius: 10px;
padding: 8px;
margin-bottom: 10px;
}
label {
margin-bottom: 5px;
}
input {
margin-top: 10px;
padding: 5px;
border: 1px solid #000;
border-radius: 5px;
}
}
</style>
交互实现
TODO
小结
到这里我们已经能够实现一个动态表单的大致结构了,以上代码可以渲染并且展示了,只是对于一个完整的功能来说,还需要再进一步地开发。且看下一篇