@vuehookform/core 0.4.6 → 0.5.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/README.md CHANGED
@@ -84,13 +84,21 @@ const addresses = fields('addresses')
84
84
 
85
85
  <template>
86
86
  <div v-for="field in addresses.value" :key="field.key">
87
+ <!-- Option 1: Scoped methods (recommended) - type-safe, cleaner -->
88
+ <input v-bind="field.register('street')" />
89
+ <input v-bind="field.register('city')" />
90
+
91
+ <!-- Option 2: Manual path building -->
87
92
  <input v-bind="register(`addresses.${field.index}.street`)" />
93
+
88
94
  <button @click="field.remove()">Remove</button>
89
95
  </div>
90
96
  <button @click="addresses.append({ street: '', city: '' })">Add Address</button>
91
97
  </template>
92
98
  ```
93
99
 
100
+ Field array items provide **scoped methods** (`field.register()`, `field.setValue()`, `field.watch()`, etc.) that automatically build the full path with type safety.
101
+
94
102
  ### Validation Modes
95
103
 
96
104
  ```typescript
@@ -1,8 +1,23 @@
1
+ import { ComputedRef } from 'vue';
1
2
  import { FormContext } from './formContext';
2
- import { FieldArray, FieldArrayOptions, Path } from '../types';
3
+ import { FieldArray, FieldArrayOptions, Path, RegisterOptions, RegisterReturn, SetValueOptions, FieldState, ErrorOption } from '../types';
4
+ /**
5
+ * Form methods required by field array for scoped item methods.
6
+ * Passed from useForm to enable type-safe field access within array items.
7
+ */
8
+ export interface FieldArrayFormMethods {
9
+ register: (name: string, options?: RegisterOptions<unknown>) => RegisterReturn<unknown>;
10
+ setValue: (name: string, value: unknown, options?: SetValueOptions) => void;
11
+ getValues: (name: string) => unknown;
12
+ watch: (name: string) => ComputedRef<unknown>;
13
+ getFieldState: (name: string) => FieldState;
14
+ trigger: (name?: string | string[]) => Promise<boolean>;
15
+ clearErrors: (name?: string | string[]) => void;
16
+ setError: (name: string, error: ErrorOption) => void;
17
+ }
3
18
  /**
4
19
  * Create field array management functions
5
20
  */
6
- export declare function createFieldArrayManager<FormValues>(ctx: FormContext<FormValues>, validate: (fieldPath?: string) => Promise<boolean>, setFocus: (name: string) => void): {
21
+ export declare function createFieldArrayManager<FormValues>(ctx: FormContext<FormValues>, validate: (fieldPath?: string) => Promise<boolean>, setFocus: (name: string) => void, formMethods: FieldArrayFormMethods): {
7
22
  fields: <TPath extends Path<FormValues>>(name: TPath, options?: FieldArrayOptions) => FieldArray;
8
23
  };
package/dist/index.d.ts CHANGED
@@ -17,8 +17,8 @@
17
17
  export { useForm } from './useForm';
18
18
  export { provideForm, useFormContext, FormContextKey } from './context';
19
19
  export { useWatch, type UseWatchOptions } from './useWatch';
20
- export { useController, type UseControllerOptions, type UseControllerReturn, type ControllerFieldProps, } from './useController';
20
+ export { useController, type UseControllerOptions, type UseControllerReturn, type ControllerFieldProps, type LooseControllerOptions, } from './useController';
21
21
  export { useFormState, type UseFormStateOptions, type FormStateKey } from './useFormState';
22
22
  export { isFieldError } from './types';
23
- export type { UseFormOptions, UseFormReturn, UseFormReturn as Control, RegisterOptions, RegisterReturn, FormState, FieldState, FieldErrors, FieldError, FieldErrorValue, ErrorOption, SetErrorsOptions, FieldArray, FieldArrayItem, FieldArrayOptions, FieldArrayRules, FieldArrayFocusOptions, InferSchema, FormValues, FormPath, Path, PathValue, ArrayElement, ArrayPath, FieldPath, ValidationMode, SetFocusOptions, ResetOptions, ResetFieldOptions, AsyncDefaultValues, TriggerOptions, SetValueOptions, UnregisterOptions, CriteriaMode, } from './types';
23
+ export type { UseFormOptions, UseFormReturn, UseFormReturn as Control, LooseControl, RegisterOptions, RegisterReturn, FormState, FieldState, FieldErrors, FieldError, FieldErrorValue, ErrorOption, SetErrorsOptions, FieldArray, FieldArrayItem, FieldArrayOptions, FieldArrayRules, FieldArrayFocusOptions, InferSchema, FormValues, FormPath, Path, PathValue, ArrayElement, ArrayPath, FieldPath, ValidationMode, SetFocusOptions, ResetOptions, ResetFieldOptions, AsyncDefaultValues, TriggerOptions, SetValueOptions, UnregisterOptions, CriteriaMode, } from './types';
24
24
  export { get, set, unset, clearPathCache } from './utils/paths';
package/dist/types.d.ts CHANGED
@@ -363,15 +363,104 @@ export interface RegisterReturn<TValue = unknown> {
363
363
  disabled?: boolean;
364
364
  }
365
365
  /**
366
- * Field metadata for dynamic arrays
366
+ * Field metadata for dynamic arrays with scoped methods for type-safe field access.
367
+ *
368
+ * Scoped methods provide full type safety when accessing fields within array items,
369
+ * solving the type inference problem with dynamic paths like `items.${index}.name`.
370
+ *
371
+ * @template TItem - The type of items in the array (inferred from field path)
372
+ *
373
+ * @example Basic usage
374
+ * ```ts
375
+ * const items = fields('items')
376
+ * items.value.forEach((field) => {
377
+ * field.register('name') // RegisterReturn<string> - fully typed!
378
+ * field.setValue('price', 25) // Type-checked against TItem
379
+ * field.watch('price') // ComputedRef<number>
380
+ * })
381
+ * ```
382
+ *
383
+ * @example In template with v-for
384
+ * ```vue
385
+ * <div v-for="field in items.value" :key="field.key">
386
+ * <input v-bind="field.register('name')" />
387
+ * <span v-if="field.getFieldState('name').error">
388
+ * {{ field.getFieldState('name').error }}
389
+ * </span>
390
+ * </div>
391
+ * ```
367
392
  */
368
- export interface FieldArrayItem {
393
+ export interface FieldArrayItem<TItem = unknown> {
369
394
  /** Stable key for v-for */
370
395
  key: string;
371
396
  /** Current index in array */
372
397
  index: number;
373
398
  /** Remove this item */
374
399
  remove: () => void;
400
+ /**
401
+ * Register a field within this array item.
402
+ * Automatically builds the full path (e.g., 'items.0.name').
403
+ *
404
+ * @param name - Field path relative to the item (e.g., 'name', 'address.city')
405
+ * @param options - Registration options (validation, controlled mode, etc.)
406
+ * @returns Props to bind to the input element
407
+ *
408
+ * @example
409
+ * ```vue
410
+ * <input v-bind="field.register('name')" />
411
+ * ```
412
+ */
413
+ register: <TPath extends Path<TItem>>(name: TPath, options?: RegisterOptions<PathValue<TItem, TPath>>) => RegisterReturn<PathValue<TItem, TPath>>;
414
+ /**
415
+ * Set value for a field within this array item.
416
+ *
417
+ * @param name - Field path relative to the item
418
+ * @param value - New value (typed based on path)
419
+ * @param options - Control side effects (validation, dirty, touched)
420
+ */
421
+ setValue: <TPath extends Path<TItem>>(name: TPath, value: PathValue<TItem, TPath>, options?: SetValueOptions) => void;
422
+ /**
423
+ * Get current value of a field within this array item.
424
+ *
425
+ * @param name - Field path relative to the item
426
+ * @returns The field's current value
427
+ */
428
+ getValue: <TPath extends Path<TItem>>(name: TPath) => PathValue<TItem, TPath>;
429
+ /**
430
+ * Watch a field within this array item reactively.
431
+ *
432
+ * @param name - Field path relative to the item
433
+ * @returns ComputedRef that updates when the field changes
434
+ */
435
+ watch: <TPath extends Path<TItem>>(name: TPath) => ComputedRef<PathValue<TItem, TPath>>;
436
+ /**
437
+ * Get the state of a field within this array item.
438
+ * Note: Returns a snapshot, not reactive. Use watch() or formState for reactive access.
439
+ *
440
+ * @param name - Field path relative to the item
441
+ * @returns Field state with isDirty, isTouched, invalid, error
442
+ */
443
+ getFieldState: <TPath extends Path<TItem>>(name: TPath) => FieldState;
444
+ /**
445
+ * Trigger validation for fields within this array item.
446
+ *
447
+ * @param name - Optional field path(s) relative to the item. Validates entire item if omitted.
448
+ * @returns Promise resolving to true if valid
449
+ */
450
+ trigger: <TPath extends Path<TItem>>(name?: TPath | TPath[]) => Promise<boolean>;
451
+ /**
452
+ * Clear errors for fields within this array item.
453
+ *
454
+ * @param name - Optional field path(s) relative to the item. Clears all item errors if omitted.
455
+ */
456
+ clearErrors: <TPath extends Path<TItem>>(name?: TPath | TPath[]) => void;
457
+ /**
458
+ * Set an error for a field within this array item.
459
+ *
460
+ * @param name - Field path relative to the item
461
+ * @param error - Error option with message
462
+ */
463
+ setError: <TPath extends Path<TItem>>(name: TPath, error: ErrorOption) => void;
375
464
  }
376
465
  /**
377
466
  * Focus options for field array operations
@@ -438,7 +527,7 @@ export interface FieldArrayOptions<T = unknown> {
438
527
  */
439
528
  export interface FieldArray<TItem = unknown> {
440
529
  /** Current field items with metadata. Reactive - updates when array methods are called. */
441
- value: FieldArrayItem[];
530
+ value: FieldArrayItem<TItem>[];
442
531
  /** Append item(s) to end of array. Returns false if maxLength exceeded. */
443
532
  append: (value: TItem | TItem[], options?: FieldArrayFocusOptions) => boolean;
444
533
  /** Prepend item(s) to beginning of array. Returns false if maxLength exceeded. */
@@ -693,6 +782,20 @@ export interface UseFormOptions<TSchema extends ZodType> {
693
782
  */
694
783
  shouldUseNativeValidation?: boolean;
695
784
  }
785
+ /**
786
+ * Loose control type for reusable components where schema type is unknown.
787
+ * Use this when building form field components that accept any form control.
788
+ *
789
+ * @example
790
+ * ```ts
791
+ * // Reusable field component
792
+ * function FormInput(props: { name: string; control: LooseControl }) {
793
+ * const { field } = useController(props) // No cast needed
794
+ * return <input v-bind="field" />
795
+ * }
796
+ * ```
797
+ */
798
+ export type LooseControl = UseFormReturn<ZodType<any>>;
696
799
  /**
697
800
  * Return value from useForm composable.
698
801
  * Provides full type safety with autocomplete for field paths and typed values.
@@ -712,14 +815,22 @@ export interface UseFormReturn<TSchema extends ZodType> {
712
815
  * <input v-bind="register('email')" />
713
816
  * <input v-bind="register('age', { validate: (v) => v >= 0 || 'Must be positive' })" />
714
817
  */
715
- register: <TPath extends Path<InferSchema<TSchema>>>(name: TPath, options?: RegisterOptions<PathValue<InferSchema<TSchema>, TPath>>) => RegisterReturn<PathValue<InferSchema<TSchema>, TPath>>;
818
+ register: {
819
+ <TPath extends Path<InferSchema<TSchema>>>(name: TPath, options?: RegisterOptions<PathValue<InferSchema<TSchema>, TPath>>): RegisterReturn<PathValue<InferSchema<TSchema>, TPath>>;
820
+ /** Loose overload for dynamic paths - returns unknown-typed value */
821
+ (name: string, options?: RegisterOptions<unknown>): RegisterReturn<unknown>;
822
+ };
716
823
  /**
717
824
  * Unregister a field to clean up refs and options
718
825
  * Call this when a field is unmounted to prevent memory leaks
719
826
  * @param name - Field path to unregister
720
827
  * @param options - Options for what state to preserve
721
828
  */
722
- unregister: <TPath extends Path<InferSchema<TSchema>>>(name: TPath, options?: UnregisterOptions) => void;
829
+ unregister: {
830
+ <TPath extends Path<InferSchema<TSchema>>>(name: TPath, options?: UnregisterOptions): void;
831
+ /** Loose overload for dynamic paths */
832
+ (name: string, options?: UnregisterOptions): void;
833
+ };
723
834
  /**
724
835
  * Handle form submission
725
836
  * @param onValid - Callback called with valid data
@@ -747,7 +858,11 @@ export interface UseFormReturn<TSchema extends ZodType> {
747
858
  * @param value - New value (typed to match field)
748
859
  * @param options - Options for validation/dirty/touched behavior
749
860
  */
750
- setValue: <TPath extends Path<InferSchema<TSchema>>>(name: TPath, value: PathValue<InferSchema<TSchema>, TPath>, options?: SetValueOptions) => void;
861
+ setValue: {
862
+ <TPath extends Path<InferSchema<TSchema>>>(name: TPath, value: PathValue<InferSchema<TSchema>, TPath>, options?: SetValueOptions): void;
863
+ /** Loose overload for dynamic paths */
864
+ (name: string, value: unknown, options?: SetValueOptions): void;
865
+ };
751
866
  /**
752
867
  * Reset form to default values
753
868
  * @param values - Optional new default values
@@ -759,7 +874,11 @@ export interface UseFormReturn<TSchema extends ZodType> {
759
874
  * @param name - Field path
760
875
  * @param options - Options for what state to preserve (with typed defaultValue)
761
876
  */
762
- resetField: <TPath extends Path<InferSchema<TSchema>>>(name: TPath, options?: ResetFieldOptions<PathValue<InferSchema<TSchema>, TPath>>) => void;
877
+ resetField: {
878
+ <TPath extends Path<InferSchema<TSchema>>>(name: TPath, options?: ResetFieldOptions<PathValue<InferSchema<TSchema>, TPath>>): void;
879
+ /** Loose overload for dynamic paths */
880
+ (name: string, options?: ResetFieldOptions<unknown>): void;
881
+ };
763
882
  /**
764
883
  * Watch field value(s) reactively
765
884
  * @overload Watch all form values
@@ -770,23 +889,39 @@ export interface UseFormReturn<TSchema extends ZodType> {
770
889
  (): ComputedRef<InferSchema<TSchema>>;
771
890
  <TPath extends Path<InferSchema<TSchema>>>(name: TPath): ComputedRef<PathValue<InferSchema<TSchema>, TPath>>;
772
891
  <TPath extends Path<InferSchema<TSchema>>>(names: TPath[]): ComputedRef<Partial<InferSchema<TSchema>>>;
892
+ /** Loose overload for dynamic paths - returns unknown */
893
+ (name: string): ComputedRef<unknown>;
894
+ /** Loose overload for dynamic path arrays */
895
+ (names: string[]): ComputedRef<Record<string, unknown>>;
773
896
  };
774
897
  /**
775
898
  * Manually trigger validation
776
899
  * @param name - Optional field path (validates all if not provided)
777
900
  */
778
- validate: <TPath extends Path<InferSchema<TSchema>>>(name?: TPath) => Promise<boolean>;
901
+ validate: {
902
+ <TPath extends Path<InferSchema<TSchema>>>(name?: TPath): Promise<boolean>;
903
+ /** Loose overload for dynamic paths */
904
+ (name?: string): Promise<boolean>;
905
+ };
779
906
  /**
780
907
  * Clear errors for specified fields or all errors
781
908
  * @param name - Optional field path or array of paths
782
909
  */
783
- clearErrors: <TPath extends Path<InferSchema<TSchema>>>(name?: TPath | TPath[] | 'root' | `root.${string}`) => void;
910
+ clearErrors: {
911
+ <TPath extends Path<InferSchema<TSchema>>>(name?: TPath | TPath[] | 'root' | `root.${string}`): void;
912
+ /** Loose overload for dynamic paths */
913
+ (name?: string | string[]): void;
914
+ };
784
915
  /**
785
916
  * Set an error for a specific field
786
917
  * @param name - Field path or root error
787
918
  * @param error - Error option with message
788
919
  */
789
- setError: <TPath extends Path<InferSchema<TSchema>>>(name: TPath | 'root' | `root.${string}`, error: ErrorOption) => void;
920
+ setError: {
921
+ <TPath extends Path<InferSchema<TSchema>>>(name: TPath | 'root' | `root.${string}`, error: ErrorOption): void;
922
+ /** Loose overload for dynamic paths */
923
+ (name: string, error: ErrorOption): void;
924
+ };
790
925
  /**
791
926
  * Set multiple errors at once. Useful for server-side validation errors
792
927
  * or bulk error handling scenarios.
@@ -822,7 +957,11 @@ export interface UseFormReturn<TSchema extends ZodType> {
822
957
  * focusField('email')
823
958
  * }
824
959
  */
825
- hasErrors: <TPath extends Path<InferSchema<TSchema>>>(fieldPath?: TPath | 'root' | `root.${string}`) => boolean;
960
+ hasErrors: {
961
+ <TPath extends Path<InferSchema<TSchema>>>(fieldPath?: TPath | 'root' | `root.${string}`): boolean;
962
+ /** Loose overload for dynamic paths */
963
+ (fieldPath?: string): boolean;
964
+ };
826
965
  /**
827
966
  * Get validation errors for the form or a specific field
828
967
  *
@@ -849,24 +988,40 @@ export interface UseFormReturn<TSchema extends ZodType> {
849
988
  (): InferSchema<TSchema>;
850
989
  <TPath extends Path<InferSchema<TSchema>>>(name: TPath): PathValue<InferSchema<TSchema>, TPath>;
851
990
  <TPath extends Path<InferSchema<TSchema>>>(names: TPath[]): Partial<InferSchema<TSchema>>;
991
+ /** Loose overload for dynamic paths - returns unknown */
992
+ (name: string): unknown;
993
+ /** Loose overload for dynamic path arrays */
994
+ (names: string[]): Record<string, unknown>;
852
995
  };
853
996
  /**
854
997
  * Get the state of an individual field
855
998
  * @param name - Field path
856
999
  */
857
- getFieldState: <TPath extends Path<InferSchema<TSchema>>>(name: TPath) => FieldState;
1000
+ getFieldState: {
1001
+ <TPath extends Path<InferSchema<TSchema>>>(name: TPath): FieldState;
1002
+ /** Loose overload for dynamic paths */
1003
+ (name: string): FieldState;
1004
+ };
858
1005
  /**
859
1006
  * Manually trigger validation for specific fields or entire form
860
1007
  * @param name - Optional field path or array of paths
861
1008
  * @param options - Optional trigger options (e.g., markAsSubmitted)
862
1009
  */
863
- trigger: <TPath extends Path<InferSchema<TSchema>>>(name?: TPath | TPath[], options?: TriggerOptions) => Promise<boolean>;
1010
+ trigger: {
1011
+ <TPath extends Path<InferSchema<TSchema>>>(name?: TPath | TPath[], options?: TriggerOptions): Promise<boolean>;
1012
+ /** Loose overload for dynamic paths */
1013
+ (name?: string | string[], options?: TriggerOptions): Promise<boolean>;
1014
+ };
864
1015
  /**
865
1016
  * Programmatically focus a field
866
1017
  * @param name - Field path
867
1018
  * @param options - Focus options
868
1019
  */
869
- setFocus: <TPath extends Path<InferSchema<TSchema>>>(name: TPath, options?: SetFocusOptions) => void;
1020
+ setFocus: {
1021
+ <TPath extends Path<InferSchema<TSchema>>>(name: TPath, options?: SetFocusOptions): void;
1022
+ /** Loose overload for dynamic paths */
1023
+ (name: string, options?: SetFocusOptions): void;
1024
+ };
870
1025
  /**
871
1026
  * Form configuration options (mode, reValidateMode).
872
1027
  * Useful for composables like useController that need to respect validation modes.
@@ -1,6 +1,6 @@
1
1
  import { ComputedRef, Ref } from 'vue';
2
2
  import { ZodType } from 'zod';
3
- import { UseFormReturn, Path, PathValue, InferSchema, FieldState } from './types';
3
+ import { UseFormReturn, Path, PathValue, InferSchema, FieldState, LooseControl } from './types';
4
4
  /**
5
5
  * Options for useController composable
6
6
  */
@@ -36,6 +36,27 @@ export interface UseControllerReturn<TValue> {
36
36
  /** Current field state (errors, dirty, touched) */
37
37
  fieldState: ComputedRef<FieldState>;
38
38
  }
39
+ /**
40
+ * Loose options for useController when schema type is unknown.
41
+ * Use this for building reusable field components that work with any form.
42
+ *
43
+ * @example
44
+ * ```ts
45
+ * // Reusable field component
46
+ * function FormInput(props: { name: string; control: LooseControl }) {
47
+ * const { field, fieldState } = useController(props)
48
+ * // field.value is unknown, fieldState is reactive
49
+ * }
50
+ * ```
51
+ */
52
+ export interface LooseControllerOptions {
53
+ /** Field name/path as a string */
54
+ name: string;
55
+ /** Form control from useForm (uses context if not provided) */
56
+ control?: LooseControl;
57
+ /** Default value for the field */
58
+ defaultValue?: unknown;
59
+ }
39
60
  /**
40
61
  * Hook for controlled components that need fine-grained control over field state
41
62
  *
@@ -59,5 +80,14 @@ export interface UseControllerReturn<TValue> {
59
80
  * // />
60
81
  * // <span v-if="fieldState.value.error">{{ fieldState.value.error }}</span>
61
82
  * ```
83
+ *
84
+ * @example Reusable component with loose typing
85
+ * ```ts
86
+ * // FormInput.vue
87
+ * const props = defineProps<{ name: string; control: LooseControl }>()
88
+ * const { field, fieldState } = useController(props)
89
+ * // No 'as never' cast needed!
90
+ * ```
62
91
  */
92
+ export declare function useController(options: LooseControllerOptions): UseControllerReturn<unknown>;
63
93
  export declare function useController<TSchema extends ZodType, TPath extends Path<InferSchema<TSchema>>>(options: UseControllerOptions<TSchema, TPath>): UseControllerReturn<PathValue<InferSchema<TSchema>, TPath>>;
@@ -839,6 +839,7 @@ function createFieldRegistration(ctx, validate) {
839
839
  let handlers = ctx.fieldHandlers.get(name);
840
840
  if (!handlers) {
841
841
  const runCustomValidation = async (fieldName, value, requestId, resetGenAtStart) => {
842
+ if (!ctx.fieldRefs.has(fieldName)) return;
842
843
  const fieldOpts = ctx.fieldOptions.get(fieldName);
843
844
  if (!fieldOpts?.validate || fieldOpts.disabled) return;
844
845
  const error = await fieldOpts.validate(value);
@@ -1009,7 +1010,7 @@ function createFieldRegistration(ctx, validate) {
1009
1010
  unregister
1010
1011
  };
1011
1012
  }
1012
- function createFieldArrayManager(ctx, validate, setFocus) {
1013
+ function createFieldArrayManager(ctx, validate, setFocus, formMethods) {
1013
1014
  function fields(name, options) {
1014
1015
  if (__DEV__) {
1015
1016
  const syntaxError = validatePathSyntax(name);
@@ -1053,16 +1054,76 @@ function createFieldArrayManager(ctx, validate, setFocus) {
1053
1054
  if (item) indexCache.set(item.key, i);
1054
1055
  }
1055
1056
  };
1056
- const createItem = (key) => ({
1057
- key,
1058
- get index() {
1059
- return indexCache.get(key) ?? -1;
1060
- },
1061
- remove() {
1062
- const currentIndex = indexCache.get(key) ?? -1;
1063
- if (currentIndex !== -1) removeAt(currentIndex);
1064
- }
1065
- });
1057
+ const buildPath = (index, subPath) => {
1058
+ return subPath ? `${name}.${index}.${subPath}` : `${name}.${index}`;
1059
+ };
1060
+ const createScopedMethods = (indexGetter) => {
1061
+ return {
1062
+ register: (subPath, options$1) => formMethods.register(buildPath(indexGetter(), subPath), options$1),
1063
+ setValue: (subPath, value, options$1) => formMethods.setValue(buildPath(indexGetter(), subPath), value, options$1),
1064
+ getValue: (subPath) => formMethods.getValues(buildPath(indexGetter(), subPath)),
1065
+ watch: (subPath) => formMethods.watch(buildPath(indexGetter(), subPath)),
1066
+ getFieldState: (subPath) => formMethods.getFieldState(buildPath(indexGetter(), subPath)),
1067
+ trigger: (subPath) => {
1068
+ const index = indexGetter();
1069
+ if (!subPath) return formMethods.trigger(buildPath(index));
1070
+ if (Array.isArray(subPath)) return formMethods.trigger(subPath.map((p) => buildPath(index, p)));
1071
+ return formMethods.trigger(buildPath(index, subPath));
1072
+ },
1073
+ clearErrors: (subPath) => {
1074
+ const index = indexGetter();
1075
+ if (!subPath) {
1076
+ formMethods.clearErrors(buildPath(index));
1077
+ return;
1078
+ }
1079
+ if (Array.isArray(subPath)) {
1080
+ formMethods.clearErrors(subPath.map((p) => buildPath(index, p)));
1081
+ return;
1082
+ }
1083
+ formMethods.clearErrors(buildPath(index, subPath));
1084
+ },
1085
+ setError: (subPath, error) => formMethods.setError(buildPath(indexGetter(), subPath), error)
1086
+ };
1087
+ };
1088
+ const createItem = (key) => {
1089
+ const getIndex = () => indexCache.get(key) ?? -1;
1090
+ let scoped = null;
1091
+ const getScoped = () => scoped ??= createScopedMethods(getIndex);
1092
+ return {
1093
+ key,
1094
+ get index() {
1095
+ return getIndex();
1096
+ },
1097
+ remove() {
1098
+ const currentIndex = getIndex();
1099
+ if (currentIndex !== -1) removeAt(currentIndex);
1100
+ },
1101
+ get register() {
1102
+ return getScoped().register;
1103
+ },
1104
+ get setValue() {
1105
+ return getScoped().setValue;
1106
+ },
1107
+ get getValue() {
1108
+ return getScoped().getValue;
1109
+ },
1110
+ get watch() {
1111
+ return getScoped().watch;
1112
+ },
1113
+ get getFieldState() {
1114
+ return getScoped().getFieldState;
1115
+ },
1116
+ get trigger() {
1117
+ return getScoped().trigger;
1118
+ },
1119
+ get clearErrors() {
1120
+ return getScoped().clearErrors;
1121
+ },
1122
+ get setError() {
1123
+ return getScoped().setError;
1124
+ }
1125
+ };
1126
+ };
1066
1127
  if (fa.items.value.length === 0 && fa.values.length > 0) {
1067
1128
  fa.items.value = fa.values.map(() => createItem(generateId()));
1068
1129
  rebuildIndexCache();
@@ -1071,7 +1132,7 @@ function createFieldArrayManager(ctx, validate, setFocus) {
1071
1132
  if (!focusOptions?.shouldFocus) return;
1072
1133
  await (0, vue.nextTick)();
1073
1134
  const focusItemOffset = focusOptions?.focusIndex ?? 0;
1074
- let fieldPath = `${name}.${baseIndex + Math.min(focusItemOffset, addedCount - 1)}`;
1135
+ let fieldPath = `${name}.${baseIndex + Math.max(0, Math.min(focusItemOffset, addedCount - 1))}`;
1075
1136
  if (focusOptions?.focusName) fieldPath = `${fieldPath}.${focusOptions.focusName}`;
1076
1137
  setFocus(fieldPath);
1077
1138
  };
@@ -1242,13 +1303,10 @@ function createFieldArrayManager(ctx, validate, setFocus) {
1242
1303
  set(ctx.formData, name, newValues);
1243
1304
  const newItems = [...fa.items.value];
1244
1305
  const itemA = newItems[indexA];
1245
- const itemB = newItems[indexB];
1246
- if (itemA && itemB) {
1247
- newItems[indexA] = itemB;
1248
- newItems[indexB] = itemA;
1249
- fa.items.value = newItems;
1250
- swapInCache(indexA, indexB);
1251
- }
1306
+ newItems[indexA] = newItems[indexB];
1307
+ newItems[indexB] = itemA;
1308
+ fa.items.value = newItems;
1309
+ swapInCache(indexA, indexB);
1252
1310
  updateFieldDirtyState(ctx.dirtyFields, ctx.defaultValues, ctx.defaultValueHashes, name, get(ctx.formData, name));
1253
1311
  validateIfNeeded();
1254
1312
  return true;
@@ -1268,16 +1326,14 @@ function createFieldArrayManager(ctx, validate, setFocus) {
1268
1326
  }
1269
1327
  const newItems = [...fa.items.value];
1270
1328
  const [removedItem] = newItems.splice(from, 1);
1271
- if (removedItem) {
1272
- newItems.splice(to, 0, removedItem);
1273
- fa.items.value = newItems;
1274
- const minIdx = Math.min(from, to);
1275
- const maxIdx = Math.max(from, to);
1276
- const items = fa.items.value;
1277
- for (let i = minIdx; i <= maxIdx; i++) {
1278
- const item = items[i];
1279
- if (item) indexCache.set(item.key, i);
1280
- }
1329
+ newItems.splice(to, 0, removedItem);
1330
+ fa.items.value = newItems;
1331
+ const minIdx = Math.min(from, to);
1332
+ const maxIdx = Math.max(from, to);
1333
+ const items = fa.items.value;
1334
+ for (let i = minIdx; i <= maxIdx; i++) {
1335
+ const item = items[i];
1336
+ if (item) indexCache.set(item.key, i);
1281
1337
  }
1282
1338
  updateFieldDirtyState(ctx.dirtyFields, ctx.defaultValues, ctx.defaultValueHashes, name, get(ctx.formData, name));
1283
1339
  validateIfNeeded();
@@ -1384,8 +1440,6 @@ function useForm(options) {
1384
1440
  el.focus();
1385
1441
  if (focusOptions?.shouldSelect && el instanceof HTMLInputElement && typeof el.select === "function") el.select();
1386
1442
  }
1387
- const setFocusWrapper = (name) => setFocus(name);
1388
- const { fields } = createFieldArrayManager(ctx, validate, setFocusWrapper);
1389
1443
  let lastSyncTime = 0;
1390
1444
  const SYNC_DEBOUNCE_MS = 16;
1391
1445
  function syncWithDebounce() {
@@ -1529,6 +1583,7 @@ function useForm(options) {
1529
1583
  Object.assign(ctx.formData, newValues);
1530
1584
  if (!opts.keepErrors) {
1531
1585
  ctx.errors.value = {};
1586
+ ctx.externalErrors.value = {};
1532
1587
  ctx.persistentErrorFields.clear();
1533
1588
  }
1534
1589
  if (!opts.keepTouched) ctx.touchedFields.value = {};
@@ -1629,7 +1684,8 @@ function useForm(options) {
1629
1684
  for (const field of fieldsToClean) {
1630
1685
  clearFieldErrors(ctx.errors, field);
1631
1686
  clearFieldErrors(ctx.externalErrors, field);
1632
- ctx.persistentErrorFields.delete(field);
1687
+ const prefix = `${field}.`;
1688
+ for (const persistentField of ctx.persistentErrorFields) if (persistentField === field || persistentField.startsWith(prefix)) ctx.persistentErrorFields.delete(persistentField);
1633
1689
  }
1634
1690
  }
1635
1691
  function setError(name, error) {
@@ -1723,6 +1779,17 @@ function useForm(options) {
1723
1779
  }
1724
1780
  return await validate(name);
1725
1781
  }
1782
+ const setFocusWrapper = (name) => setFocus(name);
1783
+ const { fields } = createFieldArrayManager(ctx, validate, setFocusWrapper, {
1784
+ register: (name, options$1) => register(name, options$1),
1785
+ setValue: (name, value, options$1) => setValue(name, value, options$1),
1786
+ getValues: (name) => getValues(name),
1787
+ watch: (name) => watch$1(name),
1788
+ getFieldState: (name) => getFieldState(name),
1789
+ trigger: (name) => trigger(name),
1790
+ clearErrors: (name) => clearErrors(name),
1791
+ setError: (name, error) => setError(name, error)
1792
+ });
1726
1793
  return {
1727
1794
  register,
1728
1795
  unregister,
@@ -837,6 +837,7 @@ function createFieldRegistration(ctx, validate) {
837
837
  let handlers = ctx.fieldHandlers.get(name);
838
838
  if (!handlers) {
839
839
  const runCustomValidation = async (fieldName, value, requestId, resetGenAtStart) => {
840
+ if (!ctx.fieldRefs.has(fieldName)) return;
840
841
  const fieldOpts = ctx.fieldOptions.get(fieldName);
841
842
  if (!fieldOpts?.validate || fieldOpts.disabled) return;
842
843
  const error = await fieldOpts.validate(value);
@@ -1007,7 +1008,7 @@ function createFieldRegistration(ctx, validate) {
1007
1008
  unregister
1008
1009
  };
1009
1010
  }
1010
- function createFieldArrayManager(ctx, validate, setFocus) {
1011
+ function createFieldArrayManager(ctx, validate, setFocus, formMethods) {
1011
1012
  function fields(name, options) {
1012
1013
  if (__DEV__) {
1013
1014
  const syntaxError = validatePathSyntax(name);
@@ -1051,16 +1052,76 @@ function createFieldArrayManager(ctx, validate, setFocus) {
1051
1052
  if (item) indexCache.set(item.key, i);
1052
1053
  }
1053
1054
  };
1054
- const createItem = (key) => ({
1055
- key,
1056
- get index() {
1057
- return indexCache.get(key) ?? -1;
1058
- },
1059
- remove() {
1060
- const currentIndex = indexCache.get(key) ?? -1;
1061
- if (currentIndex !== -1) removeAt(currentIndex);
1062
- }
1063
- });
1055
+ const buildPath = (index, subPath) => {
1056
+ return subPath ? `${name}.${index}.${subPath}` : `${name}.${index}`;
1057
+ };
1058
+ const createScopedMethods = (indexGetter) => {
1059
+ return {
1060
+ register: (subPath, options$1) => formMethods.register(buildPath(indexGetter(), subPath), options$1),
1061
+ setValue: (subPath, value, options$1) => formMethods.setValue(buildPath(indexGetter(), subPath), value, options$1),
1062
+ getValue: (subPath) => formMethods.getValues(buildPath(indexGetter(), subPath)),
1063
+ watch: (subPath) => formMethods.watch(buildPath(indexGetter(), subPath)),
1064
+ getFieldState: (subPath) => formMethods.getFieldState(buildPath(indexGetter(), subPath)),
1065
+ trigger: (subPath) => {
1066
+ const index = indexGetter();
1067
+ if (!subPath) return formMethods.trigger(buildPath(index));
1068
+ if (Array.isArray(subPath)) return formMethods.trigger(subPath.map((p) => buildPath(index, p)));
1069
+ return formMethods.trigger(buildPath(index, subPath));
1070
+ },
1071
+ clearErrors: (subPath) => {
1072
+ const index = indexGetter();
1073
+ if (!subPath) {
1074
+ formMethods.clearErrors(buildPath(index));
1075
+ return;
1076
+ }
1077
+ if (Array.isArray(subPath)) {
1078
+ formMethods.clearErrors(subPath.map((p) => buildPath(index, p)));
1079
+ return;
1080
+ }
1081
+ formMethods.clearErrors(buildPath(index, subPath));
1082
+ },
1083
+ setError: (subPath, error) => formMethods.setError(buildPath(indexGetter(), subPath), error)
1084
+ };
1085
+ };
1086
+ const createItem = (key) => {
1087
+ const getIndex = () => indexCache.get(key) ?? -1;
1088
+ let scoped = null;
1089
+ const getScoped = () => scoped ??= createScopedMethods(getIndex);
1090
+ return {
1091
+ key,
1092
+ get index() {
1093
+ return getIndex();
1094
+ },
1095
+ remove() {
1096
+ const currentIndex = getIndex();
1097
+ if (currentIndex !== -1) removeAt(currentIndex);
1098
+ },
1099
+ get register() {
1100
+ return getScoped().register;
1101
+ },
1102
+ get setValue() {
1103
+ return getScoped().setValue;
1104
+ },
1105
+ get getValue() {
1106
+ return getScoped().getValue;
1107
+ },
1108
+ get watch() {
1109
+ return getScoped().watch;
1110
+ },
1111
+ get getFieldState() {
1112
+ return getScoped().getFieldState;
1113
+ },
1114
+ get trigger() {
1115
+ return getScoped().trigger;
1116
+ },
1117
+ get clearErrors() {
1118
+ return getScoped().clearErrors;
1119
+ },
1120
+ get setError() {
1121
+ return getScoped().setError;
1122
+ }
1123
+ };
1124
+ };
1064
1125
  if (fa.items.value.length === 0 && fa.values.length > 0) {
1065
1126
  fa.items.value = fa.values.map(() => createItem(generateId()));
1066
1127
  rebuildIndexCache();
@@ -1069,7 +1130,7 @@ function createFieldArrayManager(ctx, validate, setFocus) {
1069
1130
  if (!focusOptions?.shouldFocus) return;
1070
1131
  await nextTick();
1071
1132
  const focusItemOffset = focusOptions?.focusIndex ?? 0;
1072
- let fieldPath = `${name}.${baseIndex + Math.min(focusItemOffset, addedCount - 1)}`;
1133
+ let fieldPath = `${name}.${baseIndex + Math.max(0, Math.min(focusItemOffset, addedCount - 1))}`;
1073
1134
  if (focusOptions?.focusName) fieldPath = `${fieldPath}.${focusOptions.focusName}`;
1074
1135
  setFocus(fieldPath);
1075
1136
  };
@@ -1240,13 +1301,10 @@ function createFieldArrayManager(ctx, validate, setFocus) {
1240
1301
  set(ctx.formData, name, newValues);
1241
1302
  const newItems = [...fa.items.value];
1242
1303
  const itemA = newItems[indexA];
1243
- const itemB = newItems[indexB];
1244
- if (itemA && itemB) {
1245
- newItems[indexA] = itemB;
1246
- newItems[indexB] = itemA;
1247
- fa.items.value = newItems;
1248
- swapInCache(indexA, indexB);
1249
- }
1304
+ newItems[indexA] = newItems[indexB];
1305
+ newItems[indexB] = itemA;
1306
+ fa.items.value = newItems;
1307
+ swapInCache(indexA, indexB);
1250
1308
  updateFieldDirtyState(ctx.dirtyFields, ctx.defaultValues, ctx.defaultValueHashes, name, get(ctx.formData, name));
1251
1309
  validateIfNeeded();
1252
1310
  return true;
@@ -1266,16 +1324,14 @@ function createFieldArrayManager(ctx, validate, setFocus) {
1266
1324
  }
1267
1325
  const newItems = [...fa.items.value];
1268
1326
  const [removedItem] = newItems.splice(from, 1);
1269
- if (removedItem) {
1270
- newItems.splice(to, 0, removedItem);
1271
- fa.items.value = newItems;
1272
- const minIdx = Math.min(from, to);
1273
- const maxIdx = Math.max(from, to);
1274
- const items = fa.items.value;
1275
- for (let i = minIdx; i <= maxIdx; i++) {
1276
- const item = items[i];
1277
- if (item) indexCache.set(item.key, i);
1278
- }
1327
+ newItems.splice(to, 0, removedItem);
1328
+ fa.items.value = newItems;
1329
+ const minIdx = Math.min(from, to);
1330
+ const maxIdx = Math.max(from, to);
1331
+ const items = fa.items.value;
1332
+ for (let i = minIdx; i <= maxIdx; i++) {
1333
+ const item = items[i];
1334
+ if (item) indexCache.set(item.key, i);
1279
1335
  }
1280
1336
  updateFieldDirtyState(ctx.dirtyFields, ctx.defaultValues, ctx.defaultValueHashes, name, get(ctx.formData, name));
1281
1337
  validateIfNeeded();
@@ -1382,8 +1438,6 @@ function useForm(options) {
1382
1438
  el.focus();
1383
1439
  if (focusOptions?.shouldSelect && el instanceof HTMLInputElement && typeof el.select === "function") el.select();
1384
1440
  }
1385
- const setFocusWrapper = (name) => setFocus(name);
1386
- const { fields } = createFieldArrayManager(ctx, validate, setFocusWrapper);
1387
1441
  let lastSyncTime = 0;
1388
1442
  const SYNC_DEBOUNCE_MS = 16;
1389
1443
  function syncWithDebounce() {
@@ -1527,6 +1581,7 @@ function useForm(options) {
1527
1581
  Object.assign(ctx.formData, newValues);
1528
1582
  if (!opts.keepErrors) {
1529
1583
  ctx.errors.value = {};
1584
+ ctx.externalErrors.value = {};
1530
1585
  ctx.persistentErrorFields.clear();
1531
1586
  }
1532
1587
  if (!opts.keepTouched) ctx.touchedFields.value = {};
@@ -1627,7 +1682,8 @@ function useForm(options) {
1627
1682
  for (const field of fieldsToClean) {
1628
1683
  clearFieldErrors(ctx.errors, field);
1629
1684
  clearFieldErrors(ctx.externalErrors, field);
1630
- ctx.persistentErrorFields.delete(field);
1685
+ const prefix = `${field}.`;
1686
+ for (const persistentField of ctx.persistentErrorFields) if (persistentField === field || persistentField.startsWith(prefix)) ctx.persistentErrorFields.delete(persistentField);
1631
1687
  }
1632
1688
  }
1633
1689
  function setError(name, error) {
@@ -1721,6 +1777,17 @@ function useForm(options) {
1721
1777
  }
1722
1778
  return await validate(name);
1723
1779
  }
1780
+ const setFocusWrapper = (name) => setFocus(name);
1781
+ const { fields } = createFieldArrayManager(ctx, validate, setFocusWrapper, {
1782
+ register: (name, options$1) => register(name, options$1),
1783
+ setValue: (name, value, options$1) => setValue(name, value, options$1),
1784
+ getValues: (name) => getValues(name),
1785
+ watch: (name) => watch$1(name),
1786
+ getFieldState: (name) => getFieldState(name),
1787
+ trigger: (name) => trigger(name),
1788
+ clearErrors: (name) => clearErrors(name),
1789
+ setError: (name, error) => setError(name, error)
1790
+ });
1724
1791
  return {
1725
1792
  register,
1726
1793
  unregister,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vuehookform/core",
3
- "version": "0.4.6",
3
+ "version": "0.5.0",
4
4
  "description": "TypeScript-first form library for Vue 3, inspired by React Hook Form. Form-level state management with Zod validation.",
5
5
  "type": "module",
6
6
  "workspaces": [