龙空技术网

Vue3引入Formily实践实录

爱摸鱼的程序员 1806

前言:

此时大家对“react select联动”大致比较重视,小伙伴们都想要了解一些“react select联动”的相关资讯。那么小编同时在网络上收集了一些关于“react select联动””的相关知识,希望我们能喜欢,你们一起来学习一下吧!

调研

最近收到了一个任务,需要实现一个动态的表单。效果如下:

图一图二

「主要逻辑」:第一个下拉框的数据能决定第二个下拉框的内容,第二个下拉框的内容是远程加载的;第三个下拉框的内容能决定后续控件的形式;后面的加号是增加行,括号是增加括号,删除是删除这一行。最后的检索式,是整体表单计算出来的。

「分析」:这个表单逻辑和联动都比较复杂和频繁,而且有些还是远程获取数据,并不是写死的。如果说用elementplus的组件来做,处理逻辑、联动和数据回显的时候都会比较麻烦,肯定不简洁。我之前有做过一个小型的复杂表单联动,有过这种手动处理逻辑和联动的麻烦经历。所以我就决定不使用之前的方式处理现在的业务,需要寻找一个新的方式来解决这个需求。

「方案」:在这里我就省去了找解决方案的过程,直接说答案,最后是决定使用Formily。原因有几个:

大厂出品:后续有不会的地方,能在网上搜得到生态丰富:涵盖了vue和react,elementui、elementplus、antd等不同的版本了解

我此次负责的项目的技术栈是Vue3,所以我主要去了解了以下几个模块:

Fromily主站Formily Vue核心库Formily elementplus「主站文档」首先是对Formily进行了解释,说明了Formily这个产品的定位和功能,其次的「场景案例」以及「进阶指南」的代码实例非常友好(案例使用react写的,使用vue的时候有一些差别)。读完此文档后对于Formily有了一定的了解;除此之外,主站中的API内容在后续实践中比较重要,也需要熟悉。「Vue核心库」中,讲解了核心架构以及核心概念,核心概念中最重要的是三种开发模式:Template 开发模式该模式主要是使用 Field/ArrayField/ObjectField/VoidField 组件

<template>  <FormProvider :form="form">    <Field name="input" :component="[Input, { placeholder:'请输入' }]" />  </FormProvider></template><script>  import { Input } from 'ant-design-vue'  import { createForm } from '@formily/core'  import { FormProvider, Field } from '@formily/vue'  import 'ant-design-vue/dist/antd.css'  export default {    components: { FormProvider, Field },    data() {      return {        Input,        form: createForm(),      }    },  }</script>

JSON Schema 开发模式

该模式是给 SchemaField 的 schema 属性传递 JSON Schema 即可

<template>   <FormProvider :form="form">     <SchemaField :schema="schema" />   </FormProvider> </template> <script>   import { Input } from 'ant-design-vue'   import { createForm } from '@formily/core'   import { FormProvider, createSchemaField } from '@formily/vue'   import 'ant-design-vue/dist/antd.css'   const { SchemaField } = createSchemaField({     components: {       Input,     },   })   export default {     components: { FormProvider, SchemaField },     data() {       return {         form: createForm(),         schema: {           type: 'object',           properties: {             input: {               type: 'string',               'x-component': 'Input',               'x-component-props': {                 placeholder: '请输入',               },             },           },         },       }     },   } </script>

Markup Schema 开发模式该模式算是一个对源码开发比较友好的 Schema 开发模式,同样是使用 SchemaField 相关组件。Markup Schema 模式主要有以下几个特点:主要依赖 SchemaStringField/SchemaArrayField/SchemaObjectField...这类描述标签来表达 Schema每个描述标签都代表一个 Schema 节点,与 JSON-Schema 等价SchemaField 子节点不能随意插 UI 元素,因为 SchemaField 只会解析子节点的所有 Schema 描述标签,然后转换成 JSON Schema,最终交给RecursionField渲染,如果想要插入 UI 元素,可以在 SchemaVoidField 上传x-content属性来插入 UI 元素

<template>  <FormProvider :form="form">    <SchemaField>      <SchemaStringField        x-component="Input"        :x-component-props="{ placeholder: '请输入' }"      />      <div>我不会被渲染</div>      <SchemaVoidField x-content="我会被渲染" />      <SchemaVoidField :x-content="Comp" />    </SchemaField>  </FormProvider></template><script>  import { Input } from 'ant-design-vue'  import { createForm } from '@formily/core'  import { FormProvider, createSchemaField } from '@formily/vue'  import 'ant-design-vue/dist/antd.css'  const SchemaComponents = createSchemaField({    components: {      Input,    },  })  const Comp = {    render(h) {      return h('div', ['我也会被渲染'])    },  }  export default {    components: { FormProvider, ...SchemaComponents },    data() {      return {        form: createForm(),        Comp,      }    },  }</script>

「Formily elementplus」

这个就类似于组件库,讲解具体组件如何使用。

熟悉

这个阶段熟悉了一些官网案例的用法。在三种使用模式中,最后选择了「JSON Schema」模式,这种模式组件看着更简洁,只需掌握配置规则。

运用因为 Element-Plus 是基于 Sass 构建的,如果你用 Webpack 配置请使用以下两个 Sass 工具

"sass": "^1.32.11","sass-loader": "^8.0.2"

安装

$ npm install --save element-plus$ npm install --save @formily/core @formily/vue @vue/composition-api @formily/element-plus

我的目录结构是这样:

核心的Formily代码在「filter.vue』中,JSON配置我提炼到了「form_obj.js」里

filter.vue:

<template>  <FormProvider :form="form" class="lkkkkkkk">    <SchemaField :schema="schema" :scope="{ useAsyncDataSource, loadData }" />    <div class="btn flex flex-right">      <div class="btn-inner">        <el-button type="primary" plain :disabled="valid" @click="saveFilter">保存</el-button>        <el-button type="primary" plain @click="resetFilter">重置</el-button>        <Submit plain @submit-failed="submitFailed" @submit="submit">查询</Submit>      </div>    </div>  </FormProvider></template><script setup>  import { createForm } from '@formily/core';  import { FormProvider, createSchemaField } from '@formily/vue';  import {    Submit,    FormItem,    Space,    Input,    Select,    DatePicker,    ArrayItems,    InputNumber,  } from '@formily/element-plus';  import conditionResult from './conditionResult.vue';  import { onMounted, ref } from 'vue';  import { action } from '@formily/reactive';  import { getFormObj, arrToText } from './form_obj';  import { setLocal, getLocal } from '@/utils';  const { SchemaField } = createSchemaField({    components: {      FormItem,      Space,      Input,      Select,      DatePicker,      ArrayItems,      InputNumber,      conditionResult,    },  });  const form = createForm();  const schema = ref();  const fieldMap = new Map();  const valid = ref(true);  const fieldCollect = (arr) => {    arr.forEach((item) => {      fieldMap.set(item.value, item);    });  };  // 模拟远程加载数据  const loadData = async (field) => {    const table = field.query('.table').get('value');    if (!table) return [];    return new Promise((resolve) => {      setTimeout(() => {        if (table === 1) {          const arr = [            {              label: 'AAA',              value: 'aaa',            },            {              label: 'BBB',              value: 'ccc',            },          ];          resolve(arr);          fieldCollect(arr);        } else if (table === 2) {          const arr = [            {              label: 'CCC',              value: 'ccc',            },            {              label: 'DDD',              value: 'ddd',            },          ];          resolve(arr);          fieldCollect(arr);        }      }, 1000);    });  };  // 远程数据处理  const useAsyncDataSource = (service) => (field) => {    field.loading = true;    service(field).then(      action.bound((data) => {        field.dataSource = data;        field.loading = false;      })    );  };  // 获取表数据  const getTables = () => {    return new Promise((resolve) => {      setTimeout(() => {        resolve([          { label: '就诊信息表', value: 1 },          { label: '诊断信息表', value: 2 },        ]);      }, 1000);    });  };  // 初始化表单  const initForm = async () => {    const tables = await getTables();    schema.value = getFormObj(tables);    const originFilter = getLocal('formily');    // 设置初始值或者是回显值    if (originFilter) {      form.setInitialValues(originFilter);      // form.setInitialValues({      //   array: [      //     {      //       table: 1,      //       field: '',      //       condition: 'contain',      //       text: '',      //       relationship: 'none',      //       bracket: 'none',      //     },      //   ],      //   escape: '',      // });    }  };  // 保存  const saveFilter = () => {    setLocal('formily', form.values);  };  // 重置  const resetFilter = () => {    form.setValues(getLocal('formily'));  };  // 查询  const submit = (values) => {    // 将数组转换成中文释义。    const sentence = arrToText(fieldMap, values.array);    console.log(sentence);    // 将值设置到检索式中    form.setValuesIn('escape', sentence);    valid.value = false;  };  const submitFailed = () => {    valid.value = true;  };  onMounted(() => {    initForm();  });</script>

form_obj.js:

...// Formily配置export function getFormObj(tables) {  return {    type: 'object',    properties: {      array: {        type: 'array',        'x-component': 'ArrayItems',        'x-decorator': 'FormItem',        title: '检索条件',        items: {          type: 'object',          properties: {            space: {              type: 'void',              'x-component': 'Space',              properties: {                sort: {                  type: 'void',                  'x-decorator': 'FormItem',                  'x-component': 'ArrayItems.SortHandle',                },                table: {                  type: 'string',                  title: '信息表',                  enum: tables,                  required: true,                  'x-decorator': 'FormItem',                  'x-component': 'Select',                  'x-component-props': {                    style: {                      width: '160px',                    },                  },                },                field: {                  type: 'string',                  title: '字段',                  required: true,                  // default: 1,                  // enum: [                  //   { label: '入院年龄', value: 1 },                  //   { label: '主要诊断', value: 2 },                  //   { label: '手术名称', value: 3 },                  // ],                  'x-decorator': 'FormItem',                  'x-component': 'Select',                  'x-component-props': {                    style: {                      width: '160px',                    },                  },                  'x-reactions': ['{{useAsyncDataSource(loadData)}}'],                },                condition: {                  type: 'string',                  title: '条件',                  required: true,                  //   default: 'contain',                  enum: conditionArr,                  'x-decorator': 'FormItem',                  'x-component': 'Select',                  'x-component-props': {                    style: {                      width: '130px',                    },                  },                },                text: {                  type: 'string',                  required: true,                  'x-decorator': 'FormItem',                  'x-component': 'Input',                  'x-reactions': [                    {                      dependencies: ['.condition'],                      fulfill: {                        state: {                          visible: "{{$deps[0] === 'contain'}}",                        },                      },                    },                  ],                  'x-component-props': {                    style: {                      width: '160px',                    },                    placeholder: '请选择',                  },                },                range: {                  type: 'object',                  properties: {                    space: {                      type: 'void',                      'x-component': 'Space',                      properties: {                        start: {                          type: 'number',                          required: true,                          'x-reactions': `{{(field) => {                            field.selfErrors =                              field.query('.end').value() <= field.value ? '左边必须小于右边' : ''                          }}}`,                          //   default: 1,                          'x-decorator': 'FormItem',                          'x-component': 'InputNumber',                          'x-component-props': {                            style: {                              width: '150px',                            },                            placeholder: '左临界数值',                          },                        },                        end: {                          type: 'number',                          required: true,                          //   default: 10,                          'x-decorator': 'FormItem',                          'x-component': 'InputNumber',                          'x-reactions': {                            dependencies: ['.start'],                            fulfill: {                              state: {                                selfErrors: "{{$deps[0] >= $self.value ? '左边必须小于右边' : ''}}",                              },                            },                          },                          'x-component-props': {                            style: {                              width: '150px',                            },                            placeholder: '右临界数值',                          },                        },                      },                    },                  },                  'x-reactions': [                    {                      dependencies: ['.condition'],                      fulfill: {                        state: {                          visible: "{{$deps[0] === 'range'}}",                        },                      },                    },                  ],                },                relationship: {                  type: 'string',                  title: '关系',                  required: true,                  //   default: '',                  enum: relationArr,                  'x-decorator': 'FormItem',                  'x-component': 'Select',                  'x-component-props': {                    style: {                      width: '160px',                    },                  },                },                bracket: {                  type: 'string',                  title: '括号',                  required: true,                  //   default: '',                  enum: bracketArr,                  'x-decorator': 'FormItem',                  'x-component': 'Select',                  'x-component-props': {                    style: {                      width: '130px',                    },                  },                },                copy: {                  type: 'void',                  'x-decorator': 'FormItem',                  'x-component': 'ArrayItems.Copy',                },                remove: {                  type: 'void',                  'x-decorator': 'FormItem',                  'x-component': 'ArrayItems.Remove',                },              },            },          },        },        properties: {          add: {            type: 'void',            title: '添加条目',            'x-component': 'ArrayItems.Addition',          },        },      },      // properties: {      // },      escape: {        type: 'string',        title: '检索式',        'x-component': 'conditionResult',        'x-decorator': 'FormItem',      },    },  };}...

此案例的完整代码,我放在我的github了,有需要自取。

延伸

完成这个案例之后,后续还有一个类似的表单需求,我本来是准备用这个来做的,但是这个需求是要放到IE上运行,所以我留了一个心眼。先写了一个小案例,测试了一下Vue2+elementui+Formily打包后在IE浏览器能否运行,最后发现是不可以。

后续再查资料中发现确实是不兼容IE的,大家在使用的时候要考虑这个场景。

总结

以上就是在Vue3中引入Formily解决需求的过程,经历了调研、了解、熟悉、运用的过程,Formily是一个比较好的表单处理工具,解决了表单联动、逻辑处理和回显的痛点,如果大家遇到此类需求,可以考虑一下使用这个工具,但是此工具不兼容IE的情况也要考虑进去。「吐槽一句就是,Formily文档写得其实不是很明朗」

作者:李仲轩

链接:

标签: #react select联动