formstand 0.3.0 → 0.4.1

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/dist/index.d.cts CHANGED
@@ -59,6 +59,7 @@ declare const flattenIssues: (issues: readonly z.core.$ZodIssue[]) => ErrorMap;
59
59
  declare const validateSync: <TSchema extends z.ZodType>(schema: TSchema, values: z.input<TSchema>) => SettledValidationResult<z.output<TSchema>>;
60
60
  declare const validateAsync: <TSchema extends z.ZodType>(schema: TSchema, values: z.input<TSchema>) => Promise<SettledValidationResult<z.output<TSchema>>>;
61
61
  declare const isAsyncRequiredError: (e: unknown) => boolean;
62
+ declare const emptyValueForSchema: (schema: z.ZodType) => null | undefined;
62
63
 
63
64
  type ReadonlyStoreApi<T> = Pick<StoreApi<T>, "getState" | "getInitialState" | "subscribe">;
64
65
  type CreateFormOptions<TSchema extends z.ZodType> = Readonly<{
@@ -80,6 +81,9 @@ type SubmitResult<TOutput> = Readonly<{
80
81
  errors: ErrorMap;
81
82
  }> | Readonly<{
82
83
  kind: "skipped";
84
+ }> | Readonly<{
85
+ kind: "error";
86
+ error: unknown;
83
87
  }>;
84
88
  type ArrayItemOf<T> = T extends readonly (infer U)[] ? U : never;
85
89
  type ErrorPath<TValues> = FieldPath<TValues> | "";
@@ -169,14 +173,6 @@ declare function useFormSelectorShallow<TSchema extends z.ZodType, U>(form: Form
169
173
  declare function useFormSelectorShallow<U>(form: FormStateApi & {
170
174
  readonly schema?: undefined;
171
175
  }, selector: (state: FormState<unknown>) => U): U;
172
- /**
173
- * @deprecated Renamed to `useFormSelector` — React DOM ships its own
174
- * (deprecated) `useFormState`, and auto-imports regularly grab the wrong one.
175
- * This alias will be removed before 1.0.
176
- */
177
- declare const useFormState: typeof useFormSelector;
178
- /** @deprecated Renamed to `useFormSelectorShallow`; see `useFormState`. */
179
- declare const useFormStateShallow: typeof useFormSelectorShallow;
180
176
 
181
177
  declare const useFormError: (form: FormStateApi) => readonly string[] | undefined;
182
178
 
@@ -190,7 +186,7 @@ type FieldFormApi = Readonly<{
190
186
  store: ReadonlyStore$1<FormState<unknown>>;
191
187
  setValue(path: string, value: unknown): void;
192
188
  setTouched(path: string, touched?: boolean): void;
193
- setError(path: string, errors: readonly string[]): void;
189
+ setError(path: string, errors: string | readonly string[]): void;
194
190
  clearErrors(path?: string): void;
195
191
  validateField(path: string): FieldValidationResult;
196
192
  validateFieldAsync(path: string): Promise<FieldValidationResult>;
@@ -210,61 +206,63 @@ type UseFieldReturn<TValue> = Readonly<{
210
206
  isValidating: boolean;
211
207
  setValue: (value: TValue) => void;
212
208
  setTouched: (touched?: boolean) => void;
213
- setError: (errors: readonly string[]) => void;
209
+ setError: (errors: string | readonly string[]) => void;
214
210
  clearError: () => void;
215
211
  validate: () => FieldValidationResult;
216
212
  validateAsync: () => Promise<FieldValidationResult>;
217
213
  onBlur: () => void;
218
214
  }>;
219
- declare function useField<TSchema extends z.ZodType, P extends FieldPath<z.input<TSchema>>>(form: Form<TSchema>, path: P, options?: UseFieldOptions): UseFieldReturn<FieldValue<z.input<TSchema>, P>>;
220
215
  declare function useField<TSchema extends z.ZodType>(form: Form<TSchema>, pathSelector: (state: FormState<z.input<TSchema>>) => string, options?: UseFieldOptions): UseFieldReturn<unknown>;
221
216
  declare function useField<TValue = unknown>(form: FieldFormApi & {
222
217
  readonly schema?: undefined;
223
218
  }, path: string | ((state: FormState<unknown>) => string), options?: UseFieldOptions): UseFieldReturn<TValue>;
219
+ declare function useField<TSchema extends z.ZodType, P extends FieldPath<z.input<TSchema>>>(form: Form<TSchema>, path: P, options?: UseFieldOptions): UseFieldReturn<FieldValue<z.input<TSchema>, P>>;
224
220
 
221
+ type PathsOf<F extends FieldFormApi> = F extends Form<infer TSchema extends z.ZodType> ? FieldPath<z.input<TSchema>> : string;
225
222
  type FieldRef<T> = ((instance: T | null) => void) | Readonly<{
226
223
  current: T | null;
227
224
  }> | null;
228
- type TextFieldProps = Readonly<{
229
- form: FieldFormApi;
230
- path: string;
225
+ type TextFieldProps<F extends FieldFormApi = FieldFormApi> = Readonly<{
226
+ form: F;
227
+ path: PathsOf<F>;
231
228
  label?: ReactNode;
232
229
  placeholder?: string;
233
230
  type?: "text" | "password" | "email" | "url" | "tel";
234
231
  autoComplete?: string;
235
232
  ref?: FieldRef<HTMLInputElement>;
236
233
  }>;
237
- declare const TextField: ({ form, path, label, placeholder, type, autoComplete, ref, }: TextFieldProps) => react.JSX.Element;
238
- type NumberFieldProps = Readonly<{
239
- form: FieldFormApi;
240
- path: string;
234
+ declare const TextField: <F extends FieldFormApi>({ form, path, label, placeholder, type, autoComplete, ref, }: TextFieldProps<F>) => react.JSX.Element;
235
+ type NumberFieldProps<F extends FieldFormApi = FieldFormApi> = Readonly<{
236
+ form: F;
237
+ path: PathsOf<F>;
241
238
  label?: ReactNode;
242
239
  placeholder?: string;
243
240
  ref?: FieldRef<HTMLInputElement>;
244
241
  }>;
245
- declare const NumberField: ({ form, path, label, placeholder, ref, }: NumberFieldProps) => react.JSX.Element;
246
- type CheckboxFieldProps = Readonly<{
247
- form: FieldFormApi;
248
- path: string;
242
+ declare const NumberField: <F extends FieldFormApi>({ form, path, label, placeholder, ref, }: NumberFieldProps<F>) => react.JSX.Element;
243
+ type CheckboxFieldProps<F extends FieldFormApi = FieldFormApi> = Readonly<{
244
+ form: F;
245
+ path: PathsOf<F>;
249
246
  label?: ReactNode;
250
247
  ref?: FieldRef<HTMLInputElement>;
251
248
  }>;
252
- declare const CheckboxField: ({ form, path, label, ref }: CheckboxFieldProps) => react.JSX.Element;
249
+ declare const CheckboxField: <F extends FieldFormApi>({ form, path, label, ref, }: CheckboxFieldProps<F>) => react.JSX.Element;
253
250
  type SelectFieldOption<T extends string> = Readonly<{
254
251
  value: T;
255
252
  label: ReactNode;
256
253
  }>;
257
- type SelectFieldProps<T extends string> = Readonly<{
258
- form: FieldFormApi;
259
- path: string;
254
+ type SelectFieldProps<T extends string, F extends FieldFormApi = FieldFormApi> = Readonly<{
255
+ form: F;
256
+ path: PathsOf<F>;
260
257
  label?: ReactNode;
261
258
  options: readonly SelectFieldOption<T>[];
262
259
  placeholder?: string;
263
260
  ref?: FieldRef<HTMLSelectElement>;
264
261
  }>;
265
- declare const SelectField: <T extends string>({ form, path, label, options, placeholder, ref, }: SelectFieldProps<T>) => react.JSX.Element;
262
+ declare const SelectField: <T extends string, F extends FieldFormApi>({ form, path, label, options, placeholder, ref, }: SelectFieldProps<T, F>) => react.JSX.Element;
266
263
 
267
264
  declare const focusFirstError: (errors: ErrorMap, root?: ParentNode) => boolean;
265
+ declare const focusField: (path: string, root?: ParentNode) => boolean;
268
266
 
269
267
  type ReadonlyStore<T> = Pick<StoreApi<T>, "getState" | "getInitialState" | "subscribe">;
270
268
  type FieldArrayFormApi = Readonly<{
@@ -337,4 +335,4 @@ declare const numberInputProps: <T extends number | null | undefined>(field: Use
337
335
  declare const checkboxProps: <T extends boolean | null | undefined>(field: UseFieldReturn<T>) => CheckboxProps;
338
336
  declare const selectProps: <T extends string | null | undefined>(field: UseFieldReturn<T>) => SelectProps;
339
337
 
340
- export { type BoolMap, CheckboxField, type CheckboxFieldProps, type CheckboxProps, type CreateFormOptions, type ErrorMap, type FieldArrayEntry, type FieldArrayFormApi, type FieldFormApi, type FieldPath, type FieldPathArg, type FieldRef, type FieldSnapshot, type FieldValidationResult, type FieldValue, type Form, type FormContextApi, type FormProviderProps, type FormState, type FormStateApi, type InvalidSubmitHandler, NumberField, type NumberFieldProps, type NumberInputProps, type ParsedNumberText, type PathSegment, type ReadonlyStoreApi, type ResetOptions, SelectField, type SelectFieldOption, type SelectFieldProps, type SelectProps, type SettledFieldValidationResult, type SettledValidationResult, type SubmitHandler, type SubmitOptions, type SubmitResult, TextField, type TextFieldProps, type TextInputProps, type UseFieldArrayReturn, type UseFieldOptions, type UseFieldReturn, type ValidationMode, type ValidationResult, type ValidationTrigger, checkboxProps, createForm, createFormContext, flattenIssues, focusFirstError, getAtPath, isAsyncRequiredError, numberInputProps, numberToInputText, parseNumberText, parsePath, selectProps, setAtPath, shouldValidateOn, textInputProps, useField, useFieldArray, useForm, useFormError, useFormSelector, useFormSelectorShallow, useFormState, useFormStateShallow, useIsDirty, useIsSubmitting, useIsValid, useSubmitCount, validateAsync, validateSync };
338
+ export { type BoolMap, CheckboxField, type CheckboxFieldProps, type CheckboxProps, type CreateFormOptions, type ErrorMap, type FieldArrayEntry, type FieldArrayFormApi, type FieldFormApi, type FieldPath, type FieldPathArg, type FieldRef, type FieldSnapshot, type FieldValidationResult, type FieldValue, type Form, type FormContextApi, type FormProviderProps, type FormState, type FormStateApi, type InvalidSubmitHandler, NumberField, type NumberFieldProps, type NumberInputProps, type ParsedNumberText, type PathSegment, type PathsOf, type ReadonlyStoreApi, type ResetOptions, SelectField, type SelectFieldOption, type SelectFieldProps, type SelectProps, type SettledFieldValidationResult, type SettledValidationResult, type SubmitHandler, type SubmitOptions, type SubmitResult, TextField, type TextFieldProps, type TextInputProps, type UseFieldArrayReturn, type UseFieldOptions, type UseFieldReturn, type ValidationMode, type ValidationResult, type ValidationTrigger, checkboxProps, createForm, createFormContext, emptyValueForSchema, flattenIssues, focusField, focusFirstError, getAtPath, isAsyncRequiredError, numberInputProps, numberToInputText, parseNumberText, parsePath, selectProps, setAtPath, shouldValidateOn, textInputProps, useField, useFieldArray, useForm, useFormError, useFormSelector, useFormSelectorShallow, useIsDirty, useIsSubmitting, useIsValid, useSubmitCount, validateAsync, validateSync };
package/dist/index.d.ts CHANGED
@@ -59,6 +59,7 @@ declare const flattenIssues: (issues: readonly z.core.$ZodIssue[]) => ErrorMap;
59
59
  declare const validateSync: <TSchema extends z.ZodType>(schema: TSchema, values: z.input<TSchema>) => SettledValidationResult<z.output<TSchema>>;
60
60
  declare const validateAsync: <TSchema extends z.ZodType>(schema: TSchema, values: z.input<TSchema>) => Promise<SettledValidationResult<z.output<TSchema>>>;
61
61
  declare const isAsyncRequiredError: (e: unknown) => boolean;
62
+ declare const emptyValueForSchema: (schema: z.ZodType) => null | undefined;
62
63
 
63
64
  type ReadonlyStoreApi<T> = Pick<StoreApi<T>, "getState" | "getInitialState" | "subscribe">;
64
65
  type CreateFormOptions<TSchema extends z.ZodType> = Readonly<{
@@ -80,6 +81,9 @@ type SubmitResult<TOutput> = Readonly<{
80
81
  errors: ErrorMap;
81
82
  }> | Readonly<{
82
83
  kind: "skipped";
84
+ }> | Readonly<{
85
+ kind: "error";
86
+ error: unknown;
83
87
  }>;
84
88
  type ArrayItemOf<T> = T extends readonly (infer U)[] ? U : never;
85
89
  type ErrorPath<TValues> = FieldPath<TValues> | "";
@@ -169,14 +173,6 @@ declare function useFormSelectorShallow<TSchema extends z.ZodType, U>(form: Form
169
173
  declare function useFormSelectorShallow<U>(form: FormStateApi & {
170
174
  readonly schema?: undefined;
171
175
  }, selector: (state: FormState<unknown>) => U): U;
172
- /**
173
- * @deprecated Renamed to `useFormSelector` — React DOM ships its own
174
- * (deprecated) `useFormState`, and auto-imports regularly grab the wrong one.
175
- * This alias will be removed before 1.0.
176
- */
177
- declare const useFormState: typeof useFormSelector;
178
- /** @deprecated Renamed to `useFormSelectorShallow`; see `useFormState`. */
179
- declare const useFormStateShallow: typeof useFormSelectorShallow;
180
176
 
181
177
  declare const useFormError: (form: FormStateApi) => readonly string[] | undefined;
182
178
 
@@ -190,7 +186,7 @@ type FieldFormApi = Readonly<{
190
186
  store: ReadonlyStore$1<FormState<unknown>>;
191
187
  setValue(path: string, value: unknown): void;
192
188
  setTouched(path: string, touched?: boolean): void;
193
- setError(path: string, errors: readonly string[]): void;
189
+ setError(path: string, errors: string | readonly string[]): void;
194
190
  clearErrors(path?: string): void;
195
191
  validateField(path: string): FieldValidationResult;
196
192
  validateFieldAsync(path: string): Promise<FieldValidationResult>;
@@ -210,61 +206,63 @@ type UseFieldReturn<TValue> = Readonly<{
210
206
  isValidating: boolean;
211
207
  setValue: (value: TValue) => void;
212
208
  setTouched: (touched?: boolean) => void;
213
- setError: (errors: readonly string[]) => void;
209
+ setError: (errors: string | readonly string[]) => void;
214
210
  clearError: () => void;
215
211
  validate: () => FieldValidationResult;
216
212
  validateAsync: () => Promise<FieldValidationResult>;
217
213
  onBlur: () => void;
218
214
  }>;
219
- declare function useField<TSchema extends z.ZodType, P extends FieldPath<z.input<TSchema>>>(form: Form<TSchema>, path: P, options?: UseFieldOptions): UseFieldReturn<FieldValue<z.input<TSchema>, P>>;
220
215
  declare function useField<TSchema extends z.ZodType>(form: Form<TSchema>, pathSelector: (state: FormState<z.input<TSchema>>) => string, options?: UseFieldOptions): UseFieldReturn<unknown>;
221
216
  declare function useField<TValue = unknown>(form: FieldFormApi & {
222
217
  readonly schema?: undefined;
223
218
  }, path: string | ((state: FormState<unknown>) => string), options?: UseFieldOptions): UseFieldReturn<TValue>;
219
+ declare function useField<TSchema extends z.ZodType, P extends FieldPath<z.input<TSchema>>>(form: Form<TSchema>, path: P, options?: UseFieldOptions): UseFieldReturn<FieldValue<z.input<TSchema>, P>>;
224
220
 
221
+ type PathsOf<F extends FieldFormApi> = F extends Form<infer TSchema extends z.ZodType> ? FieldPath<z.input<TSchema>> : string;
225
222
  type FieldRef<T> = ((instance: T | null) => void) | Readonly<{
226
223
  current: T | null;
227
224
  }> | null;
228
- type TextFieldProps = Readonly<{
229
- form: FieldFormApi;
230
- path: string;
225
+ type TextFieldProps<F extends FieldFormApi = FieldFormApi> = Readonly<{
226
+ form: F;
227
+ path: PathsOf<F>;
231
228
  label?: ReactNode;
232
229
  placeholder?: string;
233
230
  type?: "text" | "password" | "email" | "url" | "tel";
234
231
  autoComplete?: string;
235
232
  ref?: FieldRef<HTMLInputElement>;
236
233
  }>;
237
- declare const TextField: ({ form, path, label, placeholder, type, autoComplete, ref, }: TextFieldProps) => react.JSX.Element;
238
- type NumberFieldProps = Readonly<{
239
- form: FieldFormApi;
240
- path: string;
234
+ declare const TextField: <F extends FieldFormApi>({ form, path, label, placeholder, type, autoComplete, ref, }: TextFieldProps<F>) => react.JSX.Element;
235
+ type NumberFieldProps<F extends FieldFormApi = FieldFormApi> = Readonly<{
236
+ form: F;
237
+ path: PathsOf<F>;
241
238
  label?: ReactNode;
242
239
  placeholder?: string;
243
240
  ref?: FieldRef<HTMLInputElement>;
244
241
  }>;
245
- declare const NumberField: ({ form, path, label, placeholder, ref, }: NumberFieldProps) => react.JSX.Element;
246
- type CheckboxFieldProps = Readonly<{
247
- form: FieldFormApi;
248
- path: string;
242
+ declare const NumberField: <F extends FieldFormApi>({ form, path, label, placeholder, ref, }: NumberFieldProps<F>) => react.JSX.Element;
243
+ type CheckboxFieldProps<F extends FieldFormApi = FieldFormApi> = Readonly<{
244
+ form: F;
245
+ path: PathsOf<F>;
249
246
  label?: ReactNode;
250
247
  ref?: FieldRef<HTMLInputElement>;
251
248
  }>;
252
- declare const CheckboxField: ({ form, path, label, ref }: CheckboxFieldProps) => react.JSX.Element;
249
+ declare const CheckboxField: <F extends FieldFormApi>({ form, path, label, ref, }: CheckboxFieldProps<F>) => react.JSX.Element;
253
250
  type SelectFieldOption<T extends string> = Readonly<{
254
251
  value: T;
255
252
  label: ReactNode;
256
253
  }>;
257
- type SelectFieldProps<T extends string> = Readonly<{
258
- form: FieldFormApi;
259
- path: string;
254
+ type SelectFieldProps<T extends string, F extends FieldFormApi = FieldFormApi> = Readonly<{
255
+ form: F;
256
+ path: PathsOf<F>;
260
257
  label?: ReactNode;
261
258
  options: readonly SelectFieldOption<T>[];
262
259
  placeholder?: string;
263
260
  ref?: FieldRef<HTMLSelectElement>;
264
261
  }>;
265
- declare const SelectField: <T extends string>({ form, path, label, options, placeholder, ref, }: SelectFieldProps<T>) => react.JSX.Element;
262
+ declare const SelectField: <T extends string, F extends FieldFormApi>({ form, path, label, options, placeholder, ref, }: SelectFieldProps<T, F>) => react.JSX.Element;
266
263
 
267
264
  declare const focusFirstError: (errors: ErrorMap, root?: ParentNode) => boolean;
265
+ declare const focusField: (path: string, root?: ParentNode) => boolean;
268
266
 
269
267
  type ReadonlyStore<T> = Pick<StoreApi<T>, "getState" | "getInitialState" | "subscribe">;
270
268
  type FieldArrayFormApi = Readonly<{
@@ -337,4 +335,4 @@ declare const numberInputProps: <T extends number | null | undefined>(field: Use
337
335
  declare const checkboxProps: <T extends boolean | null | undefined>(field: UseFieldReturn<T>) => CheckboxProps;
338
336
  declare const selectProps: <T extends string | null | undefined>(field: UseFieldReturn<T>) => SelectProps;
339
337
 
340
- export { type BoolMap, CheckboxField, type CheckboxFieldProps, type CheckboxProps, type CreateFormOptions, type ErrorMap, type FieldArrayEntry, type FieldArrayFormApi, type FieldFormApi, type FieldPath, type FieldPathArg, type FieldRef, type FieldSnapshot, type FieldValidationResult, type FieldValue, type Form, type FormContextApi, type FormProviderProps, type FormState, type FormStateApi, type InvalidSubmitHandler, NumberField, type NumberFieldProps, type NumberInputProps, type ParsedNumberText, type PathSegment, type ReadonlyStoreApi, type ResetOptions, SelectField, type SelectFieldOption, type SelectFieldProps, type SelectProps, type SettledFieldValidationResult, type SettledValidationResult, type SubmitHandler, type SubmitOptions, type SubmitResult, TextField, type TextFieldProps, type TextInputProps, type UseFieldArrayReturn, type UseFieldOptions, type UseFieldReturn, type ValidationMode, type ValidationResult, type ValidationTrigger, checkboxProps, createForm, createFormContext, flattenIssues, focusFirstError, getAtPath, isAsyncRequiredError, numberInputProps, numberToInputText, parseNumberText, parsePath, selectProps, setAtPath, shouldValidateOn, textInputProps, useField, useFieldArray, useForm, useFormError, useFormSelector, useFormSelectorShallow, useFormState, useFormStateShallow, useIsDirty, useIsSubmitting, useIsValid, useSubmitCount, validateAsync, validateSync };
338
+ export { type BoolMap, CheckboxField, type CheckboxFieldProps, type CheckboxProps, type CreateFormOptions, type ErrorMap, type FieldArrayEntry, type FieldArrayFormApi, type FieldFormApi, type FieldPath, type FieldPathArg, type FieldRef, type FieldSnapshot, type FieldValidationResult, type FieldValue, type Form, type FormContextApi, type FormProviderProps, type FormState, type FormStateApi, type InvalidSubmitHandler, NumberField, type NumberFieldProps, type NumberInputProps, type ParsedNumberText, type PathSegment, type PathsOf, type ReadonlyStoreApi, type ResetOptions, SelectField, type SelectFieldOption, type SelectFieldProps, type SelectProps, type SettledFieldValidationResult, type SettledValidationResult, type SubmitHandler, type SubmitOptions, type SubmitResult, TextField, type TextFieldProps, type TextInputProps, type UseFieldArrayReturn, type UseFieldOptions, type UseFieldReturn, type ValidationMode, type ValidationResult, type ValidationTrigger, checkboxProps, createForm, createFormContext, emptyValueForSchema, flattenIssues, focusField, focusFirstError, getAtPath, isAsyncRequiredError, numberInputProps, numberToInputText, parseNumberText, parsePath, selectProps, setAtPath, shouldValidateOn, textInputProps, useField, useFieldArray, useForm, useFormError, useFormSelector, useFormSelectorShallow, useIsDirty, useIsSubmitting, useIsValid, useSubmitCount, validateAsync, validateSync };
package/dist/index.js CHANGED
@@ -186,6 +186,39 @@ var emptyValueForSchema = (schema) => {
186
186
  };
187
187
  return walk(schema, false);
188
188
  };
189
+ var unwrapForWalk = (s) => {
190
+ if (s instanceof z.ZodOptional || s instanceof z.ZodNullable) {
191
+ return s.unwrap();
192
+ }
193
+ if (s instanceof z.ZodDefault || s instanceof z.ZodPrefault || s instanceof z.ZodCatch || s instanceof z.ZodReadonly || s instanceof z.ZodNonOptional) {
194
+ return s.def.innerType;
195
+ }
196
+ if (s instanceof z.ZodPipe) return s.def.in;
197
+ return null;
198
+ };
199
+ var isLeafSchema = (s) => s instanceof z.ZodString || s instanceof z.ZodNumber || s instanceof z.ZodBoolean || s instanceof z.ZodBigInt || s instanceof z.ZodDate || s instanceof z.ZodEnum || s instanceof z.ZodLiteral || s instanceof z.ZodNull || s instanceof z.ZodUndefined || s instanceof z.ZodVoid || s instanceof z.ZodNaN || s instanceof z.ZodSymbol;
200
+ var SCHEMA_WALK_BUDGET = 64;
201
+ var schemaHasPath = (schema, path) => {
202
+ const walk = (s, segments, budget) => {
203
+ if (budget <= 0) return true;
204
+ const inner = unwrapForWalk(s);
205
+ if (inner !== null) return walk(inner, segments, budget - 1);
206
+ const [head, ...rest] = segments;
207
+ if (head === void 0) return true;
208
+ if (s instanceof z.ZodObject) {
209
+ const shape = s.shape;
210
+ const child = shape[String(head)];
211
+ if (child !== void 0) return walk(child, rest, budget - 1);
212
+ const catchall = s.def.catchall;
213
+ return catchall !== void 0 && !(catchall instanceof z.ZodNever);
214
+ }
215
+ if (s instanceof z.ZodArray) {
216
+ return typeof head === "number" ? walk(s.element, rest, budget - 1) : false;
217
+ }
218
+ return !isLeafSchema(s);
219
+ };
220
+ return walk(schema, parsePath(path), SCHEMA_WALK_BUDGET);
221
+ };
189
222
  var hasChecks = (s) => {
190
223
  const checks = s.def.checks;
191
224
  return checks !== void 0 && checks.length > 0;
@@ -227,17 +260,19 @@ var dirtyPathsOf = (values, initial, prefix) => {
227
260
  if (valuesEqual(values, initial)) return [];
228
261
  if (isPlainObject(values) && isPlainObject(initial)) {
229
262
  const keys = /* @__PURE__ */ new Set([...Object.keys(values), ...Object.keys(initial)]);
230
- return [...keys].flatMap(
263
+ const childPaths = [...keys].flatMap(
231
264
  (k) => dirtyPathsOf(
232
265
  values[k],
233
266
  initial[k],
234
267
  prefix === "" ? k : `${prefix}.${k}`
235
268
  )
236
269
  );
270
+ return childPaths.length > 0 ? childPaths : [prefix];
237
271
  }
238
272
  return [prefix];
239
273
  };
240
274
  var FORM_SEQ_KEY = "__form__";
275
+ var SUBMIT_SEQ_KEY = "__submit__";
241
276
  var stringArraysEqual = (a, b) => a === b || a.length === b.length && a.every((v, i) => v === b[i]);
242
277
  var reuseErrorRefs = (next, prev) => {
243
278
  if (next === prev) return prev;
@@ -357,6 +392,16 @@ var createForm = (schema, options) => {
357
392
  subschemaCache.set(path, sub);
358
393
  return sub;
359
394
  };
395
+ const checkedSchemaPaths = /* @__PURE__ */ new Set();
396
+ const warnMissingSchemaPath = (fn, path) => {
397
+ if (path === "" || checkedSchemaPaths.has(path)) return;
398
+ checkedSchemaPaths.add(path);
399
+ if (!schemaHasPath(schema, path)) {
400
+ console.warn(
401
+ `[formstand] ${fn}("${path}"): path does not exist in the schema \u2014 result will always be valid.`
402
+ );
403
+ }
404
+ };
360
405
  const fieldScopeFor = (path, values) => {
361
406
  const sub = path === "" ? null : subschemaAt(path);
362
407
  if (sub === null) {
@@ -394,6 +439,7 @@ var createForm = (schema, options) => {
394
439
  serverErrors: omitScope(state.serverErrors, [path])
395
440
  });
396
441
  const validateField = (path) => {
442
+ warnMissingSchemaPath("validateField", path);
397
443
  try {
398
444
  const scoped = scopedFieldErrors(path);
399
445
  store.setState((state) => ({
@@ -419,18 +465,18 @@ var createForm = (schema, options) => {
419
465
  };
420
466
  };
421
467
  const sequences = /* @__PURE__ */ new Map();
422
- const nextSeq = (key) => {
423
- const next = (sequences.get(key) ?? 0) + 1;
424
- sequences.set(key, next);
425
- return next;
468
+ const claimPass = (key) => {
469
+ const pass = Symbol(key);
470
+ sequences.set(key, pass);
471
+ return pass;
426
472
  };
427
473
  const validateAsyncOnForm = async () => {
428
- const seq = nextSeq(FORM_SEQ_KEY);
474
+ const pass = claimPass(FORM_SEQ_KEY);
429
475
  const valuesAtStart = store.getState().values;
430
476
  store.setState((state) => ({ ...state, isValidatingForm: true }));
431
477
  const result = await validateAsync(schema, valuesAtStart);
432
478
  const live = store.getState();
433
- const stillOwns = sequences.get(FORM_SEQ_KEY) === seq;
479
+ const stillOwns = sequences.get(FORM_SEQ_KEY) === pass;
434
480
  if (!stillOwns || live.values !== valuesAtStart) {
435
481
  if (stillOwns) {
436
482
  store.setState((state) => ({ ...state, isValidatingForm: false }));
@@ -453,7 +499,7 @@ var createForm = (schema, options) => {
453
499
  const result2 = await validateAsyncOnForm();
454
500
  return result2.kind === "valid";
455
501
  }
456
- const seqs = new Map(paths.map((p) => [p, nextSeq(p)]));
502
+ const passes = new Map(paths.map((p) => [p, claimPass(p)]));
457
503
  const valuesAtStart = store.getState().values;
458
504
  store.setState((state) => ({
459
505
  ...state,
@@ -468,7 +514,9 @@ var createForm = (schema, options) => {
468
514
  (k) => !inScopeOfAny(k, paths)
469
515
  );
470
516
  const live = store.getState();
471
- const stillCurrent = paths.filter((p) => sequences.get(p) === seqs.get(p));
517
+ const stillCurrent = paths.filter(
518
+ (p) => sequences.get(p) === passes.get(p)
519
+ );
472
520
  if (live.values !== valuesAtStart) {
473
521
  if (stillCurrent.length > 0) {
474
522
  store.setState((state) => ({
@@ -502,7 +550,14 @@ var createForm = (schema, options) => {
502
550
  return requestedValid;
503
551
  };
504
552
  const validateFieldAsync = async (path) => {
505
- const seq = nextSeq(path);
553
+ warnMissingSchemaPath("validateFieldAsync", path);
554
+ if (path === "") {
555
+ const result = await validateAsyncOnForm();
556
+ return fieldResult(
557
+ result.kind === "invalid" ? result.errors : emptyErrors
558
+ );
559
+ }
560
+ const pass = claimPass(path);
506
561
  const valuesAtStart = store.getState().values;
507
562
  store.setState((state) => ({
508
563
  ...state,
@@ -510,7 +565,7 @@ var createForm = (schema, options) => {
510
565
  }));
511
566
  const scoped = await scopedFieldErrorsAsync(path, valuesAtStart);
512
567
  const live = store.getState();
513
- const stillOwns = sequences.get(path) === seq;
568
+ const stillOwns = sequences.get(path) === pass;
514
569
  if (!stillOwns || live.values !== valuesAtStart) {
515
570
  if (stillOwns) {
516
571
  store.setState((state) => ({
@@ -561,7 +616,13 @@ var createForm = (schema, options) => {
561
616
  )
562
617
  }),
563
618
  touched: reKeyByArrayPath(state.touched, path, mapper),
564
- isValidating: reKeyByArrayPath(state.isValidating, path, mapper)
619
+ // Drop (don't re-key) in-flight flags under the array: the op changes
620
+ // `values`, so every pass they belong to fails its values-changed
621
+ // guard and never commits — but each pass's cleanup omits its
622
+ // ORIGINAL key, so a re-keyed entry would be orphaned and stick
623
+ // forever. Dropped, the flag is gone now and the cleanup is a
624
+ // harmless no-op.
625
+ isValidating: omitScope(state.isValidating, [path])
565
626
  };
566
627
  });
567
628
  };
@@ -583,33 +644,44 @@ var createForm = (schema, options) => {
583
644
  submitCount: state.submitCount + 1
584
645
  }));
585
646
  try {
586
- const result = await validateAsync(schema, store.getState().values);
647
+ const pass = claimPass(SUBMIT_SEQ_KEY);
648
+ const valuesAtStart = store.getState().values;
649
+ const result = await validateAsync(schema, valuesAtStart);
650
+ const stale = sequences.get(SUBMIT_SEQ_KEY) !== pass || store.getState().values !== valuesAtStart;
587
651
  if (result.kind === "invalid") {
588
- store.setState((state) => {
589
- const committed = commitFullPass(state, result.errors);
590
- return {
591
- ...state,
592
- ...committed,
593
- // Mark every errored field touched so touched-gated error UIs
594
- // show something after the canonical first failed submit. The
595
- // "" key (schema-level refine) has no field to touch.
596
- touched: {
597
- ...state.touched,
598
- ...Object.fromEntries(
599
- Object.keys(committed.errors).filter((k) => k !== "").map((k) => [k, true])
600
- )
601
- }
602
- };
603
- });
604
- const merged = store.getState().errors;
652
+ if (!stale) {
653
+ store.setState((state) => {
654
+ const committed = commitFullPass(state, result.errors);
655
+ return {
656
+ ...state,
657
+ ...committed,
658
+ // Mark every errored field touched so touched-gated error UIs
659
+ // show something after the canonical first failed submit. The
660
+ // "" key (schema-level refine) has no field to touch.
661
+ touched: {
662
+ ...state.touched,
663
+ ...Object.fromEntries(
664
+ Object.keys(committed.errors).filter((k) => k !== "").map((k) => [k, true])
665
+ )
666
+ }
667
+ };
668
+ });
669
+ }
670
+ const merged = stale ? result.errors : store.getState().errors;
605
671
  onInvalid?.(merged);
606
672
  return { kind: "invalid", errors: merged };
607
673
  }
608
- store.setState((state) => ({
609
- ...state,
610
- ...commitFullPass(state, emptyErrors)
611
- }));
612
- await onValid(result.data);
674
+ if (!stale) {
675
+ store.setState((state) => ({
676
+ ...state,
677
+ ...commitFullPass(state, emptyErrors)
678
+ }));
679
+ }
680
+ try {
681
+ await onValid(result.data);
682
+ } catch (error) {
683
+ return { kind: "error", error };
684
+ }
613
685
  return { kind: "valid", data: result.data };
614
686
  } finally {
615
687
  inFlight.count -= 1;
@@ -741,7 +813,12 @@ var createForm = (schema, options) => {
741
813
  ...errorChannels(state, {
742
814
  schemaErrors: emptyErrors,
743
815
  serverErrors: emptyErrors
744
- })
816
+ }),
817
+ // Clearing `sequences` disowns every in-flight pass, including its
818
+ // flag cleanup (the ownership check fails, so it never writes) —
819
+ // clear the flags here too, mirroring reset(), or they'd stick.
820
+ isValidating: emptyBools,
821
+ isValidatingForm: false
745
822
  }));
746
823
  },
747
824
  reset: (nextInitial, resetOptions) => {
@@ -847,7 +924,13 @@ var createForm = (schema, options) => {
847
924
  errors: reuseErrorRefs(
848
925
  mergeErrorChannels(schemaErrors, serverErrors),
849
926
  snap.errors ?? emptyErrors
850
- )
927
+ ),
928
+ // In-flight state is owned by live passes, never by snapshots: a
929
+ // snapshotted flag has no pass left to clear it (the pass that set
930
+ // it cleared its ORIGINAL entry when it settled), so restoring one
931
+ // would stick it forever.
932
+ isValidating: emptyBools,
933
+ isValidatingForm: false
851
934
  };
852
935
  })
853
936
  });
@@ -906,8 +989,6 @@ function useFormSelector(form, selector) {
906
989
  function useFormSelectorShallow(form, selector) {
907
990
  return useStore(form.store, useShallow(selector));
908
991
  }
909
- var useFormState = useFormSelector;
910
- var useFormStateShallow = useFormSelectorShallow;
911
992
  var useFormError = (form) => useStore(
912
993
  form.store,
913
994
  useShallow((state) => state.errors[""])
@@ -1005,7 +1086,10 @@ function useField(form, pathArg, options) {
1005
1086
  [form, path]
1006
1087
  );
1007
1088
  const setError = useCallback(
1008
- (errors) => form.setError(path, errors),
1089
+ // Normalize the string shorthand HERE, before forwarding: custom
1090
+ // FieldFormApi implementations written against the pre-0.4 surface are
1091
+ // typed for readonly string[] and must never receive a bare string.
1092
+ (errors) => form.setError(path, typeof errors === "string" ? [errors] : errors),
1009
1093
  [form, path]
1010
1094
  );
1011
1095
  const clearError = useCallback(
@@ -1204,7 +1288,12 @@ var NumberField = ({
1204
1288
  /* @__PURE__ */ jsx(ErrorText, { id: errorId, error: field.error })
1205
1289
  ] });
1206
1290
  };
1207
- var CheckboxField = ({ form, path, label, ref }) => {
1291
+ var CheckboxField = ({
1292
+ form,
1293
+ path,
1294
+ label,
1295
+ ref
1296
+ }) => {
1208
1297
  const id = useId();
1209
1298
  const errorId = `${id}-error`;
1210
1299
  const field = useField(form, path);
@@ -1248,7 +1337,12 @@ var SelectField = ({
1248
1337
  "aria-describedby": hasError(field.error) ? errorId : void 0,
1249
1338
  children: [
1250
1339
  showEmptyOption ? /* @__PURE__ */ jsx("option", { value: "", disabled: true, children: placeholder ?? "" }) : null,
1251
- options.map((opt) => /* @__PURE__ */ jsx("option", { value: opt.value, children: opt.label }, opt.value))
1340
+ options.map((opt, index) => (
1341
+ // Index-qualified: option lists may legitimately repeat a value
1342
+ // (e.g. differently-labelled aliases), and bare value keys would
1343
+ // collide.
1344
+ /* @__PURE__ */ jsx("option", { value: opt.value, children: opt.label }, `${index}-${opt.value}`)
1345
+ ))
1252
1346
  ]
1253
1347
  }
1254
1348
  ),
@@ -1257,24 +1351,43 @@ var SelectField = ({
1257
1351
  };
1258
1352
 
1259
1353
  // src/react/focusError.ts
1354
+ var focusableControls = (scope) => [
1355
+ ...scope.querySelectorAll(
1356
+ "input[name], select[name], textarea[name]"
1357
+ )
1358
+ ].filter(
1359
+ (el) => !el.matches(':disabled, [type="hidden"]') && el.closest("dialog:not([open])") === null
1360
+ );
1361
+ var holdsFocus = (el) => {
1362
+ const rootNode = el.getRootNode();
1363
+ const active = rootNode instanceof Document || rootNode instanceof ShadowRoot ? rootNode.activeElement : document.activeElement;
1364
+ return active === el;
1365
+ };
1366
+ var focusFirstOf = (candidates) => candidates.some((el) => {
1367
+ el.focus();
1368
+ return holdsFocus(el);
1369
+ });
1370
+ var nameMatchesAny = (el, paths) => {
1371
+ const name = el.getAttribute("name");
1372
+ return name !== null && paths.some((p) => isPathOrChild(name, p));
1373
+ };
1260
1374
  var focusFirstError = (errors, root) => {
1261
1375
  const scope = root ?? document;
1262
1376
  const erroredPaths = Object.keys(errors).filter(
1263
1377
  (k) => k !== "" && (errors[k]?.length ?? 0) > 0
1264
1378
  );
1265
1379
  const hasRootError = (errors[""]?.length ?? 0) > 0;
1266
- const controls = [
1267
- ...scope.querySelectorAll(
1268
- "input[name], select[name], textarea[name]"
1269
- )
1270
- ].filter((el) => !el.matches(':disabled, [type="hidden"]'));
1271
- const fieldMatch = controls.find((el) => {
1272
- const name = el.getAttribute("name");
1273
- return name !== null && erroredPaths.some((k) => isPathOrChild(name, k));
1274
- });
1275
- const target = fieldMatch ?? (hasRootError ? controls[0] : void 0);
1276
- target?.focus();
1277
- return target !== void 0;
1380
+ const controls = focusableControls(scope);
1381
+ const fieldMatches = controls.filter(
1382
+ (el) => nameMatchesAny(el, erroredPaths)
1383
+ );
1384
+ const rootFallbackApplies = hasRootError && (root !== void 0 || document.querySelectorAll("form").length <= 1);
1385
+ const candidates = fieldMatches.length > 0 ? fieldMatches : rootFallbackApplies ? controls : [];
1386
+ return focusFirstOf(candidates);
1387
+ };
1388
+ var focusField = (path, root) => {
1389
+ const controls = focusableControls(root ?? document);
1390
+ return path === "" ? (root !== void 0 || document.querySelectorAll("form").length <= 1) && focusFirstOf(controls) : focusFirstOf(controls.filter((el) => nameMatchesAny(el, [path])));
1278
1391
  };
1279
1392
  var EMPTY_ID_STATE = { items: [], ids: [], counter: 0 };
1280
1393
  var EMPTY_ITEMS = [];
@@ -1381,6 +1494,6 @@ var useFieldArray = (form, pathArg) => {
1381
1494
  );
1382
1495
  };
1383
1496
 
1384
- export { CheckboxField, NumberField, SelectField, TextField, checkboxProps, createForm, createFormContext, flattenIssues, focusFirstError, getAtPath, isAsyncRequiredError, numberInputProps, numberToInputText, parseNumberText, parsePath, selectProps, setAtPath, shouldValidateOn, textInputProps, useField, useFieldArray, useForm, useFormError, useFormSelector, useFormSelectorShallow, useFormState, useFormStateShallow, useIsDirty, useIsSubmitting, useIsValid, useSubmitCount, validateAsync, validateSync };
1497
+ export { CheckboxField, NumberField, SelectField, TextField, checkboxProps, createForm, createFormContext, emptyValueForSchema, flattenIssues, focusField, focusFirstError, getAtPath, isAsyncRequiredError, numberInputProps, numberToInputText, parseNumberText, parsePath, selectProps, setAtPath, shouldValidateOn, textInputProps, useField, useFieldArray, useForm, useFormError, useFormSelector, useFormSelectorShallow, useIsDirty, useIsSubmitting, useIsValid, useSubmitCount, validateAsync, validateSync };
1385
1498
  //# sourceMappingURL=index.js.map
1386
1499
  //# sourceMappingURL=index.js.map