此文章记录从调研、了解、熟悉、运用、延伸Formily的过程
调研
最近收到了一个任务,需要实现一个动态的表单。效果如下:
-
图一
-
图二
「主要逻辑」:第一个下拉框的数据能决定第二个下拉框的内容,第二个下拉框的内容是远程加载的;第三个下拉框的内容能决定后续控件的形式;后面的加号是增加行,括号是增加括号,删除是删除这一行。最后的检索式,是整体表单计算出来的。
「分析」:这个表单逻辑和联动都比较复杂和频繁,而且有些还是远程获取数据,并不是写死的。如果说用elementplus的组件来做,处理逻辑、联动和数据回显的时候都会比较麻烦,肯定不简洁。我之前有做过一个小型的复杂表单联动,有过这种手动处理逻辑和联动的麻烦经历。所以我就决定不使用之前的方式处理现在的业务,需要寻找一个新的方式来解决这个需求。
「方案」:在这里我就省去了找解决方案的过程,直接说答案,最后是决定使用Formily。原因有几个:
- 大厂出品:后续有不会的地方,能在网上搜得到
- 生态丰富:涵盖了vue和react,elementui、elementplus、antd等不同的版本
了解
我此次负责的项目的技术栈是Vue3,所以我主要去了解了以下几个模块:
-
「主站文档」首先是对Formily进行了解释,说明了Formily这个产品的定位和功能,其次的「场景案例」以及「进阶指南」的代码实例非常友好(案例使用react写的,使用vue的时候有一些差别)。读完此文档后对于Formily有了一定的了解;除此之外,主站中的API内容在后续实践中比较重要,也需要熟悉。
-
「Vue核心库」中,讲解了核心架构以及核心概念,核心概念中最重要的是三种开发模式:
-
Template 开发模式
该模式主要是使用 Field/ArrayField/ObjectField/VoidField 组件
html
<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 即可
html
<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 元素
html
<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 工具
json
"sass": "^1.32.11",
"sass-loader": "^8.0.2"
- 安装
ruby
$ 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:
html
<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:
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文档写得其实不是很明朗」
ok,就这样!下一篇文章再见