mirror of
https://github.com/ArthurDanjou/ui.git
synced 2026-01-14 12:14:41 +01:00
fix(Form): input and output type inference (#3938)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
This commit is contained in:
@@ -2,23 +2,23 @@
|
|||||||
import type { DeepReadonly } from 'vue'
|
import type { DeepReadonly } from 'vue'
|
||||||
import type { AppConfig } from '@nuxt/schema'
|
import type { AppConfig } from '@nuxt/schema'
|
||||||
import theme from '#build/ui/form'
|
import theme from '#build/ui/form'
|
||||||
import type { FormSchema, FormError, FormInputEvents, FormErrorEvent, FormSubmitEvent, FormEvent, Form, FormErrorWithId } from '../types/form'
|
import type { FormSchema, FormError, FormInputEvents, FormErrorEvent, FormSubmitEvent, FormEvent, Form, FormErrorWithId, InferInput, InferOutput } from '../types/form'
|
||||||
import type { ComponentConfig } from '../types/utils'
|
import type { ComponentConfig } from '../types/utils'
|
||||||
|
|
||||||
type FormConfig = ComponentConfig<typeof theme, AppConfig, 'form'>
|
type FormConfig = ComponentConfig<typeof theme, AppConfig, 'form'>
|
||||||
|
|
||||||
export interface FormProps<T extends object> {
|
export interface FormProps<S extends FormSchema> {
|
||||||
id?: string | number
|
id?: string | number
|
||||||
/** Schema to validate the form state. Supports Standard Schema objects, Yup, Joi, and Superstructs. */
|
/** Schema to validate the form state. Supports Standard Schema objects, Yup, Joi, and Superstructs. */
|
||||||
schema?: FormSchema<T>
|
schema?: S
|
||||||
/** An object representing the current state of the form. */
|
/** An object representing the current state of the form. */
|
||||||
state: Partial<T>
|
state: Partial<InferInput<S>>
|
||||||
/**
|
/**
|
||||||
* Custom validation function to validate the form state.
|
* Custom validation function to validate the form state.
|
||||||
* @param state - The current state of the form.
|
* @param state - The current state of the form.
|
||||||
* @returns A promise that resolves to an array of FormError objects, or an array of FormError objects directly.
|
* @returns A promise that resolves to an array of FormError objects, or an array of FormError objects directly.
|
||||||
*/
|
*/
|
||||||
validate?: (state: Partial<T>) => Promise<FormError[]> | FormError[]
|
validate?: (state: Partial<InferInput<S>>) => Promise<FormError[]> | FormError[]
|
||||||
/**
|
/**
|
||||||
* The list of input events that trigger the form validation.
|
* The list of input events that trigger the form validation.
|
||||||
* @defaultValue `['blur', 'change', 'input']`
|
* @defaultValue `['blur', 'change', 'input']`
|
||||||
@@ -50,11 +50,11 @@ export interface FormProps<T extends object> {
|
|||||||
*/
|
*/
|
||||||
loadingAuto?: boolean
|
loadingAuto?: boolean
|
||||||
class?: any
|
class?: any
|
||||||
onSubmit?: ((event: FormSubmitEvent<T>) => void | Promise<void>) | (() => void | Promise<void>)
|
onSubmit?: ((event: FormSubmitEvent<InferOutput<S>>) => void | Promise<void>) | (() => void | Promise<void>)
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FormEmits<T extends object> {
|
export interface FormEmits<S extends FormSchema> {
|
||||||
(e: 'submit', payload: FormSubmitEvent<T>): void
|
(e: 'submit', payload: FormSubmitEvent<InferOutput<S>>): void
|
||||||
(e: 'error', payload: FormErrorEvent): void
|
(e: 'error', payload: FormErrorEvent): void
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,7 +63,7 @@ export interface FormSlots {
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts" setup generic="T extends object">
|
<script lang="ts" setup generic="S extends FormSchema">
|
||||||
import { provide, inject, nextTick, ref, onUnmounted, onMounted, computed, useId, readonly } from 'vue'
|
import { provide, inject, nextTick, ref, onUnmounted, onMounted, computed, useId, readonly } from 'vue'
|
||||||
import { useEventBus } from '@vueuse/core'
|
import { useEventBus } from '@vueuse/core'
|
||||||
import { useAppConfig } from '#imports'
|
import { useAppConfig } from '#imports'
|
||||||
@@ -72,7 +72,10 @@ import { tv } from '../utils/tv'
|
|||||||
import { validateSchema } from '../utils/form'
|
import { validateSchema } from '../utils/form'
|
||||||
import { FormValidationException } from '../types/form'
|
import { FormValidationException } from '../types/form'
|
||||||
|
|
||||||
const props = withDefaults(defineProps<FormProps<T>>(), {
|
type I = InferInput<S>
|
||||||
|
type O = InferOutput<S>
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<FormProps<S>>(), {
|
||||||
validateOn() {
|
validateOn() {
|
||||||
return ['input', 'blur', 'change'] as FormInputEvents[]
|
return ['input', 'blur', 'change'] as FormInputEvents[]
|
||||||
},
|
},
|
||||||
@@ -82,7 +85,7 @@ const props = withDefaults(defineProps<FormProps<T>>(), {
|
|||||||
loadingAuto: true
|
loadingAuto: true
|
||||||
})
|
})
|
||||||
|
|
||||||
const emits = defineEmits<FormEmits<T>>()
|
const emits = defineEmits<FormEmits<S>>()
|
||||||
defineSlots<FormSlots>()
|
defineSlots<FormSlots>()
|
||||||
|
|
||||||
const appConfig = useAppConfig() as FormConfig['AppConfig']
|
const appConfig = useAppConfig() as FormConfig['AppConfig']
|
||||||
@@ -91,7 +94,7 @@ const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.form || {})
|
|||||||
|
|
||||||
const formId = props.id ?? useId() as string
|
const formId = props.id ?? useId() as string
|
||||||
|
|
||||||
const bus = useEventBus<FormEvent<T>>(`form-${formId}`)
|
const bus = useEventBus<FormEvent<I>>(`form-${formId}`)
|
||||||
const parentBus = props.attach && inject(
|
const parentBus = props.attach && inject(
|
||||||
formBusInjectionKey,
|
formBusInjectionKey,
|
||||||
undefined
|
undefined
|
||||||
@@ -149,12 +152,12 @@ onUnmounted(() => {
|
|||||||
const errors = ref<FormErrorWithId[]>([])
|
const errors = ref<FormErrorWithId[]>([])
|
||||||
provide('form-errors', errors)
|
provide('form-errors', errors)
|
||||||
|
|
||||||
const inputs = ref<{ [P in keyof T]?: { id?: string, pattern?: RegExp } }>({})
|
const inputs = ref<{ [P in keyof I]?: { id?: string, pattern?: RegExp } }>({})
|
||||||
provide(formInputsInjectionKey, inputs as any)
|
provide(formInputsInjectionKey, inputs as any)
|
||||||
|
|
||||||
const dirtyFields = new Set<keyof T>()
|
const dirtyFields = new Set<keyof I>()
|
||||||
const touchedFields = new Set<keyof T>()
|
const touchedFields = new Set<keyof I>()
|
||||||
const blurredFields = new Set<keyof T>()
|
const blurredFields = new Set<keyof I>()
|
||||||
|
|
||||||
function resolveErrorIds(errs: FormError[]): FormErrorWithId[] {
|
function resolveErrorIds(errs: FormError[]): FormErrorWithId[] {
|
||||||
return errs.map(err => ({
|
return errs.map(err => ({
|
||||||
@@ -163,7 +166,7 @@ function resolveErrorIds(errs: FormError[]): FormErrorWithId[] {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
const transformedState = ref<T | null>(null)
|
const transformedState = ref<O | null>(null)
|
||||||
|
|
||||||
async function getErrors(): Promise<FormErrorWithId[]> {
|
async function getErrors(): Promise<FormErrorWithId[]> {
|
||||||
let errs = props.validate ? (await props.validate(props.state)) ?? [] : []
|
let errs = props.validate ? (await props.validate(props.state)) ?? [] : []
|
||||||
@@ -180,12 +183,15 @@ async function getErrors(): Promise<FormErrorWithId[]> {
|
|||||||
return resolveErrorIds(errs)
|
return resolveErrorIds(errs)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function _validate(opts: { name?: keyof T | (keyof T)[], silent?: boolean, nested?: boolean, transform?: boolean } = { silent: false, nested: true, transform: false }): Promise<T | false> {
|
type ValidateOpts<Silent extends boolean> = { name?: keyof I | (keyof I)[], silent?: Silent, nested?: boolean, transform?: boolean }
|
||||||
const names = opts.name && !Array.isArray(opts.name) ? [opts.name] : opts.name as (keyof T)[]
|
async function _validate(opts: ValidateOpts<false>): Promise<O>
|
||||||
|
async function _validate(opts: ValidateOpts<true>): Promise<O | false>
|
||||||
|
async function _validate(opts: ValidateOpts<boolean> = { silent: false, nested: true, transform: false }): Promise<O | false> {
|
||||||
|
const names = opts.name && !Array.isArray(opts.name) ? [opts.name] : opts.name as (keyof O)[]
|
||||||
|
|
||||||
const nestedValidatePromises = !names && opts.nested
|
const nestedValidatePromises = !names && opts.nested
|
||||||
? Array.from(nestedForms.value.values()).map(
|
? Array.from(nestedForms.value.values()).map(
|
||||||
({ validate }) => validate(opts).then(() => undefined).catch((error: Error) => {
|
({ validate }) => validate(opts as any).then(() => undefined).catch((error: Error) => {
|
||||||
if (!(error instanceof FormValidationException)) {
|
if (!(error instanceof FormValidationException)) {
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
@@ -221,7 +227,7 @@ async function _validate(opts: { name?: keyof T | (keyof T)[], silent?: boolean,
|
|||||||
Object.assign(props.state, transformedState.value)
|
Object.assign(props.state, transformedState.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
return props.state as T
|
return props.state as O
|
||||||
}
|
}
|
||||||
|
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
@@ -230,7 +236,7 @@ provide(formLoadingInjectionKey, readonly(loading))
|
|||||||
async function onSubmitWrapper(payload: Event) {
|
async function onSubmitWrapper(payload: Event) {
|
||||||
loading.value = props.loadingAuto && true
|
loading.value = props.loadingAuto && true
|
||||||
|
|
||||||
const event = payload as FormSubmitEvent<any>
|
const event = payload as FormSubmitEvent<O>
|
||||||
|
|
||||||
try {
|
try {
|
||||||
event.data = await _validate({ nested: true, transform: props.transform })
|
event.data = await _validate({ nested: true, transform: props.transform })
|
||||||
@@ -259,11 +265,11 @@ provide(formOptionsInjectionKey, computed(() => ({
|
|||||||
validateOnInputDelay: props.validateOnInputDelay
|
validateOnInputDelay: props.validateOnInputDelay
|
||||||
})))
|
})))
|
||||||
|
|
||||||
defineExpose<Form<T>>({
|
defineExpose<Form<I>>({
|
||||||
validate: _validate,
|
validate: _validate,
|
||||||
errors,
|
errors,
|
||||||
|
|
||||||
setErrors(errs: FormError[], name?: keyof T) {
|
setErrors(errs: FormError[], name?: keyof I) {
|
||||||
if (name) {
|
if (name) {
|
||||||
errors.value = errors.value
|
errors.value = errors.value
|
||||||
.filter(error => error.name !== name)
|
.filter(error => error.name !== name)
|
||||||
@@ -277,7 +283,7 @@ defineExpose<Form<T>>({
|
|||||||
await onSubmitWrapper(new Event('submit'))
|
await onSubmitWrapper(new Event('submit'))
|
||||||
},
|
},
|
||||||
|
|
||||||
getErrors(name?: keyof T) {
|
getErrors(name?: keyof I) {
|
||||||
if (name) {
|
if (name) {
|
||||||
return errors.value.filter(err => err.name === name)
|
return errors.value.filter(err => err.name === name)
|
||||||
}
|
}
|
||||||
@@ -296,9 +302,9 @@ defineExpose<Form<T>>({
|
|||||||
loading,
|
loading,
|
||||||
dirty: computed(() => !!dirtyFields.size),
|
dirty: computed(() => !!dirtyFields.size),
|
||||||
|
|
||||||
dirtyFields: readonly(dirtyFields) as DeepReadonly<Set<keyof T>>,
|
dirtyFields: readonly(dirtyFields) as DeepReadonly<Set<keyof I>>,
|
||||||
blurredFields: readonly(blurredFields) as DeepReadonly<Set<keyof T>>,
|
blurredFields: readonly(blurredFields) as DeepReadonly<Set<keyof I>>,
|
||||||
touchedFields: readonly(touchedFields) as DeepReadonly<Set<keyof T>>
|
touchedFields: readonly(touchedFields) as DeepReadonly<Set<keyof I>>
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -21,11 +21,26 @@ export interface Form<T extends object> {
|
|||||||
blurredFields: DeepReadonly<Set<keyof T>>
|
blurredFields: DeepReadonly<Set<keyof T>>
|
||||||
}
|
}
|
||||||
|
|
||||||
export type FormSchema<T extends object> =
|
export type FormSchema<I extends object = object, O extends object = I> =
|
||||||
| YupObjectSchema<T>
|
| YupObjectSchema<I>
|
||||||
| JoiSchema<T>
|
| JoiSchema<I>
|
||||||
| SuperstructSchema<any, any>
|
| SuperstructSchema<any, any>
|
||||||
| StandardSchemaV1
|
| StandardSchemaV1<I, O>
|
||||||
|
|
||||||
|
// Define a utility type to infer the input type based on the schema type
|
||||||
|
export type InferInput<Schema> = Schema extends StandardSchemaV1 ? StandardSchemaV1.InferInput<Schema>
|
||||||
|
: Schema extends YupObjectSchema<infer I> ? I
|
||||||
|
: Schema extends JoiSchema<infer I> ? I
|
||||||
|
: Schema extends SuperstructSchema<infer I, any> ? I
|
||||||
|
: Schema extends StandardSchemaV1 ? StandardSchemaV1.InferInput<Schema>
|
||||||
|
: never
|
||||||
|
|
||||||
|
// Define a utility type to infer the output type based on the schema type
|
||||||
|
export type InferOutput<Schema> = Schema extends StandardSchemaV1 ? StandardSchemaV1.InferOutput<Schema>
|
||||||
|
: Schema extends YupObjectSchema<infer O> ? O
|
||||||
|
: Schema extends JoiSchema<infer O> ? O
|
||||||
|
: Schema extends SuperstructSchema<infer O, any> ? O
|
||||||
|
: never
|
||||||
|
|
||||||
export type FormInputEvents = 'input' | 'blur' | 'change' | 'focus'
|
export type FormInputEvents = 'input' | 'blur' | 'change' | 'focus'
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user