@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.
@@ -1,7 +1,26 @@
1
- import { computed, getCurrentScope, isRef, onBeforeUnmount, reactive, ref, toRefs, toValue, unref, watch, type MaybeRef, type MaybeRefOrGetter, type Ref } from 'vue'
2
- import z from 'zod'
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 { ErrorBag, ValidationFunction, ValidationResult, Validator } from '../types/validation'
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> extends ValidatorOptions<T, TOut> {
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> implements Validator<T, TOut> {
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> implements Validator<T, TOut> {
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> implements Validator<T, TOut> {
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(() => new CombinedValidator(
133
- unref(options.schema) as z.ZodType<TOut, T>,
134
- unref(options.validateFn) as ValidationFunction<T, TOut>,
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 = (newErrors: ErrorBag = SuccessValidationResult.errors) => {
149
- validationState.errors = mergeErrors(unref(options.errors) ?? SuccessValidationResult.errors, newErrors)
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(() => unref(options.errors), async () => {
154
- if (validationState.isValidated) {
155
- const validationResults = await getValidationResults()
184
+ watch(
185
+ () => unref(options.errors),
186
+ async () => {
187
+ if (validationState.isValidated) {
188
+ const validationResults = await getValidationResults()
156
189
 
157
- updateErrors(validationResults.errors)
158
- } else {
159
- updateErrors()
160
- }
161
- }, { immediate: true })
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: ValidatorOptions<TData, TDataOut> | Ref<Validator<TData, TDataOut>>,
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(validator as Ref<Validator<T, TOut> | undefined>)
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 (path: string): Promise<ValidationResult<TOut>> => {
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>(flag: K, path: string) => {
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<typeof useValidation<T, TOut>>
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
- ErrorBag,
9
- ValidationErrorMessage,
10
- ValidationErrors,
11
- ValidationResult,
12
- Validator,
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: (newData: T) => void
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
@@ -1,4 +1,3 @@
1
- import type { Prop } from 'vue'
2
1
  import type { FormDataDefault } from './form'
3
2
 
4
3
  /**
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((acc, issue) => {
12
- const path = issue.path.join('.')
11
+ .reduce(
12
+ (acc, issue) => {
13
+ const path = issue.path.join('.')
13
14
 
14
- return {
15
- ...acc,
16
- [path]: [...(acc[path] ?? []), issue.message],
17
- }
18
- }, {} as ErrorBag['propertyErrors'])
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,
@@ -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 fields = useFieldRegistry(state, validationState)
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 fields = useFieldRegistry(state, validationState)
79
+ const initialDataOverride = useInitialDataOverride(toRef(state, 'initialData'))
80
+ const fields = useFieldRegistry(state, validationState, initialDataOverride)
78
81
 
79
82
  const formState = useFormState(fields)
80
83