Vue2实践(3)之用component做一个动态表单(二)

前言

在上一篇中,我们已经通过<component>实现了组件的动态渲染,为这个动态表单功能定下框架。

在这篇中,我们将着重实现功能。

功能实现

在上一篇中,我们定下了由设置组件来制作具体组件的方案,我们先来完善这一功能------从设置组件中获取完整的具体组件信息。

在这里我选用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>

设置组件与预览表单的数据交互

分析

目前在设置组件SelectInputSetting中,通过setting收集用户输入,已经能够得到一份"下拉框组件定义"数据;

接下来,只要把这份数据传递到"表单预览"中,即可。所以我们需要实现它们之间的数据交互,通常来说有许多方案,但是考虑到用户操作性,数据交互可以通过:点击、拖拽等交互实现。在这里我们选用"拖拽"交互。

实现拖拽交互

实现拖拽交互,需要使用浏览器提供的一些API。

SelectInputSetting.vue
xml 复制代码
<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
xml 复制代码
<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: 只有在 dragenterdragover 事件中调用了 event.preventDefault(),浏览器才会允许元素的拖放操作。

通过HTML的拖放API,我们将数据传递通过event进行接接收。

功能完善

  • SelectInputSetting中,选项删除按钮在当前选项hover时才出现
xml 复制代码
<style lang="scss" scoped>
    .option {
        button {
            visibility: hidden;
        }
        &:hover {
            button {
                visibility: visible;
            }
        }
    }
</style>

使用visibility属性的修改只会触发重绘,使用display实现的话会触发重排。这个跟使用v-show还是v-if的问题相似;

使用css元素实现而不是vue指令,在于css更好控制:hover

  • 组件设置完成后应该有保存选项来进行锁定,避免误操作
xml 复制代码
<!-- 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>
xml 复制代码
<!-- 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。

接下来我们还将继续实现类似的有趣实践------导航栏

相关推荐
找不到工作的菜鸟几秒前
Three.js三大组件:场景(Scene)、相机(Camera)、渲染器(Renderer)
前端·javascript·html
定栓3 分钟前
vue3入门-v-model、ref和reactive讲解
前端·javascript·vue.js
专注API从业者7 分钟前
基于 Flink 的淘宝实时数据管道设计:商品详情流式处理与异构存储
大数据·前端·数据库·数据挖掘·flink
龙在天8 分钟前
H5开发,开发照相机,以及组件封装
前端
曼妥思15 分钟前
PosterKit:跨框架海报生成工具
前端·开源
binqian35 分钟前
【异步】js中异步的实现方式 async await /Promise / Generator
开发语言·前端·javascript
Jerry说前后端43 分钟前
Android 移动端 UI 设计:前端常用设计原则总结
android·前端·ui
熊猫钓鱼1 小时前
基于Trae CN与TrendsHub快速实现的热点百事通
前端·trae
LIUENG1 小时前
Vue3 响应式原理
前端·vue.js
讨厌吃蛋黄酥1 小时前
前端居中九种方式血泪史:面试官最爱问的送命题,我一次性整明白!
前端·css