@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 +46 -6
- package/dist/core/domSync.d.ts +24 -3
- package/dist/core/formContext.d.ts +1 -1
- package/dist/types.d.ts +6 -2
- package/dist/useForm.d.ts +6 -0
- package/dist/vuehookform.cjs +101 -67
- package/dist/vuehookform.js +102 -68
- package/package.json +7 -1
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
|
|
125
|
-
|
|
|
126
|
-
| `items[0].name`
|
|
127
|
-
| `:key="index"`
|
|
128
|
-
| `formState.errors`
|
|
129
|
-
| `v-model` + `register()`
|
|
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
|
|
package/dist/core/domSync.d.ts
CHANGED
|
@@ -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<
|
|
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
|
|
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(
|
|
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: (
|
|
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: (
|
|
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
|
*
|
package/dist/vuehookform.cjs
CHANGED
|
@@ -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
|
|
226
|
-
return schema.
|
|
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
|
|
229
|
+
return getSchemaType(schema) === "object";
|
|
233
230
|
}
|
|
234
231
|
function isZodArray$1(schema) {
|
|
235
|
-
return
|
|
232
|
+
return getSchemaType(schema) === "array";
|
|
236
233
|
}
|
|
237
234
|
function unwrapSchema(schema) {
|
|
238
|
-
const
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
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 (
|
|
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
|
-
|
|
927
|
+
const inputEl = getInputElement(el);
|
|
928
|
+
if (inputEl && !opts?.controlled) {
|
|
886
929
|
const value = get(ctx.formData, name);
|
|
887
|
-
if (value !== void 0) if (
|
|
888
|
-
else
|
|
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
|
-
|
|
1091
|
-
|
|
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
|
|
1113
|
-
|
|
1114
|
-
|
|
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
|
-
|
|
1218
|
+
const newItemsArray = [
|
|
1183
1219
|
...fa.items.value.slice(0, index),
|
|
1184
1220
|
...newItems,
|
|
1185
1221
|
...fa.items.value.slice(index)
|
|
1186
1222
|
];
|
|
1187
|
-
|
|
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
|
|
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
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
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.
|
|
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 (
|
|
1356
|
-
|
|
1357
|
-
|
|
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)
|
|
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
|
-
|
|
1812
|
+
setCalledFromController(true);
|
|
1813
|
+
const state = form.getFieldState(name);
|
|
1814
|
+
setCalledFromController(false);
|
|
1815
|
+
return state;
|
|
1782
1816
|
});
|
|
1783
1817
|
return {
|
|
1784
1818
|
field: {
|
package/dist/vuehookform.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { computed, inject, nextTick,
|
|
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
|
|
224
|
-
return schema.
|
|
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
|
|
227
|
+
return getSchemaType(schema) === "object";
|
|
231
228
|
}
|
|
232
229
|
function isZodArray$1(schema) {
|
|
233
|
-
return
|
|
230
|
+
return getSchemaType(schema) === "array";
|
|
234
231
|
}
|
|
235
232
|
function unwrapSchema(schema) {
|
|
236
|
-
const
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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 (
|
|
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
|
-
|
|
925
|
+
const inputEl = getInputElement(el);
|
|
926
|
+
if (inputEl && !opts?.controlled) {
|
|
884
927
|
const value = get(ctx.formData, name);
|
|
885
|
-
if (value !== void 0) if (
|
|
886
|
-
else
|
|
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
|
-
|
|
1089
|
-
|
|
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
|
|
1111
|
-
|
|
1112
|
-
|
|
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
|
-
|
|
1216
|
+
const newItemsArray = [
|
|
1181
1217
|
...fa.items.value.slice(0, index),
|
|
1182
1218
|
...newItems,
|
|
1183
1219
|
...fa.items.value.slice(index)
|
|
1184
1220
|
];
|
|
1185
|
-
|
|
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
|
|
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
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
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
|
-
|
|
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 (
|
|
1354
|
-
|
|
1355
|
-
|
|
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)
|
|
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
|
-
|
|
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.
|
|
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": {
|