@vuehookform/core 0.4.2 → 0.4.3

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
@@ -121,12 +121,52 @@ const { value, ...bindings } = register('field', { controlled: true })
121
121
 
122
122
  ### Common Mistakes
123
123
 
124
- | Wrong | Right |
125
- | ------------------------ | ------------------------ |
126
- | `items[0].name` | `items.0.name` |
127
- | `:key="index"` | `:key="field.key"` |
128
- | `formState.errors` | `formState.value.errors` |
129
- | `v-model` + `register()` | Either one, not both |
124
+ | Wrong | Right | Why |
125
+ | ------------------------------------ | ----------------------------------------- | -------------------------------------------- |
126
+ | `items[0].name` | `items.0.name` | Always use dot notation for paths |
127
+ | `:key="index"` | `:key="field.key"` | Index can change during reordering |
128
+ | `formState.errors` | `formState.value.errors` | formState is a Ref, must access `.value` |
129
+ | `v-model` + `register()` | Either one, not both | Causes double binding conflict |
130
+ | `const state = getFieldState('x')` | `formState.value.errors.x` | getFieldState returns snapshot, not reactive |
131
+ | `<CustomInput v-bind="register()"/>` | Use `controlled: true` or `useController` | Custom components need controlled mode |
132
+
133
+ #### ⚠️ Critical: `getFieldState()` is NOT Reactive
134
+
135
+ **Problem:** Calling `getFieldState()` once returns a snapshot that never updates.
136
+
137
+ ```vue
138
+ <!-- ❌ WRONG - Error will persist even after fixing the input -->
139
+ <script setup>
140
+ const emailState = getFieldState('email')
141
+ </script>
142
+ <template>
143
+ <span v-if="emailState.error">{{ emailState.error }}</span>
144
+ </template>
145
+ ```
146
+
147
+ **Solutions:**
148
+
149
+ ```vue
150
+ <!-- ✅ Option 1: Use formState (always reactive) -->
151
+ <span v-if="formState.value.errors.email">{{ formState.value.errors.email }}</span>
152
+
153
+ <!-- ✅ Option 2: Use computed for specific field -->
154
+ <script setup>
155
+ const emailError = computed(() => formState.value.errors.email)
156
+ </script>
157
+ <template>
158
+ <span v-if="emailError">{{ emailError }}</span>
159
+ </template>
160
+
161
+ <!-- ✅ Option 3: Use useController for reusable components -->
162
+ <script setup>
163
+ const { fieldState } = useController({ name: 'email', control })
164
+ // fieldState is a ComputedRef that updates automatically
165
+ </script>
166
+ <template>
167
+ <span v-if="fieldState.error">{{ fieldState.error }}</span>
168
+ </template>
169
+ ```
130
170
 
131
171
  ## Contributing
132
172
 
@@ -1,5 +1,25 @@
1
1
  import { Ref } from 'vue';
2
2
  import { RegisterOptions } from '../types';
3
+ /**
4
+ * Extract the actual HTMLInputElement from a ref value.
5
+ * Handles both native elements and Vue component instances (PrimeVue, Vuetify, etc.)
6
+ *
7
+ * Vue component libraries typically expose:
8
+ * - $el: The root DOM element of the component
9
+ * - Some components wrap inputs in divs, so we may need to query for the input
10
+ *
11
+ * @param refValue - The value from fieldRef.value (HTMLInputElement, Component, or null)
12
+ * @returns The underlying HTMLInputElement, or null if not found
13
+ */
14
+ export declare function getInputElement(refValue: unknown): HTMLInputElement | null;
15
+ /**
16
+ * Get a focusable element from a ref value.
17
+ * Works with both native elements and Vue component instances.
18
+ *
19
+ * @param refValue - The value from fieldRef.value
20
+ * @returns The focusable HTMLElement, or null if not found
21
+ */
22
+ export declare function getFocusableElement(refValue: unknown): HTMLElement | null;
3
23
  /**
4
24
  * Sync values from uncontrolled DOM inputs to form data
5
25
  *
@@ -15,13 +35,14 @@ import { RegisterOptions } from '../types';
15
35
  * @param fieldOptions - Map of field names to their registration options
16
36
  * @param formData - The reactive form data object to update
17
37
  */
18
- export declare function syncUncontrolledInputs(fieldRefs: Map<string, Ref<HTMLInputElement | null>>, fieldOptions: Map<string, RegisterOptions>, formData: Record<string, unknown>): void;
38
+ export declare function syncUncontrolledInputs(fieldRefs: Map<string, Ref<unknown>>, fieldOptions: Map<string, RegisterOptions>, formData: Record<string, unknown>): void;
19
39
  /**
20
40
  * Update a single DOM element with a new value
21
41
  *
22
42
  * Handles both checkbox and text inputs appropriately.
43
+ * Supports both native elements and Vue component instances.
23
44
  *
24
- * @param el - The DOM input element to update
45
+ * @param refValue - The ref value (HTMLInputElement, Vue component, or null)
25
46
  * @param value - The value to set
26
47
  */
27
- export declare function updateDomElement(el: HTMLInputElement, value: unknown): void;
48
+ export declare function updateDomElement(refValue: unknown, value: unknown): void;
@@ -30,7 +30,7 @@ export interface FieldHandlers {
30
30
  /** Handler for input events, triggers validation based on mode */
31
31
  onInput: (e: Event) => Promise<void>;
32
32
  /** Handler for blur events, marks field as touched and may trigger validation */
33
- onBlur: (e: Event) => Promise<void>;
33
+ onBlur: () => Promise<void>;
34
34
  /** Ref callback to capture the DOM element reference */
35
35
  refCallback: (el: unknown) => void;
36
36
  }
package/dist/types.d.ts CHANGED
@@ -356,7 +356,7 @@ export interface RegisterReturn<TValue = unknown> {
356
356
  /** Input handler (fires on every keystroke) */
357
357
  onInput: (e: Event) => void;
358
358
  /** Blur handler */
359
- onBlur: (e: Event) => void;
359
+ onBlur: () => void;
360
360
  /** Current value (for controlled mode) - only present when controlled: true */
361
361
  value?: Ref<TValue>;
362
362
  /** Disabled state from form-level disabled option */
@@ -418,6 +418,10 @@ export interface FieldArrayOptions<T = unknown> {
418
418
  * - minLength rule would be violated (remove, removeAll, removeMany)
419
419
  * - Index is out of bounds (remove, update, swap, move)
420
420
  *
421
+ * **Reactivity:** The `value` property is fully reactive - it automatically
422
+ * updates when array methods (append, remove, swap, etc.) are called.
423
+ * Your template will re-render when items change.
424
+ *
421
425
  * @template TItem - The type of items in the array (inferred from field path)
422
426
  *
423
427
  * @example
@@ -433,7 +437,7 @@ export interface FieldArrayOptions<T = unknown> {
433
437
  * }
434
438
  */
435
439
  export interface FieldArray<TItem = unknown> {
436
- /** Current field items with metadata */
440
+ /** Current field items with metadata. Reactive - updates when array methods are called. */
437
441
  value: FieldArrayItem[];
438
442
  /** Append item(s) to end of array. Returns false if maxLength exceeded. */
439
443
  append: (value: TItem | TItem[], options?: FieldArrayFocusOptions) => boolean;
package/dist/useForm.d.ts CHANGED
@@ -1,5 +1,11 @@
1
1
  import { ZodType } from 'zod';
2
2
  import { UseFormOptions, UseFormReturn } from './types';
3
+ /**
4
+ * Set the internal flag to suppress getFieldState warnings.
5
+ * Used by useController before calling getFieldState inside computed().
6
+ * @internal
7
+ */
8
+ export declare function setCalledFromController(value: boolean): void;
3
9
  /**
4
10
  * Main form management composable
5
11
  *
@@ -222,26 +222,68 @@ function warnArrayIndexOutOfBounds(operation, path, index, length) {
222
222
  if (!__DEV__) return;
223
223
  warn(`${operation}() on "${path}": Index ${index} is out of bounds (array length: ${length}). Operation was silently ignored.`);
224
224
  }
225
- function getDefProp$1(schema, prop) {
226
- return schema.def[prop];
227
- }
228
- function getTypeName(schema) {
229
- return getDefProp$1(schema, "typeName");
225
+ function getSchemaType(schema) {
226
+ return schema.type;
230
227
  }
231
228
  function isZodObject$1(schema) {
232
- return getTypeName(schema) === "ZodObject";
229
+ return getSchemaType(schema) === "object";
233
230
  }
234
231
  function isZodArray$1(schema) {
235
- return getTypeName(schema) === "ZodArray";
232
+ return getSchemaType(schema) === "array";
236
233
  }
237
234
  function unwrapSchema(schema) {
238
- const typeName = getTypeName(schema);
239
- const innerType = getDefProp$1(schema, "innerType");
240
- const schemaType = getDefProp$1(schema, "schema");
241
- if ((typeName === "ZodOptional" || typeName === "ZodNullable" || typeName === "ZodDefault") && innerType) return unwrapSchema(innerType);
242
- if (typeName === "ZodEffects" && schemaType) return unwrapSchema(schemaType);
235
+ const schemaType = getSchemaType(schema);
236
+ if (schemaType === "optional" || schemaType === "nullable" || schemaType === "default") {
237
+ const schemaWithUnwrap = schema;
238
+ if (typeof schemaWithUnwrap.unwrap === "function") return unwrapSchema(schemaWithUnwrap.unwrap());
239
+ }
243
240
  return schema;
244
241
  }
242
+ function getInputElement(refValue) {
243
+ if (!refValue) return null;
244
+ if (refValue instanceof HTMLInputElement) return refValue;
245
+ if (refValue instanceof HTMLSelectElement || refValue instanceof HTMLTextAreaElement) return refValue;
246
+ if (typeof refValue === "object" && "$el" in refValue) {
247
+ const el = refValue.$el;
248
+ if (el instanceof HTMLInputElement) return el;
249
+ if (el instanceof HTMLSelectElement || el instanceof HTMLTextAreaElement) return el;
250
+ if (el instanceof Element) {
251
+ const input = el.querySelector("input, select, textarea");
252
+ if (input instanceof HTMLInputElement || input instanceof HTMLSelectElement || input instanceof HTMLTextAreaElement) return input;
253
+ }
254
+ }
255
+ return null;
256
+ }
257
+ function getFocusableElement(refValue) {
258
+ const input = getInputElement(refValue);
259
+ if (input) return input;
260
+ if (typeof refValue === "object" && refValue && "$el" in refValue) {
261
+ const el = refValue.$el;
262
+ if (el instanceof HTMLElement && typeof el.focus === "function") return el;
263
+ }
264
+ if (refValue instanceof HTMLElement && typeof refValue.focus === "function") return refValue;
265
+ return null;
266
+ }
267
+ function syncUncontrolledInputs(fieldRefs, fieldOptions, formData) {
268
+ for (const [name, fieldRef] of Array.from(fieldRefs.entries())) {
269
+ const el = getInputElement(fieldRef.value);
270
+ if (el) {
271
+ if (!fieldOptions.get(name)?.controlled) {
272
+ let value;
273
+ if (el.type === "checkbox") value = el.checked;
274
+ else if (el.type === "number" || el.type === "range") value = el.valueAsNumber;
275
+ else value = el.value;
276
+ set(formData, name, value);
277
+ }
278
+ }
279
+ }
280
+ }
281
+ function updateDomElement(refValue, value) {
282
+ const el = getInputElement(refValue);
283
+ if (!el) return;
284
+ if (el.type === "checkbox") el.checked = value;
285
+ else el.value = value;
286
+ }
245
287
  function createFormContext(options) {
246
288
  const formData = (0, vue.reactive)({});
247
289
  const defaultValues = (0, vue.reactive)({});
@@ -305,8 +347,8 @@ function createFormContext(options) {
305
347
  const fieldRef = fieldRefs.get(key);
306
348
  const opts = fieldOptions.get(key);
307
349
  if (fieldRef?.value && !opts?.controlled) {
308
- const el = fieldRef.value;
309
- if (el.type === "checkbox") el.checked = value;
350
+ const el = getInputElement(fieldRef.value);
351
+ if (el) if (el.type === "checkbox") el.checked = value;
310
352
  else el.value = value;
311
353
  }
312
354
  }
@@ -566,7 +608,7 @@ function createFieldError(errors, criteriaMode = "firstError") {
566
608
  function createValidation(ctx) {
567
609
  function applyNativeValidation(fieldPath, errorMessage) {
568
610
  if (!ctx.options.shouldUseNativeValidation) return;
569
- const el = ctx.fieldRefs.get(fieldPath)?.value;
611
+ const el = getInputElement(ctx.fieldRefs.get(fieldPath)?.value);
570
612
  if (el && "setCustomValidity" in el) el.setCustomValidity(errorMessage || "");
571
613
  }
572
614
  function clearAllNativeValidation() {
@@ -863,7 +905,7 @@ function createFieldRegistration(ctx, validate) {
863
905
  }
864
906
  }
865
907
  };
866
- const onBlur = async (_e) => {
908
+ const onBlur = async () => {
867
909
  markFieldTouched(ctx.touchedFields, name);
868
910
  if (shouldValidateOnBlur(ctx.options.mode ?? "onSubmit", ctx.submitCount.value > 0, ctx.options.reValidateMode)) {
869
911
  await validate(name);
@@ -882,10 +924,11 @@ function createFieldRegistration(ctx, validate) {
882
924
  if (previousEl && el) return;
883
925
  currentFieldRef.value = el;
884
926
  const opts = ctx.fieldOptions.get(name);
885
- if (el && !opts?.controlled && el instanceof HTMLInputElement) {
927
+ const inputEl = getInputElement(el);
928
+ if (inputEl && !opts?.controlled) {
886
929
  const value = get(ctx.formData, name);
887
- if (value !== void 0) if (el.type === "checkbox") el.checked = value;
888
- else el.value = value;
930
+ if (value !== void 0) if (inputEl.type === "checkbox") inputEl.checked = value;
931
+ else inputEl.value = value;
889
932
  }
890
933
  if (previousEl && !el) {
891
934
  const timer = ctx.debounceTimers.get(name);
@@ -995,20 +1038,6 @@ function createFieldArrayManager(ctx, validate, setFocus) {
995
1038
  indexCache.set(item.key, idx);
996
1039
  });
997
1040
  };
998
- const appendToCache = (startIndex) => {
999
- const items = fa.items.value;
1000
- for (let i = startIndex; i < items.length; i++) {
1001
- const item = items[i];
1002
- if (item) indexCache.set(item.key, i);
1003
- }
1004
- };
1005
- const updateCacheAfterInsert = (insertIndex, _insertCount) => {
1006
- const items = fa.items.value;
1007
- for (let i = insertIndex; i < items.length; i++) {
1008
- const item = items[i];
1009
- if (item) indexCache.set(item.key, i);
1010
- }
1011
- };
1012
1041
  const swapInCache = (indexA, indexB) => {
1013
1042
  const items = fa.items.value;
1014
1043
  const itemA = items[indexA];
@@ -1087,8 +1116,12 @@ function createFieldArrayManager(ctx, validate, setFocus) {
1087
1116
  const newValues = [...currentValues, ...values];
1088
1117
  set(ctx.formData, name, newValues);
1089
1118
  const newItems = values.map(() => createItem(generateId()));
1090
- fa.items.value = [...fa.items.value, ...newItems];
1091
- appendToCache(insertIndex);
1119
+ const newItemsArray = [...fa.items.value, ...newItems];
1120
+ for (let i = insertIndex; i < newItemsArray.length; i++) {
1121
+ const item = newItemsArray[i];
1122
+ if (item) indexCache.set(item.key, i);
1123
+ }
1124
+ fa.items.value = newItemsArray;
1092
1125
  updateFieldDirtyState(ctx.dirtyFields, ctx.defaultValues, ctx.defaultValueHashes, name, get(ctx.formData, name));
1093
1126
  validateIfNeeded();
1094
1127
  handleFocus(insertIndex, values.length, focusOptions);
@@ -1109,9 +1142,12 @@ function createFieldArrayManager(ctx, validate, setFocus) {
1109
1142
  }
1110
1143
  const newValues = [...values, ...currentValues];
1111
1144
  set(ctx.formData, name, newValues);
1112
- const newItems = values.map(() => createItem(generateId()));
1113
- fa.items.value = [...newItems, ...fa.items.value];
1114
- updateCacheAfterInsert(0, values.length);
1145
+ const newItemsArray = [...values.map(() => createItem(generateId())), ...fa.items.value];
1146
+ for (let i = 0; i < newItemsArray.length; i++) {
1147
+ const item = newItemsArray[i];
1148
+ if (item) indexCache.set(item.key, i);
1149
+ }
1150
+ fa.items.value = newItemsArray;
1115
1151
  updateFieldDirtyState(ctx.dirtyFields, ctx.defaultValues, ctx.defaultValueHashes, name, get(ctx.formData, name));
1116
1152
  validateIfNeeded();
1117
1153
  handleFocus(0, values.length, focusOptions);
@@ -1179,12 +1215,16 @@ function createFieldArrayManager(ctx, validate, setFocus) {
1179
1215
  ];
1180
1216
  set(ctx.formData, name, newValues);
1181
1217
  const newItems = values.map(() => createItem(generateId()));
1182
- fa.items.value = [
1218
+ const newItemsArray = [
1183
1219
  ...fa.items.value.slice(0, index),
1184
1220
  ...newItems,
1185
1221
  ...fa.items.value.slice(index)
1186
1222
  ];
1187
- updateCacheAfterInsert(index, values.length);
1223
+ for (let i = index; i < newItemsArray.length; i++) {
1224
+ const item = newItemsArray[i];
1225
+ if (item) indexCache.set(item.key, i);
1226
+ }
1227
+ fa.items.value = newItemsArray;
1188
1228
  updateFieldDirtyState(ctx.dirtyFields, ctx.defaultValues, ctx.defaultValueHashes, name, get(ctx.formData, name));
1189
1229
  validateIfNeeded();
1190
1230
  handleFocus(index, values.length, focusOptions);
@@ -1299,7 +1339,9 @@ function createFieldArrayManager(ctx, validate, setFocus) {
1299
1339
  return true;
1300
1340
  };
1301
1341
  return {
1302
- value: fa.items.value,
1342
+ get value() {
1343
+ return fa.items.value;
1344
+ },
1303
1345
  append,
1304
1346
  prepend,
1305
1347
  remove: removeAt,
@@ -1314,28 +1356,14 @@ function createFieldArrayManager(ctx, validate, setFocus) {
1314
1356
  }
1315
1357
  return { fields };
1316
1358
  }
1317
- function syncUncontrolledInputs(fieldRefs, fieldOptions, formData) {
1318
- for (const [name, fieldRef] of Array.from(fieldRefs.entries())) {
1319
- const el = fieldRef.value;
1320
- if (el) {
1321
- if (!fieldOptions.get(name)?.controlled) {
1322
- let value;
1323
- if (el.type === "checkbox") value = el.checked;
1324
- else if (el.type === "number" || el.type === "range") value = el.valueAsNumber;
1325
- else value = el.value;
1326
- set(formData, name, value);
1327
- }
1328
- }
1329
- }
1330
- }
1331
- function updateDomElement(el, value) {
1332
- if (el.type === "checkbox") el.checked = value;
1333
- else el.value = value;
1359
+ var isCalledFromController = false;
1360
+ function setCalledFromController(value) {
1361
+ isCalledFromController = value;
1334
1362
  }
1335
1363
  function useForm(options) {
1336
1364
  const ctx = createFormContext(options);
1337
1365
  let isSubmissionLocked = false;
1338
- (0, vue.onUnmounted)(() => {
1366
+ (0, vue.onScopeDispose)(() => {
1339
1367
  ctx.cleanup();
1340
1368
  });
1341
1369
  const { validate, clearAllPendingErrors } = createValidation(ctx);
@@ -1351,11 +1379,10 @@ function useForm(options) {
1351
1379
  }
1352
1380
  const fieldRef = ctx.fieldRefs.get(name);
1353
1381
  if (!fieldRef?.value) return;
1354
- const el = fieldRef.value;
1355
- if (typeof el.focus === "function") {
1356
- el.focus();
1357
- if (focusOptions?.shouldSelect && el instanceof HTMLInputElement && typeof el.select === "function") el.select();
1358
- }
1382
+ const el = getFocusableElement(fieldRef.value);
1383
+ if (!el) return;
1384
+ el.focus();
1385
+ if (focusOptions?.shouldSelect && el instanceof HTMLInputElement && typeof el.select === "function") el.select();
1359
1386
  }
1360
1387
  const setFocusWrapper = (name) => setFocus(name);
1361
1388
  const { fields } = createFieldArrayManager(ctx, validate, setFocusWrapper);
@@ -1512,7 +1539,7 @@ function useForm(options) {
1512
1539
  if (!opts.keepIsSubmitSuccessful) ctx.isSubmitSuccessful.value = false;
1513
1540
  ctx.fieldArrays.clear();
1514
1541
  for (const [name, fieldRef] of Array.from(ctx.fieldRefs.entries())) {
1515
- const el = fieldRef.value;
1542
+ const el = getInputElement(fieldRef.value);
1516
1543
  if (el) {
1517
1544
  const value = get(newValues, name);
1518
1545
  if (value !== void 0) if (el.type === "checkbox") el.checked = value;
@@ -1552,7 +1579,10 @@ function useForm(options) {
1552
1579
  if (!opts.keepTouched) clearFieldTouched(ctx.touchedFields, name);
1553
1580
  if (!ctx.fieldOptions.get(name)?.controlled) {
1554
1581
  const fieldRef = ctx.fieldRefs.get(name);
1555
- if (fieldRef?.value) updateDomElement(fieldRef.value, clonedValue ?? (fieldRef.value.type === "checkbox" ? false : ""));
1582
+ if (fieldRef?.value) {
1583
+ const el = getInputElement(fieldRef.value);
1584
+ updateDomElement(fieldRef.value, clonedValue ?? (el?.type === "checkbox" ? false : ""));
1585
+ }
1556
1586
  }
1557
1587
  }
1558
1588
  function watch$1(name) {
@@ -1663,6 +1693,7 @@ function useForm(options) {
1663
1693
  const schemaResult = validatePathAgainstSchema(ctx.options.schema, name);
1664
1694
  if (!schemaResult.valid) warnPathNotInSchema("getFieldState", name, schemaResult.availableFields);
1665
1695
  }
1696
+ if ((0, vue.getCurrentInstance)() && !isCalledFromController) console.warn(`[vue-hook-form] getFieldState('${name}') returns a snapshot, not reactive refs.\nFor reactive error display, use one of these alternatives:\n • formState.value.errors.${name}\n • const { fieldState } = useController({ name: '${name}', control })\n • const ${name}Error = computed(() => formState.value.errors.${name})\n\nSee docs: https://github.com/vuehookform/core#common-mistakes`);
1666
1697
  }
1667
1698
  const error = get(ctx.errors.value, name);
1668
1699
  return {
@@ -1778,7 +1809,10 @@ function useController(options) {
1778
1809
  elementRef.value = el;
1779
1810
  };
1780
1811
  const fieldState = (0, vue.computed)(() => {
1781
- return form.getFieldState(name);
1812
+ setCalledFromController(true);
1813
+ const state = form.getFieldState(name);
1814
+ setCalledFromController(false);
1815
+ return state;
1782
1816
  });
1783
1817
  return {
1784
1818
  field: {
@@ -1,4 +1,4 @@
1
- import { computed, inject, nextTick, onUnmounted, provide, reactive, ref, shallowRef, toValue, watch } from "vue";
1
+ import { computed, getCurrentInstance, inject, nextTick, onScopeDispose, provide, reactive, ref, shallowRef, toValue, watch } from "vue";
2
2
  var pathCache = /* @__PURE__ */ new Map();
3
3
  var PATH_CACHE_MAX_SIZE = 256;
4
4
  var MAX_ARRAY_INDEX = 1e4;
@@ -220,26 +220,68 @@ function warnArrayIndexOutOfBounds(operation, path, index, length) {
220
220
  if (!__DEV__) return;
221
221
  warn(`${operation}() on "${path}": Index ${index} is out of bounds (array length: ${length}). Operation was silently ignored.`);
222
222
  }
223
- function getDefProp$1(schema, prop) {
224
- return schema.def[prop];
225
- }
226
- function getTypeName(schema) {
227
- return getDefProp$1(schema, "typeName");
223
+ function getSchemaType(schema) {
224
+ return schema.type;
228
225
  }
229
226
  function isZodObject$1(schema) {
230
- return getTypeName(schema) === "ZodObject";
227
+ return getSchemaType(schema) === "object";
231
228
  }
232
229
  function isZodArray$1(schema) {
233
- return getTypeName(schema) === "ZodArray";
230
+ return getSchemaType(schema) === "array";
234
231
  }
235
232
  function unwrapSchema(schema) {
236
- const typeName = getTypeName(schema);
237
- const innerType = getDefProp$1(schema, "innerType");
238
- const schemaType = getDefProp$1(schema, "schema");
239
- if ((typeName === "ZodOptional" || typeName === "ZodNullable" || typeName === "ZodDefault") && innerType) return unwrapSchema(innerType);
240
- if (typeName === "ZodEffects" && schemaType) return unwrapSchema(schemaType);
233
+ const schemaType = getSchemaType(schema);
234
+ if (schemaType === "optional" || schemaType === "nullable" || schemaType === "default") {
235
+ const schemaWithUnwrap = schema;
236
+ if (typeof schemaWithUnwrap.unwrap === "function") return unwrapSchema(schemaWithUnwrap.unwrap());
237
+ }
241
238
  return schema;
242
239
  }
240
+ function getInputElement(refValue) {
241
+ if (!refValue) return null;
242
+ if (refValue instanceof HTMLInputElement) return refValue;
243
+ if (refValue instanceof HTMLSelectElement || refValue instanceof HTMLTextAreaElement) return refValue;
244
+ if (typeof refValue === "object" && "$el" in refValue) {
245
+ const el = refValue.$el;
246
+ if (el instanceof HTMLInputElement) return el;
247
+ if (el instanceof HTMLSelectElement || el instanceof HTMLTextAreaElement) return el;
248
+ if (el instanceof Element) {
249
+ const input = el.querySelector("input, select, textarea");
250
+ if (input instanceof HTMLInputElement || input instanceof HTMLSelectElement || input instanceof HTMLTextAreaElement) return input;
251
+ }
252
+ }
253
+ return null;
254
+ }
255
+ function getFocusableElement(refValue) {
256
+ const input = getInputElement(refValue);
257
+ if (input) return input;
258
+ if (typeof refValue === "object" && refValue && "$el" in refValue) {
259
+ const el = refValue.$el;
260
+ if (el instanceof HTMLElement && typeof el.focus === "function") return el;
261
+ }
262
+ if (refValue instanceof HTMLElement && typeof refValue.focus === "function") return refValue;
263
+ return null;
264
+ }
265
+ function syncUncontrolledInputs(fieldRefs, fieldOptions, formData) {
266
+ for (const [name, fieldRef] of Array.from(fieldRefs.entries())) {
267
+ const el = getInputElement(fieldRef.value);
268
+ if (el) {
269
+ if (!fieldOptions.get(name)?.controlled) {
270
+ let value;
271
+ if (el.type === "checkbox") value = el.checked;
272
+ else if (el.type === "number" || el.type === "range") value = el.valueAsNumber;
273
+ else value = el.value;
274
+ set(formData, name, value);
275
+ }
276
+ }
277
+ }
278
+ }
279
+ function updateDomElement(refValue, value) {
280
+ const el = getInputElement(refValue);
281
+ if (!el) return;
282
+ if (el.type === "checkbox") el.checked = value;
283
+ else el.value = value;
284
+ }
243
285
  function createFormContext(options) {
244
286
  const formData = reactive({});
245
287
  const defaultValues = reactive({});
@@ -303,8 +345,8 @@ function createFormContext(options) {
303
345
  const fieldRef = fieldRefs.get(key);
304
346
  const opts = fieldOptions.get(key);
305
347
  if (fieldRef?.value && !opts?.controlled) {
306
- const el = fieldRef.value;
307
- if (el.type === "checkbox") el.checked = value;
348
+ const el = getInputElement(fieldRef.value);
349
+ if (el) if (el.type === "checkbox") el.checked = value;
308
350
  else el.value = value;
309
351
  }
310
352
  }
@@ -564,7 +606,7 @@ function createFieldError(errors, criteriaMode = "firstError") {
564
606
  function createValidation(ctx) {
565
607
  function applyNativeValidation(fieldPath, errorMessage) {
566
608
  if (!ctx.options.shouldUseNativeValidation) return;
567
- const el = ctx.fieldRefs.get(fieldPath)?.value;
609
+ const el = getInputElement(ctx.fieldRefs.get(fieldPath)?.value);
568
610
  if (el && "setCustomValidity" in el) el.setCustomValidity(errorMessage || "");
569
611
  }
570
612
  function clearAllNativeValidation() {
@@ -861,7 +903,7 @@ function createFieldRegistration(ctx, validate) {
861
903
  }
862
904
  }
863
905
  };
864
- const onBlur = async (_e) => {
906
+ const onBlur = async () => {
865
907
  markFieldTouched(ctx.touchedFields, name);
866
908
  if (shouldValidateOnBlur(ctx.options.mode ?? "onSubmit", ctx.submitCount.value > 0, ctx.options.reValidateMode)) {
867
909
  await validate(name);
@@ -880,10 +922,11 @@ function createFieldRegistration(ctx, validate) {
880
922
  if (previousEl && el) return;
881
923
  currentFieldRef.value = el;
882
924
  const opts = ctx.fieldOptions.get(name);
883
- if (el && !opts?.controlled && el instanceof HTMLInputElement) {
925
+ const inputEl = getInputElement(el);
926
+ if (inputEl && !opts?.controlled) {
884
927
  const value = get(ctx.formData, name);
885
- if (value !== void 0) if (el.type === "checkbox") el.checked = value;
886
- else el.value = value;
928
+ if (value !== void 0) if (inputEl.type === "checkbox") inputEl.checked = value;
929
+ else inputEl.value = value;
887
930
  }
888
931
  if (previousEl && !el) {
889
932
  const timer = ctx.debounceTimers.get(name);
@@ -993,20 +1036,6 @@ function createFieldArrayManager(ctx, validate, setFocus) {
993
1036
  indexCache.set(item.key, idx);
994
1037
  });
995
1038
  };
996
- const appendToCache = (startIndex) => {
997
- const items = fa.items.value;
998
- for (let i = startIndex; i < items.length; i++) {
999
- const item = items[i];
1000
- if (item) indexCache.set(item.key, i);
1001
- }
1002
- };
1003
- const updateCacheAfterInsert = (insertIndex, _insertCount) => {
1004
- const items = fa.items.value;
1005
- for (let i = insertIndex; i < items.length; i++) {
1006
- const item = items[i];
1007
- if (item) indexCache.set(item.key, i);
1008
- }
1009
- };
1010
1039
  const swapInCache = (indexA, indexB) => {
1011
1040
  const items = fa.items.value;
1012
1041
  const itemA = items[indexA];
@@ -1085,8 +1114,12 @@ function createFieldArrayManager(ctx, validate, setFocus) {
1085
1114
  const newValues = [...currentValues, ...values];
1086
1115
  set(ctx.formData, name, newValues);
1087
1116
  const newItems = values.map(() => createItem(generateId()));
1088
- fa.items.value = [...fa.items.value, ...newItems];
1089
- appendToCache(insertIndex);
1117
+ const newItemsArray = [...fa.items.value, ...newItems];
1118
+ for (let i = insertIndex; i < newItemsArray.length; i++) {
1119
+ const item = newItemsArray[i];
1120
+ if (item) indexCache.set(item.key, i);
1121
+ }
1122
+ fa.items.value = newItemsArray;
1090
1123
  updateFieldDirtyState(ctx.dirtyFields, ctx.defaultValues, ctx.defaultValueHashes, name, get(ctx.formData, name));
1091
1124
  validateIfNeeded();
1092
1125
  handleFocus(insertIndex, values.length, focusOptions);
@@ -1107,9 +1140,12 @@ function createFieldArrayManager(ctx, validate, setFocus) {
1107
1140
  }
1108
1141
  const newValues = [...values, ...currentValues];
1109
1142
  set(ctx.formData, name, newValues);
1110
- const newItems = values.map(() => createItem(generateId()));
1111
- fa.items.value = [...newItems, ...fa.items.value];
1112
- updateCacheAfterInsert(0, values.length);
1143
+ const newItemsArray = [...values.map(() => createItem(generateId())), ...fa.items.value];
1144
+ for (let i = 0; i < newItemsArray.length; i++) {
1145
+ const item = newItemsArray[i];
1146
+ if (item) indexCache.set(item.key, i);
1147
+ }
1148
+ fa.items.value = newItemsArray;
1113
1149
  updateFieldDirtyState(ctx.dirtyFields, ctx.defaultValues, ctx.defaultValueHashes, name, get(ctx.formData, name));
1114
1150
  validateIfNeeded();
1115
1151
  handleFocus(0, values.length, focusOptions);
@@ -1177,12 +1213,16 @@ function createFieldArrayManager(ctx, validate, setFocus) {
1177
1213
  ];
1178
1214
  set(ctx.formData, name, newValues);
1179
1215
  const newItems = values.map(() => createItem(generateId()));
1180
- fa.items.value = [
1216
+ const newItemsArray = [
1181
1217
  ...fa.items.value.slice(0, index),
1182
1218
  ...newItems,
1183
1219
  ...fa.items.value.slice(index)
1184
1220
  ];
1185
- updateCacheAfterInsert(index, values.length);
1221
+ for (let i = index; i < newItemsArray.length; i++) {
1222
+ const item = newItemsArray[i];
1223
+ if (item) indexCache.set(item.key, i);
1224
+ }
1225
+ fa.items.value = newItemsArray;
1186
1226
  updateFieldDirtyState(ctx.dirtyFields, ctx.defaultValues, ctx.defaultValueHashes, name, get(ctx.formData, name));
1187
1227
  validateIfNeeded();
1188
1228
  handleFocus(index, values.length, focusOptions);
@@ -1297,7 +1337,9 @@ function createFieldArrayManager(ctx, validate, setFocus) {
1297
1337
  return true;
1298
1338
  };
1299
1339
  return {
1300
- value: fa.items.value,
1340
+ get value() {
1341
+ return fa.items.value;
1342
+ },
1301
1343
  append,
1302
1344
  prepend,
1303
1345
  remove: removeAt,
@@ -1312,28 +1354,14 @@ function createFieldArrayManager(ctx, validate, setFocus) {
1312
1354
  }
1313
1355
  return { fields };
1314
1356
  }
1315
- function syncUncontrolledInputs(fieldRefs, fieldOptions, formData) {
1316
- for (const [name, fieldRef] of Array.from(fieldRefs.entries())) {
1317
- const el = fieldRef.value;
1318
- if (el) {
1319
- if (!fieldOptions.get(name)?.controlled) {
1320
- let value;
1321
- if (el.type === "checkbox") value = el.checked;
1322
- else if (el.type === "number" || el.type === "range") value = el.valueAsNumber;
1323
- else value = el.value;
1324
- set(formData, name, value);
1325
- }
1326
- }
1327
- }
1328
- }
1329
- function updateDomElement(el, value) {
1330
- if (el.type === "checkbox") el.checked = value;
1331
- else el.value = value;
1357
+ var isCalledFromController = false;
1358
+ function setCalledFromController(value) {
1359
+ isCalledFromController = value;
1332
1360
  }
1333
1361
  function useForm(options) {
1334
1362
  const ctx = createFormContext(options);
1335
1363
  let isSubmissionLocked = false;
1336
- onUnmounted(() => {
1364
+ onScopeDispose(() => {
1337
1365
  ctx.cleanup();
1338
1366
  });
1339
1367
  const { validate, clearAllPendingErrors } = createValidation(ctx);
@@ -1349,11 +1377,10 @@ function useForm(options) {
1349
1377
  }
1350
1378
  const fieldRef = ctx.fieldRefs.get(name);
1351
1379
  if (!fieldRef?.value) return;
1352
- const el = fieldRef.value;
1353
- if (typeof el.focus === "function") {
1354
- el.focus();
1355
- if (focusOptions?.shouldSelect && el instanceof HTMLInputElement && typeof el.select === "function") el.select();
1356
- }
1380
+ const el = getFocusableElement(fieldRef.value);
1381
+ if (!el) return;
1382
+ el.focus();
1383
+ if (focusOptions?.shouldSelect && el instanceof HTMLInputElement && typeof el.select === "function") el.select();
1357
1384
  }
1358
1385
  const setFocusWrapper = (name) => setFocus(name);
1359
1386
  const { fields } = createFieldArrayManager(ctx, validate, setFocusWrapper);
@@ -1510,7 +1537,7 @@ function useForm(options) {
1510
1537
  if (!opts.keepIsSubmitSuccessful) ctx.isSubmitSuccessful.value = false;
1511
1538
  ctx.fieldArrays.clear();
1512
1539
  for (const [name, fieldRef] of Array.from(ctx.fieldRefs.entries())) {
1513
- const el = fieldRef.value;
1540
+ const el = getInputElement(fieldRef.value);
1514
1541
  if (el) {
1515
1542
  const value = get(newValues, name);
1516
1543
  if (value !== void 0) if (el.type === "checkbox") el.checked = value;
@@ -1550,7 +1577,10 @@ function useForm(options) {
1550
1577
  if (!opts.keepTouched) clearFieldTouched(ctx.touchedFields, name);
1551
1578
  if (!ctx.fieldOptions.get(name)?.controlled) {
1552
1579
  const fieldRef = ctx.fieldRefs.get(name);
1553
- if (fieldRef?.value) updateDomElement(fieldRef.value, clonedValue ?? (fieldRef.value.type === "checkbox" ? false : ""));
1580
+ if (fieldRef?.value) {
1581
+ const el = getInputElement(fieldRef.value);
1582
+ updateDomElement(fieldRef.value, clonedValue ?? (el?.type === "checkbox" ? false : ""));
1583
+ }
1554
1584
  }
1555
1585
  }
1556
1586
  function watch$1(name) {
@@ -1661,6 +1691,7 @@ function useForm(options) {
1661
1691
  const schemaResult = validatePathAgainstSchema(ctx.options.schema, name);
1662
1692
  if (!schemaResult.valid) warnPathNotInSchema("getFieldState", name, schemaResult.availableFields);
1663
1693
  }
1694
+ if (getCurrentInstance() && !isCalledFromController) console.warn(`[vue-hook-form] getFieldState('${name}') returns a snapshot, not reactive refs.\nFor reactive error display, use one of these alternatives:\n • formState.value.errors.${name}\n • const { fieldState } = useController({ name: '${name}', control })\n • const ${name}Error = computed(() => formState.value.errors.${name})\n\nSee docs: https://github.com/vuehookform/core#common-mistakes`);
1664
1695
  }
1665
1696
  const error = get(ctx.errors.value, name);
1666
1697
  return {
@@ -1776,7 +1807,10 @@ function useController(options) {
1776
1807
  elementRef.value = el;
1777
1808
  };
1778
1809
  const fieldState = computed(() => {
1779
- return form.getFieldState(name);
1810
+ setCalledFromController(true);
1811
+ const state = form.getFieldState(name);
1812
+ setCalledFromController(false);
1813
+ return state;
1780
1814
  });
1781
1815
  return {
1782
1816
  field: {
package/package.json CHANGED
@@ -1,8 +1,11 @@
1
1
  {
2
2
  "name": "@vuehookform/core",
3
- "version": "0.4.2",
3
+ "version": "0.4.3",
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
+ "workspaces": [
7
+ "e2e"
8
+ ],
6
9
  "main": "./dist/vuehookform.cjs",
7
10
  "module": "./dist/vuehookform.js",
8
11
  "types": "./dist/index.d.ts",
@@ -34,6 +37,9 @@
34
37
  "test": "vitest",
35
38
  "test:run": "vitest run",
36
39
  "test:coverage": "vitest run --coverage",
40
+ "e2e:dev": "npm run -w e2e dev",
41
+ "test:e2e": "npm run build:lib && npm run -w e2e test:e2e:ci",
42
+ "test:e2e:open": "npm run build:lib && npm run -w e2e test:e2e:open",
37
43
  "prepare": "husky"
38
44
  },
39
45
  "repository": {