feat: typing for nested config

This commit is contained in:
zernonia 2024-04-18 14:38:19 +08:00
parent 9f37b12c21
commit 49ad6c85e4
12 changed files with 118 additions and 36 deletions

View File

@ -146,7 +146,6 @@ const onSubmit = handleSubmit((values) => {
placeholder: 'Choose a color', placeholder: 'Choose a color',
}, },
}, },
marshmallows: { marshmallows: {
component: 'radio', component: 'radio',
}, },
@ -158,7 +157,15 @@ const onSubmit = handleSubmit((values) => {
}, },
subObject: { subObject: {
subField: {
label: 'custom labvel',
description: '123',
},
subSubObject: {
subSubField: {
label: 'sub suuuub',
},
},
}, },
}" }"
@submit="onSubmit" @submit="onSubmit"
@ -175,6 +182,10 @@ const onSubmit = handleSubmit((values) => {
</div> </div>
</template> </template>
<template #subObject="componentField">
<AutoFormField v-bind="{ ...componentField, name: 'subObject' }" />
</template>
<Button type="submit"> <Button type="submit">
Submit Submit
</Button> </Button>

View File

@ -1,15 +1,13 @@
<script setup lang="ts" generic="T extends ZodRawShape"> <script setup lang="ts" generic="U extends ZodRawShape, T extends ZodObject<U>">
import { computed } from 'vue' import { computed } from 'vue'
import type { ZodAny, ZodObject, ZodRawShape } from 'zod' import type { ZodAny, ZodObject, ZodRawShape, z } from 'zod'
import { getBaseType, getDefaultValueInZodStack } from './utils' import { getBaseType, getDefaultValueInZodStack } from './utils'
import type { Config } from './interface' import type { Config, ConfigItem } from './interface'
import AutoFormField from './AutoFormField.vue' import AutoFormField from './AutoFormField.vue'
const props = defineProps<{ const props = defineProps<{
schema: ZodObject<T> schema: T
fieldConfig?: { fieldConfig?: Config<z.infer<T>>
[key in keyof T]?: Config
}
}>() }>()
const shapes = computed(() => { const shapes = computed(() => {
@ -48,16 +46,14 @@ const shapes = computed(() => {
<slot <slot
:shape="shape" :shape="shape"
:name="key.toString()" :name="key.toString()"
:config="fieldConfig?.[key as keyof typeof fieldConfig] as Config" :config="fieldConfig?.[key as keyof typeof fieldConfig] as ConfigItem"
> >
<AutoFormField <AutoFormField
:config="fieldConfig?.[key as keyof typeof fieldConfig] as Config" :config="fieldConfig?.[key as keyof typeof fieldConfig] as ConfigItem"
:name="key.toString()" :name="key.toString()"
:shape="shape" :shape="shape"
/> />
</slot> </slot>
<!-- {{ shape.schema }} -->
</template> </template>
<slot :shapes="shapes" /> <slot :shapes="shapes" />

View File

@ -1,7 +1,6 @@
<script setup lang="ts"> <script setup lang="ts" generic="U extends ZodAny">
import type { ZodAny } from 'zod' import type { ZodAny } from 'zod'
import type { Config, ConfigItem } from './interface'
import type { Config } from './interface'
import { DEFAULT_ZOD_HANDLERS, INPUT_COMPONENTS } from './constant' import { DEFAULT_ZOD_HANDLERS, INPUT_COMPONENTS } from './constant'
defineProps<{ defineProps<{
@ -12,16 +11,21 @@ defineProps<{
options?: any[] options?: any[]
schema?: ZodAny schema?: ZodAny
} }
config?: Config config?: ConfigItem | Config<U>
}>() }>()
function isValidConfig(config: any): config is ConfigItem {
return !!config?.component
}
</script> </script>
<template> <template>
<component <component
:is="INPUT_COMPONENTS[config?.component ?? DEFAULT_ZOD_HANDLERS[shape.type]] " :is="isValidConfig(config) ? INPUT_COMPONENTS[config.component!] : INPUT_COMPONENTS[DEFAULT_ZOD_HANDLERS[shape.type]] "
:name="name" :name="name"
:required="shape.required" :required="shape.required"
:options="shape.options" :options="shape.options"
:schema="shape.schema"
:config="config" :config="config"
> >
<slot /> <slot />

View File

@ -4,6 +4,7 @@ import FieldEnum from './fields/Enum.vue'
import FieldFile from './fields/File.vue' import FieldFile from './fields/File.vue'
import FieldInput from './fields/Input.vue' import FieldInput from './fields/Input.vue'
import FieldNumber from './fields/Number.vue' import FieldNumber from './fields/Number.vue'
import FieldObject from './fields/Object.vue'
export const INPUT_COMPONENTS = { export const INPUT_COMPONENTS = {
date: FieldDate, date: FieldDate,
@ -15,6 +16,7 @@ export const INPUT_COMPONENTS = {
number: FieldNumber, number: FieldNumber,
string: FieldInput, string: FieldInput,
file: FieldFile, file: FieldFile,
object: FieldObject,
} }
/** /**
@ -30,4 +32,5 @@ export const DEFAULT_ZOD_HANDLERS: {
ZodEnum: 'select', ZodEnum: 'select',
ZodNativeEnum: 'select', ZodNativeEnum: 'select',
ZodNumber: 'number', ZodNumber: 'number',
ZodObject: 'object',
} }

View File

@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { beautifyObjectName } from '../utils' import { beautifyObjectName } from '../utils'
import type { Config } from '../interface' import type { ConfigItem } from '../interface'
import AutoFormLabel from '../AutoFormLabel.vue' import AutoFormLabel from '../AutoFormLabel.vue'
import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/lib/registry/new-york/ui/form' import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/lib/registry/new-york/ui/form'
import { Switch } from '@/lib/registry/new-york/ui/switch' import { Switch } from '@/lib/registry/new-york/ui/switch'
@ -9,7 +9,7 @@ import { Checkbox } from '@/lib/registry/new-york/ui/checkbox'
defineProps<{ defineProps<{
name: string name: string
required?: boolean required?: boolean
config?: Config config?: ConfigItem
}>() }>()
</script> </script>

View File

@ -2,8 +2,8 @@
import { DateFormatter, getLocalTimeZone } from '@internationalized/date' import { DateFormatter, getLocalTimeZone } from '@internationalized/date'
import { CalendarIcon } from '@radix-icons/vue' import { CalendarIcon } from '@radix-icons/vue'
import { beautifyObjectName } from '../utils' import { beautifyObjectName } from '../utils'
import type { Config } from '../interface'
import AutoFormLabel from '../AutoFormLabel.vue' import AutoFormLabel from '../AutoFormLabel.vue'
import type { ConfigItem } from '../interface'
import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/lib/registry/new-york/ui/form' import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/lib/registry/new-york/ui/form'
import { Calendar } from '@/lib/registry/new-york/ui/calendar' import { Calendar } from '@/lib/registry/new-york/ui/calendar'
@ -14,7 +14,7 @@ import { cn } from '@/lib/utils'
defineProps<{ defineProps<{
name: string name: string
required?: boolean required?: boolean
config?: Config config?: ConfigItem
}>() }>()
const df = new DateFormatter('en-US', { const df = new DateFormatter('en-US', {

View File

@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue' import { computed } from 'vue'
import { beautifyObjectName } from '../utils' import { beautifyObjectName } from '../utils'
import type { Config } from '../interface' import type { ConfigItem } from '../interface'
import AutoFormLabel from '../AutoFormLabel.vue' import AutoFormLabel from '../AutoFormLabel.vue'
import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/lib/registry/new-york/ui/form' import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/lib/registry/new-york/ui/form'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/lib/registry/new-york/ui/select' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/lib/registry/new-york/ui/select'
@ -12,7 +12,7 @@ const props = defineProps<{
name: string name: string
required?: boolean required?: boolean
options?: string[] options?: string[]
config?: Config config?: ConfigItem
}>() }>()
const computedOptions = computed(() => props.config?.enumProps?.options || props.options) const computedOptions = computed(() => props.config?.enumProps?.options || props.options)

View File

@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { beautifyObjectName } from '../utils' import { beautifyObjectName } from '../utils'
import type { Config } from '../interface' import type { ConfigItem } from '../interface'
import AutoFormLabel from '../AutoFormLabel.vue' import AutoFormLabel from '../AutoFormLabel.vue'
import { FormControl, FormDescription, FormField, FormItem, FormMessage } from '@/lib/registry/new-york/ui/form' import { FormControl, FormDescription, FormField, FormItem, FormMessage } from '@/lib/registry/new-york/ui/form'
import { Input } from '@/lib/registry/new-york/ui/input' import { Input } from '@/lib/registry/new-york/ui/input'
@ -8,7 +8,7 @@ import { Input } from '@/lib/registry/new-york/ui/input'
defineProps<{ defineProps<{
name: string name: string
required?: boolean required?: boolean
config?: Config config?: ConfigItem
}>() }>()
async function parseFileAsString(file: File | undefined): Promise<string> { async function parseFileAsString(file: File | undefined): Promise<string> {

View File

@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { beautifyObjectName } from '../utils' import { beautifyObjectName } from '../utils'
import type { Config } from '../interface' import type { Config, ConfigItem } from '../interface'
import AutoFormLabel from '../AutoFormLabel.vue' import AutoFormLabel from '../AutoFormLabel.vue'
import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/lib/registry/new-york/ui/form' import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/lib/registry/new-york/ui/form'
import { Input } from '@/lib/registry/new-york/ui/input' import { Input } from '@/lib/registry/new-york/ui/input'
@ -9,7 +9,7 @@ import { Textarea } from '@/lib/registry/new-york/ui/textarea'
defineProps<{ defineProps<{
name: string name: string
required?: boolean required?: boolean
config?: Config config?: ConfigItem
}>() }>()
</script> </script>

View File

@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { beautifyObjectName } from '../utils' import { beautifyObjectName } from '../utils'
import type { Config } from '../interface' import type { Config, ConfigItem } from '../interface'
import AutoFormLabel from '../AutoFormLabel.vue' import AutoFormLabel from '../AutoFormLabel.vue'
import { FormControl, FormDescription, FormField, FormItem, FormMessage } from '@/lib/registry/new-york/ui/form' import { FormControl, FormDescription, FormField, FormItem, FormMessage } from '@/lib/registry/new-york/ui/form'
import { Input } from '@/lib/registry/new-york/ui/input' import { Input } from '@/lib/registry/new-york/ui/input'
@ -12,7 +12,7 @@ defineOptions({
defineProps<{ defineProps<{
name: string name: string
required?: boolean required?: boolean
config?: Config config?: ConfigItem
}>() }>()
</script> </script>

View File

@ -0,0 +1,68 @@
<script setup lang="ts" generic="T extends ZodRawShape">
import type { ZodAny, ZodObject, ZodRawShape } from 'zod'
import { computed } from 'vue'
import type { Config, ConfigItem } from '../interface'
import { beautifyObjectName, getBaseSchema, getBaseType, getDefaultValueInZodStack } from '../utils'
import AutoFormField from '../AutoFormField.vue'
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/lib/registry/new-york/ui/accordion'
const props = defineProps<{
name: string
required?: boolean
config?: Config<T>
schema?: ZodObject<T>
}>()
const shapes = computed(() => {
// @ts-expect-error ignore {} not assignable to object
const val: {
[key in keyof T]: {
type: string
default: any
required?: boolean
options?: string[]
schema?: ZodAny
}
} = {}
if (!props.schema)
return
const shape = getBaseSchema(props.schema)?.shape
if (!shape)
return
Object.keys(shape).forEach((name) => {
const item = shape[name] as ZodAny
let options = 'values' in item._def ? item._def.values as string[] : undefined
if (!Array.isArray(options) && typeof options === 'object')
options = Object.values(options)
val[name as keyof T] = {
type: getBaseType(item),
default: getDefaultValueInZodStack(item),
options,
required: !['ZodOptional', 'ZodNullable'].includes(item._def.typeName),
schema: item,
}
})
return val
})
</script>
<template>
<Accordion type="multiple" class="w-full" collapsible>
<AccordionItem :value="name" class="border-none">
<AccordionTrigger class="text-base">
{{ beautifyObjectName(name) }}
</AccordionTrigger>
<AccordionContent class="p-2 space-y-5">
<template v-for="(shape, key) in shapes" :key="key">
<AutoFormField
:config="config?.[key as keyof typeof config] as ConfigItem"
:name="key.toString()"
:shape="shape"
/>
</template>
</AccordionContent>
</AccordionItem>
</Accordion>
</template>

View File

@ -1,8 +1,8 @@
import type { InputHTMLAttributes, SelectHTMLAttributes } from 'vue' import type { InputHTMLAttributes, SelectHTMLAttributes } from 'vue'
import type * as z from 'zod' import type { z } from 'zod'
import type { INPUT_COMPONENTS } from './constant' import type { INPUT_COMPONENTS } from './constant'
export interface Config { export interface ConfigItem {
label?: string label?: string
description?: string description?: string
component?: keyof typeof INPUT_COMPONENTS component?: keyof typeof INPUT_COMPONENTS
@ -10,11 +10,11 @@ export interface Config {
enumProps?: SelectHTMLAttributes & { options?: any[] } enumProps?: SelectHTMLAttributes & { options?: any[] }
} }
export type FieldConfig<SchemaType extends z.infer<z.ZodObject<any, any>>> = { export type Config<SchemaType extends object> = {
// If SchemaType.key is an object, create a nested FieldConfig, otherwise FieldConfigItem // If SchemaType.key is an object, create a nested Config, otherwise ConfigItem
[Key in keyof SchemaType]?: SchemaType[Key] extends object [Key in keyof SchemaType]?: SchemaType[Key] extends object
? FieldConfig<z.infer<SchemaType[Key]>> ? Config<SchemaType[Key]>
: FieldConfig<z.infer<SchemaType[Key]>> // TODO; : ConfigItem;
} }
export enum DependencyType { export enum DependencyType {