Skip to content

fix(Form): conditionally type form data via transform prop #4188

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: v3
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 17 additions & 17 deletions src/runtime/components/Form.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@
import type { DeepReadonly } from 'vue'
import type { AppConfig } from '@nuxt/schema'
import theme from '#build/ui/form'
import type { FormSchema, FormError, FormInputEvents, FormErrorEvent, FormSubmitEvent, FormEvent, Form, FormErrorWithId, InferInput, InferOutput } from '../types/form'
import type { FormSchema, FormError, FormInputEvents, FormErrorEvent, FormSubmitEvent, FormEvent, Form, FormErrorWithId, InferInput, InferOutput, FormData } from '../types/form'
import type { ComponentConfig } from '../types/utils'

type FormConfig = ComponentConfig<typeof theme, AppConfig, 'form'>

export interface FormProps<S extends FormSchema> {
export interface FormProps<S extends FormSchema, T extends boolean = true> {
id?: string | number
/** Schema to validate the form state. Supports Standard Schema objects, Yup, Joi, and Superstructs. */
schema?: S
Expand Down Expand Up @@ -35,7 +35,7 @@ export interface FormProps<S extends FormSchema> {
* If true, schema transformations will be applied to the state on submit.
* @defaultValue `true`
*/
transform?: boolean
transform?: T

/**
* If true, this form will attach to its parent Form (if any) and validate at the same time.
Expand All @@ -50,11 +50,11 @@ export interface FormProps<S extends FormSchema> {
*/
loadingAuto?: boolean
class?: any
onSubmit?: ((event: FormSubmitEvent<InferOutput<S>>) => void | Promise<void>) | (() => void | Promise<void>)
onSubmit?: ((event: FormSubmitEvent<FormData<S, T>>) => void | Promise<void>) | (() => void | Promise<void>)
}

export interface FormEmits<S extends FormSchema> {
(e: 'submit', payload: FormSubmitEvent<InferOutput<S>>): void
export interface FormEmits<S extends FormSchema, T extends boolean = true> {
(e: 'submit', payload: FormSubmitEvent<FormData<S, T>>): void
(e: 'error', payload: FormErrorEvent): void
}

Expand All @@ -63,7 +63,7 @@ export interface FormSlots {
}
</script>

<script lang="ts" setup generic="S extends FormSchema">
<script lang="ts" setup generic="S extends FormSchema, T extends boolean = true">
import { provide, inject, nextTick, ref, onUnmounted, onMounted, computed, useId, readonly } from 'vue'
import { useEventBus } from '@vueuse/core'
import { useAppConfig } from '#imports'
Expand All @@ -75,17 +75,17 @@ import { FormValidationException } from '../types/form'
type I = InferInput<S>
type O = InferOutput<S>

const props = withDefaults(defineProps<FormProps<S>>(), {
const props = withDefaults(defineProps<FormProps<S, T>>(), {
validateOn() {
return ['input', 'blur', 'change'] as FormInputEvents[]
},
validateOnInputDelay: 300,
attach: true,
transform: true,
transform: () => true as T,
loadingAuto: true
})

const emits = defineEmits<FormEmits<S>>()
const emits = defineEmits<FormEmits<S, T>>()
defineSlots<FormSlots>()

const appConfig = useAppConfig() as FormConfig['AppConfig']
Expand Down Expand Up @@ -183,10 +183,10 @@ async function getErrors(): Promise<FormErrorWithId[]> {
return resolveErrorIds(errs)
}

type ValidateOpts<Silent extends boolean> = { name?: keyof I | (keyof I)[], silent?: Silent, nested?: boolean, transform?: boolean }
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> {
type ValidateOpts<Silent extends boolean, Transform extends boolean> = { name?: keyof I | (keyof I)[], silent?: Silent, nested?: boolean, transform?: Transform }
async function _validate<T extends boolean>(opts: ValidateOpts<false, T>): Promise<FormData<S, T>>
async function _validate<T extends boolean>(opts: ValidateOpts<true, T>): Promise<FormData<S, T> | false>
async function _validate<T extends boolean>(opts: ValidateOpts<boolean, boolean> = { silent: false, nested: true, transform: false }): Promise<FormData<S, T> | false> {
const names = opts.name && !Array.isArray(opts.name) ? [opts.name] : opts.name as (keyof O)[]

const nestedValidatePromises = !names && opts.nested
Expand Down Expand Up @@ -227,7 +227,7 @@ async function _validate(opts: ValidateOpts<boolean> = { silent: false, nested:
Object.assign(props.state, transformedState.value)
}

return props.state as O
return props.state as FormData<S, T>
}

const loading = ref(false)
Expand All @@ -236,7 +236,7 @@ provide(formLoadingInjectionKey, readonly(loading))
async function onSubmitWrapper(payload: Event) {
loading.value = props.loadingAuto && true

const event = payload as FormSubmitEvent<O>
const event = payload as FormSubmitEvent<FormData<S, T>>

try {
event.data = await _validate({ nested: true, transform: props.transform })
Expand Down Expand Up @@ -265,7 +265,7 @@ provide(formOptionsInjectionKey, computed(() => ({
validateOnInputDelay: props.validateOnInputDelay
})))

defineExpose<Form<I>>({
defineExpose<Form<S>>({
validate: _validate,
errors,

Expand Down
16 changes: 9 additions & 7 deletions src/runtime/types/form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,20 @@ import type { ObjectSchema as YupObjectSchema } from 'yup'
import type { GetObjectField } from './utils'
import type { Struct as SuperstructSchema } from 'superstruct'

export interface Form<T extends object> {
validate (opts?: { name?: keyof T | (keyof T)[], silent?: boolean, nested?: boolean, transform?: boolean }): Promise<T | false>
export interface Form<S extends FormSchema> {
validate<T extends boolean>(opts?: { name?: keyof FormData<S, false> | (keyof FormData<S, false>)[], silent?: boolean, nested?: boolean, transform?: T }): Promise<FormData<S, T> | false>
clear (path?: string): void
errors: Ref<FormError[]>
setErrors (errs: FormError[], name?: keyof T): void
getErrors (name?: keyof T): FormError[]
setErrors (errs: FormError[], name?: keyof FormData<S, false>): void
getErrors (name?: keyof FormData<S, false>): FormError[]
submit (): Promise<void>
disabled: ComputedRef<boolean>
dirty: ComputedRef<boolean>
loading: Ref<boolean>

dirtyFields: DeepReadonly<Set<keyof T>>
touchedFields: DeepReadonly<Set<keyof T>>
blurredFields: DeepReadonly<Set<keyof T>>
dirtyFields: DeepReadonly<Set<keyof FormData<S, false>>>
touchedFields: DeepReadonly<Set<keyof FormData<S, false>>>
blurredFields: DeepReadonly<Set<keyof FormData<S, false>>>
}

export type FormSchema<I extends object = object, O extends object = I> =
Expand All @@ -42,6 +42,8 @@ export type InferOutput<Schema> = Schema extends StandardSchemaV1 ? StandardSche
: Schema extends SuperstructSchema<infer O, any> ? O
: never

export type FormData<S extends FormSchema, T extends boolean = true> = T extends true ? InferOutput<S> : InferInput<S>

export type FormInputEvents = 'input' | 'blur' | 'change' | 'focus'

export interface FormError<P extends string = string> {
Expand Down
Loading