@teamnovu/kit-vue-forms 0.2.17 → 0.3.0
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/CHANGELOG.md +41 -0
- package/dist/components/Field.vue.d.ts +4 -1
- package/dist/components/FormFieldWrapper.vue.d.ts +1 -3
- package/dist/composables/useFieldRegistry.d.ts +2 -1
- package/dist/composables/useInitialDataOverride.d.ts +13 -0
- package/dist/composables/useSubform.d.ts +2 -2
- package/dist/index.js +503 -440
- package/dist/types/form.d.ts +15 -1
- package/dist/utils/path.d.ts +1 -0
- package/dist/utils/submitHandler.d.ts +1 -1
- package/docs/reference.md +112 -0
- package/package.json +1 -1
- package/src/components/FormFieldWrapper.vue +2 -0
- package/src/composables/useField.ts +10 -19
- package/src/composables/useFieldArray.ts +16 -0
- package/src/composables/useFieldRegistry.ts +33 -10
- package/src/composables/useForm.ts +5 -3
- package/src/composables/useInitialDataOverride.ts +130 -0
- package/src/composables/useSubform.ts +8 -4
- package/src/composables/useValidation.ts +72 -26
- package/src/types/form.ts +23 -6
- package/src/types/util.ts +0 -1
- package/src/utils/path.ts +11 -0
- package/src/utils/submitHandler.ts +1 -1
- package/src/utils/zod.ts +11 -8
- package/tests/formState.test.ts +6 -3
- package/tests/initialDataOverride.test.ts +479 -0
- package/tests/path-utils.test.ts +0 -1
- package/tests/useFieldArray.test.ts +112 -0
|
@@ -1,7 +1,26 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
computed,
|
|
3
|
+
getCurrentScope,
|
|
4
|
+
isRef,
|
|
5
|
+
onBeforeUnmount,
|
|
6
|
+
reactive,
|
|
7
|
+
ref,
|
|
8
|
+
toRefs,
|
|
9
|
+
toValue,
|
|
10
|
+
unref,
|
|
11
|
+
watch,
|
|
12
|
+
type MaybeRef,
|
|
13
|
+
type MaybeRefOrGetter,
|
|
14
|
+
type Ref,
|
|
15
|
+
} from 'vue'
|
|
16
|
+
import type z from 'zod'
|
|
3
17
|
import type { FormDataDefault } from '../types/form'
|
|
4
|
-
import type {
|
|
18
|
+
import type {
|
|
19
|
+
ErrorBag,
|
|
20
|
+
ValidationFunction,
|
|
21
|
+
ValidationResult,
|
|
22
|
+
Validator,
|
|
23
|
+
} from '../types/validation'
|
|
5
24
|
import { hasErrors, isValidResult, mergeErrors } from '../utils/validation'
|
|
6
25
|
import { flattenError } from '../utils/zod'
|
|
7
26
|
|
|
@@ -35,7 +54,8 @@ export interface ValidatorOptions<T, TOut = T> {
|
|
|
35
54
|
validateFn?: MaybeRef<ValidationFunction<T, TOut> | undefined>
|
|
36
55
|
}
|
|
37
56
|
|
|
38
|
-
export interface ValidationOptions<T, TOut = T>
|
|
57
|
+
export interface ValidationOptions<T, TOut = T>
|
|
58
|
+
extends ValidatorOptions<T, TOut> {
|
|
39
59
|
errors?: MaybeRef<ErrorBag | undefined>
|
|
40
60
|
validationBeforeSubmit?: ValidationFlags
|
|
41
61
|
validationAfterSubmit?: ValidationFlags
|
|
@@ -48,7 +68,8 @@ export const SuccessValidationResult: ValidationResult<never> = {
|
|
|
48
68
|
},
|
|
49
69
|
}
|
|
50
70
|
|
|
51
|
-
class ZodSchemaValidator<T extends FormDataDefault, TOut = T>
|
|
71
|
+
class ZodSchemaValidator<T extends FormDataDefault, TOut = T>
|
|
72
|
+
implements Validator<T, TOut> {
|
|
52
73
|
constructor(private schema?: z.ZodType<TOut, T>) {}
|
|
53
74
|
|
|
54
75
|
async validate(data: T): Promise<ValidationResult<TOut>> {
|
|
@@ -74,7 +95,8 @@ class ZodSchemaValidator<T extends FormDataDefault, TOut = T> implements Validat
|
|
|
74
95
|
}
|
|
75
96
|
}
|
|
76
97
|
|
|
77
|
-
class FunctionValidator<T extends FormDataDefault, TOut = T>
|
|
98
|
+
class FunctionValidator<T extends FormDataDefault, TOut = T>
|
|
99
|
+
implements Validator<T, TOut> {
|
|
78
100
|
constructor(private validateFn?: ValidationFunction<T, TOut>) {}
|
|
79
101
|
|
|
80
102
|
async validate(data: T): Promise<ValidationResult<TOut>> {
|
|
@@ -101,7 +123,8 @@ class FunctionValidator<T extends FormDataDefault, TOut = T> implements Validato
|
|
|
101
123
|
}
|
|
102
124
|
}
|
|
103
125
|
|
|
104
|
-
class CombinedValidator<T extends FormDataDefault, TOut = T>
|
|
126
|
+
class CombinedValidator<T extends FormDataDefault, TOut = T>
|
|
127
|
+
implements Validator<T, TOut> {
|
|
105
128
|
private schemaValidator: ZodSchemaValidator<T, TOut>
|
|
106
129
|
private functionValidator: FunctionValidator<T, TOut>
|
|
107
130
|
|
|
@@ -129,10 +152,13 @@ class CombinedValidator<T extends FormDataDefault, TOut = T> implements Validato
|
|
|
129
152
|
export function createValidator<T extends FormDataDefault, TOut = T>(
|
|
130
153
|
options: ValidatorOptions<T, TOut>,
|
|
131
154
|
): Ref<Validator<T, TOut> | undefined> {
|
|
132
|
-
return computed(
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
155
|
+
return computed(
|
|
156
|
+
() =>
|
|
157
|
+
new CombinedValidator(
|
|
158
|
+
unref(options.schema) as z.ZodType<TOut, T>,
|
|
159
|
+
unref(options.validateFn) as ValidationFunction<T, TOut>,
|
|
160
|
+
),
|
|
161
|
+
)
|
|
136
162
|
}
|
|
137
163
|
|
|
138
164
|
export function useValidation<T extends FormDataDefault, TOut = T>(
|
|
@@ -145,20 +171,29 @@ export function useValidation<T extends FormDataDefault, TOut = T>(
|
|
|
145
171
|
errors: unref(options.errors) ?? SuccessValidationResult.errors,
|
|
146
172
|
})
|
|
147
173
|
|
|
148
|
-
const updateErrors = (
|
|
149
|
-
|
|
174
|
+
const updateErrors = (
|
|
175
|
+
newErrors: ErrorBag = SuccessValidationResult.errors,
|
|
176
|
+
) => {
|
|
177
|
+
validationState.errors = mergeErrors(
|
|
178
|
+
unref(options.errors) ?? SuccessValidationResult.errors,
|
|
179
|
+
newErrors,
|
|
180
|
+
)
|
|
150
181
|
}
|
|
151
182
|
|
|
152
183
|
// Watch for changes in the error bag and update validation state
|
|
153
|
-
watch(
|
|
154
|
-
|
|
155
|
-
|
|
184
|
+
watch(
|
|
185
|
+
() => unref(options.errors),
|
|
186
|
+
async () => {
|
|
187
|
+
if (validationState.isValidated) {
|
|
188
|
+
const validationResults = await getValidationResults()
|
|
156
189
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
190
|
+
updateErrors(validationResults.errors)
|
|
191
|
+
} else {
|
|
192
|
+
updateErrors()
|
|
193
|
+
}
|
|
194
|
+
},
|
|
195
|
+
{ immediate: true },
|
|
196
|
+
)
|
|
162
197
|
|
|
163
198
|
// Watch for changes in validation function or schema
|
|
164
199
|
// to trigger validation. Only run if validation is already validated.
|
|
@@ -187,11 +222,15 @@ export function useValidation<T extends FormDataDefault, TOut = T>(
|
|
|
187
222
|
})
|
|
188
223
|
|
|
189
224
|
const defineValidator = <TData extends T, TDataOut extends TOut>(
|
|
190
|
-
options:
|
|
225
|
+
options:
|
|
226
|
+
| ValidatorOptions<TData, TDataOut>
|
|
227
|
+
| Ref<Validator<TData, TDataOut>>,
|
|
191
228
|
) => {
|
|
192
229
|
const validator = isRef(options) ? options : createValidator(options)
|
|
193
230
|
|
|
194
|
-
validationState.validators.push(
|
|
231
|
+
validationState.validators.push(
|
|
232
|
+
validator as Ref<Validator<T, TOut> | undefined>,
|
|
233
|
+
)
|
|
195
234
|
|
|
196
235
|
if (getCurrentScope()) {
|
|
197
236
|
onBeforeUnmount(() => {
|
|
@@ -240,7 +279,9 @@ export function useValidation<T extends FormDataDefault, TOut = T>(
|
|
|
240
279
|
}
|
|
241
280
|
}
|
|
242
281
|
|
|
243
|
-
const validateField = async (
|
|
282
|
+
const validateField = async (
|
|
283
|
+
path: string,
|
|
284
|
+
): Promise<ValidationResult<TOut>> => {
|
|
244
285
|
const validationResults = await getValidationResults()
|
|
245
286
|
|
|
246
287
|
updateErrors({
|
|
@@ -272,7 +313,10 @@ export function useValidation<T extends FormDataDefault, TOut = T>(
|
|
|
272
313
|
return toValue(flags?.[flag] ?? false)
|
|
273
314
|
}
|
|
274
315
|
|
|
275
|
-
const validateStrategy = <K extends keyof ValidationFlags>(
|
|
316
|
+
const validateStrategy = <K extends keyof ValidationFlags>(
|
|
317
|
+
flag: K,
|
|
318
|
+
path: string,
|
|
319
|
+
) => {
|
|
276
320
|
if (!canValidate(flag)) {
|
|
277
321
|
return
|
|
278
322
|
}
|
|
@@ -292,4 +336,6 @@ export function useValidation<T extends FormDataDefault, TOut = T>(
|
|
|
292
336
|
}
|
|
293
337
|
}
|
|
294
338
|
|
|
295
|
-
export type ValidationState<T extends FormDataDefault, TOut = T> = ReturnType<
|
|
339
|
+
export type ValidationState<T extends FormDataDefault, TOut = T> = ReturnType<
|
|
340
|
+
typeof useValidation<T, TOut>
|
|
341
|
+
>
|
package/src/types/form.ts
CHANGED
|
@@ -5,11 +5,11 @@ import type { SubformOptions } from '../composables/useSubform'
|
|
|
5
5
|
import type { ValidatorOptions } from '../composables/useValidation'
|
|
6
6
|
import type { EntityPaths, Paths, PickEntity, PickProps } from './util'
|
|
7
7
|
import type {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
8
|
+
ErrorBag,
|
|
9
|
+
ValidationErrorMessage,
|
|
10
|
+
ValidationErrors,
|
|
11
|
+
ValidationResult,
|
|
12
|
+
Validator,
|
|
13
13
|
} from './validation'
|
|
14
14
|
|
|
15
15
|
export type FormDataDefault = object
|
|
@@ -29,6 +29,13 @@ export interface FieldItem<Item, Path extends string> {
|
|
|
29
29
|
export interface FieldArray<Item, Path extends string> {
|
|
30
30
|
items: ShallowRef<FieldItem<Item, Path>[]>
|
|
31
31
|
push: (item: Item) => FieldItem<Item, Path>
|
|
32
|
+
/**
|
|
33
|
+
* Pushes a new item and anchors its subtree as the baseline for that index
|
|
34
|
+
* (subtree-scoped `setInitialData`). The new item's subfields are clean from
|
|
35
|
+
* the moment they're registered, while the array field itself stays dirty
|
|
36
|
+
* because its baseline still reflects the external `initialData`.
|
|
37
|
+
*/
|
|
38
|
+
pushPristine: (item: Item) => FieldItem<Item, Path>
|
|
32
39
|
remove: (id: string) => void
|
|
33
40
|
insert: (item: Item, index: number) => FieldItem<Item, Path>
|
|
34
41
|
field: FormField<Item[], Path>
|
|
@@ -45,8 +52,18 @@ export interface FormField<T, P extends string> {
|
|
|
45
52
|
/**
|
|
46
53
|
* Sets the initial data for the field. If the field is not dirty, it also updates the current data.
|
|
47
54
|
* @param newData - The new initial data to set.
|
|
55
|
+
* @param options - Optional. Pass `{ replace: true }` to replace the subtree entirely instead of deep-merging.
|
|
56
|
+
* Pass `{ scope: 'subtree' }` to anchor the baseline only for this field and its descendants — ancestors
|
|
57
|
+
* continue to read from the external initialData and stay dirty if the override changed the tree shape.
|
|
58
|
+
* Defaults to `{ scope: 'tree' }`, which makes the override visible to ancestors as well.
|
|
48
59
|
*/
|
|
49
|
-
setInitialData: (
|
|
60
|
+
setInitialData: (
|
|
61
|
+
newData: T,
|
|
62
|
+
options?: {
|
|
63
|
+
replace?: boolean
|
|
64
|
+
scope?: 'tree' | 'subtree'
|
|
65
|
+
},
|
|
66
|
+
) => void
|
|
50
67
|
onBlur: () => void
|
|
51
68
|
onFocus: () => void
|
|
52
69
|
reset: () => void
|
package/src/types/util.ts
CHANGED
package/src/utils/path.ts
CHANGED
|
@@ -103,6 +103,17 @@ export function joinPath<Base extends string, Sub extends string>(
|
|
|
103
103
|
return `${basePath}.${subPath}` as JoinPath<Base, Sub>
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
+
export function dropOverridesAtAndBelow(
|
|
107
|
+
overrides: Map<string, unknown>,
|
|
108
|
+
path: string,
|
|
109
|
+
): void {
|
|
110
|
+
for (const key of [...overrides.keys()]) {
|
|
111
|
+
if (key === path || path === '' || key.startsWith(path + '.')) {
|
|
112
|
+
overrides.delete(key)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
106
117
|
export function filterErrorsForPath(errors: ErrorBag, path: string): ErrorBag {
|
|
107
118
|
// Handle empty path - return all errors
|
|
108
119
|
if (!path) {
|
|
@@ -5,7 +5,7 @@ import { isValidResult } from './validation'
|
|
|
5
5
|
|
|
6
6
|
export function makeSubmitHandler<T extends FormDataDefault, TOut = T>(
|
|
7
7
|
form: Form<T, TOut>,
|
|
8
|
-
validationState: ValidationState<T, TOut>,
|
|
8
|
+
validationState: Pick<ValidationState<T, TOut>, 'canValidate'>,
|
|
9
9
|
) {
|
|
10
10
|
return (onSubmit: (data: TOut) => Awaitable<void>) => {
|
|
11
11
|
return async (event?: SubmitEvent) => {
|
package/src/utils/zod.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import z from 'zod'
|
|
1
|
+
import type z from 'zod'
|
|
2
2
|
import type { ErrorBag } from '../types/validation'
|
|
3
3
|
|
|
4
4
|
export function flattenError(error: z.ZodError): ErrorBag {
|
|
@@ -8,14 +8,17 @@ export function flattenError(error: z.ZodError): ErrorBag {
|
|
|
8
8
|
|
|
9
9
|
const propertyErrors = error.issues
|
|
10
10
|
.filter(issue => issue.path.length > 0)
|
|
11
|
-
.reduce(
|
|
12
|
-
|
|
11
|
+
.reduce(
|
|
12
|
+
(acc, issue) => {
|
|
13
|
+
const path = issue.path.join('.')
|
|
13
14
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
return {
|
|
16
|
+
...acc,
|
|
17
|
+
[path]: [...(acc[path] ?? []), issue.message],
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
{} as ErrorBag['propertyErrors'],
|
|
21
|
+
)
|
|
19
22
|
|
|
20
23
|
return {
|
|
21
24
|
general,
|
package/tests/formState.test.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest'
|
|
2
|
-
import { reactive } from 'vue'
|
|
2
|
+
import { reactive, toRef } from 'vue'
|
|
3
3
|
import { useFieldRegistry } from '../src/composables/useFieldRegistry'
|
|
4
4
|
import { useFormState } from '../src/composables/useFormState'
|
|
5
5
|
import { useForm } from '../src'
|
|
6
|
+
import { useInitialDataOverride } from '../src/composables/useInitialDataOverride'
|
|
6
7
|
import { useValidation } from '../src/composables/useValidation'
|
|
7
8
|
|
|
8
9
|
describe('useFormState', () => {
|
|
@@ -51,7 +52,8 @@ describe('useFormState', () => {
|
|
|
51
52
|
initialData,
|
|
52
53
|
})
|
|
53
54
|
const validationState = useValidation(state, {})
|
|
54
|
-
const
|
|
55
|
+
const initialDataOverride = useInitialDataOverride(toRef(state, 'initialData'))
|
|
56
|
+
const fields = useFieldRegistry(state, validationState, initialDataOverride)
|
|
55
57
|
|
|
56
58
|
const nameField = fields.defineField({ path: 'name' })
|
|
57
59
|
fields.defineField({ path: 'email' })
|
|
@@ -74,7 +76,8 @@ describe('useFormState', () => {
|
|
|
74
76
|
initialData,
|
|
75
77
|
})
|
|
76
78
|
const validationState = useValidation(state, {})
|
|
77
|
-
const
|
|
79
|
+
const initialDataOverride = useInitialDataOverride(toRef(state, 'initialData'))
|
|
80
|
+
const fields = useFieldRegistry(state, validationState, initialDataOverride)
|
|
78
81
|
|
|
79
82
|
const formState = useFormState(fields)
|
|
80
83
|
|