# 表单系统(低代码表单)

## 低代码表单

实现三个目标

* 根据一个特定的数据自动生成表单
* 获取填写数据并支持校验功能
* 能自定义属性作用到input等原始组件上

首先讲讲总体思想，所谓的低代码表单就是后端传递一个特殊结构的对象数组，然后确定根据这个对象数组来自动生成表单，这个对象结构可以是这样的:

```jsx
interface FormItem {
	value: "" // 预填
	config: {  // 配置
		field: 0 // 字段类型
		validator:  '[0-9]{2}' // 正则校验
		SelectOptions: ['男','女'] // 选项
		placeholder: "xxx"  // 需要透传到原生元素节点的html属性
	}
}
```

然后我们定义好每个原子组件，比如Input组件、Select组件等，然后利用vue动态组件来动态地选择渲染哪个组件。在这个过程中，我们定义Form组件，作为一个整体的表单组件，它会接收前面说的对象数组并负责迭代渲染每一个具体的组件，而在Form组件到原生元素节点之间建立一个Field组件，来做为中间层进行一些处理，例如数据处理。

按这种设计思路，组件数据流就是这样的：

```jsx
  Form
		|
 	Filed
	  |
	原生节点
```

**如何传递html属性到原生表单元素上？**

通过$attrs透传config对象

**如何获取每个表单子项的数据到Form组件中并实现数据校验？**

在Field组件监听onChange事件可以获取数据，并通过`emit(’update:modelValue’)` 将数据传递到Form表单组件中。这样的优点是能够实时地获取数据，在某些时候会很有用，但是缺点也有很多：

1. 对一些表单元素无效
2. 如果用户不输入任何值，则传递到Form组件的值为空，而不是预填值

因此我们还需要做另外一个方案来弥补这些缺点，并且还能实现校验。

这个方案的思路是通过`defineExpose`暴露一个校验函数，函数校验成功则返回输入数据，否则返回提示信息。

在用户点击表单提交时可以利用此校验函数进行校验，如果想用户每输入一个表单项就校验的话可以监听`onBlur`事件。

#### 简易版本完整代码：

```jsx
// App.vue
<template>
  <div>
    <div>
      <h3>操作区</h3>
      <div  v-for="(item, index) in operators" :key="index">
        <span>{{FieldMap[item.config.field]}}</span>
        <button @click="addField(item)">添加</button>
      </div>
    </div>
    <Form :formData="formData" v-model="formValue" />
  </div>
</template>
<script setup>
import { reactive, watch, ref } from 'vue';
import { FILES_MAP } from './form/field/fields';
import Form from './form/Form.vue';

const FieldMap = reactive(FILES_MAP)

const operators = [
  {
    value: '10',  // 预填数值
    config: {
      field: 0, // 类型
      type: 'number',   // 透传
      placeholder: "手机号码",  // 透传
      validator:  '[0-9]{2}'   // 校验
      
    }
  },
  {
    value: '女',
    config: {
      field: 1,
      placeholder: "性别",
      SelectOptions: ["男", "女"],
      
    }
  }
]

const formData = reactive([])
const formValue = ref([])  // 表单各项输入值

watch(formValue, (val)=>{
  console.log(val);  // 观察数值是否异常
})

function addField(item){
  formData.push(item)
}

</script>
```

```jsx
// Form.vue
<template>
    <div>
				// 遍历渲染
        <Field v-for="(field, index) in formData" :key="index" :field="field" v-model="formValue[index]" ref="fieldRefs" />
        <button @click="submit">提交</button>
    </div>
</template>
<script setup>
import { reactive, ref } from 'vue';
import Field from './field/Field.vue';

const emit = defineEmits(['update:modelValue'])

const props = defineProps({
    formData: {
        type: Array,
        default: ()=>[]
    }
})

// 数据的值，初始时使用预填值
const formValue = reactive(props.formData.map(item => item.value))
// 在v-for中获取所有的ref
const fieldRefs = ref([])

// 提交时进行校验
function submit(){
    const validators = fieldRefs.value.map((fieldRef)=> fieldRef.validate)
    Promise.all(validators.map(validate=> validate())).then(res=>{
        formValue.splice(0, formValue.length)
        formValue.push(...res)
        console.log('res',res);
        emit('update:modelValue', formValue)
    }).catch(err=>{
        alert(err)
    })
}
</script>
```

Field组件，包裹元素节点组件，在这里通过动态组件渲染特定的表单元素组件，另外也负责将数据回传会Form组件中。

```jsx
// Field.vue
<template>
    <div>
        <component :is="filedCom" v-bind="field.config" :value="field.value" @change="onChange" ></component>
    </div>
</template>
<script setup>
import { computed, toRef,ref  } from '@vue/reactivity';
import { FIELDS_COMPONENTS, FILES_MAP } from './fields';

// v-model 绑定
const emit = defineEmits({
    "update:modelValue": String
})

const props = defineProps(['field'])

const filed = toRef(props, 'field')
// 用户输入的数值，初始化时使用预填值
const value = ref(props.field.value)

// 根据field字段选择组件
const filedCom = computed(()=> FIELDS_COMPONENTS[FILES_MAP[filed.value.config.field]])

function onChange(e){
    value.value = e.target?.value || ''
    emit("update:modelValue", value.value)
}

// 定义校验函数，校验函数不仅仅起到校验作用，还要传递数据
async function validate(){
    const validator = filed.value.config?.validator
    if(validator){
        if(new RegExp(validator).test(value.value)){
            return value.value
        }else{
            return Promise.reject("错误提示消息")
        }
    }

    return value.value
}
defineExpose({validate})
</script>
```

field文件，保存组件映射。

```jsx
// field.js
import InputCom from "../components/input.vue"
import SelectCom from "../components/select.vue"
// 表单组件映射
export const FIELDS_COMPONENTS = {
    'INPUT': InputCom,
    "SELECT": SelectCom
}
// field字段映射，和后端约定好有哪些类型
export const FILES_MAP = {
    0: 'INPUT',
    1: 'SELECT'
}
```

原生表单组件，可以通过css美化样式，可以根据不同的元素类型做一些特定的处理。

```jsx
// input.vue
<template>
    <input />
</template>

// select.vue
<template>
    <select>
        <option v-for="item in $attrs.SelectOptions" :key="item">{{
                item
        }}</option>
    </select>
</template>
```

#### 实现无限嵌套

实现无限嵌套是指表单子项里嵌套着另一个表单，例如新加一个表格类型，表格类型可以渲染输入框、多选框，甚至也可以再渲染表格类型。

实现的思路也不难，首先约定好表格类型的field值，假设是3，然后我们在config字段里加上一个新字段fields，它只在表格类型时有效，fields实际上也是一个FormItem对象数组，然后fields数组中又可以嵌套表格类型，如此形成无限嵌套。

在组件上，可以利用递归处理这种嵌套情况，可以递归Form表单组件，也可以递归Field字段组件。具体这里不展开讲

#### 小结

上面的实现比较简单，只实现了文本输入框和多选框，实际中要处理的情况更复杂，不过有了思路后面的组件扩展也比较简单。

需要注意的是，defineExpose暴露函数时，函数的返回值如果是reactive或者ref，那么就会将响应式传递到父组件中，这不符合单向数据流的思想，并且会导致项目变得难以维护，最好是先消除响应式再传递数据，简单的解决思路是：

```jsx
return data.value //ref
return JSON.parse(JSON.stringify(toRaw(data))) // reactive
```

toRaw函数会返回reactive的原始对象，但是不建议直接操作原始对象，最好使用深拷贝拷贝出一个新的对象进行操作，最简单的深拷贝就是使用JSON。更多细节见Vue组件通讯

## 动态表单

所谓的动态表单，是指在多表单系统（一个页面多个表单）中，用户输入某个字段达到一定条件时可以影响另一个表单，举个例子，例如第一个表单的select选项框选择男时，另一个表单的输入框placeholder显示`先生你好`，如果是女则显示`女生你好`。

实现起来并不难，因为我们前面已经实现了Form组件的`v-model` ，**这里需要注意的是设计思路应该是自上而下的，因为单向数据流更容易维护。**

解决思路是在两表单上增加一个逻辑层，然后监听表单的数据，当达到要求条件时操作`FormItem.config`配置对象来实现改变表单结构的目的。

```jsx
// App.js
<Form :formData="formData" v-model="formValue" />
<Form :formData="formData" v-model="formValue" />
// Field中监听change事件，当change时也向上传数据
<Field v-for="(field, index) in formData" :key="index" :field="field" @change="submit"
            v-model="formValue[index]" ref="fieldRefs" />

// App.js
const formData = reactive([{
  value: '',
  config: {
    field: 1,
    placeholder: "性别",
    SelectOptions: ["男", "女"],

  }
}])
const formValue = ref([])

const formData2 = reactive([{
  value: '',  // 预填数值
  config: {
    field: 0, // 类型
    type: 'number',
    placeholder: "手机号码",  // 透传
    validator: '[0-9]{2}'

  }
},])
const formValue2 = ref([])

watch(formValue, ([val]) => {
  if(val === '男'){
     formData2[0].config.placeholder = "先生你好"
  }else {
    formData2[0].config.placeholder = "女士你好"
  }
})
```

## 代码示例

[PlayCode - Javascript Playground](https://playcode.io/945711)


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://1425816423.gitbook.io/my-knowledge-base/jing-yan-ji-lu/jing-yan-zong-jie/biao-dan-xi-tong-di-dai-ma-biao-dan.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
