From 85330589ade37aad1f77de8ec84e2def8da1065d Mon Sep 17 00:00:00 2001 From: Romain Hamel Date: Mon, 6 Jan 2025 13:42:55 +0100 Subject: [PATCH 1/2] fix(Form)!: include nested state in submit data --- src/runtime/components/Form.vue | 5 +++-- test/components/Form.spec.ts | 9 +++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/runtime/components/Form.vue b/src/runtime/components/Form.vue index e573730bde..43279d59eb 100644 --- a/src/runtime/components/Form.vue +++ b/src/runtime/components/Form.vue @@ -158,6 +158,8 @@ async function _validate(opts: { name?: string | string[], silent?: boolean, nes throw new FormValidationException(formId, errors.value, childErrors) } + Object.assign(props.state, transformedState.value) + return props.state as T } @@ -170,8 +172,7 @@ async function onSubmitWrapper(payload: Event) { const event = payload as FormSubmitEvent try { - await _validate({ nested: true }) - event.data = props.schema ? transformedState.value : props.state + event.data = await _validate({ nested: true }) await props.onSubmit?.(event) } catch (error) { if (!(error instanceof FormValidationException)) { diff --git a/test/components/Form.spec.ts b/test/components/Form.spec.ts index d1cfe58fe9..144a698fad 100644 --- a/test/components/Form.spec.ts +++ b/test/components/Form.spec.ts @@ -347,6 +347,15 @@ describe('Form', () => { expect(nestedField.text()).toBe('Required') }) + test('submit event contains nested attributes', async () => { + state.email = 'bob@dylan.com' + state.password = 'strongpassword' + state.nested.field = 'nested' + + await form.value.submit() + expect(wrapper.setupState.onSubmit).toHaveBeenCalledWith(expect.objectContaining({ data: { email: 'bob@dylan.com', password: 'strongpassword', nested: { field: 'nested' } } })) + }) + test('submit works when child is disabled', async () => { await form.value.submit() expect(wrapper.setupState.onError).toHaveBeenCalledTimes(1) From 93aea7ee4d82c44d047cad8160a85ccd3197cfc1 Mon Sep 17 00:00:00 2001 From: Romain Hamel Date: Mon, 6 Jan 2025 14:26:00 +0100 Subject: [PATCH 2/2] chore(form): up --- docs/content/3.components/form.md | 2 +- src/runtime/components/Form.vue | 14 ++++++++------ src/runtime/types/form.ts | 4 ++-- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/docs/content/3.components/form.md b/docs/content/3.components/form.md index bbbc04d6c5..bd617b9c62 100644 --- a/docs/content/3.components/form.md +++ b/docs/content/3.components/form.md @@ -195,7 +195,7 @@ This will give you access to the following: | Name | Type | | ---- | ---- | | `submit()`{lang="ts-type"} | `Promise`{lang="ts-type"}

Triggers form submission.

| -| `validate(path?: string \| string[], opts: { silent?: boolean })`{lang="ts-type"} | `Promise`{lang="ts-type"}

Triggers form validation. Will raise any errors unless `opts.silent` is set to true.

| +| `validate(opts: { name?: string \| string[], silent?: boolean, nested?: boolean, transform?: boolean })`{lang="ts-type"} | `Promise`{lang="ts-type"}

Triggers form validation. Will raise any errors unless `opts.silent` is set to true.

| | `clear(path?: string)`{lang="ts-type"} | `void`

Clears form errors associated with a specific path. If no path is provided, clears all form errors.

| | `getErrors(path?: string)`{lang="ts-type"} | `FormError[]`{lang="ts-type"}

Retrieves form errors associated with a specific path. If no path is provided, returns all form errors.

| | `setErrors(errors: FormError[], path?: string)`{lang="ts-type"} | `void`

Sets form errors for a given path. If no path is provided, overrides all errors.

| diff --git a/src/runtime/components/Form.vue b/src/runtime/components/Form.vue index 43279d59eb..0432555769 100644 --- a/src/runtime/components/Form.vue +++ b/src/runtime/components/Form.vue @@ -60,7 +60,7 @@ const parentBus = inject( provide(formBusInjectionKey, bus) -const nestedForms = ref any }>>(new Map()) +const nestedForms = ref>(new Map()) onMounted(async () => { bus.on(async (event) => { @@ -121,12 +121,12 @@ async function getErrors(): Promise { return resolveErrorIds(errs) } -async function _validate(opts: { name?: string | string[], silent?: boolean, nested?: boolean } = { silent: false, nested: true }): Promise { +async function _validate(opts: { name?: string | string[], silent?: boolean, nested?: boolean, transform?: boolean } = { silent: false, nested: true, transform: false }): Promise { const names = opts.name && !Array.isArray(opts.name) ? [opts.name] : opts.name as string[] const nestedValidatePromises = !names && opts.nested ? Array.from(nestedForms.value.values()).map( - ({ validate }) => validate().then(() => undefined).catch((error: Error) => { + ({ validate }) => validate(opts).then(() => undefined).catch((error: Error) => { if (!(error instanceof FormValidationException)) { throw error } @@ -151,14 +151,16 @@ async function _validate(opts: { name?: string | string[], silent?: boolean, nes errors.value = await getErrors() } - const childErrors = (await Promise.all(nestedValidatePromises)).filter(val => val) + const childErrors = (await Promise.all(nestedValidatePromises)).filter(val => val !== undefined) if (errors.value.length + childErrors.length > 0) { if (opts.silent) return false throw new FormValidationException(formId, errors.value, childErrors) } - Object.assign(props.state, transformedState.value) + if (opts.transform) { + Object.assign(props.state, transformedState.value) + } return props.state as T } @@ -172,7 +174,7 @@ async function onSubmitWrapper(payload: Event) { const event = payload as FormSubmitEvent try { - event.data = await _validate({ nested: true }) + event.data = await _validate({ nested: true, transform: true }) await props.onSubmit?.(event) } catch (error) { if (!(error instanceof FormValidationException)) { diff --git a/src/runtime/types/form.ts b/src/runtime/types/form.ts index 3d165e9166..06e9d5a217 100644 --- a/src/runtime/types/form.ts +++ b/src/runtime/types/form.ts @@ -8,7 +8,7 @@ import type { GetObjectField } from './utils' import type { Struct as SuperstructSchema } from 'superstruct' export interface Form { - validate (opts?: { name: string | string[], silent?: false, nested?: boolean }): Promise + validate (opts?: { name?: string | string[], silent?: boolean, nested?: boolean, transform?: boolean }): Promise clear (path?: string): void errors: Ref setErrors (errs: FormError[], path?: string): void @@ -95,7 +95,7 @@ export class FormValidationException extends Error { errors: FormErrorWithId[] children?: FormValidationException[] - constructor(formId: string | number, errors: FormErrorWithId[], childErrors: FormValidationException[]) { + constructor(formId: string | number, errors: FormErrorWithId[], childErrors?: FormValidationException[]) { super('Form validation exception') this.formId = formId this.errors = errors