diff --git a/docs/content/docs/2.components/form-field.md b/docs/content/docs/2.components/form-field.md
index da1a402f00..3d0932a1ca 100644
--- a/docs/content/docs/2.components/form-field.md
+++ b/docs/content/docs/2.components/form-field.md
@@ -116,6 +116,33 @@ slots:
:u-input{placeholder="Enter your email" class="w-full"}
::
+### Multiple fields
+
+You can pass an array of names to the `name` prop to track errors for multiple fields. This is useful when you have a complex field that is composed of multiple inputs.
+Make sure you're passing manually name to fields inside the form field to link fields to state (and then errors accordingly).
+Error messages of fields inside the form field will be displayed in the form field itself.
+
+::component-code
+---
+prettier: true
+props:
+ label: Duration
+ name: ['duration.value', 'duration.unit']
+slots:
+ default: |
+
+
+
+
+
+---
+
+
+
+
+
+::
+
### Error
Use the `error` prop to display an error message below the form control. When used together with the `help` prop, the `error` prop takes precedence.
diff --git a/docs/content/docs/2.components/form.md b/docs/content/docs/2.components/form.md
index 4bd2642520..821d29c7d1 100644
--- a/docs/content/docs/2.components/form.md
+++ b/docs/content/docs/2.components/form.md
@@ -76,6 +76,8 @@ It requires two props:
Errors are reported directly to the [FormField](/docs/components/form-field) component based on the `name` or `error-pattern` prop. This means the validation rules defined for the `email` attribute in your schema will be applied to ``{lang="vue"}.
+You can also [pass an array of names to the `name` prop](/docs/components/form-field#multiple-fields) to track errors for multiple fields. This is useful when you have a complex field that is composed of multiple inputs.
+
Nested validation rules are handled using dot notation. For example, a rule like `{ user: z.object({ email: z.string() }) }`{lang="ts"} will be applied to ``{lang="vue"}.
### Custom validation
diff --git a/playgrounds/nuxt/app/pages/components/form.vue b/playgrounds/nuxt/app/pages/components/form.vue
index 9e88f0705d..92127cc388 100644
--- a/playgrounds/nuxt/app/pages/components/form.vue
+++ b/playgrounds/nuxt/app/pages/components/form.vue
@@ -7,8 +7,10 @@ import FormExampleNested from '../../../../../docs/app/components/content/exampl
const schema = z.object({
email: z.email(),
- password: z.string('Password is required').min(8),
- tos: z.literal(true)
+ password: z.string().min(8, 'Password must be at least 8 characters'),
+ tos: z.literal(true),
+ duration: z.number().min(1),
+ durationUnit: z.enum(['min', 'h'])
})
type Schema = z.input
@@ -42,6 +44,13 @@ const disabled = ref(false)
+
+
+
+
+
+
+
diff --git a/src/runtime/components/FormField.vue b/src/runtime/components/FormField.vue
index ffe5381497..bbc3b69253 100644
--- a/src/runtime/components/FormField.vue
+++ b/src/runtime/components/FormField.vue
@@ -13,7 +13,7 @@ export interface FormFieldProps {
*/
as?: any
/** The name of the FormField. Also used to match form errors. */
- name?: string
+ name?: string | string[]
/** A regular expression to match form error names. */
errorPattern?: RegExp
label?: string
@@ -60,6 +60,8 @@ const slots = defineSlots()
const appConfig = useAppConfig() as FormField['AppConfig']
+const name = props.name ? (Array.isArray(props.name) ? props.name : [props.name]) : []
+
const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.formField || {}) })({
size: props.size,
required: props.required
@@ -67,7 +69,20 @@ const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.formField ||
const formErrors = inject[ | null>(formErrorsInjectionKey, null)
-const error = computed(() => props.error || formErrors?.value?.find(error => error.name === props.name || (props.errorPattern && error.name?.match(props.errorPattern)))?.message)
+const error = computed(() => {
+ if (props.error) return props.error
+ if (!formErrors?.value) return undefined
+
+ function matchPattern(errorName: string) {
+ return props.errorPattern && errorName?.match(props.errorPattern)
+ }
+
+ return formErrors.value.find(error => (
+ !error.name // if no name is provided, it matches any error
+ || name.includes(error.name)
+ || matchPattern(error.name)
+ ))?.message
+})
const id = ref(useId())
// Copies id's initial value to bind aria-attributes such as aria-describedby.
@@ -75,11 +90,17 @@ const id = ref(useId())
const ariaId = id.value
const formInputs = inject(formInputsInjectionKey, undefined)
-watch(id, () => {
- if (formInputs && props.name) {
- formInputs.value[props.name] = { id: id.value, pattern: props.errorPattern }
- }
-}, { immediate: true })
+watch(
+ id,
+ () => {
+ if (formInputs && name?.length > 0) {
+ name.forEach((name) => {
+ formInputs.value[name] = { id: id.value, pattern: props.errorPattern }
+ })
+ }
+ },
+ { immediate: true }
+)
provide(inputIdInjectionKey, id)
diff --git a/src/runtime/composables/useFormField.ts b/src/runtime/composables/useFormField.ts
index 354c6772ec..523dfbf49f 100644
--- a/src/runtime/composables/useFormField.ts
+++ b/src/runtime/composables/useFormField.ts
@@ -43,6 +43,12 @@ export function useFormField(props?: Props, opts?: { bind?: boolean, defer
}
}
+ const fieldName = computed(() => {
+ const name = props?.name ?? formField?.value.name
+ if (Array.isArray(name)) return undefined
+ return name
+ })
+
function emitFormEvent(type: FormInputEvents, name?: string, eager?: boolean) {
if (formBus && formField && name) {
formBus.emit({ type, name, eager })
@@ -50,27 +56,30 @@ export function useFormField(props?: Props, opts?: { bind?: boolean, defer
}
function emitFormBlur() {
- emitFormEvent('blur', formField?.value.name)
+ emitFormEvent('blur', fieldName.value)
}
function emitFormFocus() {
- emitFormEvent('focus', formField?.value.name)
+ emitFormEvent('focus', fieldName.value)
}
function emitFormChange() {
- emitFormEvent('change', formField?.value.name)
+ emitFormEvent('change', fieldName.value)
}
const emitFormInput = useDebounceFn(
() => {
- emitFormEvent('input', formField?.value.name, !opts?.deferInputValidation || formField?.value.eagerValidation)
+ emitFormEvent('input', fieldName.value, !opts?.deferInputValidation || formField?.value.eagerValidation)
},
formField?.value.validateOnInputDelay ?? formOptions?.value.validateOnInputDelay ?? 0
)
return {
id: computed(() => props?.id ?? inputId?.value),
- name: computed(() => props?.name ?? formField?.value.name),
+ name: computed(() => {
+ const name = props?.name ?? formField?.value.name
+ return Array.isArray(name) ? undefined : name
+ }),
size: computed(() => props?.size ?? formField?.value.size),
color: computed(() => formField?.value.error ? 'error' : props?.color),
highlight: computed(() => formField?.value.error ? true : props?.highlight),
diff --git a/src/runtime/types/form.ts b/src/runtime/types/form.ts
index e86f3e1b45..1d8c928b89 100644
--- a/src/runtime/types/form.ts
+++ b/src/runtime/types/form.ts
@@ -88,7 +88,7 @@ export interface FormInjectedOptions {
}
export interface FormFieldInjectedOptions {
- name?: string
+ name?: string | string[]
size?: GetObjectField
error?: string | boolean
eagerValidation?: boolean
]