@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.
Files changed (45) hide show
  1. package/PLAN.md +209 -0
  2. package/dist/composables/useField.d.ts +12 -0
  3. package/dist/composables/useFieldRegistry.d.ts +15 -0
  4. package/dist/composables/useForm.d.ts +10 -0
  5. package/dist/composables/useFormState.d.ts +7 -0
  6. package/dist/composables/useSubform.d.ts +5 -0
  7. package/dist/composables/useValidation.d.ts +30 -0
  8. package/dist/index.d.ts +6 -0
  9. package/dist/index.mjs +414 -0
  10. package/dist/types/form.d.ts +41 -0
  11. package/dist/types/util.d.ts +26 -0
  12. package/dist/types/validation.d.ts +16 -0
  13. package/dist/utils/general.d.ts +2 -0
  14. package/dist/utils/path.d.ts +11 -0
  15. package/dist/utils/type-helpers.d.ts +3 -0
  16. package/dist/utils/validation.d.ts +3 -0
  17. package/dist/utils/zod.d.ts +3 -0
  18. package/package.json +41 -0
  19. package/src/composables/useField.ts +74 -0
  20. package/src/composables/useFieldRegistry.ts +53 -0
  21. package/src/composables/useForm.ts +54 -0
  22. package/src/composables/useFormData.ts +16 -0
  23. package/src/composables/useFormState.ts +21 -0
  24. package/src/composables/useSubform.ts +173 -0
  25. package/src/composables/useValidation.ts +227 -0
  26. package/src/index.ts +11 -0
  27. package/src/types/form.ts +58 -0
  28. package/src/types/util.ts +73 -0
  29. package/src/types/validation.ts +22 -0
  30. package/src/utils/general.ts +7 -0
  31. package/src/utils/path.ts +87 -0
  32. package/src/utils/type-helpers.ts +3 -0
  33. package/src/utils/validation.ts +66 -0
  34. package/src/utils/zod.ts +24 -0
  35. package/tests/formState.test.ts +138 -0
  36. package/tests/integration.test.ts +200 -0
  37. package/tests/nestedPath.test.ts +651 -0
  38. package/tests/path-utils.test.ts +159 -0
  39. package/tests/subform.test.ts +1348 -0
  40. package/tests/useField.test.ts +147 -0
  41. package/tests/useForm.test.ts +178 -0
  42. package/tests/useValidation.test.ts +216 -0
  43. package/tsconfig.json +18 -0
  44. package/vite.config.js +39 -0
  45. package/vitest.config.ts +14 -0
@@ -0,0 +1,147 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { ref, nextTick } from 'vue'
3
+ import { useField } from '../src/composables/useField'
4
+
5
+ describe('useField', () => {
6
+ it('should initialize field with path', () => {
7
+ const field = useField({ path: 'name' })
8
+
9
+ expect(field.path.value).toBe('name')
10
+ expect(field.touched.value).toBe(false)
11
+ expect(field.dirty.value).toBe(false)
12
+ })
13
+
14
+ it('should initialize field with value and initial value', () => {
15
+ const field = useField({
16
+ path: 'name',
17
+ value: 'John',
18
+ initialValue: 'John',
19
+ })
20
+
21
+ expect(field.value.value).toBe('John')
22
+ expect(field.initialValue.value).toBe('John')
23
+ expect(field.dirty.value).toBe(false)
24
+ })
25
+
26
+ it('should detect dirty state when value changes', () => {
27
+ const field = useField({
28
+ path: 'name',
29
+ value: 'John',
30
+ initialValue: 'John',
31
+ })
32
+
33
+ expect(field.dirty.value).toBe(false)
34
+
35
+ field.setValue('Jane')
36
+ expect(field.value.value).toBe('Jane')
37
+ expect(field.dirty.value).toBe(true)
38
+ })
39
+
40
+ it('should handle reactive initial value', async () => {
41
+ const initialValue = ref('John')
42
+ const field = useField({
43
+ path: 'name',
44
+ initialValue,
45
+ })
46
+
47
+ expect(field.initialValue.value).toBe('John')
48
+
49
+ initialValue.value = 'Jane'
50
+ await nextTick()
51
+
52
+ expect(field.initialValue.value).toBe('Jane')
53
+ })
54
+
55
+ it('should handle touched state', () => {
56
+ const field = useField({ path: 'name' })
57
+
58
+ expect(field.touched.value).toBe(false)
59
+
60
+ field.onBlur()
61
+ expect(field.touched.value).toBe(true)
62
+ })
63
+
64
+ it('should handle errors', () => {
65
+ const field = useField({
66
+ path: 'name',
67
+ errors: ['Required field'],
68
+ })
69
+
70
+ expect(field.errors.value).toEqual(['Required field'])
71
+
72
+ field.setErrors(['New error'])
73
+ expect(field.errors.value).toEqual(['New error'])
74
+
75
+ field.clearErrors()
76
+ expect(field.errors.value).toEqual([])
77
+ })
78
+
79
+ it('should handle reactive errors', async () => {
80
+ const errors = ref(['Initial error'])
81
+ const field = useField({
82
+ path: 'name',
83
+ errors,
84
+ })
85
+
86
+ expect(field.errors.value).toEqual(['Initial error'])
87
+
88
+ errors.value = ['Updated error']
89
+ await nextTick()
90
+
91
+ expect(field.errors.value).toEqual(['Updated error'])
92
+ })
93
+
94
+ it('should reset field to initial state', () => {
95
+ const field = useField({
96
+ path: 'name',
97
+ value: 'John',
98
+ initialValue: 'Initial',
99
+ })
100
+
101
+ field.setValue('Modified')
102
+ field.onBlur()
103
+ field.setErrors(['Error'])
104
+
105
+ expect(field.value.value).toBe('Modified')
106
+ expect(field.touched.value).toBe(true)
107
+ expect(field.errors.value).toEqual(['Error'])
108
+ expect(field.dirty.value).toBe(true)
109
+
110
+ field.reset()
111
+
112
+ expect(field.value.value).toBe('Initial')
113
+ expect(field.touched.value).toBe(false)
114
+ expect(field.errors.value).toEqual([])
115
+ expect(field.dirty.value).toBe(false)
116
+ })
117
+
118
+ it('should handle complex object values', () => {
119
+ const initialValue = { nested: { value: 'test' } }
120
+ const field = useField({
121
+ path: 'complex',
122
+ value: initialValue,
123
+ initialValue,
124
+ })
125
+
126
+ expect(field.value.value).toEqual(initialValue)
127
+ expect(field.dirty.value).toBe(false)
128
+
129
+ field.setValue({ nested: { value: 'changed' } })
130
+ expect(field.dirty.value).toBe(true)
131
+ })
132
+
133
+ it('should handle array values', () => {
134
+ const initialValue = ['a', 'b', 'c']
135
+ const field = useField({
136
+ path: 'array',
137
+ value: initialValue,
138
+ initialValue,
139
+ })
140
+
141
+ expect(field.value.value).toEqual(initialValue)
142
+ expect(field.dirty.value).toBe(false)
143
+
144
+ field.setValue(['a', 'b', 'c', 'd'])
145
+ expect(field.dirty.value).toBe(true)
146
+ })
147
+ })
@@ -0,0 +1,178 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { ref, nextTick } from 'vue'
3
+ import { useForm } from '../src/composables/useForm'
4
+ import { z } from 'zod'
5
+
6
+ describe('useForm', () => {
7
+ it('should initialize form with initial data', () => {
8
+ const initialData = {
9
+ name: 'John',
10
+ age: 30,
11
+ }
12
+ const form = useForm({ initialData })
13
+
14
+ expect(form.formData.value).toEqual(initialData)
15
+ expect(form.initialData.value).toEqual(initialData)
16
+ })
17
+
18
+ it('should initialize form with reactive initial data', async () => {
19
+ const initialData = ref({
20
+ name: 'John',
21
+ age: 30,
22
+ })
23
+ const form = useForm({ initialData })
24
+
25
+ expect(form.formData.value).toEqual({
26
+ name: 'John',
27
+ age: 30,
28
+ })
29
+
30
+ // Update reactive initial data
31
+ initialData.value = {
32
+ name: 'Jane',
33
+ age: 25,
34
+ }
35
+ await nextTick()
36
+
37
+ expect(form.initialData.value).toEqual({
38
+ name: 'Jane',
39
+ age: 25,
40
+ })
41
+ })
42
+
43
+ it('should have initial state values', () => {
44
+ const form = useForm({ initialData: { name: 'John' } })
45
+
46
+ expect(form.isDirty.value).toBe(false)
47
+ expect(form.isTouched.value).toBe(false)
48
+ expect(form.isValid.value).toBe(true)
49
+ expect(form.isValidated.value).toBe(false)
50
+ })
51
+
52
+ it('should define fields and auto-register them', () => {
53
+ const form = useForm({
54
+ initialData: {
55
+ name: 'John',
56
+ email: 'john@example.com',
57
+ },
58
+ })
59
+
60
+ const nameField = form.defineField({ path: 'name' })
61
+ const emailField = form.defineField({ path: 'email' })
62
+
63
+ expect(nameField.path.value).toBe('name')
64
+ expect(emailField.path.value).toBe('email')
65
+ expect(form.getFields().length).toBe(2)
66
+ })
67
+
68
+ it('should get registered fields', () => {
69
+ const form = useForm({
70
+ initialData: {
71
+ name: 'John',
72
+ email: 'john@example.com',
73
+ },
74
+ })
75
+
76
+ const nameField = form.defineField({ path: 'name' })
77
+ form.defineField({ path: 'email' })
78
+
79
+ const retrievedField = form.getField('name')
80
+ expect(retrievedField?.path.value).toBe('name')
81
+ expect(retrievedField).toBe(nameField)
82
+ })
83
+
84
+ it('should return undefined for non-existent fields', () => {
85
+ const form = useForm({ initialData: { name: 'John' } })
86
+
87
+ const field = form.getField('nonexistent')
88
+ expect(field).toBeUndefined()
89
+ })
90
+
91
+ it('should handle nested object initial data', () => {
92
+ const initialData = {
93
+ user: {
94
+ name: 'John',
95
+ address: {
96
+ street: '123 Main St',
97
+ city: 'New York',
98
+ },
99
+ },
100
+ }
101
+ const form = useForm({ initialData })
102
+
103
+ expect(form.formData.value).toEqual(initialData)
104
+ expect(form.initialData.value).toEqual(initialData)
105
+ })
106
+
107
+ it('should validate with schema', async () => {
108
+ const schema = z.object({
109
+ name: z.string().min(2),
110
+ age: z.number().min(18),
111
+ })
112
+
113
+ const form = useForm({
114
+ initialData: {
115
+ name: 'A',
116
+ age: 16,
117
+ },
118
+ schema,
119
+ })
120
+
121
+ const result = await form.validateForm()
122
+
123
+ expect(result.isValid).toBe(false)
124
+ expect(form.isValidated.value).toBe(true)
125
+ expect(form.errors.value.propertyErrors.name).toBeDefined()
126
+ expect(form.errors.value.propertyErrors.age).toBeDefined()
127
+ })
128
+
129
+ it('should validate with custom function', async () => {
130
+ const validateFn = async (data: { name: string }) => {
131
+ const errors = {
132
+ general: [],
133
+ propertyErrors: {} as Record<string, string[]>,
134
+ }
135
+
136
+ if (data.name.length < 2) {
137
+ errors.propertyErrors.name = ['Name too short']
138
+ }
139
+
140
+ return {
141
+ isValid: Object.keys(errors.propertyErrors).length === 0,
142
+ errors,
143
+ }
144
+ }
145
+
146
+ const form = useForm({
147
+ initialData: { name: 'A' },
148
+ validateFn,
149
+ })
150
+
151
+ const result = await form.validateForm()
152
+
153
+ expect(result.isValid).toBe(false)
154
+ expect(result.errors.propertyErrors.name).toEqual(['Name too short'])
155
+ })
156
+
157
+ it('should pass validation with valid data', async () => {
158
+ const schema = z.object({
159
+ name: z.string().min(2),
160
+ age: z.number().min(18),
161
+ })
162
+
163
+ const form = useForm({
164
+ initialData: {
165
+ name: 'John',
166
+ age: 30,
167
+ },
168
+ schema,
169
+ })
170
+
171
+ const result = await form.validateForm()
172
+
173
+ expect(result.isValid).toBe(true)
174
+ expect(form.isValidated.value).toBe(true)
175
+ expect(form.errors.value.general).toEqual([])
176
+ expect(form.errors.value.propertyErrors).toEqual({})
177
+ })
178
+ })
@@ -0,0 +1,216 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { nextTick, ref } from 'vue'
3
+ import { z } from 'zod'
4
+ import { SuccessValidationResult, useValidation } from '../src/composables/useValidation'
5
+ import { ErrorBag } from '../src/types/validation'
6
+ import { hasErrors } from '../src/utils/validation'
7
+
8
+ describe('useValidation', () => {
9
+ it('should initialize with no errors', () => {
10
+ const formState = { formData: { name: 'John' } }
11
+ const validation = useValidation(formState, {})
12
+
13
+ expect(validation.isValidated.value).toBe(false)
14
+ expect(validation.errors.value.general).toEqual([])
15
+ expect(validation.errors.value.propertyErrors).toEqual({})
16
+ })
17
+
18
+ it('should validate with Zod schema successfully', async () => {
19
+ const schema = z.object({
20
+ name: z.string().min(2),
21
+ age: z.number().min(18),
22
+ })
23
+
24
+ const formState = {
25
+ formData: {
26
+ name: 'John',
27
+ age: 30,
28
+ },
29
+ }
30
+ const validation = useValidation(formState, { schema })
31
+
32
+ const result = await validation.validateForm()
33
+
34
+ expect(result.isValid).toBe(true)
35
+ expect(validation.isValidated.value).toBe(true)
36
+ expect(result.errors.general).toEqual([])
37
+ expect(result.errors.propertyErrors).toEqual({})
38
+ })
39
+
40
+ it('should validate with Zod schema and return errors', async () => {
41
+ const schema = z.object({
42
+ name: z.string().min(2),
43
+ age: z.number().min(18),
44
+ })
45
+
46
+ const formState = {
47
+ formData: {
48
+ name: 'A',
49
+ age: 16,
50
+ },
51
+ }
52
+ const validation = useValidation(formState, { schema })
53
+
54
+ const result = await validation.validateForm()
55
+
56
+ expect(result.isValid).toBe(false)
57
+ expect(validation.isValidated.value).toBe(true)
58
+ expect(result.errors.propertyErrors.name).toBeDefined()
59
+ expect(result.errors.propertyErrors.age).toBeDefined()
60
+ })
61
+
62
+ it('should validate with custom validation function', async () => {
63
+ const validateFn = async (data: { name: string }) => {
64
+ const errors: ErrorBag = {
65
+ general: [],
66
+ propertyErrors: {} as Record<string, string[]>,
67
+ }
68
+
69
+ if (data.name.length < 2) {
70
+ errors.propertyErrors.name = ['Name too short']
71
+ }
72
+
73
+ if (data.name === 'admin') {
74
+ errors.general = ['Admin name not allowed']
75
+ }
76
+
77
+ return {
78
+ isValid: !hasErrors(errors),
79
+ errors,
80
+ }
81
+ }
82
+
83
+ const formState = { formData: { name: 'A' } }
84
+ const validation = useValidation(formState, { validateFn })
85
+
86
+ const result = await validation.validateForm()
87
+
88
+ expect(result.isValid).toBe(false)
89
+ expect(result.errors.propertyErrors.name).toEqual(['Name too short'])
90
+ })
91
+
92
+ it('should validate with both schema and function', async () => {
93
+ const schema = z.object({
94
+ name: z.string().min(1),
95
+ })
96
+
97
+ const validateFn = async (data: { name: string }) => ({
98
+ isValid: data.name !== 'forbidden',
99
+ errors: {
100
+ general: data.name === 'forbidden' ? ['Forbidden name'] : [],
101
+ propertyErrors: {},
102
+ },
103
+ })
104
+
105
+ const formState = { formData: { name: 'forbidden' } }
106
+ const validation = useValidation(formState, {
107
+ schema,
108
+ validateFn,
109
+ })
110
+
111
+ const result = await validation.validateForm()
112
+
113
+ expect(result.isValid).toBe(false)
114
+ expect(result.errors.general).toEqual(['Forbidden name'])
115
+ })
116
+
117
+ it('should handle reactive schema changes', async () => {
118
+ const schema = ref(z.object({
119
+ name: z.string().min(2),
120
+ }))
121
+
122
+ const formState = {
123
+ formData: {
124
+ name: 'A',
125
+ age: 25,
126
+ },
127
+ }
128
+ const validation = useValidation(formState, { schema })
129
+
130
+ // Initial validation
131
+ let result = await validation.validateForm()
132
+ expect(result.isValid).toBe(false)
133
+
134
+ // Change schema to be more permissive
135
+ schema.value = z.object({
136
+ name: z.string().min(1),
137
+ })
138
+
139
+ await nextTick()
140
+ result = await validation.validateForm()
141
+ expect(result.isValid).toBe(true)
142
+ })
143
+
144
+ it('should handle reactive validation function changes', async () => {
145
+ const strictValidation = async (data: { name: string }) => ({
146
+ isValid: data.name.length >= 5,
147
+ errors: {
148
+ general: [],
149
+ propertyErrors: data.name.length < 5 ? { name: ['Too short'] } : {},
150
+ },
151
+ })
152
+
153
+ const lenientValidation = async () => ({
154
+ isValid: true,
155
+ errors: {
156
+ general: [],
157
+ propertyErrors: {},
158
+ },
159
+ })
160
+
161
+ const validateFn = ref(strictValidation)
162
+ const formState = { formData: { name: 'John' } }
163
+ const validation = useValidation(formState, { validateFn })
164
+
165
+ // Initial validation with strict rules
166
+ let result = await validation.validateForm()
167
+ expect(result.isValid).toBe(false)
168
+
169
+ // Change to lenient validation
170
+ validateFn.value = lenientValidation
171
+ await nextTick()
172
+ result = await validation.validateForm()
173
+ expect(result.isValid).toBe(true)
174
+ })
175
+
176
+ it('should handle external error injection', async () => {
177
+ const errors = ref({
178
+ general: ['External error'],
179
+ propertyErrors: { name: ['External field error'] },
180
+ })
181
+
182
+ const formState = { formData: { name: 'John' } }
183
+ const validation = useValidation(formState, { errors })
184
+
185
+ await nextTick()
186
+
187
+ expect(validation.errors.value.general).toEqual(['External error'])
188
+ expect(validation.errors.value.propertyErrors.name).toEqual(['External field error'])
189
+ })
190
+
191
+ it('should merge validation errors with external errors', async () => {
192
+ const schema = z.object({
193
+ name: z.string().min(2),
194
+ })
195
+
196
+ const errors = ref<ErrorBag>(SuccessValidationResult.errors)
197
+
198
+ const formState = { formData: { name: 'A' } }
199
+ const validation = useValidation(formState, {
200
+ schema,
201
+ errors,
202
+ })
203
+
204
+ errors.value = {
205
+ general: ['External error'],
206
+ propertyErrors: { email: ['External email error'] },
207
+ }
208
+
209
+ const result = await validation.validateForm()
210
+
211
+ expect(result.isValid).toBe(false)
212
+ expect(result.errors.general).toEqual(['External error'])
213
+ expect(result.errors.propertyErrors.name).toEqual(['Too small: expected string to have >=2 characters']) // From schema validation
214
+ expect(result.errors.propertyErrors.email).toEqual(['External email error'])
215
+ })
216
+ })
package/tsconfig.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "dist",
5
+ "verbatimModuleSyntax": true,
6
+ "types": [
7
+ "vue",
8
+ "vite/client",
9
+ ],
10
+ },
11
+ "include": [
12
+ "src/**/*",
13
+ ],
14
+ "exclude": [
15
+ "dist",
16
+ "node_modules"
17
+ ]
18
+ }
package/vite.config.js ADDED
@@ -0,0 +1,39 @@
1
+ import vue from '@vitejs/plugin-vue'
2
+ import { resolve } from 'path'
3
+ import { defineConfig } from 'vite'
4
+ import dts from 'vite-plugin-dts'
5
+ import pkg from './package.json'
6
+
7
+ export default defineConfig({
8
+ plugins: [
9
+ vue(),
10
+ dts({
11
+ staticImport: true,
12
+ }),
13
+ ],
14
+ build: {
15
+ emptyOutDir: false,
16
+ lib: {
17
+ formats: ['es'],
18
+ // Could also be a dictionary or array of multiple entry points
19
+ entry: {
20
+ index: resolve(__dirname, 'src/index.ts'),
21
+ },
22
+ },
23
+ rollupOptions: {
24
+ // make sure to externalize deps that shouldn't be bundled
25
+ // into your library
26
+ external: [
27
+ ...Object.keys(pkg.dependencies ?? {}),
28
+ ...Object.keys(pkg.peerDependencies ?? {}),
29
+ ],
30
+ output: {
31
+ // Provide global variables to use in the UMD build
32
+ // for externalized deps
33
+ globals: {
34
+ vue: 'Vue',
35
+ },
36
+ },
37
+ },
38
+ },
39
+ })
@@ -0,0 +1,14 @@
1
+ import { defineConfig } from 'vitest/config'
2
+ import { resolve } from 'path'
3
+
4
+ export default defineConfig({
5
+ test: {
6
+ environment: 'happy-dom',
7
+ globals: true,
8
+ },
9
+ resolve: {
10
+ alias: {
11
+ '@': resolve(__dirname, './src'),
12
+ },
13
+ },
14
+ })