Vue2实践(3)之用component做一个动态表单(二)
前言
在上一篇中,我们已经通过<component>
实现了组件的动态渲染,为这个动态表单功能定下框架。
在这篇中,我们将着重实现功能。
功能实现
在上一篇中,我们定下了由设置组件来制作具体组件的方案,我们先来完善这一功能——从设置组件中获取完整的具体组件信息。
在这里我选用SelectInputSetting
来做例子
<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>
设置组件与预览表单的数据交互
分析
目前在设置组件SelectInputSetting
中,通过setting
收集用户输入,已经能够得到一份“下拉框组件定义”数据;
接下来,只要把这份数据传递到“表单预览”中,即可。所以我们需要实现它们之间的数据交互,通常来说有许多方案,但是考虑到用户操作性,数据交互可以通过:点击、拖拽等交互实现。在这里我们选用“拖拽”交互。
实现拖拽交互
实现拖拽交互,需要使用浏览器提供的一些API。
SelectInputSetting.vue
<template>
<div>
<TextInput draggable="true" :field="labelField" v-model="setting.name" @dragstart="dragstart" />
<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: '',
},
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);
},
// 开始拖动事件
dragstart(e) {
const dataStr = JSON.stringify(this.setting);
e.dataTransfer.setData('application/json', dataStr);
}
}
}
</script>
draggable标识
: 应用于HTML元素,用于标识元素是否允许使用浏览器原生行为或HTML 拖放操作 API拖动。true时元素可以被拖动。
dragstart事件
: dragstart
事件在用户开始拖动元素或被选择的文本时调用。
通过HTML的拖放API,我们将数据传递通过event
进行传递。
DynamicForm.vue
<template>
<div class="container">
<div class="main-area" @drop="addComponent" @dragover.prevent>
<!-- 表单预览域 -->
<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: [],
}),
methods: {
addComponent(e) {
e.preventDefault(); // drop事件必须阻止默认行为
const dataStr = e.dataTransfer.getData('application/json');
const data = JSON.parse(dataStr);
data.id = Date.now().toString(); // 添加一个唯一标识用于diff
this.fields.push(data);
}
}
}
</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>
drop事件
: 事件在元素或文本选择被放置到有效的放置目标上时触发。为确保 drop
事件始终按预期触发,应当在处理 dragover
事件的代码部分始终包含 preventDefault()
调用。
dragover.prevent
: 只有在 dragenter
和 dragover
事件中调用了 event.preventDefault()
,浏览器才会允许元素的拖放操作。
通过HTML的拖放API,我们将数据传递通过event
进行接接收。
功能完善
-
SelectInputSetting
中,选项删除按钮在当前选项hover时才出现
<style lang="scss" scoped>
.option {
button {
visibility: hidden;
}
&:hover {
button {
visibility: visible;
}
}
}
</style>
使用visibility
属性的修改只会触发重绘,使用display
实现的话会触发重排。这个跟使用v-show
还是v-if
的问题相似;
使用css元素实现而不是vue指令,在于css更好控制:hover
。
- 组件设置完成后应该有保存选项来进行锁定,避免误操作
<!-- TextInput -->
<template>
<div class="text-input-container">
<label :for="field.name" class="text-input-label">{{ field.name }}:</label>
<input class="text-input" type="text" :placeholder="field.placeholder" :value="value"
@input="$emit('input', $event.target.value)" :required="field.required" :readonly="disabled" />
</div>
</template>
<script>
export default {
props: ['field', 'disabled', 'value'],
}
</script>
<!-- SelectInputSetting -->
<template>
<div :class="{'drag':isFinished}" :draggable="isFinished" @dragstart="dragstart">
<TextInput :disabled="isFinished" :field="labelField" v-model="setting.name" />
<!-- 这里如果不使用item.key而是使用index,会因为节点复用导致显示错误 -->
<div class="option" v-for="(item, index) in setting.options" :key="item.key">
<TextInput :disabled="isFinished" class="option-content" :field="item" v-model="setting.options[index].value" />
<button v-show="!isFinished" @click="deleteOption(index)">删除</button>
</div>
<button v-show="!isFinished" @click="addOption">添加选项</button>
<button v-show="!isFinished" @click="finish">完成</button>
</div>
</template>
<script>
import TextInput from './TextInput.vue';
export default {
components: {
TextInput
},
data: () => ({
labelField: {
name: '选项名称',
placeholder: '请输入选项名称',
value: '',
},
setting: {
id: '',
editor: 'SelectInputSetting',
type: 'SelectInput',
name: '',
value: '',
options:[],
optionCount: 0
},
isFinished: false,
}),
methods: {
addOption() {
this.setting.options.push({
name: '选项内容',
placeholder: '请输入选项名称',
value: '',
key: this.setting.optionCount++
})
},
deleteOption(index) {
this.setting.options.splice(index, 1);
},
dragstart(e) {
const dataStr = JSON.stringify(this.setting);
e.dataTransfer.setData('application/json', dataStr);
},
finish() {
this.isFinished = true;
},
}
}
</script>
<style lang="scss" scoped>
.drag {
&:hover {
cursor: all-scroll; // 修改鼠标样式,更符合移动组件的暗示
}
}
</style>
小结
至此我们已经完成了一个相对简单的动态表单组件。能从中体会组件的设计思想、代码组织,并且了解到一些具体实现需要调用的API。
接下来我们还将继续实现类似的有趣实践——导航栏