0.2.9 • Published 2 years ago

vue-formula v0.2.9

Weekly downloads
-
License
-
Repository
-
Last release
2 years 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

2 years ago

0.2.8

2 years ago

0.2.7

2 years ago

0.2.6

2 years ago

0.2.5

2 years ago

0.2.4

2 years ago

0.2.3

2 years ago

0.2.2

2 years ago

0.2.1

2 years ago

0.1.6

2 years ago

0.1.5

2 years ago

0.1.4

2 years ago

0.1.3

2 years ago

0.1.2

2 years ago

0.1.1

2 years ago

0.1.0

2 years ago