@teamnovu/kit-vue-forms 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/PLAN.md +209 -0
- package/dist/composables/useField.d.ts +12 -0
- package/dist/composables/useFieldRegistry.d.ts +15 -0
- package/dist/composables/useForm.d.ts +10 -0
- package/dist/composables/useFormState.d.ts +7 -0
- package/dist/composables/useSubform.d.ts +5 -0
- package/dist/composables/useValidation.d.ts +30 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.mjs +414 -0
- package/dist/types/form.d.ts +41 -0
- package/dist/types/util.d.ts +26 -0
- package/dist/types/validation.d.ts +16 -0
- package/dist/utils/general.d.ts +2 -0
- package/dist/utils/path.d.ts +11 -0
- package/dist/utils/type-helpers.d.ts +3 -0
- package/dist/utils/validation.d.ts +3 -0
- package/dist/utils/zod.d.ts +3 -0
- package/package.json +41 -0
- package/src/composables/useField.ts +74 -0
- package/src/composables/useFieldRegistry.ts +53 -0
- package/src/composables/useForm.ts +54 -0
- package/src/composables/useFormData.ts +16 -0
- package/src/composables/useFormState.ts +21 -0
- package/src/composables/useSubform.ts +173 -0
- package/src/composables/useValidation.ts +227 -0
- package/src/index.ts +11 -0
- package/src/types/form.ts +58 -0
- package/src/types/util.ts +73 -0
- package/src/types/validation.ts +22 -0
- package/src/utils/general.ts +7 -0
- package/src/utils/path.ts +87 -0
- package/src/utils/type-helpers.ts +3 -0
- package/src/utils/validation.ts +66 -0
- package/src/utils/zod.ts +24 -0
- package/tests/formState.test.ts +138 -0
- package/tests/integration.test.ts +200 -0
- package/tests/nestedPath.test.ts +651 -0
- package/tests/path-utils.test.ts +159 -0
- package/tests/subform.test.ts +1348 -0
- package/tests/useField.test.ts +147 -0
- package/tests/useForm.test.ts +178 -0
- package/tests/useValidation.test.ts +216 -0
- package/tsconfig.json +18 -0
- package/vite.config.js +39 -0
- package/vitest.config.ts +14 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { computed, unref, type MaybeRef } from 'vue'
|
|
2
|
+
import type { Paths, PickProps, SplitPath } from '../types/util'
|
|
3
|
+
import type { ErrorBag, ValidationErrors } from '../types/validation'
|
|
4
|
+
|
|
5
|
+
export function splitPath(path: string): string[] {
|
|
6
|
+
if (path === '') {
|
|
7
|
+
return []
|
|
8
|
+
}
|
|
9
|
+
return path.split(/\s*\.\s*/).filter(Boolean)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function getNestedValue<T, K extends Paths<T>>(obj: T, path: K | SplitPath<K>) {
|
|
13
|
+
const splittedPath = Array.isArray(path) ? path : splitPath(path)
|
|
14
|
+
return splittedPath.reduce(
|
|
15
|
+
(current, key) => current?.[key],
|
|
16
|
+
obj as Record<string, never>,
|
|
17
|
+
) as PickProps<T, K>
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function setNestedValue<T, K extends Paths<T>>(obj: T, path: K | SplitPath<K>, value: PickProps<T, K>): void {
|
|
21
|
+
const keys = Array.isArray(path) ? path : splitPath(path)
|
|
22
|
+
if (keys.length === 0) {
|
|
23
|
+
throw new Error('Path cannot be empty')
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const lastKey = keys.at(-1)!
|
|
27
|
+
const target = keys
|
|
28
|
+
.slice(0, -1)
|
|
29
|
+
.reduce(
|
|
30
|
+
(current, key) => current[key],
|
|
31
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
32
|
+
obj as Record<string, any>,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
target[lastKey] = value
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const getLens = <T, K extends Paths<T>>(formData: MaybeRef<T>, key: MaybeRef<K | SplitPath<K>>) => {
|
|
39
|
+
return computed({
|
|
40
|
+
get() {
|
|
41
|
+
return getNestedValue(unref(formData), unref(key))
|
|
42
|
+
},
|
|
43
|
+
set(value: PickProps<T, K>) {
|
|
44
|
+
setNestedValue(unref(formData), unref(key), value)
|
|
45
|
+
},
|
|
46
|
+
})
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
type JoinPath<Base extends string, Sub extends string> = `${Base}${Base extends '' ? '' : Sub extends '' ? '' : '.'}${Sub}`
|
|
50
|
+
export function joinPath<Base extends string, Sub extends string>(basePath: Base, subPath: Sub): JoinPath<Base, Sub> {
|
|
51
|
+
if (!basePath && !subPath) {
|
|
52
|
+
return '' as JoinPath<Base, Sub>
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (!basePath && subPath) {
|
|
56
|
+
return subPath as JoinPath<Base, Sub>
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (!subPath && basePath) {
|
|
60
|
+
return basePath as JoinPath<Base, Sub>
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return `${basePath}.${subPath}` as JoinPath<Base, Sub>
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function filterErrorsForPath(errors: ErrorBag, path: string): ErrorBag {
|
|
67
|
+
// Handle empty path - return all errors
|
|
68
|
+
if (!path) {
|
|
69
|
+
return errors
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const pathPrefix = `${path}.`
|
|
73
|
+
const filteredPropertyErrors: Record<string, ValidationErrors> = Object.fromEntries(
|
|
74
|
+
Object.entries(errors.propertyErrors)
|
|
75
|
+
.filter(([errorPath]) => {
|
|
76
|
+
return errorPath.startsWith(pathPrefix)
|
|
77
|
+
})
|
|
78
|
+
.map(
|
|
79
|
+
([errorPath, errorMessages]) => [errorPath.slice(pathPrefix.length), errorMessages],
|
|
80
|
+
),
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
general: errors.general, // Keep general errors
|
|
85
|
+
propertyErrors: filteredPropertyErrors,
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { ErrorBag, ValidationErrors } from '../types/validation'
|
|
2
|
+
|
|
3
|
+
function deduplicate<T extends Array<unknown>>(arr: T): T {
|
|
4
|
+
return arr.filter(
|
|
5
|
+
(value, index, self) => self.indexOf(value) === index,
|
|
6
|
+
) as T
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function mergeErrorMessages(...msgs: ValidationErrors[]) {
|
|
10
|
+
return msgs.slice(1).reduce((acc, msg) => {
|
|
11
|
+
if (!acc && !msg) {
|
|
12
|
+
return undefined
|
|
13
|
+
}
|
|
14
|
+
const hasMsgErrors = (msg?.length ?? 0) > 0
|
|
15
|
+
if (!acc && (msg?.length ?? 0) > 0) {
|
|
16
|
+
return msg
|
|
17
|
+
}
|
|
18
|
+
if (!hasMsgErrors) {
|
|
19
|
+
return acc
|
|
20
|
+
}
|
|
21
|
+
const allMessages = (acc ?? []).concat(msg!)
|
|
22
|
+
return deduplicate(allMessages)
|
|
23
|
+
}, msgs[0] as ValidationErrors)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function mergePropertyErrors(...propertyErrors: Record<string, ValidationErrors>[]): Record<string, ValidationErrors> {
|
|
27
|
+
const allKeys = propertyErrors.map(errs => Object.keys(errs)).flat()
|
|
28
|
+
|
|
29
|
+
return allKeys.reduce((acc, key) => {
|
|
30
|
+
const values = propertyErrors.map(errs => errs[key]).filter(Boolean) as ValidationErrors[]
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
...acc,
|
|
34
|
+
[key]: mergeErrorMessages(...values),
|
|
35
|
+
}
|
|
36
|
+
}, {} as Record<string, ValidationErrors>)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function mergeErrors(...errorBags: ErrorBag[]): ErrorBag {
|
|
40
|
+
if (!errorBags.length) {
|
|
41
|
+
return {
|
|
42
|
+
general: [],
|
|
43
|
+
propertyErrors: {},
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const firstBag = errorBags[0]
|
|
48
|
+
|
|
49
|
+
if (errorBags.length === 1) {
|
|
50
|
+
return firstBag
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return errorBags.slice(1).reduce(
|
|
54
|
+
(acc, current) => ({
|
|
55
|
+
general: mergeErrorMessages(acc.general, current.general),
|
|
56
|
+
propertyErrors: mergePropertyErrors(acc.propertyErrors ?? {}, current.propertyErrors ?? {}),
|
|
57
|
+
}),
|
|
58
|
+
firstBag,
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function hasErrors(errorBag: ErrorBag): boolean {
|
|
63
|
+
const hasGeneralErrors = (errorBag.general?.length ?? 0) > 0
|
|
64
|
+
const hasPropertyErrors = Object.entries(errorBag.propertyErrors).filter(([, errors]) => errors?.length).length > 0
|
|
65
|
+
return hasGeneralErrors || hasPropertyErrors
|
|
66
|
+
}
|
package/src/utils/zod.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import z from 'zod'
|
|
2
|
+
import type { ErrorBag } from '../types/validation'
|
|
3
|
+
|
|
4
|
+
export function flattenError(error: z.ZodError): ErrorBag {
|
|
5
|
+
const general = error.issues
|
|
6
|
+
.filter(issue => issue.path.length === 0)
|
|
7
|
+
.map(issue => issue.message)
|
|
8
|
+
|
|
9
|
+
const propertyErrors = error.issues
|
|
10
|
+
.filter(issue => issue.path.length > 0)
|
|
11
|
+
.reduce((acc, issue) => {
|
|
12
|
+
const path = issue.path.join('.')
|
|
13
|
+
|
|
14
|
+
return {
|
|
15
|
+
...acc,
|
|
16
|
+
[path]: [...(acc[path] ?? []), issue.message],
|
|
17
|
+
}
|
|
18
|
+
}, {} as ErrorBag['propertyErrors'])
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
general,
|
|
22
|
+
propertyErrors,
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { reactive } from 'vue'
|
|
3
|
+
import { FieldRegistry, useFieldRegistry } from '../src/composables/useFieldRegistry'
|
|
4
|
+
import { useFormState } from '../src/composables/useFormState'
|
|
5
|
+
|
|
6
|
+
describe('useFormState', () => {
|
|
7
|
+
it('should detect dirty state when form data changes', () => {
|
|
8
|
+
const initialData = {
|
|
9
|
+
name: 'John',
|
|
10
|
+
age: 30,
|
|
11
|
+
}
|
|
12
|
+
const formData = reactive({ ...initialData })
|
|
13
|
+
const fields = {} as FieldRegistry<typeof initialData>
|
|
14
|
+
|
|
15
|
+
const formState = useFormState({
|
|
16
|
+
formData,
|
|
17
|
+
initialData,
|
|
18
|
+
}, fields)
|
|
19
|
+
|
|
20
|
+
expect(formState.isDirty.value).toBe(false)
|
|
21
|
+
|
|
22
|
+
formData.name = 'Jane'
|
|
23
|
+
expect(formState.isDirty.value).toBe(true)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('should not be dirty when data equals initial data', () => {
|
|
27
|
+
const initialData = {
|
|
28
|
+
name: 'John',
|
|
29
|
+
age: 30,
|
|
30
|
+
}
|
|
31
|
+
const formData = reactive({ ...initialData })
|
|
32
|
+
const fields = {} as FieldRegistry<typeof initialData>
|
|
33
|
+
|
|
34
|
+
const formState = useFormState({
|
|
35
|
+
formData,
|
|
36
|
+
initialData,
|
|
37
|
+
}, fields)
|
|
38
|
+
|
|
39
|
+
expect(formState.isDirty.value).toBe(false)
|
|
40
|
+
|
|
41
|
+
formData.name = 'Jane'
|
|
42
|
+
expect(formState.isDirty.value).toBe(true)
|
|
43
|
+
|
|
44
|
+
formData.name = 'John' // Back to initial
|
|
45
|
+
expect(formState.isDirty.value).toBe(false)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('should detect touched state when any field is touched', () => {
|
|
49
|
+
const data = {
|
|
50
|
+
name: 'John',
|
|
51
|
+
email: 'john@example.com',
|
|
52
|
+
}
|
|
53
|
+
const formData = reactive(data)
|
|
54
|
+
const fields = useFieldRegistry({
|
|
55
|
+
formData,
|
|
56
|
+
initialData: data,
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
const nameField = fields.defineField({ path: 'name' })
|
|
60
|
+
fields.defineField({ path: 'email' })
|
|
61
|
+
|
|
62
|
+
const formState = useFormState({
|
|
63
|
+
formData,
|
|
64
|
+
initialData: {
|
|
65
|
+
name: 'John',
|
|
66
|
+
email: 'john@example.com',
|
|
67
|
+
},
|
|
68
|
+
}, fields)
|
|
69
|
+
|
|
70
|
+
expect(formState.isTouched.value).toBe(false)
|
|
71
|
+
|
|
72
|
+
nameField.onBlur()
|
|
73
|
+
expect(formState.isTouched.value).toBe(true)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('should handle empty fields map', () => {
|
|
77
|
+
const data = { name: 'John' }
|
|
78
|
+
const formData = reactive(data)
|
|
79
|
+
const fields = useFieldRegistry({
|
|
80
|
+
formData,
|
|
81
|
+
initialData: data,
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
const formState = useFormState({
|
|
85
|
+
formData,
|
|
86
|
+
initialData: { name: 'John' },
|
|
87
|
+
}, fields)
|
|
88
|
+
|
|
89
|
+
expect(formState.isDirty.value).toBe(false)
|
|
90
|
+
expect(formState.isTouched.value).toBe(false)
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('should handle complex nested object changes', () => {
|
|
94
|
+
const initialData = {
|
|
95
|
+
user: {
|
|
96
|
+
name: 'John',
|
|
97
|
+
address: {
|
|
98
|
+
street: '123 Main St',
|
|
99
|
+
city: 'New York',
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
}
|
|
103
|
+
const formData = reactive(JSON.parse(JSON.stringify(initialData)))
|
|
104
|
+
const fields = useFieldRegistry({
|
|
105
|
+
formData,
|
|
106
|
+
initialData,
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
const formState = useFormState({
|
|
110
|
+
formData,
|
|
111
|
+
initialData,
|
|
112
|
+
}, fields)
|
|
113
|
+
|
|
114
|
+
expect(formState.isDirty.value).toBe(false)
|
|
115
|
+
|
|
116
|
+
formData.user.address.city = 'Boston'
|
|
117
|
+
expect(formState.isDirty.value).toBe(true)
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it('should handle array changes', () => {
|
|
121
|
+
const initialData = { tags: ['vue', 'typescript'] }
|
|
122
|
+
const formData = reactive({ tags: [...initialData.tags] })
|
|
123
|
+
const fields = useFieldRegistry({
|
|
124
|
+
formData,
|
|
125
|
+
initialData,
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
const formState = useFormState({
|
|
129
|
+
formData,
|
|
130
|
+
initialData,
|
|
131
|
+
}, fields)
|
|
132
|
+
|
|
133
|
+
expect(formState.isDirty.value).toBe(false)
|
|
134
|
+
|
|
135
|
+
formData.tags.push('forms')
|
|
136
|
+
expect(formState.isDirty.value).toBe(true)
|
|
137
|
+
})
|
|
138
|
+
})
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { nextTick, ref } from 'vue'
|
|
3
|
+
import { z } from 'zod'
|
|
4
|
+
import { useForm } from '../src/composables/useForm'
|
|
5
|
+
|
|
6
|
+
describe('Integration Tests', () => {
|
|
7
|
+
it('should handle complete form workflow', async () => {
|
|
8
|
+
const schema = z.object({
|
|
9
|
+
name: z.string().min(2, 'Name must be at least 2 characters'),
|
|
10
|
+
email: z.email('Invalid email'),
|
|
11
|
+
age: z.number().min(18, 'Must be 18 or older'),
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
const form = useForm({
|
|
15
|
+
initialData: {
|
|
16
|
+
name: '',
|
|
17
|
+
email: '',
|
|
18
|
+
age: 0,
|
|
19
|
+
},
|
|
20
|
+
schema,
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
// Create fields
|
|
24
|
+
const nameField = form.defineField({ path: 'name' })
|
|
25
|
+
const emailField = form.defineField({ path: 'email' })
|
|
26
|
+
const ageField = form.defineField({ path: 'age' })
|
|
27
|
+
|
|
28
|
+
// Initial state
|
|
29
|
+
expect(form.isDirty.value).toBe(false)
|
|
30
|
+
expect(form.isTouched.value).toBe(false)
|
|
31
|
+
expect(form.isValid.value).toBe(true) // No validation run yet
|
|
32
|
+
expect(form.isValidated.value).toBe(false)
|
|
33
|
+
|
|
34
|
+
// Fill out form
|
|
35
|
+
nameField.setValue('John')
|
|
36
|
+
emailField.setValue('john@example.com')
|
|
37
|
+
ageField.setValue(25)
|
|
38
|
+
|
|
39
|
+
// Form should be dirty now
|
|
40
|
+
expect(form.isDirty.value).toBe(true)
|
|
41
|
+
|
|
42
|
+
// Touch fields
|
|
43
|
+
nameField.onBlur()
|
|
44
|
+
emailField.onBlur()
|
|
45
|
+
|
|
46
|
+
// Form should be touched
|
|
47
|
+
expect(form.isTouched.value).toBe(true)
|
|
48
|
+
|
|
49
|
+
// Validate form
|
|
50
|
+
const result = await form.validateForm()
|
|
51
|
+
expect(result.isValid).toBe(true)
|
|
52
|
+
expect(form.isValidated.value).toBe(true)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('should handle form with validation errors', async () => {
|
|
56
|
+
const schema = z.object({
|
|
57
|
+
name: z.string().min(2),
|
|
58
|
+
email: z.string().email(),
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
const form = useForm({
|
|
62
|
+
initialData: {
|
|
63
|
+
name: 'A',
|
|
64
|
+
email: 'invalid',
|
|
65
|
+
},
|
|
66
|
+
schema,
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
const nameField = form.defineField({ path: 'name' })
|
|
70
|
+
const emailField = form.defineField({ path: 'email' })
|
|
71
|
+
|
|
72
|
+
// Validate with invalid data
|
|
73
|
+
const result = await form.validateForm()
|
|
74
|
+
expect(result.isValid).toBe(false)
|
|
75
|
+
expect(result.errors.propertyErrors.name).toBeDefined()
|
|
76
|
+
expect(result.errors.propertyErrors.email).toBeDefined()
|
|
77
|
+
|
|
78
|
+
// Fix the data
|
|
79
|
+
nameField.setValue('John')
|
|
80
|
+
emailField.setValue('john@example.com')
|
|
81
|
+
|
|
82
|
+
// Re-validate
|
|
83
|
+
const result2 = await form.validateForm()
|
|
84
|
+
expect(result2.isValid).toBe(true)
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('should handle reactive initial data changes', async () => {
|
|
88
|
+
const initialData = ref({
|
|
89
|
+
name: 'John',
|
|
90
|
+
age: 30,
|
|
91
|
+
})
|
|
92
|
+
const form = useForm({ initialData })
|
|
93
|
+
|
|
94
|
+
const nameField = form.defineField({ path: 'name' })
|
|
95
|
+
|
|
96
|
+
expect(nameField.value.value).toEqual('John')
|
|
97
|
+
expect(form.formData.value).toEqual({
|
|
98
|
+
name: 'John',
|
|
99
|
+
age: 30,
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
// Change initial data
|
|
103
|
+
initialData.value = {
|
|
104
|
+
name: 'Jane',
|
|
105
|
+
age: 25,
|
|
106
|
+
}
|
|
107
|
+
await nextTick()
|
|
108
|
+
|
|
109
|
+
expect(form.initialData.value).toEqual({
|
|
110
|
+
name: 'Jane',
|
|
111
|
+
age: 25,
|
|
112
|
+
})
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it('should handle nested object validation', async () => {
|
|
116
|
+
const schema = z.object({
|
|
117
|
+
user: z.object({
|
|
118
|
+
name: z.string().min(2),
|
|
119
|
+
contact: z.object({
|
|
120
|
+
email: z.string().email(),
|
|
121
|
+
}),
|
|
122
|
+
}),
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
const form = useForm({
|
|
126
|
+
initialData: {
|
|
127
|
+
user: {
|
|
128
|
+
name: 'A',
|
|
129
|
+
contact: {
|
|
130
|
+
email: 'invalid',
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
schema,
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
const result = await form.validateForm()
|
|
138
|
+
expect(result.isValid).toBe(false)
|
|
139
|
+
expect(result.errors.propertyErrors['user.name']).toBeDefined()
|
|
140
|
+
expect(result.errors.propertyErrors['user.contact.email']).toBeDefined()
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
it('should merge multiple validation sources', async () => {
|
|
144
|
+
const schema = z.object({
|
|
145
|
+
name: z.string().min(1),
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
const validateFn = async (data: { name: string }) => ({
|
|
149
|
+
isValid: data.name !== 'forbidden',
|
|
150
|
+
errors: {
|
|
151
|
+
general: data.name === 'forbidden' ? ['Forbidden name'] : [],
|
|
152
|
+
propertyErrors: {},
|
|
153
|
+
},
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
const externalErrors = ref({
|
|
157
|
+
general: ['External error'],
|
|
158
|
+
propertyErrors: { name: ['External field error'] },
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
const form = useForm({
|
|
162
|
+
initialData: { name: 'forbidden' },
|
|
163
|
+
schema,
|
|
164
|
+
validateFn,
|
|
165
|
+
errors: externalErrors,
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
const result = await form.validateForm()
|
|
169
|
+
expect(result.isValid).toBe(false)
|
|
170
|
+
|
|
171
|
+
// Should have errors from all sources
|
|
172
|
+
expect(result.errors.general).toContain('Forbidden name')
|
|
173
|
+
expect(result.errors.general).toContain('External error')
|
|
174
|
+
expect(result.errors.propertyErrors.name).toContain('External field error')
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
it('should handle field registry operations', () => {
|
|
178
|
+
const form = useForm({
|
|
179
|
+
initialData: {
|
|
180
|
+
name: 'John',
|
|
181
|
+
email: 'john@test.com',
|
|
182
|
+
},
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
// Define fields
|
|
186
|
+
const nameField = form.defineField({ path: 'name' })
|
|
187
|
+
form.defineField({ path: 'email' })
|
|
188
|
+
|
|
189
|
+
// Check registry
|
|
190
|
+
expect(form.getFields().length).toBe(2)
|
|
191
|
+
|
|
192
|
+
// Get specific field
|
|
193
|
+
const retrievedNameField = form.getField('name')
|
|
194
|
+
expect(retrievedNameField).toBe(nameField)
|
|
195
|
+
|
|
196
|
+
// Try to get non-existent field
|
|
197
|
+
const nonExistentField = form.getField('nonexistent')
|
|
198
|
+
expect(nonExistentField).toBeUndefined()
|
|
199
|
+
})
|
|
200
|
+
})
|