laitimes

Vue3引入Formily实践实录

author:A programmer who loves to fish

Research

Recently received a task to implement a dynamic form. Here's what it looks like:

  • Figure 1
Vue3引入Formily实践实录
  • Figure II
Vue3引入Formily实践实录

"Main logic": the data of the first drop-down box can determine the content of the second drop-down box, the content of the second drop-down box is loaded remotely, the content of the third drop-down box can determine the form of the subsequent control, and the plus sign after it is to add a line, the parentheses are to add parentheses, and delete is to delete this line. The final search query is calculated from the overall form.

"Analysis": The logic and linkage of this form are complex and frequent, and some of them are still remotely obtained, not written dead. If you use ElementPlus components to do it, it will be more troublesome to deal with logic, linkage and data echo, and it will definitely not be concise. I've done a small complex form linkage before, and I've had this troublesome experience of manually handling logic and linkage. So I decided not to use the previous way to deal with the current business, and I needed to find a new way to solve this need.

"Solution": Here I don't have to go through the process of finding a solution, just say the answer, and finally decide to use Formily. There are several reasons for this:

  • Produced by a large factory: If there is a place that will not be followed up, it can be searched on the Internet
Vue3引入Formily实践实录
  • Rich ecosystem: Covering Vue and React, different versions such as ElementUI, ElementPlus, and Antd
Vue3引入Formily实践实录

understand

The technology stack of the project I am responsible for this time is Vue3, so I mainly learned about the following modules:

  • Fromily主站
  • Formily Vue核心库
  • Formily elementplus
  1. The "Master Documentation" first explains Formily, explaining the positioning and functions of Formily as a product, and secondly, the code examples of "Scenario Cases" and "Advanced Guides" are very friendly (the examples are written in React, and there are some differences when using Vue). After reading this document, you have a certain understanding of Formily, and in addition, the API content in the master site is more important in the subsequent practice and needs to be familiarized.
  2. In the "Vue Core Library", the core architecture and core concepts are explained, and the most important of the core concepts are three development models:
  • Template 开发模式
  • This pattern mainly uses the Field/ArrayField/ObjectField/VoidField components
<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 development pattern

该模式是给 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 开发模式
  • This mode is a relatively friendly schema development mode for source code development, and it also uses SchemaField-related components.
  • The Markup Schema pattern has the following features:
    • 主要依赖 SchemaStringField/SchemaArrayField/SchemaObjectField... 这类描述标签来表达 Schema
    • Each description tag represents a schema node, which is equivalent to JSON-Schema
    • SchemaField child nodes cannot insert UI elements at will, because SchemaField will only parse all schema description tags of child nodes, and then convert them into JSON Schema, and finally hand them over to RecursionField for rendering
<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>
           
  1. 「Formily elementplus」

This is similar to a component library that explains how to use specific components.

familiar

At this stage, I am familiar with the usage of some official website cases. Among the three usage modes, the "JSON Schema" mode was finally selected, which is a schema component that looks more concise and only needs to master the configuration rules.

wield

  • Because Element-Plus is built on top of Sass, if you're configuring with Webpack, use the following two Sass tools
"sass": "^1.32.11",
"sass-loader": "^8.0.2"
           
  • Installation
$ npm install --save element-plus
$ npm install --save @formily/core @formily/vue @vue/composition-api @formily/element-plus
           

My directory structure looks like this:

Vue3引入Formily实践实录

The core Formily code is in "filter.vue", and the JSON configuration I distilled into "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',
      },
    },
  };
}
...
           

The full code of this case, I put it on my github, and I need to pick it up.

stretching

After completing this case, there is a similar form requirement in the follow-up, which I was going to use to do, but this requirement is to be run on IE, so I kept an eye on it. I first wrote a small case, and tested whether Vue2+elementui+Forform could run in IE after packaging, and finally found that it was not.

Follow-up re-checking of the data found that it is indeed incompatible with IE, and everyone should consider this scene when using it.

summary

The above is the process of introducing Forform in Vue3 to solve the requirements, through the process of research, understanding, familiarization, and application, Forformily is a better form processing tool, which solves the pain points of form linkage, logical processing and echo, if you encounter such needs, you can consider using this tool, but this tool is not compatible with IE should also be considered. "The complaint is that the Forformily document is actually not very clear."

Author: Li Zhongxuan

Link: https://juejin.cn/post/7312646198094086185