feat: export field component, expose more slotprops

This commit is contained in:
zernonia 2024-04-18 17:18:40 +08:00
parent 318d4c8643
commit 7370383fbe
11 changed files with 159 additions and 125 deletions

View File

@ -1,13 +1,12 @@
<script setup lang="ts"> <script setup lang="ts">
import { h } from 'vue' import * as z from 'zod'
import { h, reactive } from 'vue'
import { useForm } from 'vee-validate' import { useForm } from 'vee-validate'
import { toTypedSchema } from '@vee-validate/zod' import { toTypedSchema } from '@vee-validate/zod'
import * as z from 'zod'
import AutoFormField from '../ui/auto-form/AutoFormField.vue'
import { Button } from '@/lib/registry/new-york/ui/button' import { Button } from '@/lib/registry/new-york/ui/button'
import { toast } from '@/lib/registry/new-york/ui/toast' import { toast } from '@/lib/registry/new-york/ui/toast'
import { AutoForm, getBaseSchema, getBaseType } from '@/lib/registry/new-york/ui/auto-form' import type { Config } from '@/lib/registry/new-york/ui/auto-form'
import { AutoForm, AutoFormField } from '@/lib/registry/new-york/ui/auto-form'
enum Sports { enum Sports {
Football = 'Football/Soccer', Football = 'Football/Soccer',
@ -115,59 +114,63 @@ const onSubmit = handleSubmit((values) => {
description: h('pre', { class: 'mt-2 w-[340px] rounded-md bg-slate-950 p-4' }, h('code', { class: 'text-white' }, JSON.stringify(values, null, 2))), description: h('pre', { class: 'mt-2 w-[340px] rounded-md bg-slate-950 p-4' }, h('code', { class: 'text-white' }, JSON.stringify(values, null, 2))),
}) })
}) })
const config
= reactive({
username: {
label: '123',
description: 'Test description',
inputProps: {
id: '123',
},
},
password: {
inputProps: {
id: '345',
type: 'password',
},
},
acceptTerms: {
description: 'this is custom',
},
sendMeMails: {
component: 'switch',
},
color: {
enumProps: {
options: ['red', 'green', 'blue'],
placeholder: 'Choose a color',
},
},
marshmallows: {
component: 'radio',
},
bio: {
component: 'textarea',
},
file: {
component: 'file',
},
subObject: {
subField: {
label: 'custom labvel',
description: '123',
},
subSubObject: {
subSubField: {
label: 'sub suuuub',
},
},
},
}) as Config<z.infer<typeof schema>>
</script> </script>
<template> <template>
<AutoForm <AutoForm
class="w-2/3 space-y-6" class="w-2/3 space-y-6"
:schema="schema" :schema="schema"
:field-config="{ :field-config="config"
username: {
description: 'Test description',
inputProps: {
id: '123',
},
},
password: {
inputProps: {
id: '345',
type: 'password',
},
},
acceptTerms: {
description: 'this is custom',
},
sendMeMails: {
component: 'switch',
},
color: {
enumProps: {
options: ['red', 'green', 'blue'],
placeholder: 'Choose a color',
},
},
marshmallows: {
component: 'radio',
},
bio: {
component: 'textarea',
},
file: {
component: 'file',
},
subObject: {
subField: {
label: 'custom labvel',
description: '123',
},
subSubObject: {
subSubField: {
label: 'sub suuuub',
},
},
},
}"
@submit="onSubmit" @submit="onSubmit"
> >
<template #username="componentField"> <template #username="componentField">

View File

@ -10,6 +10,10 @@ const props = defineProps<{
fieldConfig?: Config<z.infer<T>> fieldConfig?: Config<z.infer<T>>
}>() }>()
const emits = defineEmits<{
submit: [event: Event]
}>()
const shapes = computed(() => { const shapes = computed(() => {
// @ts-expect-error ignore {} not assignable to object // @ts-expect-error ignore {} not assignable to object
const val: { [key in keyof T]: Shape } = {} const val: { [key in keyof T]: Shape } = {}
@ -33,11 +37,11 @@ const shapes = computed(() => {
</script> </script>
<template> <template>
<form> <form @submit="emits('submit', $event)">
<template v-for="(shape, key) of shapes" :key="key"> <template v-for="(shape, key) of shapes" :key="key">
<slot <slot
:shape="shape" :shape="shape"
:name="key.toString()" :name="key.toString() as keyof z.infer<T>"
:config="fieldConfig?.[key as keyof typeof fieldConfig] as ConfigItem" :config="fieldConfig?.[key as keyof typeof fieldConfig] as ConfigItem"
> >
<AutoFormField <AutoFormField

View File

@ -14,12 +14,14 @@ defineProps<{
</script> </script>
<template> <template>
<FormField v-slot="{ componentField }" :name="name"> <FormField v-slot="slotProps" :name="name">
<FormItem> <FormItem>
<div class="space-y-0 mb-3 flex items-center gap-3"> <div class="space-y-0 mb-3 flex items-center gap-3">
<FormControl> <FormControl>
<Switch v-if="config?.component === 'switch'" v-bind="{ ...componentField }" /> <slot v-bind="slotProps">
<Checkbox v-else v-bind="{ ...componentField }" /> <Switch v-if="config?.component === 'switch'" v-bind="{ ...slotProps.componentField }" />
<Checkbox v-else v-bind="{ ...slotProps.componentField }" />
</slot>
</FormControl> </FormControl>
<AutoFormLabel v-if="!config?.hideLabel" :required="required"> <AutoFormLabel v-if="!config?.hideLabel" :required="required">
{{ config?.label || beautifyObjectName(name) }} {{ config?.label || beautifyObjectName(name) }}

View File

@ -23,31 +23,33 @@ const df = new DateFormatter('en-US', {
</script> </script>
<template> <template>
<FormField v-slot="{ componentField }" :name="name"> <FormField v-slot="slotProps" :name="name">
<FormItem> <FormItem>
<AutoFormLabel v-if="!config?.hideLabel" :required="required"> <AutoFormLabel v-if="!config?.hideLabel" :required="required">
{{ config?.label || beautifyObjectName(name) }} {{ config?.label || beautifyObjectName(name) }}
</AutoFormLabel> </AutoFormLabel>
<FormControl> <FormControl>
<div> <slot v-bind="slotProps">
<Popover> <div>
<PopoverTrigger as-child> <Popover>
<Button <PopoverTrigger as-child>
variant="outline" <Button
:class="cn( variant="outline"
'w-full justify-start text-left font-normal', :class="cn(
!componentField.modelValue && 'text-muted-foreground', 'w-full justify-start text-left font-normal',
)" !slotProps.componentField.modelValue && 'text-muted-foreground',
> )"
<CalendarIcon class="mr-2 h-4 w-4" /> >
{{ componentField.modelValue ? df.format(componentField.modelValue.toDate(getLocalTimeZone())) : "Pick a date" }} <CalendarIcon class="mr-2 h-4 w-4" />
</Button> {{ slotProps.componentField.modelValue ? df.format(slotProps.componentField.modelValue.toDate(getLocalTimeZone())) : "Pick a date" }}
</PopoverTrigger> </Button>
<PopoverContent class="w-auto p-0"> </PopoverTrigger>
<Calendar initial-focus v-bind="{ ...componentField }" /> <PopoverContent class="w-auto p-0">
</PopoverContent> <Calendar initial-focus v-bind="slotProps.componentField" />
</Popover> </PopoverContent>
</div> </Popover>
</div>
</slot>
</FormControl> </FormControl>
<FormDescription v-if="config?.description"> <FormDescription v-if="config?.description">

View File

@ -19,29 +19,31 @@ const computedOptions = computed(() => props.config?.enumProps?.options || props
</script> </script>
<template> <template>
<FormField v-slot="{ componentField }" :name="name"> <FormField v-slot="slotProps" :name="name">
<FormItem> <FormItem>
<AutoFormLabel v-if="!config?.hideLabel" :required="required"> <AutoFormLabel v-if="!config?.hideLabel" :required="required">
{{ config?.label || beautifyObjectName(name) }} {{ config?.label || beautifyObjectName(name) }}
</AutoFormLabel> </AutoFormLabel>
<FormControl> <FormControl>
<RadioGroup v-if="config?.component === 'radio'" :orientation="'vertical'" v-bind="{ ...componentField }"> <slot v-bind="slotProps">
<div v-for="(option, index) in computedOptions" :key="option" class="mb-2 flex items-center gap-3 space-y-0"> <RadioGroup v-if="config?.component === 'radio'" :orientation="'vertical'" v-bind="{ ...slotProps.componentField }">
<RadioGroupItem :id="`${option}-${index}`" :value="option" /> <div v-for="(option, index) in computedOptions" :key="option" class="mb-2 flex items-center gap-3 space-y-0">
<Label :for="`${option}-${index}`">{{ beautifyObjectName(option) }}</Label> <RadioGroupItem :id="`${option}-${index}`" :value="option" />
</div> <Label :for="`${option}-${index}`">{{ beautifyObjectName(option) }}</Label>
</RadioGroup> </div>
</RadioGroup>
<Select v-else v-bind="{ ...componentField }"> <Select v-else v-bind="{ ...slotProps.componentField }">
<SelectTrigger class="w-full"> <SelectTrigger class="w-full">
<SelectValue :placeholder="config?.enumProps?.placeholder" /> <SelectValue :placeholder="config?.enumProps?.placeholder" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem v-for="option in computedOptions" :key="option" :value="option"> <SelectItem v-for="option in computedOptions" :key="option" :value="option">
{{ beautifyObjectName(option) }} {{ beautifyObjectName(option) }}
</SelectItem> </SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
</slot>
</FormControl> </FormControl>
<FormDescription v-if="config?.description"> <FormDescription v-if="config?.description">

View File

@ -28,21 +28,23 @@ async function parseFileAsString(file: File | undefined): Promise<string> {
</script> </script>
<template> <template>
<FormField v-slot="{ componentField }" :name="name"> <FormField v-slot="slotProps" :name="name">
<FormItem v-bind="$attrs"> <FormItem v-bind="$attrs">
<AutoFormLabel v-if="!config?.hideLabel" :required="required"> <AutoFormLabel v-if="!config?.hideLabel" :required="required">
{{ config?.label || beautifyObjectName(name) }} {{ config?.label || beautifyObjectName(name) }}
</AutoFormLabel> </AutoFormLabel>
<FormControl> <FormControl>
<Input <slot v-bind="slotProps">
type="file" <Input
v-bind="{ ...config?.inputProps }" type="file"
@change="async (ev: InputEvent) => { v-bind="{ ...config?.inputProps }"
const file = (ev.target as HTMLInputElement).files?.[0] @change="async (ev: InputEvent) => {
const parsed = await parseFileAsString(file) const file = (ev.target as HTMLInputElement).files?.[0]
componentField.onInput(parsed) const parsed = await parseFileAsString(file)
}" slotProps.componentField.onInput(parsed)
/> }"
/>
</slot>
</FormControl> </FormControl>
<FormDescription v-if="config?.description"> <FormDescription v-if="config?.description">
{{ config.description }} {{ config.description }}

View File

@ -14,14 +14,16 @@ defineProps<{
</script> </script>
<template> <template>
<FormField v-slot="{ componentField }" :name="name"> <FormField v-slot="slotProps" :name="name">
<FormItem v-bind="$attrs"> <FormItem v-bind="$attrs">
<AutoFormLabel v-if="!config?.hideLabel" :required="required"> <AutoFormLabel v-if="!config?.hideLabel" :required="required">
{{ config?.label || beautifyObjectName(name) }} {{ config?.label || beautifyObjectName(name) }}
</AutoFormLabel> </AutoFormLabel>
<FormControl> <FormControl>
<Textarea v-if="config?.component === 'textarea'" type="text" v-bind="{ ...componentField, ...config?.inputProps }" /> <slot v-bind="slotProps">
<Input v-else type="text" v-bind="{ ...componentField, ...config?.inputProps }" /> <Textarea v-if="config?.component === 'textarea'" type="text" v-bind="{ ...slotProps.componentField, ...config?.inputProps }" />
<Input v-else type="text" v-bind="{ ...slotProps.componentField, ...config?.inputProps }" />
</slot>
</FormControl> </FormControl>
<FormDescription v-if="config?.description"> <FormDescription v-if="config?.description">
{{ config.description }} {{ config.description }}

View File

@ -17,13 +17,15 @@ defineProps<{
</script> </script>
<template> <template>
<FormField v-slot="{ componentField }" :name="name"> <FormField v-slot="slotProps" :name="name">
<FormItem> <FormItem>
<AutoFormLabel v-if="!config?.hideLabel" :required="required"> <AutoFormLabel v-if="!config?.hideLabel" :required="required">
{{ config?.label || beautifyObjectName(name) }} {{ config?.label || beautifyObjectName(name) }}
</AutoFormLabel> </AutoFormLabel>
<FormControl> <FormControl>
<Input type="number" v-bind="{ ...$attrs, ...componentField, ...config?.inputProps }" /> <slot v-bind="slotProps">
<Input type="number" v-bind="{ ...slotProps.componentField, ...config?.inputProps }" />
</slot>
</FormControl> </FormControl>
<FormDescription v-if="config?.description"> <FormDescription v-if="config?.description">
{{ config.description }} {{ config.description }}

View File

@ -49,20 +49,24 @@ const shapes = computed(() => {
</script> </script>
<template> <template>
<Accordion type="multiple" class="w-full" collapsible> <section>
<AccordionItem :value="name" class="border-none"> <slot v-bind="props">
<AccordionTrigger class="text-base"> <Accordion type="multiple" class="w-full" collapsible>
{{ schema?.description || beautifyObjectName(name) }} <AccordionItem :value="name" class="border-none">
</AccordionTrigger> <AccordionTrigger class="text-base">
<AccordionContent class="p-2 space-y-5"> {{ schema?.description || beautifyObjectName(name) }}
<template v-for="(shape, key) in shapes" :key="key"> </AccordionTrigger>
<AutoFormField <AccordionContent class="p-2 space-y-5">
:config="config?.[key as keyof typeof config] as ConfigItem" <template v-for="(shape, key) in shapes" :key="key">
:name="key.toString()" <AutoFormField
:shape="shape" :config="config?.[key as keyof typeof config] as ConfigItem"
/> :name="key.toString()"
</template> :shape="shape"
</AccordionContent> />
</AccordionItem> </template>
</Accordion> </AccordionContent>
</AccordionItem>
</Accordion>
</slot>
</section>
</template> </template>

View File

@ -0,0 +1,7 @@
export { default as AutoFormFieldBoolean } from './Boolean.vue'
export { default as AutoFormFieldDate } from './Date.vue'
export { default as AutoFormFieldEnum } from './Enum.vue'
export { default as AutoFormFieldFile } from './File.vue'
export { default as AutoFormFieldInput } from './Input.vue'
export { default as AutoFormFieldNumber } from './Number.vue'
export { default as AutoFormFieldObject } from './Object.vue'

View File

@ -1,2 +1,6 @@
export { default as AutoForm } from './AutoForm.vue' export { default as AutoForm } from './AutoForm.vue'
export { default as AutoFormField } from './AutoFormField.vue'
export { getObjectFormSchema, getBaseSchema, getBaseType, getDefaultValues } from './utils' export { getObjectFormSchema, getBaseSchema, getBaseType, getDefaultValues } from './utils'
export type { Config, ConfigItem } from './interface'
export * from './fields'