diff --git a/docs/app/components/content/examples/form/FormExampleOnError.vue b/docs/app/components/content/examples/form/FormExampleOnError.vue index 46c142b94a..226421cfc1 100644 --- a/docs/app/components/content/examples/form/FormExampleOnError.vue +++ b/docs/app/components/content/examples/form/FormExampleOnError.vue @@ -22,10 +22,9 @@ async function onSubmit(event: FormSubmitEvent) { } async function onError(event: FormErrorEvent) { - if (event?.errors?.[0]?.id) { - const element = document.getElementById(event.errors[0].id) - element?.focus() - element?.scrollIntoView({ behavior: 'smooth', block: 'center' }) + const error = event.errors[0] + if (error) { + toast.add({ title: 'Error', description: `Please check the form ${error.name}.`, color: 'error' }) } } diff --git a/docs/content/docs/2.components/form.md b/docs/content/docs/2.components/form.md index 8d3beb394e..ef9a2d9d8b 100644 --- a/docs/content/docs/2.components/form.md +++ b/docs/content/docs/2.components/form.md @@ -158,7 +158,11 @@ You can listen to the `@error` event to handle errors. This event is triggered w - `name` - the `name` of the `FormField` - `message` - the error message to display. -Here's an example that focuses the first input element with an error after the form is submitted: +::tip +By default, the form automatically focuses the first field with an error when submission fails through the `focus-on-error` prop. You can disable this by passing `:focus-on-error="false"`. +:: + +Here's an example of using the `@error` event to add custom behavior, such as displaying a toast notification when validation fails: ::component-example --- diff --git a/src/runtime/components/Form.vue b/src/runtime/components/Form.vue index 8ba83d55c4..c4ca94bfc7 100644 --- a/src/runtime/components/Form.vue +++ b/src/runtime/components/Form.vue @@ -60,6 +60,11 @@ export type FormProps>) => void | Promise) | (() => void | Promise) @@ -82,7 +87,7 @@ import { useAppConfig } from '#imports' import { formOptionsInjectionKey, formInputsInjectionKey, formBusInjectionKey, formLoadingInjectionKey, formErrorsInjectionKey, formStateInjectionKey } from '../composables/useFormField' import { tv } from '../utils/tv' import { useComponentProps } from '../composables/useComponentProps' -import { validateSchema, getAtPath, setAtPath } from '../utils/form' +import { validateSchema, getAtPath, setAtPath, scrollToErrorEl } from '../utils/form' import { FormValidationException } from '../types/form' type I = InferInput @@ -93,6 +98,7 @@ const _props = withDefaults(defineProps>(), { return ['input', 'blur', 'change'] as FormInputEvents[] }, validateOnInputDelay: 300, + focusOnError: true, transform: () => true as T, loadingAuto: true }) @@ -288,6 +294,11 @@ async function onSubmitWrapper(payload: Event) { } finally { loading.value = false } + + if (props.focusOnError && errors.value.length > 0) { + await nextTick() + scrollToErrorEl(errors.value) + } } // eslint-disable-next-line vue/no-dupe-keys diff --git a/src/runtime/utils/form.ts b/src/runtime/utils/form.ts index 514deffc44..d5bbd45d6b 100644 --- a/src/runtime/utils/form.ts +++ b/src/runtime/utils/form.ts @@ -1,6 +1,6 @@ import type { StandardSchemaV1 } from '@standard-schema/spec' import type { Struct } from 'superstruct' -import type { FormSchema, ValidateReturnSchema } from '../types/form' +import type { FormErrorWithId, FormSchema, ValidateReturnSchema } from '../types/form' export function isSuperStructSchema(schema: any): schema is Struct { return ( @@ -114,3 +114,13 @@ export function setAtPath( return data } + +export function scrollToErrorEl(errors: FormErrorWithId[]) { + const error = errors[0] + if (error) { + const el = document.getElementById(error.id!) + if (el) { + el.focus() + } + } +} diff --git a/test/components/Form.spec.ts b/test/components/Form.spec.ts index 05d02da0cc..78af36c333 100644 --- a/test/components/Form.spec.ts +++ b/test/components/Form.spec.ts @@ -275,6 +275,24 @@ describe('Form', () => { expect(passwordField.text()).toBe('Invalid input: expected string, received undefined') }) + it('focuses on first error element on submit error', async () => { + const emailInputMock = { focus: vi.fn() } + const passwordInputMock = { focus: vi.fn() } + const getElementByIdSpy = vi.spyOn(document, 'getElementById').mockImplementation((id) => { + if (id === 'email') return emailInputMock as any + if (id === 'password') return passwordInputMock as any + return null + }) + + await form.submit() + await flushPromises() + + expect(emailInputMock.focus).toHaveBeenCalledTimes(1) + expect(passwordInputMock.focus).toHaveBeenCalledTimes(0) + + getElementByIdSpy.mockRestore() + }) + it('validate on submit works', async () => { state.email = 'bob@dylan.com' state.password = 'strongpassword'