refactor(Form): remove state assignment and opt-in to nested forms

This commit is contained in:
Romain Hamel
2025-04-16 18:10:54 +02:00
parent 4d875c03a2
commit 385cbeec6c
4 changed files with 89 additions and 16 deletions

View File

@@ -39,7 +39,7 @@ async function onSubmit(event: FormSubmitEvent<Schema>) {
<UCheckbox v-model="state.news" name="news" label="Register to our newsletter" @update:model-value="state.email = undefined" /> <UCheckbox v-model="state.news" name="news" label="Register to our newsletter" @update:model-value="state.email = undefined" />
</div> </div>
<UForm v-if="state.news" :state="state" :schema="nestedSchema"> <UForm v-if="state.news" :state="state" :schema="nestedSchema" nested>
<UFormField label="Email" name="email"> <UFormField label="Email" name="email">
<UInput v-model="state.email" placeholder="john@lennon.com" /> <UInput v-model="state.email" placeholder="john@lennon.com" />
</UFormField> </UFormField>

View File

@@ -51,7 +51,14 @@ async function onSubmit(event: FormSubmitEvent<Schema>) {
<UInput v-model="state.customer" placeholder="Wonka Industries" /> <UInput v-model="state.customer" placeholder="Wonka Industries" />
</UFormField> </UFormField>
<UForm v-for="item, count in state.items" :key="count" :state="item" :schema="itemSchema" class="flex gap-2"> <UForm
v-for="item, count in state.items"
:key="count"
:state="item"
:schema="itemSchema"
nested
class="flex gap-2"
>
<UFormField :label="!count ? 'Description' : undefined" name="description"> <UFormField :label="!count ? 'Description' : undefined" name="description">
<UInput v-model="item.description" /> <UInput v-model="item.description" />
</UFormField> </UFormField>

View File

@@ -0,0 +1,71 @@
<template>
<UContainer>
<UForm :schema :state @submit="onSubmit">
<UFormField label="A" name="a">
<UInput v-model="state.a" />
</UFormField>
<UFormField label="B" name="b">
<UInput v-model="state.b" />
</UFormField>
<UButton type="submit">
Submit
</UButton>
</UForm>
{{ output }}
</UContainer>
</template>
<script lang="ts" setup>
import type { FormSubmitEvent } from '@nuxt/ui'
import * as v from 'valibot'
const _schemaStringFiltered = v.pipe(v.string(), v.trim())
const schema = v.object({
a: v.string(),
b: v.union([
v.pipe(
v.array(_schemaStringFiltered),
v.filterItems((item, index, array) => (array.indexOf(item) === index || item !== ''))
),
v.pipe(
v.string(),
v.trim(),
v.transform(
(item) => {
if (item === '') return undefined
return item
.split(',')
.map(val => val.trim())
.filter(val => val !== '')
}
)
)
])
})
const state = reactive<{
a: string
b: string
}>({
a: 'hello, world',
b: 'hello, world'
})
const output = reactive<{
a: string
b?: string[]
}>({
a: '',
b: []
})
function onSubmit(event: FormSubmitEvent<v.InferOutput<typeof schema>>) {
console.log('typeof `a`:', typeof event.data.a) // should be string
console.log('typeof `b`:', typeof event.data.b) // should be object (array of strings)
output.a = event.data.a
output.b = event.data.b
}
</script>

View File

@@ -19,7 +19,7 @@ export interface FormProps<I extends object, O extends object = I> {
* @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<I> | O) => Promise<FormError[]> | FormError[] validate?: (state: Partial<I>) => 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']`
@@ -32,11 +32,11 @@ export interface FormProps<I extends object, O extends object = I> {
* @defaultValue `300` * @defaultValue `300`
*/ */
validateOnInputDelay?: number validateOnInputDelay?: number
/** /**
* If true, schema transformations will be applied to the state on submit. * If true and nested in another form, this form will attach to its parent and validate at the same time.
* @defaultValue `true`
*/ */
transform?: boolean nested?: boolean
/** /**
* When `true`, all form elements will be disabled on `@submit` event. * When `true`, all form elements will be disabled on `@submit` event.
* This will cause any focused input elements to lose their focus state. * This will cause any focused input elements to lose their focus state.
@@ -71,7 +71,6 @@ const props = withDefaults(defineProps<FormProps<I, O>>(), {
return ['input', 'blur', 'change'] as FormInputEvents[] return ['input', 'blur', 'change'] as FormInputEvents[]
}, },
validateOnInputDelay: 300, validateOnInputDelay: 300,
transform: true,
loadingAuto: true loadingAuto: true
}) })
@@ -127,14 +126,14 @@ onUnmounted(() => {
}) })
onMounted(async () => { onMounted(async () => {
if (parentBus) { if (props.nested && parentBus) {
await nextTick() await nextTick()
parentBus.emit({ type: 'attach', validate: _validate, formId }) parentBus.emit({ type: 'attach', validate: _validate, formId })
} }
}) })
onUnmounted(() => { onUnmounted(() => {
if (parentBus) { if (props.nested && parentBus) {
parentBus.emit({ type: 'detach', formId }) parentBus.emit({ type: 'detach', formId })
} }
}) })
@@ -173,7 +172,7 @@ async function getErrors(): Promise<FormErrorWithId[]> {
return resolveErrorIds(errs) return resolveErrorIds(errs)
} }
async function _validate(opts: { name?: keyof I | (keyof I)[], silent?: boolean, nested?: boolean, transform?: boolean } = { silent: false, nested: true, transform: false }): Promise<O | false> { async function _validate(opts: { name?: keyof I | (keyof I)[], silent?: boolean, nested?: boolean } = { silent: false, nested: true }): Promise<O | false> {
const names = opts.name && !Array.isArray(opts.name) ? [opts.name] : opts.name as (keyof I)[] const names = opts.name && !Array.isArray(opts.name) ? [opts.name] : opts.name as (keyof I)[]
const nestedValidatePromises = !names && opts.nested const nestedValidatePromises = !names && opts.nested
@@ -210,11 +209,7 @@ async function _validate(opts: { name?: keyof I | (keyof I)[], silent?: boolean,
throw new FormValidationException(formId, errors.value, childErrors) throw new FormValidationException(formId, errors.value, childErrors)
} }
if (opts.transform) { return transformedState.value
Object.assign(props.state, transformedState.value)
}
return props.state as O
} }
const loading = ref(false) const loading = ref(false)
@@ -226,7 +221,7 @@ async function onSubmitWrapper(payload: Event) {
const event = payload as FormSubmitEvent<any> const event = payload as FormSubmitEvent<any>
try { try {
event.data = await _validate({ nested: true, transform: props.transform }) event.data = await _validate({ nested: true })
await props.onSubmit?.(event) await props.onSubmit?.(event)
dirtyFields.clear() dirtyFields.clear()
} catch (error) { } catch (error) {