調研
最近收到了一個任務,需要實作一個動态的表單。效果如下:
- 圖一
- 圖二
「主要邏輯」:第一個下拉框的資料能決定第二個下拉框的内容,第二個下拉框的内容是遠端加載的;第三個下拉框的内容能決定後續控件的形式;後面的加号是增加行,括号是增加括号,删除是删除這一行。最後的檢索式,是整體表單計算出來的。
「分析」:這個表單邏輯和關聯都比較複雜和頻繁,而且有些還是遠端擷取資料,并不是寫死的。如果說用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文檔寫得其實不是很明朗」
作者:李仲軒
連結:https://juejin.cn/post/7312646198094086185