diff --git a/docs/components/content/examples/FormExampleBasic.vue b/docs/components/content/examples/FormExampleBasic.vue
index b711cc6f35..25e5cbb3bf 100644
--- a/docs/components/content/examples/FormExampleBasic.vue
+++ b/docs/components/content/examples/FormExampleBasic.vue
@@ -1,5 +1,5 @@
-
+
diff --git a/docs/components/content/examples/FormExampleElements.vue b/docs/components/content/examples/FormExampleElements.vue
index 7aacc1bd79..69ad917a9d 100644
--- a/docs/components/content/examples/FormExampleElements.vue
+++ b/docs/components/content/examples/FormExampleElements.vue
@@ -1,6 +1,6 @@
-
+
diff --git a/docs/components/content/examples/FormExampleJoi.vue b/docs/components/content/examples/FormExampleJoi.vue
index 049ddb4b79..1a617000d9 100644
--- a/docs/components/content/examples/FormExampleJoi.vue
+++ b/docs/components/content/examples/FormExampleJoi.vue
@@ -1,6 +1,6 @@
-
+
diff --git a/docs/components/content/examples/FormExampleOnError.vue b/docs/components/content/examples/FormExampleOnError.vue
new file mode 100644
index 0000000000..842ee645c9
--- /dev/null
+++ b/docs/components/content/examples/FormExampleOnError.vue
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Submit
+
+
+
diff --git a/docs/components/content/examples/FormExampleValibot.vue b/docs/components/content/examples/FormExampleValibot.vue
index c1cefe3a8e..0864d71b11 100644
--- a/docs/components/content/examples/FormExampleValibot.vue
+++ b/docs/components/content/examples/FormExampleValibot.vue
@@ -1,6 +1,6 @@
-
+
diff --git a/docs/components/content/examples/FormExampleYup.vue b/docs/components/content/examples/FormExampleYup.vue
index 0528888888..99b8801040 100644
--- a/docs/components/content/examples/FormExampleYup.vue
+++ b/docs/components/content/examples/FormExampleYup.vue
@@ -1,6 +1,6 @@
-
+
diff --git a/docs/components/content/examples/FormExampleZod.vue b/docs/components/content/examples/FormExampleZod.vue
index 234200438e..378c83b66d 100644
--- a/docs/components/content/examples/FormExampleZod.vue
+++ b/docs/components/content/examples/FormExampleZod.vue
@@ -1,6 +1,6 @@
-
+
diff --git a/docs/content/1.getting-started/5.examples.md b/docs/content/1.getting-started/5.examples.md
index ff70aaade1..c78e523164 100644
--- a/docs/content/1.getting-started/5.examples.md
+++ b/docs/content/1.getting-started/5.examples.md
@@ -130,6 +130,7 @@ Our theming system provides a lot of flexibility to customize the components.
Here is some examples of what you can do with the [CommandPalette](/navigation/command-palette).
#### Algolia
+
::component-example
---
padding: false
diff --git a/docs/content/3.forms/10.form.md b/docs/content/3.forms/10.form.md
index 32e259c718..a75a04d538 100644
--- a/docs/content/3.forms/10.form.md
+++ b/docs/content/3.forms/10.form.md
@@ -35,7 +35,6 @@ You can provide a schema from [Yup](#yup), [Zod](#zod) or [Joi](#joi), [Valibot]
:component-example{component="form-example-joi" :componentProps='{"class": "space-y-4 w-60"}'}
-
### Valibot
:component-example{component="form-example-valibot" :componentProps='{"class": "space-y-4 w-60"}'}
@@ -87,7 +86,7 @@ You can manually set errors after form submission if required. To do this, simpl
```vue
-
+
@@ -138,6 +137,18 @@ The Form component automatically triggers validation upon `submit`, `input`, `bl
Take a look at the component!
::
+## Error event :u-badge{label="New" class="align-middle ml-2 !rounded-full" variant="subtle"}
+
+You can listen to the `@error` event to handle errors. This event is triggered when the form is validated and contains an array of `FormError` objects with the following fields:
+
+- `id` - the identifier of the form element.
+- `path` - the path to the form element matching the `name`.
+- `message` - the error message to display.
+
+Here is an example of how to focus the first form element with an error:
+
+:component-example{component="form-example-on-error" :componentProps='{"class": "space-y-4 w-60"}'}
+
## Props
:component-props
diff --git a/src/runtime/components/forms/Form.vue b/src/runtime/components/forms/Form.vue
index 49ec5d751f..19f0831645 100644
--- a/src/runtime/components/forms/Form.vue
+++ b/src/runtime/components/forms/Form.vue
@@ -11,9 +11,17 @@ import type { ZodSchema } from 'zod'
import type { ValidationError as JoiError, Schema as JoiSchema } from 'joi'
import type { ObjectSchema as YupObjectSchema, ValidationError as YupError } from 'yup'
import type { ObjectSchemaAsync as ValibotObjectSchema } from 'valibot'
-import type { FormError, FormEvent, FormEventType, FormSubmitEvent, Form } from '../../types/form'
+import type { FormError, FormEvent, FormEventType, FormSubmitEvent, FormErrorEvent, Form } from '../../types/form'
import { uid } from '../../utils/uid'
+class FormException extends Error {
+ constructor (message: string) {
+ super(message)
+ this.message = message
+ Object.setPrototypeOf(this, FormException.prototype)
+ }
+}
+
export default defineComponent({
props: {
schema: {
@@ -39,7 +47,7 @@ export default defineComponent({
default: () => ['blur', 'input', 'change', 'submit']
}
},
- emits: ['submit'],
+ emits: ['submit', 'error'],
setup (props, { expose, emit }) {
const bus = useEventBus(`form-${uid()}`)
@@ -52,6 +60,8 @@ export default defineComponent({
const errors = ref([])
provide('form-errors', errors)
provide('form-events', bus)
+ const inputs = ref({})
+ provide('form-inputs', inputs)
async function getErrors (): Promise {
let errs = await props.validate(props.state)
@@ -87,7 +97,7 @@ export default defineComponent({
}
if (!opts.silent && errors.value.length > 0) {
- throw new Error(
+ throw new FormException(
`Form validation failed: ${JSON.stringify(errors.value, null, 2)}`
)
}
@@ -95,12 +105,29 @@ export default defineComponent({
}
async function onSubmit (event: SubmitEvent) {
- if (props.validateOn?.includes('submit')) {
- await validate()
+ try {
+ if (props.validateOn?.includes('submit')) {
+ await validate()
+ }
+ const submitEvent: FormSubmitEvent = {
+ ...event,
+ data: props.state
+ }
+ emit('submit', submitEvent)
+ } catch (error) {
+ if (!(error instanceof FormException)) {
+ throw error
+ }
+
+ const errorEvent: FormErrorEvent = {
+ ...event,
+ errors: errors.value.map((err) => ({
+ ...err,
+ id: inputs.value[err.path]
+ }))
+ }
+ emit('error', errorEvent)
}
- const submitEvent = event as FormSubmitEvent
- submitEvent.data = props.state
- emit('submit', event)
}
expose({
diff --git a/src/runtime/composables/useFormGroup.ts b/src/runtime/composables/useFormGroup.ts
index 2e54a8378a..e3c27d6043 100644
--- a/src/runtime/composables/useFormGroup.ts
+++ b/src/runtime/composables/useFormGroup.ts
@@ -1,22 +1,32 @@
-import { inject, ref, computed } from 'vue'
+import { inject, ref, computed, onMounted } from 'vue'
import { type UseEventBusReturn, useDebounceFn } from '@vueuse/core'
import type { FormEvent, FormEventType, InjectedFormGroupValue } from '../types/form'
type InputProps = {
id?: string
- size?: string
+ size?: string | number | symbol
color?: string
name?: string
}
export const useFormGroup = (inputProps?: InputProps, config?: any) => {
const formBus = inject | undefined>('form-events', undefined)
- const formGroup = inject('form-group', undefined)
+ const formGroup = inject('form-group', undefined)
+ const formInputs = inject('form-inputs', undefined)
- if (formGroup) {
- // Updates for="..." attribute on label if inputProps.id is provided
- formGroup.inputId.value = inputProps?.id ?? formGroup?.inputId.value
- }
+ const inputId = ref(inputProps?.id)
+
+ onMounted(() => {
+ inputId.value = inputProps?.id ?? formGroup?.inputId.value
+ if (formGroup) {
+ // Updates for="..." attribute on label if inputProps.id is provided
+ formGroup.inputId.value = inputId.value
+
+ if (formInputs) {
+ formInputs.value[formGroup.name.value] = inputId
+ }
+ }
+ })
const blurred = ref(false)
@@ -42,7 +52,7 @@ export const useFormGroup = (inputProps?: InputProps, config?: any) => {
}, 300)
return {
- inputId: computed(() => inputProps.id ?? formGroup?.inputId.value),
+ inputId,
name: computed(() => inputProps?.name ?? formGroup?.name.value),
size: computed(() => inputProps?.size ?? formGroup?.size.value ?? config?.default?.size),
color: computed(() => formGroup?.error?.value ? 'red' : inputProps?.color),
diff --git a/src/runtime/types/form.d.ts b/src/runtime/types/form.d.ts
index 69694a98c8..c9cc405524 100644
--- a/src/runtime/types/form.d.ts
+++ b/src/runtime/types/form.d.ts
@@ -1,10 +1,16 @@
-export interface FormError {
- path: string
+import { Ref } from 'vue'
+
+export interface FormError {
+ path: T
message: string
}
+export interface FormErrorWithId extends FormError {
+ id: string
+}
+
export interface Form {
- validate(path?: string, opts: { silent?: boolean }): Promise
+ validate(path?: string, opts?: { silent?: boolean }): Promise
clear(path?: string): void
errors: Ref
setErrors(errs: FormError[], path?: string): void
@@ -12,17 +18,18 @@ export interface Form {
}
export type FormSubmitEvent = SubmitEvent & { data: T }
+export type FormErrorEvent = SubmitEvent & { errors: FormErrorWithId[] }
export type FormEventType = 'blur' | 'input' | 'change' | 'submit'
export interface FormEvent {
type: FormEventType
- path: string
+ path?: string
}
export interface InjectedFormGroupValue {
- inputId: Ref
+ inputId: Ref
name: Ref
- size: Ref
+ size: Ref
error: Ref
}