Skip to content

Commit 9825aad

Browse files
authored
Merge pull request #2 from jbaubree/feat/add-deep-strategy
feat: add deep strategy for error format
2 parents 09e843f + 9e5216b commit 9825aad

File tree

20 files changed

+760
-143
lines changed

20 files changed

+760
-143
lines changed

README.md

Lines changed: 69 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,9 @@ const {
7373
errorCount,
7474
clearErrors,
7575
getErrorMessage,
76+
errorPaths,
7677
focusFirstErroredInput,
78+
cleanup,
7779
} = useFormValidation(schema, form)
7880

7981
// Submit your form
@@ -95,6 +97,7 @@ const options = {
9597
// Custom validation logic
9698
return {} // Return errors if any
9799
},
100+
errorStrategy: 'flatten' // or 'deep'
98101
}
99102

100103
const { validate } = useFormValidation(schema, form, options)
@@ -104,9 +107,11 @@ const { validate } = useFormValidation(schema, form, options)
104107

105108
- validate(): Triggers the validation process.
106109
- clearErrors(): Resets the validation errors.
107-
- getErrorMessage(path: keyof F): Retrieves the error message for a specific field.
110+
- getErrorMessage(path: keyof F | string): Retrieves the error message for a specific field. Supports nested paths like "user.name".
111+
- errorPaths: Computed property that returns an array of all error paths, including nested ones (e.g., ["email", "user.name"]).
108112
- focusFirstErroredInput(): Focuses the first input with an error.
109-
- focusInput(inputName: keyof F): Focuses a specific input by its name.
113+
- focusInput(inputName: keyof F | string): Focuses a specific input by its name. Supports nested paths like "user.name".
114+
- cleanup(): Manually cleans up watchers, event listeners, and caches. Automatically called on component unmount when used inside a component context.
110115

111116
## API Reference
112117

@@ -117,6 +122,7 @@ declare function useFormValidation<S extends InputSchema<F>, F extends Form>(
117122
options?: {
118123
mode?: 'eager' | 'lazy' | 'agressive' | 'onBlur' // lazy by default
119124
transformFn?: GetErrorsFn<S, F>
125+
errorStrategy?: 'flatten' | 'deep'
120126
}
121127
): ReturnType<F>
122128
```
@@ -128,6 +134,7 @@ declare function useFormValidation<S extends InputSchema<F>, F extends Form>(
128134
- **options**: Optional configuration object.
129135
- **mode**: (optional) Validation mode (`'eager'` for immediate validation,`'agressive'` for validation on load, `'lazy'` for validation on form changes or `'onBlur'` for validation on input blur).
130136
- **transformFn**: (optional) A transformation function that can be used when integrating a different validation library. It allows you to transform data before it is validated. Use this option only if you are integrating another validation library that requires specific data handling.
137+
- **errorStrategy**: (optional) Error format mode (`'flatten'` user.name has an error, `'deep'` for { user: { name: 'name has an error' } }).
131138

132139
#### Return Value
133140

@@ -139,9 +146,67 @@ Returns an object containing the following properties:
139146
- `isLoading`: Reactive reference indicating if the form validation is in progress.
140147
- `errorCount`: Reactive reference to the number of errors.
141148
- `clearErrors`: Function to clear validation errors.
142-
- `getErrorMessage`: Function to get the error message for a specific field.
149+
- `getErrorMessage`: Function to get the error message for a specific field. Supports nested paths like "user.name".
150+
- `errorPaths`: Computed property that returns an array of all error paths, including nested ones (e.g., ["email", "user.name"]).
143151
- `focusFirstErroredInput`: Function to focus the first input with an error.
144-
- `focusInput`: Function to focus a specific input.
152+
- `focusInput`: Function to focus a specific input. Supports nested paths like "user.name".
153+
- `cleanup`: Function to manually clean up watchers, event listeners, and caches. This is automatically called when the component unmounts if used within a component context, but can be called manually when needed (e.g., when using the composable outside of a component or for manual cleanup).
154+
155+
## Deep Strategy Example
156+
157+
When using the `errorStrategy: 'deep'` option, you can work with nested form structures and automatically get all error paths:
158+
159+
```vue
160+
<script setup>
161+
import { ref } from 'vue'
162+
import { useFormValidation } from 'vue-use-form-validation'
163+
import * as z from 'zod'
164+
165+
const schema = z.object({
166+
user: z.object({
167+
name: z.string().min(1, 'Name is required'),
168+
}),
169+
email: z.string().email('Invalid email'),
170+
})
171+
172+
const form = ref({
173+
user: { name: '' },
174+
email: '',
175+
})
176+
177+
const {
178+
validate,
179+
hasError,
180+
errorPaths,
181+
getErrorMessage,
182+
focusInput,
183+
} = useFormValidation(schema, form, { errorStrategy: 'deep' })
184+
185+
async function onSubmit() {
186+
await validate()
187+
if (!isValid.value) {
188+
// errorPaths.value will contain ["user.name", "email"] when there are errors
189+
console.log('Error paths:', errorPaths.value)
190+
}
191+
}
192+
</script>
193+
194+
<template>
195+
<form @submit.prevent="onSubmit">
196+
<input v-model="form.user.name" name="user.name" placeholder="Name">
197+
<input v-model="form.email" name="email" placeholder="Email">
198+
199+
<!-- Display all errors with automatic deep path handling -->
200+
<div v-if="hasError">
201+
<div v-for="errorPath in errorPaths" :key="errorPath">
202+
<button @click="focusInput({ inputName: errorPath })">
203+
{{ getErrorMessage(errorPath) }}
204+
</button>
205+
</div>
206+
</div>
207+
</form>
208+
</template>
209+
```
145210

146211
## Example
147212

playground/src/App.vue

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,26 +7,34 @@ import * as z from 'zod'
77
const toast = useToast()
88
99
const schema = z.object({
10+
user: z.object({
11+
name: z.string().min(1),
12+
}),
1013
email: z.string().email(),
1114
password: z.string().min(8),
1215
})
1316
1417
const form = ref({
18+
user: {
19+
name: '',
20+
},
1521
email: '',
1622
password: '',
1723
})
1824
1925
const {
20-
errors,
2126
errorCount,
2227
hasError,
2328
isLoading,
2429
isValid,
2530
focusFirstErroredInput,
2631
focusInput,
2732
getErrorMessage,
33+
errorPaths,
2834
validate,
29-
} = useFormValidation(schema, form)
35+
clearErrors,
36+
cleanup,
37+
} = useFormValidation(schema, form, { errorStrategy: 'deep' })
3038
3139
async function onSubmit() {
3240
await validate()
@@ -42,28 +50,31 @@ async function onSubmit() {
4250
<UContainer class="py-5">
4351
<UText as="h1" label="Form example" class="mb-5" :ui="{ font: 'font-bold', size: 'text-2xl' }" />
4452
<form class="flex flex-col gap-3 mb-5">
53+
<UFormGroup label="User Name" :error="getErrorMessage('user.name')" is-required>
54+
<UInput v-model="form.user.name" name="user.name" type="text" placeholder="Enter your name" autofocus size="md" />
55+
</UFormGroup>
4556
<UFormGroup label="Email" :error="getErrorMessage('email')" is-required>
46-
<UInput v-model="form.email" name="email" type="email" placeholder="[email protected]" autofocus size="md" />
57+
<UInput v-model="form.email" name="email" type="email" placeholder="[email protected]" size="md" />
4758
</UFormGroup>
4859
<UFormGroup label="Password" :error="getErrorMessage('password')" is-required>
4960
<UInput v-model="form.password" name="password" type="password" placeholder="**********" size="md" />
5061
</UFormGroup>
5162
<UButton label="Submit" color="pilot" is-block :is-loading="isLoading" @click="onSubmit" />
63+
<UButton label="Clear errors" color="orange" is-block @click="clearErrors()" />
64+
<UButton label="Cleanup form validation" color="red" is-block @click="cleanup()" />
5265
</form>
5366
<UCard v-if="hasError" :ui="{ background: 'bg-red-200 dark:bg-red-600', body: { base: 'flex flex-col items-start gap-2' } }">
5467
<UText :label="`Form has ${errorCount} ${errorCount > 1 ? 'errors' : 'error'}:`" :ui="{ font: 'font-bold', size: 'text-lg' }" class="mb-1" />
5568
<div
56-
v-for="
57-
error, i in Object.keys(errors) as Array<keyof typeof errors>
58-
"
69+
v-for="errorPath, i in errorPaths"
5970
:key="i"
6071
class="flex items-center"
6172
>
6273
<UIcon name="icon-ph-dot-bold" color="dark" />
6374
<UButton
6475
class="ml-3" color="dark" :is-padded="false" variant="link"
65-
:label="getErrorMessage(error)"
66-
@click="focusInput({ inputName: error })"
76+
:label="getErrorMessage(errorPath)"
77+
@click="focusInput({ inputName: errorPath })"
6778
/>
6879
</div>
6980
</UCard>

src/errors.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,21 @@
11
import type { MaybeRefOrGetter } from 'vue'
2-
import type { FieldErrors, Form, GetErrorsFn, InputSchema } from './types'
2+
import type { ErrorStrategy, FieldErrors, Form, GetErrorsFn, InputSchema } from './types'
33
import { toValue } from 'vue'
44
import { validators } from './validators'
55

66
export async function getErrors<S extends InputSchema<F>, F extends Form>(
77
schema: S,
88
form: MaybeRefOrGetter<F>,
99
transformFn: GetErrorsFn<S, F> | null,
10+
errorStrategy: ErrorStrategy,
1011
): Promise<FieldErrors<F>> {
1112
const formValue = toValue(form)
1213
const schemaValue = toValue(schema)
1314
if (transformFn)
14-
return await transformFn(schemaValue, formValue)
15+
return await transformFn(schemaValue, formValue, errorStrategy)
1516
for (const validator of Object.values(validators)) {
1617
if (validator.check(schemaValue)) {
17-
return await validator.getErrors(schemaValue, formValue)
18+
return await validator.getErrors(schemaValue, formValue, errorStrategy)
1819
}
1920
}
2021
return {}

src/types.ts

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,24 @@ interface SuperstructSchema<F> extends AnyObject {
1919
export type Validator = 'Joi' | 'SuperStruct' | 'Valibot' | 'Yup' | 'Zod'
2020
export type ValidationMode = 'eager' | 'lazy' | 'agressive' | 'onBlur'
2121
export type Awaitable<T> = T | PromiseLike<T>
22-
export type FieldErrors<F> = Partial<Record<keyof F, string>>
22+
export type ErrorStrategy = 'flatten' | 'deep'
23+
24+
type DeepErrors<T> = {
25+
[K in keyof T]?: T[K] extends Record<string, any>
26+
? DeepErrors<T[K]>
27+
: string
28+
}
29+
export type FieldErrors<F, Strategy extends ErrorStrategy = ErrorStrategy> =
30+
Strategy extends 'flatten'
31+
? Partial<Record<keyof F, string>>
32+
: {
33+
[K in keyof F]?: F[K] extends Record<string, any>
34+
? DeepErrors<F[K]>
35+
: string
36+
}
37+
2338
export type Form = Record<string, unknown>
24-
export type GetErrorsFn<S, F extends Form> = (schema: S, form: F) => Awaitable<FieldErrors<F>>
39+
export type GetErrorsFn<S, F extends Form> = (schema: S, form: F, errorStrategy: ErrorStrategy) => Awaitable<FieldErrors<F>>
2540

2641
export type InputSchema<F extends Form> =
2742
| ZodSchema<F>
@@ -30,15 +45,17 @@ export type InputSchema<F extends Form> =
3045
| Schema<F>
3146
| SuperstructSchema<F>
3247

33-
export interface ReturnType<F> {
34-
validate: () => Promise<FieldErrors<F>>
35-
errors: Ref<FieldErrors<F>>
48+
export interface ReturnType<F, Strategy extends ErrorStrategy = ErrorStrategy> {
49+
validate: () => Promise<FieldErrors<F, Strategy>>
50+
errors: Ref<FieldErrors<F, Strategy>>
3651
errorCount: ComputedRef<number>
3752
isLoading: Ref<boolean>
3853
isValid: ComputedRef<boolean>
3954
hasError: ComputedRef<boolean>
4055
clearErrors: () => void
41-
getErrorMessage: (path: keyof F) => string | undefined
56+
getErrorMessage: (path: keyof F | string) => string | undefined
57+
errorPaths: ComputedRef<string[]>
4258
focusFirstErroredInput: () => void
43-
focusInput: (options: { inputName: keyof F }) => void
59+
focusInput: (options: { inputName: keyof F | string }) => void
60+
cleanup: () => void
4461
}

0 commit comments

Comments
 (0)