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

前言

这一篇我们继续用Vue2实现一个经典需求:动态表单。动态表单的核心逻辑就是使用component

ini 复制代码
<component :is="cutomComponent" />

它将根据customComponent的值动态地渲染组件。

需求分析

一个表单通常由两部分组成:

  1. 表单组件:输入框、单选框、下拉框等;
  2. 表单信息:输入框的名称、单选框的名称和选项等

基础组件

目录结构

这些是我们需要提前在项目中实现的基础组件:

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数据,可以描述不同的组件数据、组件设置数据。

页面设计

  1. 表单预览,用于预览动态生成的表单
  2. 表单组件,逐个通过基础组件创建所需的表单控件
  3. 交互,将创建好的表单控件拖拽到表单预览区域,生成动态的预览表单

代码实现

结合以上两点,我们可以开始写代码了:

输入框

展示组件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

小结

到这里我们已经能够实现一个动态表单的大致结构了,以上代码可以渲染并且展示了,只是对于一个完整的功能来说,还需要再进一步地开发。且看下一篇

相关推荐
訾博ZiBo21 分钟前
【Vibe Coding】001-前端界面常用布局
前端
软件技术NINI21 分钟前
MATLAB疑难诊疗:从调试到优化的全攻略
javascript·css·python·html
IT_陈寒24 分钟前
《Redis性能翻倍的7个冷门技巧,90%开发者都不知道!》
前端·人工智能·后端
歪歪10034 分钟前
React Native开发Android&IOS流程完整指南
android·开发语言·前端·react native·ios·前端框架
知识分享小能手36 分钟前
uni-app 入门学习教程,从入门到精通,uni-app组件 —— 知识点详解与实战案例(4)
前端·javascript·学习·微信小程序·小程序·前端框架·uni-app
ZYMFZ43 分钟前
python面向对象
前端·数据库·python
长空任鸟飞_阿康1 小时前
在 Vue 3.5 中优雅地集成 wangEditor,并定制“AI 工具”下拉菜单(总结/润色/翻译)
前端·vue.js·人工智能
lapiii3581 小时前
快速学完React计划(第一天)
前端·react.js·前端框架
苏打水com1 小时前
从 HTML/CSS/JS 到 React:前端进阶的平滑过渡指南
前端·javascript·html