0.2.9 • Published 9 months ago

vue-formula v0.2.9

Weekly downloads
-
License
-
Repository
-
Last release
9 months ago

介绍

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 创建一个简易表单

Alt text

<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.使用内置组件

Alt text

<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 进行表单组件的渲染。

Alt text

<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 属性替换 componentrender 属性来使用 VNode 进行表单组件的渲染。

Alt text

<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类型,表单项固定为 fieldfieldtrue
fieldmodel 中对应的 key(required)stringtrue
component渲染的组件名称(上述的表单项)stringfalse
componentProps使用 component 渲染时,渲染的表单项组件对应的 props。例如 component 为 Input 时,componentsProps 的值与 ElementPlus ElInput 一致。GetFieldProps<FieldSchema'component'>false
render渲染函数,使用 vNode 渲染表单项组件(callbackParams: CallbackParams) => VNode | VNode[] | stringfalse
slot插槽名称,使用插槽渲染组件stringfalse
label文本标签stringfalse
noLabel是否不显示文本标签stringfalse
placeholder占位信息stringfalse
subLabel次文本标签stringfalse
helpMessage帮助信息stringfalse
colProps多列布局 props,可参考 ElementPlus Col 组件参数(一致)Partial\false
formItemPropsElFormItem 的对应的 propsPartialfalse
defaultValue默认值anyfalse
valueFieldv-model 绑定的值,默认为 modelValuestringfalse
rules校验规则FormItemRule[]false
dynamicRules动态校验规则(callbackParams: CallbackParams) => FormItemRule[]false
required必填boolean | (callbackParams: CallbackParams) => booleanfalse
disabled只读boolean | (callbackParams: CallbackParams) => booleanfalse
show显示(css 层面的显示隐藏)boolean | (callbackParams: CallbackParams) => booleanfalse
ifShow显示(渲染层面的显示隐藏)boolean | (callbackParams: CallbackParams) => booleanfalse
loading加载中boolean | (callbackParams: CallbackParams) => booleanfalse

容器

内置的容器

内置组件需要通过导入Container来进行使用,使用时会根据组件的类型提示相应的组件参数

目前内置的容器只有一个,为Container.Container。后续会继续迭代

  • Container(内置基础容器)

如何使用容器

Alt text

<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容器类型,固定为 containercontainertrue
name容器名称stringtrue
component渲染的容器组件stringtrue
componentProps容器组件参数Recordablefalse
properties容器内部渲染的表单项数据Recordabletrue
title标题stringfalse
slots容器内部插槽string[]false
rowProps布局属性,与 ElementPlus ElRow 的 props 一致Partialfalse
colProps多列布局 props,可参考 ElementPlus Col 组件参数(一致)Partialfalse
description描述stringfalse
helpMessage帮助信息stringfalse
disabled只读boolean | (callbackParams: CallbackParams) => booleanfalse
show显示(css 层面的显示隐藏)boolean | (callbackParams: CallbackParams) => booleanfalse
ifShow显示(渲染层面的显示隐藏)boolean | (callbackParams: CallbackParams) => booleanfalse
loading加载中boolean | (callbackParams: CallbackParams) => booleanfalse

Container(内置基础容器)的说明//TODO:

插槽

Container 内部提供了

headerTitleheaderTitleAfterheaderContentSeparator

这三个内置可选插槽。使用 slots 属性可以使用指定对应位置的插槽名称用于渲染对应插槽内容

以下是例子: Alt text

<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是否允许部分折叠展开booleanfalse

列表

内置的列表

内置列表需要通过导入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>

Alt text

列表数据结构(Schema 结构)

属性(property)说明(description)类型(type)是否必填
type列表类型,固定为 listlisttrue
name列表名称stringtrue
component渲染的列表组件名称(对应上面列表组件中的 TableList)stringtrue
componentProps列表组件参数Recordablefalse
items列表内部渲染的表单项数据Recordabletrue
title标题stringfalse
description描述stringfalse
helpMessage帮助信息stringfalse
rowProps布局属性,与 ElementPlus ElRow 的 props 一致Partialfalse
colProps多列布局 props,可参考 ElementPlus Col 组件参数(一致)Partialfalse
min最小列表项个数numberfalse
max最大列表项个数numberfalse
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带索引的子标题是否显示booleanfalse
isCopyButtonVisible复制按钮是否显示booleanfalse
areUpDownButtonsVisible上移下移按钮是否显示booleanfalse
isDeleteButtonVisible删除按钮是否显示booleanfalse
canDeleteLastOne只剩一个元素时是否允许删除booleanfalse
confirmBeforeDelete删除一个元素时是否需要确认框booleanfalse
listItemRowProps列表中的每一个列表项的行布局booleanfalse
listItemContentColProps列表中的每一个列表项的内容区的列布局booleanfalse
listItemButtonColProps列表中的每一个列表项的按钮区的列布局booleanfalse
listItemContentRowProps列表中的每一个列表项的内容区内的行布局booleanfalse

只读、必填、显示隐藏

只读

表单项(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 可以生成一下几个条件:

  1. 控制字段 为 disabled 时,将 用户名、基本信息、联系人 更改成 只读(表单项控制表单项、表单项控制容器、表单项控制列表
  2. 年龄字段 小于 18 时,工作字段 设置成 只读(各个列表项内部的控制
  3. 始终只读的表单项 由于 disabled 字段设置了false ,所以一直是只读的(静态值控制

Alt text

Alt text

必填

必填仅仅是针对于表单项而言,即只有表单项才存在这个属性

必填逻辑上于只读的逻辑大体相同,是通过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) => voidfalse
shouldUpdate是否需要更新(params: DependencyCallbackParams) => booleanfalse
valueGetter值的获取方法(params: DependencyCallbackParams) => Promisefalse
modelValueUpdateList对表单值进行更新操作的数组{ modelPath: string | string[], formatterBeforeUpdate?: (value: any) => Promise }[]false
schemaValueUpdateList对布局数据进行更新操作的数组{ schemaPath: string | string[],prop: string, formatterBeforeUpdate?: (value: any) => Promise}[]false
debounce防抖booleanfalse
threshold防抖间隔时间booleanfalse

demo

Alt text

<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>
0.2.9

9 months ago

0.2.8

9 months ago

0.2.7

9 months ago

0.2.6

10 months ago

0.2.5

10 months ago

0.2.4

10 months ago

0.2.3

10 months ago

0.2.2

10 months ago

0.2.1

10 months ago

0.1.6

10 months ago

0.1.5

10 months ago

0.1.4

10 months ago

0.1.3

10 months ago

0.1.2

10 months ago

0.1.1

10 months ago

0.1.0

10 months ago