Skip to content

配置式表单

通过定义好的schema生成对应表单,参考这个项目, 效果

原理,在my-form.vue组件里面遍历schema来渲染表单控件,formData通过props传递进来,这个组件只负责渲染数据。 创建use-form.tsx在这个组合式函数里面导入myForm,通过defineComponent创建组件并返回,然后在useForm这个函数里面管理表单状态, 最终返回Form组件和formApi

代码
vue
<script setup lang="ts">
import { ref } from 'vue'
import type { FormOptions } from './types'
import { useForm } from './use-form'

const formProps: FormOptions = {
  schema: [
    {
      component: 'Input',
      label: '用户名',
      fieldName: 'username',
      defaultValue: '测试'
    }
  ]
}

const [MyForm, formApi] = useForm(formProps)
const reset = () => {
  formApi.setState({
    schema: [
      {
        component: 'Input',
        label: '重新渲染',
        fieldName: 'username',
        defaultValue: '11111111111111'
      }
    ]
  })
}
const text = ref('')
const update = () => {
  formApi.setValues({
    username: text.value
  })
}

const getValue = () => {
  const value = formApi.getValues<{
    username: string
  }>()
  console.log('获取到的值', value)
  value.username = '看看是否影响原表单'
}
</script>

<template>
  <div>
    复杂表单
    <a-button @click="reset">重新渲染</a-button>
    <div>
      <a-input v-model:value="text" />
      <a-button @click="update">修改表单值</a-button>
    </div>
    <a-button @click="getValue">获取值</a-button>

    <MyForm />
  </div>
</template>
tsx
import { defineComponent, unref, ref, shallowRef, toRaw, onMounted } from 'vue'
import type { MaybeRef } from 'vue'
import type { FormOptions } from './types'
import MyForm from './my-form.vue'
import { cloneDeep } from 'lodash-es'

export const useForm = (formOptions: MaybeRef<FormOptions>) => {
  const formKey = ref(false)
  const __formOptions = shallowRef<FormOptions>(unref(formOptions))
  const __formData = ref<Record<string, any>>({})

  // 解构schema
  const initForm = (options: MaybeRef<FormOptions>) => {
    __formOptions.value = unref(options)
    // 获取schema中字段的初始值
    const defaultValues: Record<string, any> = {}
    __formOptions.value.schema.forEach(formItem => {
      defaultValues[formItem.fieldName] = toRaw(formItem?.defaultValue)
    })
    __formData.value = defaultValues
  }
  onMounted(() => {
    initForm(formOptions)
  })

  const Form = defineComponent(() => {
    const update = (data: Record<string, any>) => {
      __formData.value = data
    }
    return () => (
      <>
        <MyForm
          key={formKey.value as boolean}
          {...__formOptions.value}
          modelValue={__formData.value}
          onUpdate:modelValue={update}
        />
      </>
    )
  })

  const formApi = {
    setState: (options: MaybeRef<FormOptions>) => {
      initForm(options)
      // 确保form组件重新渲染
      formKey.value = !formKey.value
    },
    getValues: <T extends Record<string, any>>(): T => {
      return cloneDeep(toRaw(__formData.value)) as T
    },
    setValues: (value: MaybeRef<Record<string, any>>) => {
      __formData.value = cloneDeep(toRaw(unref(value)))
    },
  }

  return [Form, formApi] as const
}
vue
<script setup lang="ts">
import { onMounted } from 'vue'
import type { FormOptions } from './types'
import { globalState } from './global-state'

defineProps<FormOptions>()
const formData = defineModel<Record<string, any>>()
onMounted(() => {
  console.log('重新渲染')
})
</script>

<template>
  <a-form v-model:model="formData">
    <template v-for="(item, key) in schema" :key="key">
      <a-form-item :label="item.label" :name="item.fieldName">
        <component
            :is="globalState.getComponents(item.component)"
            v-model:value="formData[item.fieldName]"
        />
      </a-form-item>
    </template>
  </a-form>
</template>
ts
import { Input, Select } from 'ant-design-vue'

// 所有需要用到的表单控件都需要提前在这里导入
const _components = {
  Input,
  Select
} as const;
type ComponentType = keyof (typeof _components);

type ComponentMap = {
  [key in ComponentType]: typeof _components[key]
}

export class GlobalState {
  // 所有需要用到的表单控件
  #Components: ComponentMap = _components

  public getComponents(
      componentName?: ComponentType,
  ) {
    if (componentName) {
      return this.#Components[componentName]
    }
    return this.#Components
  }
}

export const globalState = new GlobalState()
ts
import type { Component } from 'vue'

type ComponentType =
    | 'Input'
    | 'Select'

export interface FormOptions {
  schema: {
    /** 组件 */
    component: Component | ComponentType
    /** 组件参数 */
    // componentProps?: ComponentProps;
    componentProps?: any
    /** 默认值 */
    defaultValue?: any
    /** 依赖 */
    // dependencies?: FormItemDependencies;
    /** 描述 */
    description?: string
    /** 字段名 */
    fieldName: string
    /** 帮助信息 */
    help?: string
    /** 表单项 */
    label?: string
    // 自定义组件内部渲染
    // renderComponentContent?: RenderComponentContentType;
    /** 字段规则 */
    // rules?: FormSchemaRuleType;
    /** 后缀 */
    // suffix?: CustomRenderType;
  }[]
}