@teamnovu/kit-vue-forms 0.1.15 → 0.1.17

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.
@@ -44,5 +44,5 @@ export interface Form<T extends FormDataDefault> {
44
44
  reset: () => void;
45
45
  validateForm: () => Promise<ValidationResult>;
46
46
  submitHandler: (onSubmit: (data: T) => Awaitable<void>) => (event: SubmitEvent) => Promise<void>;
47
- getSubForm: <P extends EntityPaths<T>>(path: P, options?: SubformOptions<PickEntity<T, P>>) => Form<PickEntity<T, P>>;
47
+ getSubForm: <P extends EntityPaths<T>>(path: P, options?: SubformOptions<PickEntity<T, P>>) => Omit<Form<PickEntity<T, P>>, 'submitHandler'>;
48
48
  }
@@ -1,7 +1,10 @@
1
1
  import { MaybeRef, WritableComputedRef } from 'vue';
2
2
  import { Paths, PickProps, SplitPath } from '../types/util';
3
3
  import { ErrorBag } from '../types/validation';
4
+ import { FormField } from '../types/form';
4
5
  export declare function splitPath(path: string): string[];
6
+ export declare function existsPath<T, K extends Paths<T>>(obj: T, path: K | SplitPath<K>): boolean;
7
+ export declare function existsFieldPath<T, K extends Paths<T>>(field: FormField<T, K>): boolean;
5
8
  export declare function getNestedValue<T, K extends Paths<T>>(obj: T, path: K | SplitPath<K>): PickProps<T, K>;
6
9
  export declare function setNestedValue<T, K extends Paths<T>>(obj: MaybeRef<T>, path: K | SplitPath<K>, value: PickProps<T, K>): void;
7
10
  export declare const getLens: <T, K extends Paths<T>>(data: MaybeRef<T>, key: MaybeRef<K | SplitPath<K>>) => WritableComputedRef<PickProps<T, K>, PickProps<T, K>>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@teamnovu/kit-vue-forms",
3
- "version": "0.1.15",
3
+ "version": "0.1.17",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -26,7 +26,7 @@
26
26
  "@vitest/ui": "^2.0.0",
27
27
  "happy-dom": "^12.0.0",
28
28
  "vitest": "^2.0.0",
29
- "vue": "^3.5.13"
29
+ "vue": "^3.5.25"
30
30
  },
31
31
  "dependencies": {
32
32
  "@vueuse/core": "^13.5.0",
@@ -1,19 +1,28 @@
1
- import { computed, reactive, shallowRef, toRefs, watch, type MaybeRef, type MaybeRefOrGetter, type Ref } from 'vue'
1
+ import type { Awaitable } from '@vueuse/core'
2
+ import { computed, reactive, shallowRef, toRefs, unref, watch, type MaybeRef, type MaybeRefOrGetter, type Ref } from 'vue'
2
3
  import type { FormField } from '../types/form'
3
4
  import type { ValidationErrorMessage, ValidationErrors } from '../types/validation'
4
5
  import { cloneRefValue } from '../utils/general'
5
- import type { Awaitable } from '@vueuse/core'
6
6
 
7
7
  export interface UseFieldOptions<T, K extends string> {
8
8
  value?: MaybeRef<T>
9
9
  initialValue?: MaybeRefOrGetter<Readonly<T>>
10
10
  path: K
11
11
  errors?: Ref<ValidationErrors>
12
+ existsInForm?: MaybeRef<boolean>
12
13
  onBlur?: () => Awaitable<void>
13
14
  onFocus?: () => Awaitable<void>
14
15
  }
15
16
 
16
- export function useField<T, K extends string>(options: UseFieldOptions<T, K>): FormField<T, K> {
17
+ export function useField<T, K extends string>(fieldOptions: UseFieldOptions<T, K>): FormField<T, K> {
18
+ const defaultOptions = {
19
+ existsInForm: true,
20
+ }
21
+
22
+ const options = {
23
+ ...defaultOptions,
24
+ ...fieldOptions,
25
+ }
17
26
  const initialValue = shallowRef(Object.freeze(cloneRefValue(options.initialValue))) as Ref<Readonly<T | undefined>>
18
27
 
19
28
  const state = reactive({
@@ -53,7 +62,10 @@ export function useField<T, K extends string>(options: UseFieldOptions<T, K>): F
53
62
  }
54
63
 
55
64
  const reset = (): void => {
56
- state.value = cloneRefValue(state.initialValue)
65
+ const lastPathPart = state.path.split('.').at(-1) || ''
66
+ if (unref(options.existsInForm) && !/^\d+$/.test(lastPathPart)) {
67
+ state.value = cloneRefValue(state.initialValue)
68
+ }
57
69
  state.touched = false
58
70
  state.errors = []
59
71
  }
@@ -1,3 +1,4 @@
1
+ import type { Awaitable } from "@vueuse/core";
1
2
  import {
2
3
  computed,
3
4
  onScopeDispose,
@@ -6,65 +7,69 @@ import {
6
7
  toRef,
7
8
  triggerRef,
8
9
  unref,
9
- watchEffect,
10
+ watch,
10
11
  type MaybeRef,
11
- } from 'vue'
12
- import type { FieldsTuple, FormDataDefault, FormField } from '../types/form'
13
- import type { Paths, PickProps } from '../types/util'
14
- import { getLens, getNestedValue } from '../utils/path'
15
- import { Rc } from '../utils/rc'
16
- import { useField, type UseFieldOptions } from './useField'
17
- import type { ValidationState } from './useValidation'
18
- import type { Awaitable } from '@vueuse/core'
12
+ } from "vue";
13
+ import type { FieldsTuple, FormDataDefault, FormField } from "../types/form";
14
+ import type { Paths, PickProps } from "../types/util";
15
+ import { existsPath, getLens, getNestedValue } from "../utils/path";
16
+ import { Rc } from "../utils/rc";
17
+ import { useField, type UseFieldOptions } from "./useField";
18
+ import type { ValidationState } from "./useValidation";
19
19
 
20
20
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
21
- type FieldRegistryCache<T> = Map<Paths<T>, FormField<any, string>>
21
+ type FieldRegistryCache<T> = Map<Paths<T>, FormField<any, string>>;
22
22
 
23
23
  export type ResolvedFormField<T, K extends Paths<T>> = FormField<
24
24
  PickProps<T, K>,
25
25
  K
26
- >
26
+ >;
27
27
 
28
28
  export type DefineFieldOptions<F, K extends string> = Pick<
29
29
  UseFieldOptions<F, K>,
30
- 'path'
30
+ "path"
31
31
  > & {
32
- onBlur?: () => void
33
- onFocus?: () => void
34
- }
32
+ onBlur?: () => void;
33
+ onFocus?: () => void;
34
+ };
35
35
 
36
36
  interface FormState<
37
37
  T extends FormDataDefault,
38
38
  TIn extends FormDataDefault = T,
39
39
  > {
40
- data: T
41
- initialData: TIn
40
+ data: T;
41
+ initialData: TIn;
42
42
  }
43
43
 
44
44
  interface FieldRegistryOptions {
45
- keepValuesOnUnmount?: MaybeRef<boolean>
46
- onBlur?: (path: string) => Awaitable<void>
47
- onFocus?: (path: string) => Awaitable<void>
45
+ keepValuesOnUnmount?: MaybeRef<boolean>;
46
+ onBlur?: (path: string) => Awaitable<void>;
47
+ onFocus?: (path: string) => Awaitable<void>;
48
48
  }
49
49
 
50
50
  const optionDefaults = {
51
51
  keepValuesOnUnmount: true,
52
- }
52
+ };
53
53
 
54
54
  // A computed that always reflects the latest value from the getter
55
55
  // This computed forces updates even if the value is the same (to trigger watchers)
56
- function alwaysComputed<T>(getter: () => T) {
57
- const initialValueRef = shallowRef(getter())
56
+ function initialDataSync<T extends FormDataDefault>(
57
+ formState: FormState<T>,
58
+ path: Paths<T>,
59
+ ) {
60
+ const getNewValue = () => getNestedValue(formState.initialData, path);
61
+ const initialValueRef = shallowRef(getNewValue());
58
62
 
59
- watchEffect(
63
+ watch(
64
+ () => formState.initialData,
60
65
  () => {
61
- initialValueRef.value = getter()
62
- triggerRef(initialValueRef)
66
+ initialValueRef.value = getNewValue();
67
+ triggerRef(initialValueRef);
63
68
  },
64
- { flush: 'sync' },
65
- )
69
+ { flush: "sync" },
70
+ );
66
71
 
67
- return initialValueRef
72
+ return initialValueRef;
68
73
  }
69
74
 
70
75
  export function useFieldRegistry<T extends FormDataDefault>(
@@ -72,103 +77,99 @@ export function useFieldRegistry<T extends FormDataDefault>(
72
77
  validationState: ValidationState<T>,
73
78
  fieldRegistryOptions?: FieldRegistryOptions,
74
79
  ) {
75
- const fieldReferenceCounter = new Map<Paths<T>, Rc>()
76
- const fields = shallowReactive(new Map()) as FieldRegistryCache<T>
80
+ const fieldReferenceCounter = new Map<Paths<T>, Rc>();
81
+ const fields = shallowReactive(new Map()) as FieldRegistryCache<T>;
77
82
  const registryOptions = {
78
83
  ...optionDefaults,
79
84
  ...fieldRegistryOptions,
80
- }
85
+ };
81
86
 
82
87
  const registerField = <K extends Paths<T>>(
83
88
  field: ResolvedFormField<T, K>,
84
89
  ) => {
85
- const path = unref(field.path) as Paths<T>
86
- fields.set(path, field)
87
- }
90
+ const path = unref(field.path) as Paths<T>;
91
+ fields.set(path, field);
92
+ };
88
93
 
89
94
  const deregisterField = (path: Paths<T>) => {
90
95
  if (!registryOptions?.keepValuesOnUnmount) {
91
- fields.get(path)?.reset()
96
+ fields.get(path)?.reset();
92
97
  }
93
- fields.delete(path)
94
- }
98
+ fields.delete(path);
99
+ };
95
100
 
96
101
  const track = (path: Paths<T>) => {
97
102
  if (!fieldReferenceCounter.has(path)) {
98
- fieldReferenceCounter.set(path, new Rc(() => deregisterField(path)))
103
+ fieldReferenceCounter.set(path, new Rc(() => deregisterField(path)));
99
104
  } else {
100
- fieldReferenceCounter.get(path)?.inc()
105
+ fieldReferenceCounter.get(path)?.inc();
101
106
  }
102
- }
107
+ };
103
108
 
104
109
  const untrack = (path: Paths<T>) => {
105
110
  if (fieldReferenceCounter.has(path)) {
106
- fieldReferenceCounter.get(path)?.dec()
111
+ fieldReferenceCounter.get(path)?.dec();
107
112
  }
108
- }
113
+ };
109
114
 
110
115
  const getField = <K extends Paths<T>>(
111
116
  options: DefineFieldOptions<PickProps<T, K>, K>,
112
117
  ): ResolvedFormField<T, K> => {
113
- const { path } = options
118
+ const { path } = options;
114
119
 
115
120
  if (!fields.has(path)) {
116
121
  const field = useField({
117
122
  path,
118
- value: getLens(toRef(formState, 'data'), path),
119
- initialValue: alwaysComputed(() =>
120
- getNestedValue(formState.initialData, path)),
123
+ value: getLens(toRef(formState, "data"), path),
124
+ initialValue: initialDataSync(formState, path),
125
+ existsInForm: computed(() => existsPath(formState.data, unref(path))),
121
126
  errors: computed({
122
127
  get() {
123
- return validationState.errors.value.propertyErrors[path] || []
128
+ return validationState.errors.value.propertyErrors[path] || [];
124
129
  },
125
130
  set(newErrors) {
126
- validationState.errors.value.propertyErrors[path] = newErrors
131
+ validationState.errors.value.propertyErrors[path] = newErrors;
127
132
  },
128
133
  }),
129
134
  onBlur: async () => {
130
- await Promise.all(
131
- [
132
- registryOptions?.onBlur?.(unref(path)),
133
- options.onBlur?.(),
134
- ],
135
- )
135
+ await Promise.all([
136
+ registryOptions?.onBlur?.(unref(path)),
137
+ options.onBlur?.(),
138
+ ]);
136
139
  },
137
140
  onFocus: async () => {
138
- await Promise.all(
139
- [
140
- registryOptions?.onFocus?.(unref(path)),
141
- options.onFocus?.(),
142
- ],
143
- )
141
+ await Promise.all([
142
+ registryOptions?.onFocus?.(unref(path)),
143
+ options.onFocus?.(),
144
+ ]);
144
145
  },
145
- })
146
+ });
146
147
 
147
- registerField(field)
148
+ registerField(field);
148
149
  }
149
150
 
150
- const field = fields.get(path) as ResolvedFormField<T, K>
151
+ const field = fields.get(path) as ResolvedFormField<T, K>;
151
152
 
152
- track(path)
153
+ track(path);
153
154
 
154
155
  // Clean up field on unmount
155
156
  onScopeDispose(() => {
156
- untrack(path)
157
- })
157
+ untrack(path);
158
+ });
158
159
 
159
- return field
160
- }
160
+ return field;
161
+ };
161
162
 
162
163
  const defineField = <K extends Paths<T>>(
163
164
  options: DefineFieldOptions<PickProps<T, K>, K>,
164
165
  ): ResolvedFormField<T, K> => {
165
- const field = getField(options)
166
+ const field = getField(options);
166
167
 
167
168
  // TODO: If more options are ever needed than only the path we have to update the field
168
169
  // here with the new options
169
170
 
170
- return field
171
- }
171
+ return field;
172
+ };
172
173
 
173
174
  return {
174
175
  fields: computed(() => [...fields.values()] as FieldsTuple<T>),
@@ -176,9 +177,9 @@ export function useFieldRegistry<T extends FormDataDefault>(
176
177
  registerField,
177
178
  deregisterField,
178
179
  defineField,
179
- }
180
+ };
180
181
  }
181
182
 
182
183
  export type FieldRegistry<T extends FormDataDefault> = ReturnType<
183
184
  typeof useFieldRegistry<T>
184
- >
185
+ >;
@@ -10,7 +10,7 @@ import {
10
10
  type MaybeRefOrGetter,
11
11
  type Ref,
12
12
  } from 'vue'
13
- import type { AnyField, Form, FormDataDefault } from '../types/form'
13
+ import type { Form, FormDataDefault } from '../types/form'
14
14
  import type { EntityPaths, PickEntity } from '../types/util'
15
15
  import type { ValidationStrategy } from '../types/validation'
16
16
  import { cloneRefValue } from '../utils/general'
@@ -19,8 +19,6 @@ import { useFormState } from './useFormState'
19
19
  import { createSubformInterface, type SubformOptions } from './useSubform'
20
20
  import { useValidation, type ValidationOptions } from './useValidation'
21
21
 
22
- // TODO @Elias implement validation strategy handling
23
-
24
22
  export interface UseFormOptions<T extends FormDataDefault>
25
23
  extends ValidationOptions<T> {
26
24
  initialData: MaybeRefOrGetter<T>
@@ -29,8 +27,7 @@ export interface UseFormOptions<T extends FormDataDefault>
29
27
  }
30
28
 
31
29
  export function useForm<T extends FormDataDefault>(options: UseFormOptions<T>) {
32
- const initialData = computed(() =>
33
- Object.freeze(cloneRefValue(options.initialData)))
30
+ const initialData = computed(() => cloneRefValue(options.initialData))
34
31
 
35
32
  const data = ref<T>(cloneRefValue(initialData)) as Ref<T>
36
33
 
@@ -50,9 +47,9 @@ export function useForm<T extends FormDataDefault>(options: UseFormOptions<T>) {
50
47
  const validationState = useValidation(state, options)
51
48
  const fieldRegistry = useFieldRegistry(state, validationState, {
52
49
  keepValuesOnUnmount: options.keepValuesOnUnmount,
53
- onBlur: async () => {
50
+ onBlur: async (path: string) => {
54
51
  if (unref(options.validationStrategy) === 'onTouch') {
55
- validationState.validateForm()
52
+ validationState.validateField(path)
56
53
  }
57
54
  },
58
55
  })
@@ -77,13 +74,15 @@ export function useForm<T extends FormDataDefault>(options: UseFormOptions<T>) {
77
74
  const reset = () => {
78
75
  data.value = cloneRefValue(initialData)
79
76
  validationState.reset()
80
- fieldRegistry.fields.value.forEach((field: AnyField<T>) => field.reset())
77
+ for (const field of fieldRegistry.fields.value) {
78
+ field.reset()
79
+ }
81
80
  }
82
81
 
83
82
  function getSubForm<K extends EntityPaths<T>>(
84
83
  path: K,
85
84
  options?: SubformOptions<PickEntity<T, K>>,
86
- ): Form<PickEntity<T, K>> {
85
+ ): Omit<Form<PickEntity<T, K>>, 'submitHandler'> {
87
86
  return createSubformInterface(formInterface, path, options)
88
87
  }
89
88
 
@@ -50,7 +50,7 @@ export function createSubformInterface<
50
50
  mainForm: Form<T>,
51
51
  path: K,
52
52
  _options?: SubformOptions<PickEntity<T, K>>,
53
- ): Form<PickEntity<T, K>> {
53
+ ): Omit<Form<PickEntity<T, K>>, 'submitHandler'> {
54
54
  type ST = PickEntity<T, K>
55
55
  type SP = Paths<ST>
56
56
  type MP<P extends SP> = `${K}.${P}`
@@ -215,6 +215,22 @@ export function useValidation<T extends FormDataDefault>(
215
215
  }
216
216
  }
217
217
 
218
+ const validateField = async (path: string): Promise<ValidationResult> => {
219
+ const validationResults = await getValidationResults()
220
+
221
+ updateErrors({
222
+ general: validationResults.errors.general,
223
+ propertyErrors: {
224
+ [path]: validationResults.errors.propertyErrors[path],
225
+ },
226
+ })
227
+
228
+ return {
229
+ isValid: !hasErrors(validationResults.errors),
230
+ errors: validationState.errors,
231
+ }
232
+ }
233
+
218
234
  const isValid = computed(() => !hasErrors(validationState.errors))
219
235
 
220
236
  const reset = () => {
@@ -225,6 +241,7 @@ export function useValidation<T extends FormDataDefault>(
225
241
  return {
226
242
  ...toRefs(validationState),
227
243
  validateForm,
244
+ validateField,
228
245
  defineValidator,
229
246
  isValid,
230
247
  reset,
package/src/types/form.ts CHANGED
@@ -80,5 +80,5 @@ export interface Form<T extends FormDataDefault> {
80
80
  getSubForm: <P extends EntityPaths<T>>(
81
81
  path: P,
82
82
  options?: SubformOptions<PickEntity<T, P>>,
83
- ) => Form<PickEntity<T, P>>
83
+ ) => Omit<Form<PickEntity<T, P>>, 'submitHandler'>
84
84
  }
package/src/utils/path.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { computed, isRef, unref, type MaybeRef } from 'vue'
2
2
  import type { Paths, PickProps, SplitPath } from '../types/util'
3
3
  import type { ErrorBag, ValidationErrors } from '../types/validation'
4
+ import type { FormField } from '../types/form'
4
5
 
5
6
  export function splitPath(path: string): string[] {
6
7
  if (path === '') {
@@ -9,7 +10,22 @@ export function splitPath(path: string): string[] {
9
10
  return path.split(/\s*\.\s*/).filter(Boolean)
10
11
  }
11
12
 
12
- export function getNestedValue<T, K extends Paths<T>>(obj: T, path: K | SplitPath<K>) {
13
+ export function existsPath<T, K extends Paths<T>>(
14
+ obj: T,
15
+ path: K | SplitPath<K>,
16
+ ): boolean {
17
+ const splittedPath = Array.isArray(path) ? path : splitPath(path) as SplitPath<K>
18
+ return !!getNestedValue(obj, splittedPath.slice(0, -1) as SplitPath<K>)
19
+ }
20
+
21
+ export function existsFieldPath<T, K extends Paths<T>>(field: FormField<T, K>) {
22
+ return existsPath(field.data.value, field.path.value)
23
+ }
24
+
25
+ export function getNestedValue<T, K extends Paths<T>>(
26
+ obj: T,
27
+ path: K | SplitPath<K>,
28
+ ) {
13
29
  const splittedPath = Array.isArray(path) ? path : splitPath(path)
14
30
  return splittedPath.reduce(
15
31
  (current, key) => current?.[key],
@@ -17,7 +33,11 @@ export function getNestedValue<T, K extends Paths<T>>(obj: T, path: K | SplitPat
17
33
  ) as PickProps<T, K>
18
34
  }
19
35
 
20
- export function setNestedValue<T, K extends Paths<T>>(obj: MaybeRef<T>, path: K | SplitPath<K>, value: PickProps<T, K>): void {
36
+ export function setNestedValue<T, K extends Paths<T>>(
37
+ obj: MaybeRef<T>,
38
+ path: K | SplitPath<K>,
39
+ value: PickProps<T, K>,
40
+ ): void {
21
41
  const keys = Array.isArray(path) ? path : splitPath(path)
22
42
 
23
43
  const lastKey = keys.at(-1)!
@@ -30,25 +50,26 @@ export function setNestedValue<T, K extends Paths<T>>(obj: MaybeRef<T>, path: K
30
50
 
31
51
  obj.value = value
32
52
  } else {
33
- const target = keys
34
- .slice(0, -1)
35
- .reduce(
36
- (current, key) => {
37
- if (current?.[key] === undefined) {
38
- // Create the nested object if it doesn't exist
39
- current[key] = {}
40
- }
41
- return current?.[key]
42
- },
43
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
44
- unref(obj) as Record<string, any>,
45
- )
53
+ const target = keys.slice(0, -1).reduce(
54
+ (current, key) => {
55
+ if (current?.[key] === undefined) {
56
+ // Create the nested object if it doesn't exist
57
+ current[key] = {}
58
+ }
59
+ return current?.[key]
60
+ },
61
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
62
+ unref(obj) as Record<string, any>,
63
+ )
46
64
 
47
65
  target[lastKey] = value
48
66
  }
49
67
  }
50
68
 
51
- export const getLens = <T, K extends Paths<T>>(data: MaybeRef<T>, key: MaybeRef<K | SplitPath<K>>) => {
69
+ export const getLens = <T, K extends Paths<T>>(
70
+ data: MaybeRef<T>,
71
+ key: MaybeRef<K | SplitPath<K>>,
72
+ ) => {
52
73
  return computed({
53
74
  get() {
54
75
  return getNestedValue(unref(data), unref(key))
@@ -59,8 +80,14 @@ export const getLens = <T, K extends Paths<T>>(data: MaybeRef<T>, key: MaybeRef<
59
80
  })
60
81
  }
61
82
 
62
- type JoinPath<Base extends string, Sub extends string> = `${Base}${Base extends '' ? '' : Sub extends '' ? '' : '.'}${Sub}`
63
- export function joinPath<Base extends string, Sub extends string>(basePath: Base, subPath: Sub): JoinPath<Base, Sub> {
83
+ type JoinPath<
84
+ Base extends string,
85
+ Sub extends string,
86
+ > = `${Base}${Base extends '' ? '' : Sub extends '' ? '' : '.'}${Sub}`
87
+ export function joinPath<Base extends string, Sub extends string>(
88
+ basePath: Base,
89
+ subPath: Sub,
90
+ ): JoinPath<Base, Sub> {
64
91
  if (!basePath && !subPath) {
65
92
  return '' as JoinPath<Base, Sub>
66
93
  }
@@ -88,9 +115,10 @@ export function filterErrorsForPath(errors: ErrorBag, path: string): ErrorBag {
88
115
  .filter(([errorPath]) => {
89
116
  return errorPath.startsWith(pathPrefix)
90
117
  })
91
- .map(
92
- ([errorPath, errorMessages]) => [errorPath.slice(pathPrefix.length), errorMessages],
93
- ),
118
+ .map(([errorPath, errorMessages]) => [
119
+ errorPath.slice(pathPrefix.length),
120
+ errorMessages,
121
+ ]),
94
122
  )
95
123
 
96
124
  return {
@@ -405,6 +405,46 @@ describe("useForm", () => {
405
405
  expect(form.data.value.name).toBe("A");
406
406
  });
407
407
 
408
+ it("it not create empty objects if the field is going to be destroyed", async () => {
409
+ const form = useForm({
410
+ initialData: {
411
+ data: { names: [] as string[] },
412
+ },
413
+ });
414
+
415
+ const scope = effectScope();
416
+
417
+ scope.run(() => {
418
+ const nameField = form.defineField({ path: "data.names.0" });
419
+ nameField.setData("Modified");
420
+ form.reset();
421
+ });
422
+
423
+ scope.stop();
424
+
425
+ expect(form.data.value.data.names).toHaveLength(0);
426
+ });
427
+
428
+ it("it not create empty objects if the nested array field is going to be destroyed", async () => {
429
+ const form = useForm({
430
+ initialData: {
431
+ data: [] as Array<{ name: string }>,
432
+ },
433
+ });
434
+
435
+ const scope = effectScope();
436
+
437
+ scope.run(() => {
438
+ const nameField = form.defineField({ path: "data.0.name" });
439
+ nameField.setData("Modified");
440
+ form.reset();
441
+ });
442
+
443
+ scope.stop();
444
+
445
+ expect(form.data.value.data).toHaveLength(0);
446
+ });
447
+
408
448
  describe("useForm - submit handler", () => {
409
449
  it(
410
450
  "it should not call the handler when validation errors exist",
@@ -294,7 +294,31 @@ describe('useValidation', () => {
294
294
 
295
295
  const nameField = form.getField('name')
296
296
 
297
- expect(form.isValidated.value).toBe(false)
297
+ // Simulate blur event
298
+ nameField.onBlur()
299
+
300
+ // onBlur is not async but the validation runs async
301
+ await delay()
302
+
303
+ expect(form.isValid.value).toBe(false)
304
+ expect(form.errors.value.propertyErrors.name).toHaveLength(1)
305
+ })
306
+
307
+ it('should not validate other fields than the blurred one', async () => {
308
+ const schema = z.object({
309
+ name: z.string().min(2),
310
+ email: z.string().email(),
311
+ })
312
+
313
+ const initialData = { name: 'A', email: 'invalid-email' }
314
+ const form = useForm({
315
+ initialData,
316
+ schema,
317
+ validationStrategy: 'onTouch',
318
+ })
319
+
320
+ const nameField = form.getField('name')
321
+ form.getField('email')
298
322
 
299
323
  // Simulate blur event
300
324
  nameField.onBlur()
@@ -302,9 +326,9 @@ describe('useValidation', () => {
302
326
  // onBlur is not async but the validation runs async
303
327
  await delay()
304
328
 
305
- expect(form.isValidated.value).toBe(true)
306
329
  expect(form.isValid.value).toBe(false)
307
330
  expect(form.errors.value.propertyErrors.name).toHaveLength(1)
331
+ expect(form.errors.value.propertyErrors.email ?? []).toHaveLength(0)
308
332
  })
309
333
 
310
334
  it('should validate the form on form open if configured', async () => {