介绍
vue-formula 是一个基于 vue3 + typescript + element-plus 开发的表单渲染器。
通过传入指定格式的 Schema 数据来正确渲染表单和处理表单项之间的联动,同时还可以通过配置依赖关系来进行表单项之间复杂联动。
目前支持的主要功能包含:
- 渲染:支持复杂表单的渲染,包含表单项渲染、容器渲染、列表渲染、容器列表嵌套渲染、深层次嵌套渲染
- 联动:1.支持表单项之间的基本联动:只读、必填、显示隐藏;2.通过表单项依赖(字段依赖)支持更复杂的数据联动
- 组件:提供内置表单组件、容器组件和列表组件,同时支持 VNode 和 slot 来渲染用户自己的组件
- 开发体验:提供 Typescript 类型提示
基本使用
依赖安装
npm i vue-formula
说明
如果在使用的过程中,声明 schema 的时候,正确指定了 component 却发现 componentProps 没有正确进行提示时,请检查 tsconfig.json 文件,将moduleResolution
修改为Node
// tsconfig.json
{
// ...
"compilerOptions": {
//...
"moduleResolution": "Node",
//...
}
// ...
}
Start Demo 创建一个简易表单
<template>
<BasicForm :schema='schema' :field-dependencies="dependencies" @submit='onSubmit' />
</template>
<script setup lang='ts'>
import { ref } from 'vue'
import { BasicForm, Field, type LayoutSchema, type Dependency } from 'vue-formula'
// layoutSchema
const schema = ref<LayoutSchema>({
username: {
type: 'field',
field: 'username',
component: Field.Input,
label: '用户名',
},
email: {
type: 'field',
field: 'email',
component: Field.Input,
label: '邮箱',
},
hobbies: {
type: 'field',
field: 'hobbies',
component: Field.Select,
label: '爱好',
componentProps: {
options: [
{label: 'Basketball',value: 1},
{label: 'Football',value: 2}
]
}
}
})
// fieldDependencies
const dependencies: Dependency[] = [
// when username's value equals to 'change-hobbies-options', change the bobbies options and update the modelValue of hobbies with empty string
{
dependent: 'username',
shouldUpdate: ({ values }) => values['username'] === 'change-hobbies-options',
valueGetter: () => Promise.resolve([
{label: 'Music',value: 3},
{label: 'Dance',value: 4}
]),
schemaValueUpdateList: [{
schemaPath: 'hobbies',
prop: 'componentProps.options',
formatterBeforeUpdate: (data) => Promise.resolve(data)
}],
modelValueUpdateList: [{
modelPath: 'hobbies',
formatterBeforeUpdate: () => Promise.resolve("")
}]
},
// when username's value changed, the modelValue of usernameCopy will follow the change
{
dependent: 'username',
shouldUpdate: ({ values }) => values['username'],
valueGetter: ({ values }) => Promise.resolve(values['username'] + '--copy-from-username'),
modelValueUpdateList: [
{
modelPath: 'email',
}
]
},
]
// submit handler
const onSubmit = ({ model }: any) => {
console.log(model)
}
</script>
表单项
目前支持的内置组件(For Now)
- Input(输入框)
- InputNumber(数字输入框)
- Checkbox(复选框)
- Radio(单选框)
- Switch(开关)
- Rate(评分)
- ColorPicker(颜色选择器)
- DatePicker(日期选择器)
- Slider(滑块)
- TimePicker(时间选择器)
- TimeSelect(时间下拉选择列表)
- Transfer(穿梭框)
- Select(下拉选项列表)
- Divider(分割线)
如何渲染一个表单项(三种方式)
1.使用内置组件
<script setup lang='ts'>
import { Field, BasicForm } from 'vue-formula'
import type { FieldSchema, LayoutSchema } from 'vue-formula/typings/src/index'
const usernameFieldSchema: FieldSchema = {
type: 'field', //固定,表单项为值field
field: 'username',
component: Field.Input,
componentProps: {}, //会自动推断Input可用的属性
label: 'Username'
}
const testSchema: LayoutSchema = {
username: usernameFieldSchema
}
</script>
<template>
<BasicForm :schema="testSchema"></BasicForm>
</template>
内置组件需要通过导入Field来进行使用,使用时会根据组件的类型提示相应的组件参数
2.使用 VNode 进行渲染
通过使用 render
属性替换 component
属性来使用 VNode 进行表单组件的渲染。
<script setup lang='ts'>
import { Field, BasicForm } from 'vue-formula'
import type {
FieldSchema,
LayoutSchema,
CallbackParams
} from 'vue-formula/typings/src/index'
import { ElInput } from 'element-plus'
const usernameFieldSchema: FieldSchema = {
type: 'field',
field: 'username',
label: '用户名',
render: ({ formModel, field }) => {
return h(ElInput, {
placeholder: '请输入',
modelValue: formModel[field],
onInput: (value: string) => {
console.log(value)
formModel[field] = value
}
})
}
}
const testSchema: LayoutSchema = {
username: usernameFieldSchema
}
</script>
<template>
<BasicForm :schema="testSchema"></BasicForm>
</template>
3.插槽进行渲染
通过使用 slot
属性替换 component
和 render
属性来使用 VNode 进行表单组件的渲染。
<script setup lang='ts'>
import { BasicForm } from 'vue-formula'
import type {
FieldSchema,
LayoutSchema,
} from 'vue-formula/typings/src/index'
const usernameFieldSchema: FieldSchema = {
type: 'field',
field: 'username',
label: '用户名',
slot: 'UsernameInput'
}
const testSchema: LayoutSchema = {
username: usernameFieldSchema
}
<script>
<template>
<BasicForm :schema="testSchema">
<template #UsernameInput="{ formModel, field }">
<ElInput v-model="formModel[field]" />
</template>
</BasicForm>
</template>
表单项数据结构(Schema 结构)
属性(property) | 说明(description) | 类型(type) | 是否为必填 |
---|---|---|---|
type | 类型,表单项固定为 field | field | true |
field | model 中对应的 key(required) | string | true |
component | 渲染的组件名称(上述的表单项) | string | false |
componentProps | 使用 component 渲染时,渲染的表单项组件对应的 props。例如 component 为 Input 时,componentsProps 的值与 ElementPlus ElInput 一致。 | GetFieldProps<FieldSchema['component']> | false |
render | 渲染函数,使用 vNode 渲染表单项组件 | (callbackParams: CallbackParams) => VNode | VNode[] | string | false |
slot | 插槽名称,使用插槽渲染组件 | string | false |
label | 文本标签 | string | false |
noLabel | 是否不显示文本标签 | string | false |
placeholder | 占位信息 | string | false |
subLabel | 次文本标签 | string | false |
helpMessage | 帮助信息 | string | false |
colProps | 多列布局 props,可参考 ElementPlus Col 组件参数(一致) | Partial<ColProps> | false |
formItemProps | ElFormItem 的对应的 props | Partial | false |
defaultValue | 默认值 | any | false |
valueField | v-model 绑定的值,默认为 modelValue | string | false |
rules | 校验规则 | FormItemRule[] | false |
dynamicRules | 动态校验规则 | (callbackParams: CallbackParams) => FormItemRule[] | false |
required | 必填 | boolean | (callbackParams: CallbackParams) => boolean | false |
disabled | 只读 | boolean | (callbackParams: CallbackParams) => boolean | false |
show | 显示(css 层面的显示隐藏) | boolean | (callbackParams: CallbackParams) => boolean | false |
ifShow | 显示(渲染层面的显示隐藏) | boolean | (callbackParams: CallbackParams) => boolean | false |
loading | 加载中 | boolean | (callbackParams: CallbackParams) => boolean | false |
容器
内置的容器
内置组件需要通过导入Container来进行使用,使用时会根据组件的类型提示相应的组件参数
目前内置的容器只有一个,为Container.Container
。后续会继续迭代
- Container(内置基础容器)
如何使用容器
<script setup lang='tsx'>
import { BasicForm, Container, Field } from 'vue-formula'
import type { FieldSchema, ContainerSchema, LayoutSchema } from 'vue-formula/typings/src/index'
// 容器内表单项
const usernameFieldSchema: FieldSchema = {
type: 'field',
field: 'username',
component: Field.Input,
label: 'Username'
}
//容器
const BasicInfoContainer: ContainerSchema = {
name: 'BasicInfo',
type: 'container', //固定,容器的type为值container
component: Container.Container,
componentProps: {}, //容器组件参数,会根据component的类型自动推断。但目前只有一个内置容器组件🤡
title: '基本信息',
properties: {
username: usernameFieldSchema
}
}
//表单schema
const testSchema: LayoutSchema = {
BasicInfo: BasicInfoContainer
}
</script>
<template>
<BasicForm :schema='testSchema' />
</template>
容器数据结构(Schema 结构)
属性(property) | 说明(description) | 类型(type) | 是否必填 |
---|---|---|---|
type | 容器类型,固定为 container | container | true |
name | 容器名称 | string | true |
component | 渲染的容器组件 | string | true |
componentProps | 容器组件参数 | Recordable | false |
properties | 容器内部渲染的表单项数据 | Recordable | true |
title | 标题 | string | false |
slots | 容器内部插槽 | string[] | false |
rowProps | 布局属性,与 ElementPlus ElRow 的 props 一致 | Partial | false |
colProps | 多列布局 props,可参考 ElementPlus Col 组件参数(一致) | Partial | false |
description | 描述 | string | false |
helpMessage | 帮助信息 | string | false |
disabled | 只读 | boolean | (callbackParams: CallbackParams) => boolean | false |
show | 显示(css 层面的显示隐藏) | boolean | (callbackParams: CallbackParams) => boolean | false |
ifShow | 显示(渲染层面的显示隐藏) | boolean | (callbackParams: CallbackParams) => boolean | false |
loading | 加载中 | boolean | (callbackParams: CallbackParams) => boolean | false |
Container(内置基础容器)的说明//TODO:
插槽
Container 内部提供了
headerTitle
、
headerTitleAfter
、
headerContentSeparator
这三个内置可选插槽。使用 slots
属性可以使用指定对应位置的插槽名称用于渲染对应插槽内容
<template>
<BasicForm :schema="testSchema">
<template #basicInfoHeaderTitle>
<h1>This is a custom header title</h1>
</template>
<template #basicInfoHeaderTitleAfter="{ disabled }">
<ElButton :disabled='disabled'>headerTitleAfterButton</ElButton>
</template>
<template #basicInfoHeaderContentSeparator>
<h1>This is a separator area between header and content </h1>
</template>
</BasicForm>
</template>
<script setup lang='ts'>
import type { LayoutSchema } from 'vue-formula/typings/src/index'
const testSchema: LayoutSchema = {
testContainer: {
name: 'testContainer',
type: 'container',
component: Container.Container,
title: '测试容器',
slots: {
headerTitle: 'basicInfoHeaderTitle',
headerTitleAfter: 'basicInfoHeaderTitleAfter',
headerContentSeparator: 'basicInfoHeaderContentSeparator'
},
properties: {
username: {
type: 'field',
field: 'username',
component: Field.Input,
label: '用户名'
}
}
}
}
</script>
props
props 在 schema 中的componentsProps
属性传入,Container 存在以下可选属性
属性(property) | 说明(description) | 类型(type) | 是否必填 |
---|---|---|---|
expandCollapseEnabled | 是否允许部分折叠展开 | boolean | false |
列表
内置的列表
内置列表需要通过导入List来进行使用,使用时会根据组件的类型提示相应的组件参数
目前内置的容器只有一个,为List.List
。后续会继续迭代
- List(内置基础列表)
如何使用列表
<template>
<BasicForm :schema='listSchema' />
</template>
<script setup lang='ts'>
import { BasicForm, List, Field } from 'vue-formula'
import type { LayoutSchema } from 'vue-formula/typings/src/index'
const listSchema: LayoutSchema = {
contact: {
type: 'list',
name: 'contact',
component: List.List,
componentProps: {
listItemContentColProps: {
span: 18
},
listItemButtonColProps: {
span: 6
}
},
title: '联系人列表',
items: {
contactName: {
type: 'field',
field: 'contactName',
component: Field.Input,
label: '联系人名称',
colProps: {
span: 12
},
},
contactEmail: {
type: 'field',
field: 'contactEmail',
component: Field.Input,
label: '联系人邮件',
colProps: {
span: 12
}
}
}
}
}
<script>
列表数据结构(Schema 结构)
属性(property) | 说明(description) | 类型(type) | 是否必填 |
---|---|---|---|
type | 列表类型,固定为 list | list | true |
name | 列表名称 | string | true |
component | 渲染的列表组件名称(对应上面列表组件中的 TableList) | string | true |
componentProps | 列表组件参数 | Recordable | false |
items | 列表内部渲染的表单项数据 | Recordable | true |
title | 标题 | string | false |
description | 描述 | string | false |
helpMessage | 帮助信息 | string | false |
rowProps | 布局属性,与 ElementPlus ElRow 的 props 一致 | Partial | false |
colProps | 多列布局 props,可参考 ElementPlus Col 组件参数(一致) | Partial | false |
min | 最小列表项个数 | number | false |
max | 最大列表项个数 | number | false |
disabled | 只读 | boolean | (callbackParams: CallbackParams) => boolean) | false |
show | 显示(css 层面的显示隐藏) | boolean | (callbackParams: CallbackParams) => boolean) | false |
ifShow | 显示(渲染层面的显示隐藏) | boolean | (callbackParams: CallbackParams) => boolean) | false |
loading | 加载中 | boolean | (callbackParams: CallbackParams) => boolean) | false |
List(内置基础列表)的说明
props
props 在 schema 中的componentsProps
属性传入,List 存在以下可选属性
属性(property) | 说明(description) | 类型(type) | 是否必填 |
---|---|---|---|
titleContentLayoutMode | 标题和内容区布局方式(水平或垂直) | 'vertical' | 'horizontal' | false |
subIndexTitleVisible | 带索引的子标题是否显示 | boolean | false |
isCopyButtonVisible | 复制按钮是否显示 | boolean | false |
areUpDownButtonsVisible | 上移下移按钮是否显示 | boolean | false |
isDeleteButtonVisible | 删除按钮是否显示 | boolean | false |
canDeleteLastOne | 只剩一个元素时是否允许删除 | boolean | false |
confirmBeforeDelete | 删除一个元素时是否需要确认框 | boolean | false |
listItemRowProps | 列表中的每一个列表项的行布局 | boolean | false |
listItemContentColProps | 列表中的每一个列表项的内容区的列布局 | boolean | false |
listItemButtonColProps | 列表中的每一个列表项的按钮区的列布局 | boolean | false |
listItemContentRowProps | 列表中的每一个列表项的内容区内的行布局 | boolean | false |
只读、必填、显示隐藏
只读
表单项(Field)、列表(List)、容器(Container)的 Schema 中存在 disabled 属性,用于将当前组件及其内部组件状态设置成只读。
只读可以是固定的,也可以动态设置
只读的优先级为:
父组件通过 props 传递的 disabled > Schema 上的 disabled 属性获取到的值 > Schema 上 componentProps 中的 disabled
import { BasicForm, List, Field } from 'vue-formula'
import type { LayoutSchema } from 'vue-formula/typings/src/index'
//测试Schema
const testSchema: LayoutSchema = {
controlField: {
type: 'field',
field: 'controlField',
component: Field.Input,
label: '控制字段'
},
username: {
type: 'field',
field: 'username',
component: Field.Input,
label: '用户名',
disabled: ({ formModel }) => formModel['controlField'] === 'disabled'
},
basicInfo: {
type: 'container',
name: 'basicInfo',
component: Container.Container,
properties: {
someField: {
type: 'field',
field: 'someField',
component: Field.Input,
label: '某个字段'
},
alwaysDisabledField: {
type: 'field',
field: 'alwaysDisabledField',
component: Field.Input,
label: '始终只读的表单项',
disabled: true
}
},
title: '基本信息',
disabled: ({ formModel }) => formModel['controlField'] === 'disabled'
},
contact: {
type: 'list',
name: 'contact',
component: List.List,
title: '联系人',
disabled: ({ formModel }) => formModel['controlField'] === 'disabled',
items: {
name: {
type: 'field',
field: 'username',
component: Field.Input,
label: '姓名',
disabled: ({ formModel }) => formModel['controlField'] === 'disabled'
},
age: {
type: 'field',
field: 'age',
component: Field.Input,
label: '年龄'
},
job: {
type: 'field',
field: 'job',
component: Field.Input,
label: '工作(年龄大于18岁才填写)',
disabled: ({ model }) => !(model['age'] >= 18)
}
}
}
}
根据上面 Schema 中的 disabled 可以生成一下几个条件:
- 控制字段 为 disabled 时,将 用户名、基本信息、联系人 更改成 只读(表单项控制表单项、表单项控制容器、表单项控制列表)
- 年龄字段 小于 18 时,工作字段 设置成 只读(各个列表项内部的控制)
- 始终只读的表单项 由于 disabled 字段设置了
false
,所以一直是只读的(静态值控制)
必填
必填仅仅是针对于表单项而言,即只有表单项才存在这个属性
必填逻辑上于只读的逻辑大体相同,是通过required
属性进行控制的。
import { BasicForm, List, Field } from 'vue-formula'
import type { LayoutSchema } from 'vue-formula/typings/src/index'
//测试Schema
const testSchema: LayoutSchema = {
controlField: {
type: 'field',
field: 'controlField',
component: Field.Input,
label: '控制字段'
},
username: {
type: 'field',
field: 'username',
component: Field.Input,
label: '用户名',
required: ({ formModel }) => formModel['controlField'] === 'required'
}
}
显示隐藏
显示隐藏与必填、只读的逻辑也大体相同,只不过区分了样式上的显示隐藏(show)和渲染上的显示隐藏(ifShow)
import { BasicForm, List, Field } from 'vue-formula'
import type { LayoutSchema } from 'vue-formula/typings/src/index'
//测试Schema
const testSchema: LayoutSchema = {
controlField: {
type: 'field',
field: 'controlField',
component: Field.Input,
label: '控制字段'
},
ifShowField: {
type: 'field',
field: 'isShowField',
component: Field.Input,
label: '被控字段-渲染上',
ifShow: ({ formModel }) => formModel['controlField'] === 'ifShow'
},
showField: {
type: 'field',
field: 'showField',
component: Field.Input,
label: '被控字段-样式上',
ifShow: ({ formModel }) => formModel['controlField'] === 'show'
}
}
表单项依赖
配置表单项之间的依赖关系可以处理表单项之间更加复杂的操作。
依赖的数据结构
属性(property) | 说明(description) | 类型(type) | 是否必填 |
---|---|---|---|
dependent | 依赖字段 | string | string[] | true |
handler | 处理函数(优先用这个,若存在会忽略下面的属性) | (params: DependencyCallbackParams) => void | false |
shouldUpdate | 是否需要更新 | (params: DependencyCallbackParams) => boolean | false |
valueGetter | 值的获取方法 | (params: DependencyCallbackParams) => Promise | false |
modelValueUpdateList | 对表单值进行更新操作的数组 | { modelPath: string | string[], formatterBeforeUpdate?: (value: any) => Promise }[] | false |
schemaValueUpdateList | 对布局数据进行更新操作的数组 | { schemaPath: string | string[],prop: string, formatterBeforeUpdate?: (value: any) => Promise}[] | false |
debounce | 防抖 | boolean | false |
threshold | 防抖间隔时间 | boolean | false |
demo
<script setup lang='ts'>
import { BasicForm, List, Field } from 'vue-formula'
import type { LayoutSchema, Dependency } from 'vue-formula/typings/src/index'
const testSchema: LayoutSchema = {
singer: {
type: 'field',
field: 'singer',
component: Field.Select,
label:'歌手',
componentProps: {
options: [{
label: '周杰伦',
value: '周杰伦'
},{
label: 'Kanye',
value: 'Kanye'
}]
}
},
album: {
type: 'field',
field: 'album',
component: Field.Select,
label:'专辑',
componentProps: {
options: []
}
}
}
//使用handler函数
const testDependenciesInHandlerFunc: Dependency[] = [
{
dependent: 'singer',
handler({ formModel, setSchemaByPath }) {
const singer = formModel['singer']
let options = [] as any
if (singer) {
if (singer === '周杰伦') {
options = [
{
label: 'JAY',
value: 'JAY'
},
{
label: '范特西',
value: '范特西'
},
{
label: '叶惠美',
value: '叶惠美'
},
{
label: '跨时代',
value: '跨时代'
}
]
} else if (singer === 'Kanye') {
options = [
{
label: 'My Beautiful Dark Twisted Fantasy',
value: 'My Beautiful Dark Twisted Fantasy'
},
{
label: 'Yeezus',
value: 'Yeezus'
},
{
label: 'JESUS IS KING',
value: 'JESUS IS KING'
},
{
label: 'Ye',
value: 'Ye'
}
]
}
}
setSchemaByPath(
{
componentProps: {
options
}
},
'album'
)
}
},
]
//使用标准结构
const testDependencies: Dependency[] = [
{
dependent: 'singer',
valueGetter: ({ formModel }) => {
const value = formModel['singer']
return Promise.resolve(
value
? value === '周杰伦'
? [
{
label: 'JAY',
value: 'JAY'
},
{
label: '范特西',
value: '范特西'
},
{
label: '叶惠美',
value: '叶惠美'
},
{
label: '跨时代',
value: '跨时代'
}
]
: [
{
label: 'My Beautiful Dark Twisted Fantasy',
value: 'My Beautiful Dark Twisted Fantasy'
},
{
label: 'Yeezus',
value: 'Yeezus'
},
{
label: 'JESUS IS KING',
value: 'JESUS IS KING'
},
{
label: 'Ye',
value: 'Ye'
}
]
: []
)
},
modelValueUpdateList: [
{
modelPath: 'album',
formatterBeforeUpdate: (options) => Promise.resolve(options[0].value)
}
],
schemaValueUpdateList: [
{
schemaPath: 'album',
prop: 'componentProps.options'
}
]
}
]
</script>
<template>
<BasicForm :schema='testSchema' :field-dependencies="testDependencies" />
</template>