@fransek/form 0.2.0 → 0.2.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.
@@ -1,4 +1,4 @@
1
- export * from "./lib/Field";
2
- export * from "./lib/fieldState";
3
- export * from "./lib/Form";
4
- export * from "./lib/types";
1
+ export { Field } from "./lib/Field";
2
+ export { createFieldState, validate, validateAsync, validateIfDirty, validateIfDirtyAsync, } from "./lib/fieldState";
3
+ export { Form } from "./lib/Form";
4
+ export type { AsyncValidator, FieldProps, FieldRenderProps, FieldState, SyncValidator, Validation, ValidationMode, Validator, } from "./lib/types";
package/dist/cjs/index.js CHANGED
@@ -13,7 +13,4 @@ exports.validateAsync = fieldState.validateAsync;
13
13
  exports.validateIfDirty = fieldState.validateIfDirty;
14
14
  exports.validateIfDirtyAsync = fieldState.validateIfDirtyAsync;
15
15
  exports.Form = Form.Form;
16
- exports.FormContext = Form.FormContext;
17
- exports.focusFirstError = Form.focusFirstError;
18
- exports.useFormContext = Form.useFormContext;
19
16
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;"}
@@ -1,2 +1,8 @@
1
1
  import { FieldProps } from "./types";
2
+ /**
3
+ * A headless form field component that manages validation state using a render prop pattern.
4
+ *
5
+ * Connects to a parent {@link Form} for submit validation coordination.
6
+ * Passes current field state and event handlers to the `children` render function.
7
+ */
2
8
  export declare function Field<T>(props: FieldProps<T>): import("react").ReactNode;
@@ -4,10 +4,17 @@ var React = require('react');
4
4
  var fieldState = require('./fieldState.js');
5
5
  var Form = require('./Form.js');
6
6
 
7
+ /**
8
+ * A headless form field component that manages validation state using a render prop pattern.
9
+ *
10
+ * Connects to a parent {@link Form} for submit validation coordination.
11
+ * Passes current field state and event handlers to the `children` render function.
12
+ */
7
13
  function Field(props) {
8
14
  const { registerField, unregisterField, validationMode: formValidationMode, debounceMs: formDebounceMs, } = Form.useFormContext();
9
15
  const { children, state, onChange, onInput, onBlur, validation, debounceMs = formDebounceMs || 500, validationMode = formValidationMode || "touchedAndDirty", } = props;
10
16
  const stateRef = React.useRef(state);
17
+ stateRef.current = state;
11
18
  const validationTimeoutRef = React.useRef(null);
12
19
  const validationIdRef = React.useRef(0);
13
20
  const isValidatingOnBlurRef = React.useRef(false);
@@ -15,14 +22,8 @@ function Field(props) {
15
22
  const pendingValidationRef = React.useRef(null);
16
23
  const fieldRef = React.useRef(null);
17
24
  const id = React.useId();
18
- stateRef.current = state;
19
25
  React.useEffect(() => {
20
26
  async function performValidation() {
21
- if (!stateRef.current.isValidating &&
22
- stateRef.current.isDirty &&
23
- stateRef.current.isTouched) {
24
- return stateRef.current.isValid;
25
- }
26
27
  validationIdRef.current++;
27
28
  pendingValidationRef.current = fieldState.validate(stateRef.current, validation?.onChange, validation?.onBlur, validation?.onSubmit);
28
29
  if (pendingValidationRef.current.isValid) {
@@ -57,7 +58,7 @@ function Field(props) {
57
58
  const shouldValidate = validationMode === "dirty" ||
58
59
  validationMode === "touchedOrDirty" ||
59
60
  stateRef.current.isTouched;
60
- if (shouldValidate && (validation?.onChange || validation?.onChangeAsync)) {
61
+ if (shouldValidate) {
61
62
  const errorMessage = validation?.onChange?.(value);
62
63
  const willValidateAsync = Boolean(validation?.onChangeAsync && !errorMessage);
63
64
  onChange({
@@ -98,9 +99,10 @@ function Field(props) {
98
99
  const currentValidation = validationIdRef.current;
99
100
  let errorMessage = stateRef.current.errorMessage ||
100
101
  validation?.onBlur?.(stateRef.current.value);
101
- const shouldValidateOnChange = validationMode === "touched" ||
102
- validationMode === "touchedOrDirty" ||
103
- (validationMode === "touchedAndDirty" && stateRef.current.isDirty);
102
+ const shouldValidateOnChange = !stateRef.current.isTouched &&
103
+ (validationMode === "touched" ||
104
+ (validationMode === "touchedOrDirty" && !stateRef.current.isDirty) ||
105
+ (validationMode === "touchedAndDirty" && stateRef.current.isDirty));
104
106
  if (!errorMessage && shouldValidateOnChange && validation?.onChange) {
105
107
  errorMessage = validation.onChange(stateRef.current.value);
106
108
  }
@@ -1 +1 @@
1
- {"version":3,"file":"Field.js","sources":["../../../src/lib/Field.tsx"],"sourcesContent":["import { useEffect, useId, useRef } from \"react\";\nimport { validate, validateAsync } from \"./fieldState\";\nimport { useFormContext } from \"./Form\";\nimport { FieldProps, FieldState } from \"./types\";\n\nexport function Field<T>(props: FieldProps<T>) {\n const {\n registerField,\n unregisterField,\n validationMode: formValidationMode,\n debounceMs: formDebounceMs,\n } = useFormContext();\n\n const {\n children,\n state,\n onChange,\n onInput,\n onBlur,\n validation,\n debounceMs = formDebounceMs || 500,\n validationMode = formValidationMode || \"touchedAndDirty\",\n } = props;\n\n const stateRef = useRef(state);\n const validationTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(\n null,\n );\n const validationIdRef = useRef(0);\n const isValidatingOnBlurRef = useRef(false);\n const isValidatingOnChangeRef = useRef(false);\n const pendingValidationRef = useRef<FieldState<T> | null>(null);\n const fieldRef = useRef<HTMLElement | null>(null);\n const id = useId();\n\n stateRef.current = state;\n\n useEffect(() => {\n async function performValidation() {\n if (\n !stateRef.current.isValidating &&\n stateRef.current.isDirty &&\n stateRef.current.isTouched\n ) {\n return stateRef.current.isValid;\n }\n validationIdRef.current++;\n pendingValidationRef.current = validate(\n stateRef.current,\n validation?.onChange,\n validation?.onBlur,\n validation?.onSubmit,\n );\n if (pendingValidationRef.current.isValid) {\n pendingValidationRef.current = await validateAsync(\n stateRef.current,\n validation?.onChangeAsync,\n validation?.onBlurAsync,\n validation?.onSubmitAsync,\n );\n }\n return pendingValidationRef.current.isValid;\n }\n\n function commitPendingValidation() {\n if (pendingValidationRef.current) {\n onChange(pendingValidationRef.current);\n pendingValidationRef.current = null;\n }\n }\n\n registerField(\n id,\n fieldRef.current,\n performValidation,\n commitPendingValidation,\n );\n\n return () => {\n unregisterField(id);\n };\n }, [id, registerField, validation, onChange, unregisterField]);\n\n useEffect(() => {\n return () => {\n if (validationTimeoutRef.current) {\n clearTimeout(validationTimeoutRef.current);\n }\n };\n }, []);\n\n function handleChange(value: T) {\n onInput?.(value);\n\n if (validationTimeoutRef.current) {\n clearTimeout(validationTimeoutRef.current);\n }\n\n const currentValidation = ++validationIdRef.current;\n\n const shouldValidate =\n validationMode === \"dirty\" ||\n validationMode === \"touchedOrDirty\" ||\n stateRef.current.isTouched;\n\n if (shouldValidate && (validation?.onChange || validation?.onChangeAsync)) {\n const errorMessage = validation?.onChange?.(value);\n const willValidateAsync = Boolean(\n validation?.onChangeAsync && !errorMessage,\n );\n\n onChange({\n ...stateRef.current,\n value,\n errorMessage,\n isDirty: true,\n isValid: !errorMessage,\n isValidating: willValidateAsync,\n });\n\n if (willValidateAsync) {\n isValidatingOnChangeRef.current = true;\n validationTimeoutRef.current = setTimeout(async () => {\n const asyncErrorMessage = await validation?.onChangeAsync?.(value);\n\n isValidatingOnChangeRef.current = false;\n if (currentValidation === validationIdRef.current) {\n onChange({\n ...stateRef.current,\n errorMessage: asyncErrorMessage,\n isValid: !asyncErrorMessage,\n isValidating: isValidatingOnBlurRef.current,\n });\n }\n }, debounceMs);\n }\n } else {\n onChange({\n ...stateRef.current,\n value,\n isDirty: true,\n isValidating: false,\n });\n }\n }\n\n async function handleBlur() {\n onBlur?.();\n\n const currentValidation = validationIdRef.current;\n\n let errorMessage =\n stateRef.current.errorMessage ||\n validation?.onBlur?.(stateRef.current.value);\n\n const shouldValidateOnChange =\n validationMode === \"touched\" ||\n validationMode === \"touchedOrDirty\" ||\n (validationMode === \"touchedAndDirty\" && stateRef.current.isDirty);\n\n if (!errorMessage && shouldValidateOnChange && validation?.onChange) {\n errorMessage = validation.onChange(stateRef.current.value);\n }\n\n if (\n !errorMessage &&\n (validation?.onBlurAsync || validation?.onChangeAsync)\n ) {\n isValidatingOnBlurRef.current = true;\n onChange({\n ...stateRef.current,\n isValidating: true,\n isTouched: true,\n });\n\n const asyncValidations: Promise<React.ReactNode>[] = [];\n\n if (validation?.onBlurAsync) {\n asyncValidations.push(validation.onBlurAsync(stateRef.current.value));\n }\n\n if (shouldValidateOnChange && validation?.onChangeAsync) {\n asyncValidations.push(validation.onChangeAsync(stateRef.current.value));\n }\n\n const [blurError, changeError] = await Promise.all(asyncValidations);\n errorMessage = blurError || changeError;\n }\n\n isValidatingOnBlurRef.current = false;\n\n if (errorMessage && validationTimeoutRef.current) {\n clearTimeout(validationTimeoutRef.current);\n }\n\n if (currentValidation !== validationIdRef.current) {\n onChange({\n ...stateRef.current,\n isTouched: true,\n });\n return;\n }\n\n onChange({\n ...stateRef.current,\n errorMessage,\n isTouched: true,\n isValid: !errorMessage,\n isValidating: isValidatingOnChangeRef.current,\n });\n }\n\n const ref = (el: HTMLElement | null) => {\n fieldRef.current = el;\n };\n\n return children({\n ...stateRef.current,\n handleChange,\n handleBlur,\n ref,\n });\n}\n"],"names":["useFormContext","useRef","useId","useEffect","validate","validateAsync"],"mappings":"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"}
1
+ {"version":3,"file":"Field.js","sources":["../../../src/lib/Field.tsx"],"sourcesContent":["import { useEffect, useId, useRef } from \"react\";\nimport { validate, validateAsync } from \"./fieldState\";\nimport { useFormContext } from \"./Form\";\nimport { FieldProps, FieldState } from \"./types\";\n\n/**\n * A headless form field component that manages validation state using a render prop pattern.\n *\n * Connects to a parent {@link Form} for submit validation coordination.\n * Passes current field state and event handlers to the `children` render function.\n */\nexport function Field<T>(props: FieldProps<T>) {\n const {\n registerField,\n unregisterField,\n validationMode: formValidationMode,\n debounceMs: formDebounceMs,\n } = useFormContext();\n\n const {\n children,\n state,\n onChange,\n onInput,\n onBlur,\n validation,\n debounceMs = formDebounceMs || 500,\n validationMode = formValidationMode || \"touchedAndDirty\",\n } = props;\n\n const stateRef = useRef(state);\n stateRef.current = state;\n const validationTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(\n null,\n );\n const validationIdRef = useRef(0);\n const isValidatingOnBlurRef = useRef(false);\n const isValidatingOnChangeRef = useRef(false);\n const pendingValidationRef = useRef<FieldState<T> | null>(null);\n const fieldRef = useRef<HTMLElement | null>(null);\n const id = useId();\n\n useEffect(() => {\n async function performValidation() {\n validationIdRef.current++;\n pendingValidationRef.current = validate(\n stateRef.current,\n validation?.onChange,\n validation?.onBlur,\n validation?.onSubmit,\n );\n if (pendingValidationRef.current.isValid) {\n pendingValidationRef.current = await validateAsync(\n stateRef.current,\n validation?.onChangeAsync,\n validation?.onBlurAsync,\n validation?.onSubmitAsync,\n );\n }\n return pendingValidationRef.current.isValid;\n }\n\n function commitPendingValidation() {\n if (pendingValidationRef.current) {\n onChange(pendingValidationRef.current);\n pendingValidationRef.current = null;\n }\n }\n\n registerField(\n id,\n fieldRef.current,\n performValidation,\n commitPendingValidation,\n );\n\n return () => {\n unregisterField(id);\n };\n }, [id, registerField, validation, onChange, unregisterField]);\n\n useEffect(() => {\n return () => {\n if (validationTimeoutRef.current) {\n clearTimeout(validationTimeoutRef.current);\n }\n };\n }, []);\n\n function handleChange(value: T) {\n onInput?.(value);\n\n if (validationTimeoutRef.current) {\n clearTimeout(validationTimeoutRef.current);\n }\n\n const currentValidation = ++validationIdRef.current;\n\n const shouldValidate =\n validationMode === \"dirty\" ||\n validationMode === \"touchedOrDirty\" ||\n stateRef.current.isTouched;\n\n if (shouldValidate) {\n const errorMessage = validation?.onChange?.(value);\n const willValidateAsync = Boolean(\n validation?.onChangeAsync && !errorMessage,\n );\n\n onChange({\n ...stateRef.current,\n value,\n errorMessage,\n isDirty: true,\n isValid: !errorMessage,\n isValidating: willValidateAsync,\n });\n\n if (willValidateAsync) {\n isValidatingOnChangeRef.current = true;\n validationTimeoutRef.current = setTimeout(async () => {\n const asyncErrorMessage = await validation?.onChangeAsync?.(value);\n\n isValidatingOnChangeRef.current = false;\n if (currentValidation === validationIdRef.current) {\n onChange({\n ...stateRef.current,\n errorMessage: asyncErrorMessage,\n isValid: !asyncErrorMessage,\n isValidating: isValidatingOnBlurRef.current,\n });\n }\n }, debounceMs);\n }\n } else {\n onChange({\n ...stateRef.current,\n value,\n isDirty: true,\n isValidating: false,\n });\n }\n }\n\n async function handleBlur() {\n onBlur?.();\n\n const currentValidation = validationIdRef.current;\n\n let errorMessage =\n stateRef.current.errorMessage ||\n validation?.onBlur?.(stateRef.current.value);\n\n const shouldValidateOnChange =\n !stateRef.current.isTouched &&\n (validationMode === \"touched\" ||\n (validationMode === \"touchedOrDirty\" && !stateRef.current.isDirty) ||\n (validationMode === \"touchedAndDirty\" && stateRef.current.isDirty));\n\n if (!errorMessage && shouldValidateOnChange && validation?.onChange) {\n errorMessage = validation.onChange(stateRef.current.value);\n }\n\n if (\n !errorMessage &&\n (validation?.onBlurAsync || validation?.onChangeAsync)\n ) {\n isValidatingOnBlurRef.current = true;\n onChange({\n ...stateRef.current,\n isValidating: true,\n isTouched: true,\n });\n\n const asyncValidations: Promise<React.ReactNode>[] = [];\n\n if (validation?.onBlurAsync) {\n asyncValidations.push(validation.onBlurAsync(stateRef.current.value));\n }\n\n if (shouldValidateOnChange && validation?.onChangeAsync) {\n asyncValidations.push(validation.onChangeAsync(stateRef.current.value));\n }\n\n const [blurError, changeError] = await Promise.all(asyncValidations);\n errorMessage = blurError || changeError;\n }\n\n isValidatingOnBlurRef.current = false;\n\n if (errorMessage && validationTimeoutRef.current) {\n clearTimeout(validationTimeoutRef.current);\n }\n\n if (currentValidation !== validationIdRef.current) {\n onChange({\n ...stateRef.current,\n isTouched: true,\n });\n return;\n }\n\n onChange({\n ...stateRef.current,\n errorMessage,\n isTouched: true,\n isValid: !errorMessage,\n isValidating: isValidatingOnChangeRef.current,\n });\n }\n\n const ref = (el: HTMLElement | null) => {\n fieldRef.current = el;\n };\n\n return children({\n ...stateRef.current,\n handleChange,\n handleBlur,\n ref,\n });\n}\n"],"names":["useFormContext","useRef","useId","useEffect","validate","validateAsync"],"mappings":"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"}
@@ -1,10 +1,26 @@
1
1
  import React from "react";
2
2
  import { FormContextValue, ValidationMode } from "./types";
3
+ /** Props for the {@link Form} component. */
3
4
  interface FormProps extends Omit<React.ComponentProps<"form">, "onSubmit"> {
5
+ /** Default validation mode applied to all fields in the form. Defaults to `"touchedAndDirty"`. */
4
6
  validationMode?: ValidationMode;
7
+ /** Default debounce delay in milliseconds for async validators. Defaults to `500`. */
5
8
  debounceMs?: number;
6
- onSubmit?: (e: React.SubmitEvent<HTMLFormElement>, validateAllFields: () => Promise<boolean>) => void;
9
+ /**
10
+ * Submit handler called when the form is submitted.
11
+ * Receives the submit event and a `validateAllFields` function that triggers
12
+ * validation on all registered fields and returns whether the form is valid.
13
+ */
14
+ onSubmit?: (e: React.SubmitEvent<HTMLFormElement>, validateForm: () => Promise<boolean>) => void;
7
15
  }
16
+ /**
17
+ * A form component that provides context for coordinating field validation.
18
+ *
19
+ * Wraps a native `<form>` element and prevents the default submit behavior.
20
+ * On submit, the `onSubmit` callback is called with the submit event and a
21
+ * `validateAllFields` function that can be used to trigger validation on all
22
+ * registered {@link Field} components and focus the first invalid field.
23
+ */
8
24
  export declare function Form({ onSubmit, validationMode, debounceMs, ...props }: FormProps): React.JSX.Element;
9
25
  export declare const FormContext: React.Context<FormContextValue>;
10
26
  export declare function useFormContext(): FormContextValue;
@@ -2,6 +2,14 @@
2
2
 
3
3
  var React = require('react');
4
4
 
5
+ /**
6
+ * A form component that provides context for coordinating field validation.
7
+ *
8
+ * Wraps a native `<form>` element and prevents the default submit behavior.
9
+ * On submit, the `onSubmit` callback is called with the submit event and a
10
+ * `validateAllFields` function that can be used to trigger validation on all
11
+ * registered {@link Field} components and focus the first invalid field.
12
+ */
5
13
  function Form({ onSubmit, validationMode, debounceMs, ...props }) {
6
14
  const fieldsRef = React.useRef(new Map());
7
15
  const registerField = React.useCallback((id, ref, validate, commitPendingValidation) => {
@@ -14,7 +22,7 @@ function Form({ onSubmit, validationMode, debounceMs, ...props }) {
14
22
  const unregisterField = React.useCallback((id) => {
15
23
  fieldsRef.current.delete(id);
16
24
  }, []);
17
- const validateAllFields = React.useCallback(async () => {
25
+ const validateForm = React.useCallback(async () => {
18
26
  const fields = Array.from(fieldsRef.current.values());
19
27
  const validationPromises = fields.map(async (field) => ({
20
28
  isValid: await field.validate(),
@@ -34,7 +42,7 @@ function Form({ onSubmit, validationMode, debounceMs, ...props }) {
34
42
  validationMode,
35
43
  debounceMs,
36
44
  } },
37
- React.createElement("form", { onSubmit: (e) => onSubmit?.(e, validateAllFields), ...props })));
45
+ React.createElement("form", { onSubmit: (e) => onSubmit?.(e, validateForm), ...props })));
38
46
  }
39
47
  const FormContext = React.createContext({
40
48
  registerField: () => { },
@@ -52,26 +60,11 @@ function focusFirstError(results) {
52
60
  if (!firstInvalidField) {
53
61
  return;
54
62
  }
55
- let firstInvalid = firstInvalidField;
56
- if (firstInvalid.role === "radiogroup") {
57
- const radio = firstInvalid.querySelector('[role="radio"]');
58
- if (radio) {
59
- firstInvalid = radio;
60
- }
61
- }
62
- if (firstInvalid.role === "group") {
63
- const checkbox = firstInvalid.querySelector('[role="checkbox"]');
64
- if (checkbox) {
65
- firstInvalid = checkbox;
66
- }
67
- }
68
- firstInvalid.focus();
69
- const rect = firstInvalid.getBoundingClientRect();
70
- if (rect) {
71
- window.scrollTo({
72
- top: rect.top + window.scrollY - 100,
73
- });
74
- }
63
+ firstInvalidField.focus();
64
+ const rect = firstInvalidField.getBoundingClientRect();
65
+ window.scrollTo({
66
+ top: rect.top + window.scrollY - 100,
67
+ });
75
68
  }
76
69
 
77
70
  exports.Form = Form;
@@ -1 +1 @@
1
- {"version":3,"file":"Form.js","sources":["../../../src/lib/Form.tsx"],"sourcesContent":["import React, { useCallback, useRef } from \"react\";\nimport { FieldMap, FormContextValue, ValidationMode } from \"./types\";\n\ninterface FormProps extends Omit<React.ComponentProps<\"form\">, \"onSubmit\"> {\n validationMode?: ValidationMode;\n debounceMs?: number;\n onSubmit?: (\n e: React.SubmitEvent<HTMLFormElement>,\n validateAllFields: () => Promise<boolean>,\n ) => void;\n}\n\nexport function Form({\n onSubmit,\n validationMode,\n debounceMs,\n ...props\n}: FormProps) {\n const fieldsRef = useRef<FieldMap>(new Map());\n\n const registerField = useCallback(\n (\n id: string,\n ref: HTMLElement | null,\n validate: () => Promise<boolean>,\n commitPendingValidation: () => void,\n ) => {\n fieldsRef.current.set(id, {\n ref,\n validate,\n commitPendingValidation,\n });\n },\n [],\n );\n\n const unregisterField = useCallback((id: string) => {\n fieldsRef.current.delete(id);\n }, []);\n\n const validateAllFields = useCallback(async () => {\n const fields = Array.from(fieldsRef.current.values());\n const validationPromises = fields.map(async (field) => ({\n isValid: await field.validate(),\n ref: field.ref,\n }));\n const results = await Promise.all(validationPromises);\n fields.forEach((field) => field.commitPendingValidation());\n const hasErrors = results.some((result) => !result.isValid);\n if (hasErrors) {\n focusFirstError(results);\n }\n return !hasErrors;\n }, []);\n\n return (\n <FormContext.Provider\n value={{\n registerField,\n unregisterField,\n validationMode,\n debounceMs,\n }}\n >\n <form onSubmit={(e) => onSubmit?.(e, validateAllFields)} {...props} />\n </FormContext.Provider>\n );\n}\n\nexport const FormContext = React.createContext<FormContextValue>({\n registerField: () => {},\n unregisterField: () => {},\n});\n\nexport function useFormContext() {\n return React.useContext(FormContext);\n}\n\nexport function focusFirstError(\n results: {\n isValid: boolean;\n ref: HTMLElement | null;\n }[],\n) {\n const firstInvalidField = results\n .filter((field) => !field.isValid && field.ref)\n .map((field) => field.ref!)\n .sort((a, b) =>\n a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_PRECEDING ? 1 : -1,\n )\n .at(0);\n\n if (!firstInvalidField) {\n return;\n }\n\n let firstInvalid = firstInvalidField;\n\n if (firstInvalid.role === \"radiogroup\") {\n const radio = firstInvalid.querySelector<HTMLElement>('[role=\"radio\"]');\n if (radio) {\n firstInvalid = radio;\n }\n }\n\n if (firstInvalid.role === \"group\") {\n const checkbox =\n firstInvalid.querySelector<HTMLElement>('[role=\"checkbox\"]');\n if (checkbox) {\n firstInvalid = checkbox;\n }\n }\n\n firstInvalid.focus();\n const rect = firstInvalid.getBoundingClientRect();\n if (rect) {\n window.scrollTo({\n top: rect.top + window.scrollY - 100,\n });\n }\n}\n"],"names":["useRef","useCallback"],"mappings":"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"}
1
+ {"version":3,"file":"Form.js","sources":["../../../src/lib/Form.tsx"],"sourcesContent":["import React, { useCallback, useRef } from \"react\";\nimport { FieldMap, FormContextValue, ValidationMode } from \"./types\";\n\n/** Props for the {@link Form} component. */\ninterface FormProps extends Omit<React.ComponentProps<\"form\">, \"onSubmit\"> {\n /** Default validation mode applied to all fields in the form. Defaults to `\"touchedAndDirty\"`. */\n validationMode?: ValidationMode;\n /** Default debounce delay in milliseconds for async validators. Defaults to `500`. */\n debounceMs?: number;\n /**\n * Submit handler called when the form is submitted.\n * Receives the submit event and a `validateAllFields` function that triggers\n * validation on all registered fields and returns whether the form is valid.\n */\n onSubmit?: (\n e: React.SubmitEvent<HTMLFormElement>,\n validateForm: () => Promise<boolean>,\n ) => void;\n}\n\n/**\n * A form component that provides context for coordinating field validation.\n *\n * Wraps a native `<form>` element and prevents the default submit behavior.\n * On submit, the `onSubmit` callback is called with the submit event and a\n * `validateAllFields` function that can be used to trigger validation on all\n * registered {@link Field} components and focus the first invalid field.\n */\nexport function Form({\n onSubmit,\n validationMode,\n debounceMs,\n ...props\n}: FormProps) {\n const fieldsRef = useRef<FieldMap>(new Map());\n\n const registerField = useCallback(\n (\n id: string,\n ref: HTMLElement | null,\n validate: () => Promise<boolean>,\n commitPendingValidation: () => void,\n ) => {\n fieldsRef.current.set(id, {\n ref,\n validate,\n commitPendingValidation,\n });\n },\n [],\n );\n\n const unregisterField = useCallback((id: string) => {\n fieldsRef.current.delete(id);\n }, []);\n\n const validateForm = useCallback(async () => {\n const fields = Array.from(fieldsRef.current.values());\n const validationPromises = fields.map(async (field) => ({\n isValid: await field.validate(),\n ref: field.ref,\n }));\n const results = await Promise.all(validationPromises);\n fields.forEach((field) => field.commitPendingValidation());\n const hasErrors = results.some((result) => !result.isValid);\n if (hasErrors) {\n focusFirstError(results);\n }\n return !hasErrors;\n }, []);\n\n return (\n <FormContext.Provider\n value={{\n registerField,\n unregisterField,\n validationMode,\n debounceMs,\n }}\n >\n <form onSubmit={(e) => onSubmit?.(e, validateForm)} {...props} />\n </FormContext.Provider>\n );\n}\n\nexport const FormContext = React.createContext<FormContextValue>({\n registerField: () => {},\n unregisterField: () => {},\n});\n\nexport function useFormContext() {\n return React.useContext(FormContext);\n}\n\nexport function focusFirstError(\n results: {\n isValid: boolean;\n ref: HTMLElement | null;\n }[],\n) {\n const firstInvalidField = results\n .filter((field) => !field.isValid && field.ref)\n .map((field) => field.ref!)\n .sort((a, b) =>\n a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_PRECEDING ? 1 : -1,\n )\n .at(0);\n\n if (!firstInvalidField) {\n return;\n }\n\n firstInvalidField.focus();\n const rect = firstInvalidField.getBoundingClientRect();\n window.scrollTo({\n top: rect.top + window.scrollY - 100,\n });\n}\n"],"names":["useRef","useCallback"],"mappings":"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"}
@@ -1,6 +1,23 @@
1
1
  import { FieldState, SyncValidator, Validator } from "./types";
2
+ /** Creates the initial state for a field with the given initial value. */
2
3
  export declare function createFieldState<T>(initialValue: T): FieldState<T>;
4
+ /**
5
+ * Runs the provided synchronous validators against the current field value and returns an updated {@link FieldState}.
6
+ * Sets `isDirty` and `isTouched` to `true`. Stops at the first failing validator.
7
+ */
3
8
  export declare function validate<T>(state: FieldState<T>, ...validators: Array<SyncValidator<T> | undefined>): FieldState<T>;
9
+ /**
10
+ * Runs the provided synchronous and/or asynchronous validators against the current field value and returns an updated {@link FieldState}.
11
+ * Sets `isDirty` and `isTouched` to `true`. All validators run in parallel; the first truthy error message is used.
12
+ */
4
13
  export declare function validateAsync<T>(state: FieldState<T>, ...validators: Array<Validator<T> | undefined>): Promise<FieldState<T>>;
14
+ /**
15
+ * Runs the provided synchronous validators only if the field is dirty (i.e. the value has been changed).
16
+ * Returns the state unchanged if the field is not dirty.
17
+ */
5
18
  export declare function validateIfDirty<T>(state: FieldState<T>, ...validators: Array<SyncValidator<T> | undefined>): FieldState<T>;
19
+ /**
20
+ * Runs the provided synchronous and/or asynchronous validators only if the field is dirty (i.e. the value has been changed).
21
+ * Returns the state unchanged if the field is not dirty.
22
+ */
6
23
  export declare function validateIfDirtyAsync<T>(state: FieldState<T>, ...validators: Array<Validator<T> | undefined>): Promise<FieldState<T>>;
@@ -1,5 +1,6 @@
1
1
  'use strict';
2
2
 
3
+ /** Creates the initial state for a field with the given initial value. */
3
4
  function createFieldState(initialValue) {
4
5
  return {
5
6
  value: initialValue,
@@ -10,6 +11,10 @@ function createFieldState(initialValue) {
10
11
  isValidating: false,
11
12
  };
12
13
  }
14
+ /**
15
+ * Runs the provided synchronous validators against the current field value and returns an updated {@link FieldState}.
16
+ * Sets `isDirty` and `isTouched` to `true`. Stops at the first failing validator.
17
+ */
13
18
  function validate(state, ...validators) {
14
19
  for (const validator of validators) {
15
20
  const errorMessage = validator?.(state.value);
@@ -33,6 +38,10 @@ function validate(state, ...validators) {
33
38
  isValidating: false,
34
39
  };
35
40
  }
41
+ /**
42
+ * Runs the provided synchronous and/or asynchronous validators against the current field value and returns an updated {@link FieldState}.
43
+ * Sets `isDirty` and `isTouched` to `true`. All validators run in parallel; the first truthy error message is used.
44
+ */
36
45
  async function validateAsync(state, ...validators) {
37
46
  const errorMessages = await Promise.all(validators.map((validator) => validator?.(state.value)));
38
47
  const errorMessage = errorMessages.find(Boolean);
@@ -55,12 +64,20 @@ async function validateAsync(state, ...validators) {
55
64
  isValidating: false,
56
65
  };
57
66
  }
67
+ /**
68
+ * Runs the provided synchronous validators only if the field is dirty (i.e. the value has been changed).
69
+ * Returns the state unchanged if the field is not dirty.
70
+ */
58
71
  function validateIfDirty(state, ...validators) {
59
72
  if (!state.isDirty) {
60
73
  return state;
61
74
  }
62
75
  return validate(state, ...validators);
63
76
  }
77
+ /**
78
+ * Runs the provided synchronous and/or asynchronous validators only if the field is dirty (i.e. the value has been changed).
79
+ * Returns the state unchanged if the field is not dirty.
80
+ */
64
81
  async function validateIfDirtyAsync(state, ...validators) {
65
82
  if (!state.isDirty) {
66
83
  return state;
@@ -1 +1 @@
1
- {"version":3,"file":"fieldState.js","sources":["../../../src/lib/fieldState.ts"],"sourcesContent":["import { FieldState, SyncValidator, Validator } from \"./types\";\n\nexport function createFieldState<T>(initialValue: T): FieldState<T> {\n return {\n value: initialValue,\n errorMessage: undefined,\n isTouched: false,\n isDirty: false,\n isValid: true,\n isValidating: false,\n };\n}\n\nexport function validate<T>(\n state: FieldState<T>,\n ...validators: Array<SyncValidator<T> | undefined>\n): FieldState<T> {\n for (const validator of validators) {\n const errorMessage = validator?.(state.value);\n if (errorMessage) {\n return {\n ...state,\n errorMessage,\n isDirty: true,\n isTouched: true,\n isValid: false,\n isValidating: false,\n };\n }\n }\n\n return {\n ...state,\n errorMessage: undefined,\n isDirty: true,\n isTouched: true,\n isValid: true,\n isValidating: false,\n };\n}\n\nexport async function validateAsync<T>(\n state: FieldState<T>,\n ...validators: Array<Validator<T> | undefined>\n): Promise<FieldState<T>> {\n const errorMessages = await Promise.all(\n validators.map((validator) => validator?.(state.value)),\n );\n const errorMessage = errorMessages.find(Boolean);\n\n if (errorMessage) {\n return {\n ...state,\n errorMessage,\n isDirty: true,\n isTouched: true,\n isValid: false,\n isValidating: false,\n };\n }\n\n return {\n ...state,\n errorMessage: undefined,\n isDirty: true,\n isTouched: true,\n isValid: true,\n isValidating: false,\n };\n}\n\nexport function validateIfDirty<T>(\n state: FieldState<T>,\n ...validators: Array<SyncValidator<T> | undefined>\n): FieldState<T> {\n if (!state.isDirty) {\n return state;\n }\n\n return validate(state, ...validators);\n}\n\nexport async function validateIfDirtyAsync<T>(\n state: FieldState<T>,\n ...validators: Array<Validator<T> | undefined>\n): Promise<FieldState<T>> {\n if (!state.isDirty) {\n return state;\n }\n\n return validateAsync(state, ...validators);\n}\n"],"names":[],"mappings":";;AAEM,SAAU,gBAAgB,CAAI,YAAe,EAAA;IACjD,OAAO;AACL,QAAA,KAAK,EAAE,YAAY;AACnB,QAAA,YAAY,EAAE,SAAS;AACvB,QAAA,SAAS,EAAE,KAAK;AAChB,QAAA,OAAO,EAAE,KAAK;AACd,QAAA,OAAO,EAAE,IAAI;AACb,QAAA,YAAY,EAAE,KAAK;KACpB;AACH;SAEgB,QAAQ,CACtB,KAAoB,EACpB,GAAG,UAA+C,EAAA;AAElD,IAAA,KAAK,MAAM,SAAS,IAAI,UAAU,EAAE;QAClC,MAAM,YAAY,GAAG,SAAS,GAAG,KAAK,CAAC,KAAK,CAAC;QAC7C,IAAI,YAAY,EAAE;YAChB,OAAO;AACL,gBAAA,GAAG,KAAK;gBACR,YAAY;AACZ,gBAAA,OAAO,EAAE,IAAI;AACb,gBAAA,SAAS,EAAE,IAAI;AACf,gBAAA,OAAO,EAAE,KAAK;AACd,gBAAA,YAAY,EAAE,KAAK;aACpB;QACH;IACF;IAEA,OAAO;AACL,QAAA,GAAG,KAAK;AACR,QAAA,YAAY,EAAE,SAAS;AACvB,QAAA,OAAO,EAAE,IAAI;AACb,QAAA,SAAS,EAAE,IAAI;AACf,QAAA,OAAO,EAAE,IAAI;AACb,QAAA,YAAY,EAAE,KAAK;KACpB;AACH;AAEO,eAAe,aAAa,CACjC,KAAoB,EACpB,GAAG,UAA2C,EAAA;IAE9C,MAAM,aAAa,GAAG,MAAM,OAAO,CAAC,GAAG,CACrC,UAAU,CAAC,GAAG,CAAC,CAAC,SAAS,KAAK,SAAS,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,CACxD;IACD,MAAM,YAAY,GAAG,aAAa,CAAC,IAAI,CAAC,OAAO,CAAC;IAEhD,IAAI,YAAY,EAAE;QAChB,OAAO;AACL,YAAA,GAAG,KAAK;YACR,YAAY;AACZ,YAAA,OAAO,EAAE,IAAI;AACb,YAAA,SAAS,EAAE,IAAI;AACf,YAAA,OAAO,EAAE,KAAK;AACd,YAAA,YAAY,EAAE,KAAK;SACpB;IACH;IAEA,OAAO;AACL,QAAA,GAAG,KAAK;AACR,QAAA,YAAY,EAAE,SAAS;AACvB,QAAA,OAAO,EAAE,IAAI;AACb,QAAA,SAAS,EAAE,IAAI;AACf,QAAA,OAAO,EAAE,IAAI;AACb,QAAA,YAAY,EAAE,KAAK;KACpB;AACH;SAEgB,eAAe,CAC7B,KAAoB,EACpB,GAAG,UAA+C,EAAA;AAElD,IAAA,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE;AAClB,QAAA,OAAO,KAAK;IACd;AAEA,IAAA,OAAO,QAAQ,CAAC,KAAK,EAAE,GAAG,UAAU,CAAC;AACvC;AAEO,eAAe,oBAAoB,CACxC,KAAoB,EACpB,GAAG,UAA2C,EAAA;AAE9C,IAAA,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE;AAClB,QAAA,OAAO,KAAK;IACd;AAEA,IAAA,OAAO,aAAa,CAAC,KAAK,EAAE,GAAG,UAAU,CAAC;AAC5C;;;;;;;;"}
1
+ {"version":3,"file":"fieldState.js","sources":["../../../src/lib/fieldState.ts"],"sourcesContent":["import { FieldState, SyncValidator, Validator } from \"./types\";\n\n/** Creates the initial state for a field with the given initial value. */\nexport function createFieldState<T>(initialValue: T): FieldState<T> {\n return {\n value: initialValue,\n errorMessage: undefined,\n isTouched: false,\n isDirty: false,\n isValid: true,\n isValidating: false,\n };\n}\n\n/**\n * Runs the provided synchronous validators against the current field value and returns an updated {@link FieldState}.\n * Sets `isDirty` and `isTouched` to `true`. Stops at the first failing validator.\n */\nexport function validate<T>(\n state: FieldState<T>,\n ...validators: Array<SyncValidator<T> | undefined>\n): FieldState<T> {\n for (const validator of validators) {\n const errorMessage = validator?.(state.value);\n if (errorMessage) {\n return {\n ...state,\n errorMessage,\n isDirty: true,\n isTouched: true,\n isValid: false,\n isValidating: false,\n };\n }\n }\n\n return {\n ...state,\n errorMessage: undefined,\n isDirty: true,\n isTouched: true,\n isValid: true,\n isValidating: false,\n };\n}\n\n/**\n * Runs the provided synchronous and/or asynchronous validators against the current field value and returns an updated {@link FieldState}.\n * Sets `isDirty` and `isTouched` to `true`. All validators run in parallel; the first truthy error message is used.\n */\nexport async function validateAsync<T>(\n state: FieldState<T>,\n ...validators: Array<Validator<T> | undefined>\n): Promise<FieldState<T>> {\n const errorMessages = await Promise.all(\n validators.map((validator) => validator?.(state.value)),\n );\n const errorMessage = errorMessages.find(Boolean);\n\n if (errorMessage) {\n return {\n ...state,\n errorMessage,\n isDirty: true,\n isTouched: true,\n isValid: false,\n isValidating: false,\n };\n }\n\n return {\n ...state,\n errorMessage: undefined,\n isDirty: true,\n isTouched: true,\n isValid: true,\n isValidating: false,\n };\n}\n\n/**\n * Runs the provided synchronous validators only if the field is dirty (i.e. the value has been changed).\n * Returns the state unchanged if the field is not dirty.\n */\nexport function validateIfDirty<T>(\n state: FieldState<T>,\n ...validators: Array<SyncValidator<T> | undefined>\n): FieldState<T> {\n if (!state.isDirty) {\n return state;\n }\n\n return validate(state, ...validators);\n}\n\n/**\n * Runs the provided synchronous and/or asynchronous validators only if the field is dirty (i.e. the value has been changed).\n * Returns the state unchanged if the field is not dirty.\n */\nexport async function validateIfDirtyAsync<T>(\n state: FieldState<T>,\n ...validators: Array<Validator<T> | undefined>\n): Promise<FieldState<T>> {\n if (!state.isDirty) {\n return state;\n }\n\n return validateAsync(state, ...validators);\n}\n"],"names":[],"mappings":";;AAEA;AACM,SAAU,gBAAgB,CAAI,YAAe,EAAA;IACjD,OAAO;AACL,QAAA,KAAK,EAAE,YAAY;AACnB,QAAA,YAAY,EAAE,SAAS;AACvB,QAAA,SAAS,EAAE,KAAK;AAChB,QAAA,OAAO,EAAE,KAAK;AACd,QAAA,OAAO,EAAE,IAAI;AACb,QAAA,YAAY,EAAE,KAAK;KACpB;AACH;AAEA;;;AAGG;SACa,QAAQ,CACtB,KAAoB,EACpB,GAAG,UAA+C,EAAA;AAElD,IAAA,KAAK,MAAM,SAAS,IAAI,UAAU,EAAE;QAClC,MAAM,YAAY,GAAG,SAAS,GAAG,KAAK,CAAC,KAAK,CAAC;QAC7C,IAAI,YAAY,EAAE;YAChB,OAAO;AACL,gBAAA,GAAG,KAAK;gBACR,YAAY;AACZ,gBAAA,OAAO,EAAE,IAAI;AACb,gBAAA,SAAS,EAAE,IAAI;AACf,gBAAA,OAAO,EAAE,KAAK;AACd,gBAAA,YAAY,EAAE,KAAK;aACpB;QACH;IACF;IAEA,OAAO;AACL,QAAA,GAAG,KAAK;AACR,QAAA,YAAY,EAAE,SAAS;AACvB,QAAA,OAAO,EAAE,IAAI;AACb,QAAA,SAAS,EAAE,IAAI;AACf,QAAA,OAAO,EAAE,IAAI;AACb,QAAA,YAAY,EAAE,KAAK;KACpB;AACH;AAEA;;;AAGG;AACI,eAAe,aAAa,CACjC,KAAoB,EACpB,GAAG,UAA2C,EAAA;IAE9C,MAAM,aAAa,GAAG,MAAM,OAAO,CAAC,GAAG,CACrC,UAAU,CAAC,GAAG,CAAC,CAAC,SAAS,KAAK,SAAS,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,CACxD;IACD,MAAM,YAAY,GAAG,aAAa,CAAC,IAAI,CAAC,OAAO,CAAC;IAEhD,IAAI,YAAY,EAAE;QAChB,OAAO;AACL,YAAA,GAAG,KAAK;YACR,YAAY;AACZ,YAAA,OAAO,EAAE,IAAI;AACb,YAAA,SAAS,EAAE,IAAI;AACf,YAAA,OAAO,EAAE,KAAK;AACd,YAAA,YAAY,EAAE,KAAK;SACpB;IACH;IAEA,OAAO;AACL,QAAA,GAAG,KAAK;AACR,QAAA,YAAY,EAAE,SAAS;AACvB,QAAA,OAAO,EAAE,IAAI;AACb,QAAA,SAAS,EAAE,IAAI;AACf,QAAA,OAAO,EAAE,IAAI;AACb,QAAA,YAAY,EAAE,KAAK;KACpB;AACH;AAEA;;;AAGG;SACa,eAAe,CAC7B,KAAoB,EACpB,GAAG,UAA+C,EAAA;AAElD,IAAA,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE;AAClB,QAAA,OAAO,KAAK;IACd;AAEA,IAAA,OAAO,QAAQ,CAAC,KAAK,EAAE,GAAG,UAAU,CAAC;AACvC;AAEA;;;AAGG;AACI,eAAe,oBAAoB,CACxC,KAAoB,EACpB,GAAG,UAA2C,EAAA;AAE9C,IAAA,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE;AAClB,QAAA,OAAO,KAAK;IACd;AAEA,IAAA,OAAO,aAAa,CAAC,KAAK,EAAE,GAAG,UAAU,CAAC;AAC5C;;;;;;;;"}
@@ -1,3 +1,4 @@
1
+ /** Represents the state of a single form field. */
1
2
  export interface FieldState<T> {
2
3
  /** The current value of the field. */
3
4
  value: T;
@@ -7,42 +8,77 @@ export interface FieldState<T> {
7
8
  isTouched: boolean;
8
9
  /** Whether the value of the field has been changed by the user. */
9
10
  isDirty: boolean;
10
- /** Whether the field is valid */
11
+ /** Whether the field is valid. */
11
12
  isValid: boolean;
12
- /** Whether the field is currently being validated */
13
+ /** Whether the field is currently being validated. */
13
14
  isValidating: boolean;
14
15
  }
16
+ /** Props passed to the render function of a {@link Field} component. */
15
17
  export interface FieldRenderProps<T> extends FieldState<T> {
18
+ /** Callback to update the field value. Should be called when the input value changes. */
16
19
  handleChange: (value: T) => void;
20
+ /** Callback to mark the field as touched. Should be called when the input loses focus. */
17
21
  handleBlur: () => void;
22
+ /** Ref callback to attach to the root element of the input. Used to focus the field on validation errors. */
18
23
  ref: (el: HTMLElement | null) => void;
19
24
  }
25
+ /** Validator functions to run on different field events. */
20
26
  export interface Validation<T> {
27
+ /** Synchronous validator to run when the field value changes. */
21
28
  onChange?: SyncValidator<T>;
29
+ /** Asynchronous validator to run when the field value changes (debounced). */
22
30
  onChangeAsync?: AsyncValidator<T>;
31
+ /** Synchronous validator to run when the field loses focus. */
23
32
  onBlur?: SyncValidator<T>;
33
+ /** Asynchronous validator to run when the field loses focus. */
24
34
  onBlurAsync?: AsyncValidator<T>;
35
+ /** Synchronous validator to run when the form is submitted. */
25
36
  onSubmit?: SyncValidator<T>;
37
+ /** Asynchronous validator to run when the form is submitted. */
26
38
  onSubmitAsync?: AsyncValidator<T>;
27
39
  }
40
+ /**
41
+ * Specifies when validation errors are displayed.
42
+ *
43
+ * - `"touched"` — Show errors after the field has been focused and blurred.
44
+ * - `"dirty"` — Show errors after the field value has been changed.
45
+ * - `"touchedAndDirty"` — Show errors only after the field has been both touched and changed.
46
+ * - `"touchedOrDirty"` — Show errors after the field has been either touched or changed.
47
+ */
28
48
  export type ValidationMode = "touched" | "dirty" | "touchedAndDirty" | "touchedOrDirty";
49
+ /** Props for the {@link Field} component. */
29
50
  export interface FieldProps<T> {
51
+ /** The current state of the field, created with {@link createFieldState}. */
30
52
  state: FieldState<T>;
53
+ /** Render function that receives field state and event handlers. */
31
54
  children: (props: FieldRenderProps<T>) => React.ReactNode;
55
+ /** Callback invoked with the updated field state whenever the state changes. */
32
56
  onChange: (newState: FieldState<T>) => void;
57
+ /** Callback invoked with the raw input value on every change, before validation. */
33
58
  onInput?: (value: T) => void;
59
+ /** Callback invoked when the field loses focus. */
34
60
  onBlur?: () => void;
61
+ /** Validator functions to run on various field events. */
35
62
  validation?: Validation<T>;
63
+ /** Overrides the validation mode set on the parent {@link Form}. Defaults to `"touchedAndDirty"`. */
36
64
  validationMode?: ValidationMode;
65
+ /** Debounce delay in milliseconds for async validators. Overrides the value set on the parent {@link Form}. Defaults to `500`. */
37
66
  debounceMs?: number;
38
67
  }
68
+ /** A synchronous validator function. Returns an error message if validation fails, or a falsy value if it passes. */
39
69
  export type SyncValidator<T> = (value: T) => React.ReactNode;
70
+ /** An asynchronous validator function. Resolves with an error message if validation fails, or a falsy value if it passes. */
40
71
  export type AsyncValidator<T> = (value: T) => Promise<React.ReactNode>;
72
+ /** A synchronous or asynchronous validator function. */
41
73
  export type Validator<T> = SyncValidator<T> | AsyncValidator<T>;
42
74
  export interface FormContextValue {
75
+ /** Default validation mode applied to all fields in the form. */
43
76
  validationMode?: ValidationMode;
77
+ /** Default debounce delay in milliseconds for async validators. */
44
78
  debounceMs?: number;
79
+ /** Registers a field with the form for submit validation. */
45
80
  registerField: (id: string, ref: HTMLElement | null, validate: () => Promise<boolean>, commitPendingValidation: () => void) => void;
81
+ /** Unregisters a field from the form. */
46
82
  unregisterField: (id: string) => void;
47
83
  }
48
84
  export type FieldMap = Map<string, {
@@ -1,4 +1,4 @@
1
- export * from "./lib/Field";
2
- export * from "./lib/fieldState";
3
- export * from "./lib/Form";
4
- export * from "./lib/types";
1
+ export { Field } from "./lib/Field";
2
+ export { createFieldState, validate, validateAsync, validateIfDirty, validateIfDirtyAsync, } from "./lib/fieldState";
3
+ export { Form } from "./lib/Form";
4
+ export type { AsyncValidator, FieldProps, FieldRenderProps, FieldState, SyncValidator, Validation, ValidationMode, Validator, } from "./lib/types";
package/dist/esm/index.js CHANGED
@@ -1,4 +1,4 @@
1
1
  export { Field } from './lib/Field.js';
2
2
  export { createFieldState, validate, validateAsync, validateIfDirty, validateIfDirtyAsync } from './lib/fieldState.js';
3
- export { Form, FormContext, focusFirstError, useFormContext } from './lib/Form.js';
3
+ export { Form } from './lib/Form.js';
4
4
  //# sourceMappingURL=index.js.map
@@ -1,2 +1,8 @@
1
1
  import { FieldProps } from "./types";
2
+ /**
3
+ * A headless form field component that manages validation state using a render prop pattern.
4
+ *
5
+ * Connects to a parent {@link Form} for submit validation coordination.
6
+ * Passes current field state and event handlers to the `children` render function.
7
+ */
2
8
  export declare function Field<T>(props: FieldProps<T>): import("react").ReactNode;
@@ -2,10 +2,17 @@ import { useRef, useId, useEffect } from 'react';
2
2
  import { validate, validateAsync } from './fieldState.js';
3
3
  import { useFormContext } from './Form.js';
4
4
 
5
+ /**
6
+ * A headless form field component that manages validation state using a render prop pattern.
7
+ *
8
+ * Connects to a parent {@link Form} for submit validation coordination.
9
+ * Passes current field state and event handlers to the `children` render function.
10
+ */
5
11
  function Field(props) {
6
12
  const { registerField, unregisterField, validationMode: formValidationMode, debounceMs: formDebounceMs, } = useFormContext();
7
13
  const { children, state, onChange, onInput, onBlur, validation, debounceMs = formDebounceMs || 500, validationMode = formValidationMode || "touchedAndDirty", } = props;
8
14
  const stateRef = useRef(state);
15
+ stateRef.current = state;
9
16
  const validationTimeoutRef = useRef(null);
10
17
  const validationIdRef = useRef(0);
11
18
  const isValidatingOnBlurRef = useRef(false);
@@ -13,14 +20,8 @@ function Field(props) {
13
20
  const pendingValidationRef = useRef(null);
14
21
  const fieldRef = useRef(null);
15
22
  const id = useId();
16
- stateRef.current = state;
17
23
  useEffect(() => {
18
24
  async function performValidation() {
19
- if (!stateRef.current.isValidating &&
20
- stateRef.current.isDirty &&
21
- stateRef.current.isTouched) {
22
- return stateRef.current.isValid;
23
- }
24
25
  validationIdRef.current++;
25
26
  pendingValidationRef.current = validate(stateRef.current, validation?.onChange, validation?.onBlur, validation?.onSubmit);
26
27
  if (pendingValidationRef.current.isValid) {
@@ -55,7 +56,7 @@ function Field(props) {
55
56
  const shouldValidate = validationMode === "dirty" ||
56
57
  validationMode === "touchedOrDirty" ||
57
58
  stateRef.current.isTouched;
58
- if (shouldValidate && (validation?.onChange || validation?.onChangeAsync)) {
59
+ if (shouldValidate) {
59
60
  const errorMessage = validation?.onChange?.(value);
60
61
  const willValidateAsync = Boolean(validation?.onChangeAsync && !errorMessage);
61
62
  onChange({
@@ -96,9 +97,10 @@ function Field(props) {
96
97
  const currentValidation = validationIdRef.current;
97
98
  let errorMessage = stateRef.current.errorMessage ||
98
99
  validation?.onBlur?.(stateRef.current.value);
99
- const shouldValidateOnChange = validationMode === "touched" ||
100
- validationMode === "touchedOrDirty" ||
101
- (validationMode === "touchedAndDirty" && stateRef.current.isDirty);
100
+ const shouldValidateOnChange = !stateRef.current.isTouched &&
101
+ (validationMode === "touched" ||
102
+ (validationMode === "touchedOrDirty" && !stateRef.current.isDirty) ||
103
+ (validationMode === "touchedAndDirty" && stateRef.current.isDirty));
102
104
  if (!errorMessage && shouldValidateOnChange && validation?.onChange) {
103
105
  errorMessage = validation.onChange(stateRef.current.value);
104
106
  }
@@ -1 +1 @@
1
- {"version":3,"file":"Field.js","sources":["../../../src/lib/Field.tsx"],"sourcesContent":["import { useEffect, useId, useRef } from \"react\";\nimport { validate, validateAsync } from \"./fieldState\";\nimport { useFormContext } from \"./Form\";\nimport { FieldProps, FieldState } from \"./types\";\n\nexport function Field<T>(props: FieldProps<T>) {\n const {\n registerField,\n unregisterField,\n validationMode: formValidationMode,\n debounceMs: formDebounceMs,\n } = useFormContext();\n\n const {\n children,\n state,\n onChange,\n onInput,\n onBlur,\n validation,\n debounceMs = formDebounceMs || 500,\n validationMode = formValidationMode || \"touchedAndDirty\",\n } = props;\n\n const stateRef = useRef(state);\n const validationTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(\n null,\n );\n const validationIdRef = useRef(0);\n const isValidatingOnBlurRef = useRef(false);\n const isValidatingOnChangeRef = useRef(false);\n const pendingValidationRef = useRef<FieldState<T> | null>(null);\n const fieldRef = useRef<HTMLElement | null>(null);\n const id = useId();\n\n stateRef.current = state;\n\n useEffect(() => {\n async function performValidation() {\n if (\n !stateRef.current.isValidating &&\n stateRef.current.isDirty &&\n stateRef.current.isTouched\n ) {\n return stateRef.current.isValid;\n }\n validationIdRef.current++;\n pendingValidationRef.current = validate(\n stateRef.current,\n validation?.onChange,\n validation?.onBlur,\n validation?.onSubmit,\n );\n if (pendingValidationRef.current.isValid) {\n pendingValidationRef.current = await validateAsync(\n stateRef.current,\n validation?.onChangeAsync,\n validation?.onBlurAsync,\n validation?.onSubmitAsync,\n );\n }\n return pendingValidationRef.current.isValid;\n }\n\n function commitPendingValidation() {\n if (pendingValidationRef.current) {\n onChange(pendingValidationRef.current);\n pendingValidationRef.current = null;\n }\n }\n\n registerField(\n id,\n fieldRef.current,\n performValidation,\n commitPendingValidation,\n );\n\n return () => {\n unregisterField(id);\n };\n }, [id, registerField, validation, onChange, unregisterField]);\n\n useEffect(() => {\n return () => {\n if (validationTimeoutRef.current) {\n clearTimeout(validationTimeoutRef.current);\n }\n };\n }, []);\n\n function handleChange(value: T) {\n onInput?.(value);\n\n if (validationTimeoutRef.current) {\n clearTimeout(validationTimeoutRef.current);\n }\n\n const currentValidation = ++validationIdRef.current;\n\n const shouldValidate =\n validationMode === \"dirty\" ||\n validationMode === \"touchedOrDirty\" ||\n stateRef.current.isTouched;\n\n if (shouldValidate && (validation?.onChange || validation?.onChangeAsync)) {\n const errorMessage = validation?.onChange?.(value);\n const willValidateAsync = Boolean(\n validation?.onChangeAsync && !errorMessage,\n );\n\n onChange({\n ...stateRef.current,\n value,\n errorMessage,\n isDirty: true,\n isValid: !errorMessage,\n isValidating: willValidateAsync,\n });\n\n if (willValidateAsync) {\n isValidatingOnChangeRef.current = true;\n validationTimeoutRef.current = setTimeout(async () => {\n const asyncErrorMessage = await validation?.onChangeAsync?.(value);\n\n isValidatingOnChangeRef.current = false;\n if (currentValidation === validationIdRef.current) {\n onChange({\n ...stateRef.current,\n errorMessage: asyncErrorMessage,\n isValid: !asyncErrorMessage,\n isValidating: isValidatingOnBlurRef.current,\n });\n }\n }, debounceMs);\n }\n } else {\n onChange({\n ...stateRef.current,\n value,\n isDirty: true,\n isValidating: false,\n });\n }\n }\n\n async function handleBlur() {\n onBlur?.();\n\n const currentValidation = validationIdRef.current;\n\n let errorMessage =\n stateRef.current.errorMessage ||\n validation?.onBlur?.(stateRef.current.value);\n\n const shouldValidateOnChange =\n validationMode === \"touched\" ||\n validationMode === \"touchedOrDirty\" ||\n (validationMode === \"touchedAndDirty\" && stateRef.current.isDirty);\n\n if (!errorMessage && shouldValidateOnChange && validation?.onChange) {\n errorMessage = validation.onChange(stateRef.current.value);\n }\n\n if (\n !errorMessage &&\n (validation?.onBlurAsync || validation?.onChangeAsync)\n ) {\n isValidatingOnBlurRef.current = true;\n onChange({\n ...stateRef.current,\n isValidating: true,\n isTouched: true,\n });\n\n const asyncValidations: Promise<React.ReactNode>[] = [];\n\n if (validation?.onBlurAsync) {\n asyncValidations.push(validation.onBlurAsync(stateRef.current.value));\n }\n\n if (shouldValidateOnChange && validation?.onChangeAsync) {\n asyncValidations.push(validation.onChangeAsync(stateRef.current.value));\n }\n\n const [blurError, changeError] = await Promise.all(asyncValidations);\n errorMessage = blurError || changeError;\n }\n\n isValidatingOnBlurRef.current = false;\n\n if (errorMessage && validationTimeoutRef.current) {\n clearTimeout(validationTimeoutRef.current);\n }\n\n if (currentValidation !== validationIdRef.current) {\n onChange({\n ...stateRef.current,\n isTouched: true,\n });\n return;\n }\n\n onChange({\n ...stateRef.current,\n errorMessage,\n isTouched: true,\n isValid: !errorMessage,\n isValidating: isValidatingOnChangeRef.current,\n });\n }\n\n const ref = (el: HTMLElement | null) => {\n fieldRef.current = el;\n };\n\n return children({\n ...stateRef.current,\n handleChange,\n handleBlur,\n ref,\n });\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"Field.js","sources":["../../../src/lib/Field.tsx"],"sourcesContent":["import { useEffect, useId, useRef } from \"react\";\nimport { validate, validateAsync } from \"./fieldState\";\nimport { useFormContext } from \"./Form\";\nimport { FieldProps, FieldState } from \"./types\";\n\n/**\n * A headless form field component that manages validation state using a render prop pattern.\n *\n * Connects to a parent {@link Form} for submit validation coordination.\n * Passes current field state and event handlers to the `children` render function.\n */\nexport function Field<T>(props: FieldProps<T>) {\n const {\n registerField,\n unregisterField,\n validationMode: formValidationMode,\n debounceMs: formDebounceMs,\n } = useFormContext();\n\n const {\n children,\n state,\n onChange,\n onInput,\n onBlur,\n validation,\n debounceMs = formDebounceMs || 500,\n validationMode = formValidationMode || \"touchedAndDirty\",\n } = props;\n\n const stateRef = useRef(state);\n stateRef.current = state;\n const validationTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(\n null,\n );\n const validationIdRef = useRef(0);\n const isValidatingOnBlurRef = useRef(false);\n const isValidatingOnChangeRef = useRef(false);\n const pendingValidationRef = useRef<FieldState<T> | null>(null);\n const fieldRef = useRef<HTMLElement | null>(null);\n const id = useId();\n\n useEffect(() => {\n async function performValidation() {\n validationIdRef.current++;\n pendingValidationRef.current = validate(\n stateRef.current,\n validation?.onChange,\n validation?.onBlur,\n validation?.onSubmit,\n );\n if (pendingValidationRef.current.isValid) {\n pendingValidationRef.current = await validateAsync(\n stateRef.current,\n validation?.onChangeAsync,\n validation?.onBlurAsync,\n validation?.onSubmitAsync,\n );\n }\n return pendingValidationRef.current.isValid;\n }\n\n function commitPendingValidation() {\n if (pendingValidationRef.current) {\n onChange(pendingValidationRef.current);\n pendingValidationRef.current = null;\n }\n }\n\n registerField(\n id,\n fieldRef.current,\n performValidation,\n commitPendingValidation,\n );\n\n return () => {\n unregisterField(id);\n };\n }, [id, registerField, validation, onChange, unregisterField]);\n\n useEffect(() => {\n return () => {\n if (validationTimeoutRef.current) {\n clearTimeout(validationTimeoutRef.current);\n }\n };\n }, []);\n\n function handleChange(value: T) {\n onInput?.(value);\n\n if (validationTimeoutRef.current) {\n clearTimeout(validationTimeoutRef.current);\n }\n\n const currentValidation = ++validationIdRef.current;\n\n const shouldValidate =\n validationMode === \"dirty\" ||\n validationMode === \"touchedOrDirty\" ||\n stateRef.current.isTouched;\n\n if (shouldValidate) {\n const errorMessage = validation?.onChange?.(value);\n const willValidateAsync = Boolean(\n validation?.onChangeAsync && !errorMessage,\n );\n\n onChange({\n ...stateRef.current,\n value,\n errorMessage,\n isDirty: true,\n isValid: !errorMessage,\n isValidating: willValidateAsync,\n });\n\n if (willValidateAsync) {\n isValidatingOnChangeRef.current = true;\n validationTimeoutRef.current = setTimeout(async () => {\n const asyncErrorMessage = await validation?.onChangeAsync?.(value);\n\n isValidatingOnChangeRef.current = false;\n if (currentValidation === validationIdRef.current) {\n onChange({\n ...stateRef.current,\n errorMessage: asyncErrorMessage,\n isValid: !asyncErrorMessage,\n isValidating: isValidatingOnBlurRef.current,\n });\n }\n }, debounceMs);\n }\n } else {\n onChange({\n ...stateRef.current,\n value,\n isDirty: true,\n isValidating: false,\n });\n }\n }\n\n async function handleBlur() {\n onBlur?.();\n\n const currentValidation = validationIdRef.current;\n\n let errorMessage =\n stateRef.current.errorMessage ||\n validation?.onBlur?.(stateRef.current.value);\n\n const shouldValidateOnChange =\n !stateRef.current.isTouched &&\n (validationMode === \"touched\" ||\n (validationMode === \"touchedOrDirty\" && !stateRef.current.isDirty) ||\n (validationMode === \"touchedAndDirty\" && stateRef.current.isDirty));\n\n if (!errorMessage && shouldValidateOnChange && validation?.onChange) {\n errorMessage = validation.onChange(stateRef.current.value);\n }\n\n if (\n !errorMessage &&\n (validation?.onBlurAsync || validation?.onChangeAsync)\n ) {\n isValidatingOnBlurRef.current = true;\n onChange({\n ...stateRef.current,\n isValidating: true,\n isTouched: true,\n });\n\n const asyncValidations: Promise<React.ReactNode>[] = [];\n\n if (validation?.onBlurAsync) {\n asyncValidations.push(validation.onBlurAsync(stateRef.current.value));\n }\n\n if (shouldValidateOnChange && validation?.onChangeAsync) {\n asyncValidations.push(validation.onChangeAsync(stateRef.current.value));\n }\n\n const [blurError, changeError] = await Promise.all(asyncValidations);\n errorMessage = blurError || changeError;\n }\n\n isValidatingOnBlurRef.current = false;\n\n if (errorMessage && validationTimeoutRef.current) {\n clearTimeout(validationTimeoutRef.current);\n }\n\n if (currentValidation !== validationIdRef.current) {\n onChange({\n ...stateRef.current,\n isTouched: true,\n });\n return;\n }\n\n onChange({\n ...stateRef.current,\n errorMessage,\n isTouched: true,\n isValid: !errorMessage,\n isValidating: isValidatingOnChangeRef.current,\n });\n }\n\n const ref = (el: HTMLElement | null) => {\n fieldRef.current = el;\n };\n\n return children({\n ...stateRef.current,\n handleChange,\n handleBlur,\n ref,\n });\n}\n"],"names":[],"mappings":"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"}
@@ -1,10 +1,26 @@
1
1
  import React from "react";
2
2
  import { FormContextValue, ValidationMode } from "./types";
3
+ /** Props for the {@link Form} component. */
3
4
  interface FormProps extends Omit<React.ComponentProps<"form">, "onSubmit"> {
5
+ /** Default validation mode applied to all fields in the form. Defaults to `"touchedAndDirty"`. */
4
6
  validationMode?: ValidationMode;
7
+ /** Default debounce delay in milliseconds for async validators. Defaults to `500`. */
5
8
  debounceMs?: number;
6
- onSubmit?: (e: React.SubmitEvent<HTMLFormElement>, validateAllFields: () => Promise<boolean>) => void;
9
+ /**
10
+ * Submit handler called when the form is submitted.
11
+ * Receives the submit event and a `validateAllFields` function that triggers
12
+ * validation on all registered fields and returns whether the form is valid.
13
+ */
14
+ onSubmit?: (e: React.SubmitEvent<HTMLFormElement>, validateForm: () => Promise<boolean>) => void;
7
15
  }
16
+ /**
17
+ * A form component that provides context for coordinating field validation.
18
+ *
19
+ * Wraps a native `<form>` element and prevents the default submit behavior.
20
+ * On submit, the `onSubmit` callback is called with the submit event and a
21
+ * `validateAllFields` function that can be used to trigger validation on all
22
+ * registered {@link Field} components and focus the first invalid field.
23
+ */
8
24
  export declare function Form({ onSubmit, validationMode, debounceMs, ...props }: FormProps): React.JSX.Element;
9
25
  export declare const FormContext: React.Context<FormContextValue>;
10
26
  export declare function useFormContext(): FormContextValue;
@@ -1,5 +1,13 @@
1
1
  import React, { useRef, useCallback } from 'react';
2
2
 
3
+ /**
4
+ * A form component that provides context for coordinating field validation.
5
+ *
6
+ * Wraps a native `<form>` element and prevents the default submit behavior.
7
+ * On submit, the `onSubmit` callback is called with the submit event and a
8
+ * `validateAllFields` function that can be used to trigger validation on all
9
+ * registered {@link Field} components and focus the first invalid field.
10
+ */
3
11
  function Form({ onSubmit, validationMode, debounceMs, ...props }) {
4
12
  const fieldsRef = useRef(new Map());
5
13
  const registerField = useCallback((id, ref, validate, commitPendingValidation) => {
@@ -12,7 +20,7 @@ function Form({ onSubmit, validationMode, debounceMs, ...props }) {
12
20
  const unregisterField = useCallback((id) => {
13
21
  fieldsRef.current.delete(id);
14
22
  }, []);
15
- const validateAllFields = useCallback(async () => {
23
+ const validateForm = useCallback(async () => {
16
24
  const fields = Array.from(fieldsRef.current.values());
17
25
  const validationPromises = fields.map(async (field) => ({
18
26
  isValid: await field.validate(),
@@ -32,7 +40,7 @@ function Form({ onSubmit, validationMode, debounceMs, ...props }) {
32
40
  validationMode,
33
41
  debounceMs,
34
42
  } },
35
- React.createElement("form", { onSubmit: (e) => onSubmit?.(e, validateAllFields), ...props })));
43
+ React.createElement("form", { onSubmit: (e) => onSubmit?.(e, validateForm), ...props })));
36
44
  }
37
45
  const FormContext = React.createContext({
38
46
  registerField: () => { },
@@ -50,26 +58,11 @@ function focusFirstError(results) {
50
58
  if (!firstInvalidField) {
51
59
  return;
52
60
  }
53
- let firstInvalid = firstInvalidField;
54
- if (firstInvalid.role === "radiogroup") {
55
- const radio = firstInvalid.querySelector('[role="radio"]');
56
- if (radio) {
57
- firstInvalid = radio;
58
- }
59
- }
60
- if (firstInvalid.role === "group") {
61
- const checkbox = firstInvalid.querySelector('[role="checkbox"]');
62
- if (checkbox) {
63
- firstInvalid = checkbox;
64
- }
65
- }
66
- firstInvalid.focus();
67
- const rect = firstInvalid.getBoundingClientRect();
68
- if (rect) {
69
- window.scrollTo({
70
- top: rect.top + window.scrollY - 100,
71
- });
72
- }
61
+ firstInvalidField.focus();
62
+ const rect = firstInvalidField.getBoundingClientRect();
63
+ window.scrollTo({
64
+ top: rect.top + window.scrollY - 100,
65
+ });
73
66
  }
74
67
 
75
68
  export { Form, FormContext, focusFirstError, useFormContext };
@@ -1 +1 @@
1
- {"version":3,"file":"Form.js","sources":["../../../src/lib/Form.tsx"],"sourcesContent":["import React, { useCallback, useRef } from \"react\";\nimport { FieldMap, FormContextValue, ValidationMode } from \"./types\";\n\ninterface FormProps extends Omit<React.ComponentProps<\"form\">, \"onSubmit\"> {\n validationMode?: ValidationMode;\n debounceMs?: number;\n onSubmit?: (\n e: React.SubmitEvent<HTMLFormElement>,\n validateAllFields: () => Promise<boolean>,\n ) => void;\n}\n\nexport function Form({\n onSubmit,\n validationMode,\n debounceMs,\n ...props\n}: FormProps) {\n const fieldsRef = useRef<FieldMap>(new Map());\n\n const registerField = useCallback(\n (\n id: string,\n ref: HTMLElement | null,\n validate: () => Promise<boolean>,\n commitPendingValidation: () => void,\n ) => {\n fieldsRef.current.set(id, {\n ref,\n validate,\n commitPendingValidation,\n });\n },\n [],\n );\n\n const unregisterField = useCallback((id: string) => {\n fieldsRef.current.delete(id);\n }, []);\n\n const validateAllFields = useCallback(async () => {\n const fields = Array.from(fieldsRef.current.values());\n const validationPromises = fields.map(async (field) => ({\n isValid: await field.validate(),\n ref: field.ref,\n }));\n const results = await Promise.all(validationPromises);\n fields.forEach((field) => field.commitPendingValidation());\n const hasErrors = results.some((result) => !result.isValid);\n if (hasErrors) {\n focusFirstError(results);\n }\n return !hasErrors;\n }, []);\n\n return (\n <FormContext.Provider\n value={{\n registerField,\n unregisterField,\n validationMode,\n debounceMs,\n }}\n >\n <form onSubmit={(e) => onSubmit?.(e, validateAllFields)} {...props} />\n </FormContext.Provider>\n );\n}\n\nexport const FormContext = React.createContext<FormContextValue>({\n registerField: () => {},\n unregisterField: () => {},\n});\n\nexport function useFormContext() {\n return React.useContext(FormContext);\n}\n\nexport function focusFirstError(\n results: {\n isValid: boolean;\n ref: HTMLElement | null;\n }[],\n) {\n const firstInvalidField = results\n .filter((field) => !field.isValid && field.ref)\n .map((field) => field.ref!)\n .sort((a, b) =>\n a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_PRECEDING ? 1 : -1,\n )\n .at(0);\n\n if (!firstInvalidField) {\n return;\n }\n\n let firstInvalid = firstInvalidField;\n\n if (firstInvalid.role === \"radiogroup\") {\n const radio = firstInvalid.querySelector<HTMLElement>('[role=\"radio\"]');\n if (radio) {\n firstInvalid = radio;\n }\n }\n\n if (firstInvalid.role === \"group\") {\n const checkbox =\n firstInvalid.querySelector<HTMLElement>('[role=\"checkbox\"]');\n if (checkbox) {\n firstInvalid = checkbox;\n }\n }\n\n firstInvalid.focus();\n const rect = firstInvalid.getBoundingClientRect();\n if (rect) {\n window.scrollTo({\n top: rect.top + window.scrollY - 100,\n });\n }\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"Form.js","sources":["../../../src/lib/Form.tsx"],"sourcesContent":["import React, { useCallback, useRef } from \"react\";\nimport { FieldMap, FormContextValue, ValidationMode } from \"./types\";\n\n/** Props for the {@link Form} component. */\ninterface FormProps extends Omit<React.ComponentProps<\"form\">, \"onSubmit\"> {\n /** Default validation mode applied to all fields in the form. Defaults to `\"touchedAndDirty\"`. */\n validationMode?: ValidationMode;\n /** Default debounce delay in milliseconds for async validators. Defaults to `500`. */\n debounceMs?: number;\n /**\n * Submit handler called when the form is submitted.\n * Receives the submit event and a `validateAllFields` function that triggers\n * validation on all registered fields and returns whether the form is valid.\n */\n onSubmit?: (\n e: React.SubmitEvent<HTMLFormElement>,\n validateForm: () => Promise<boolean>,\n ) => void;\n}\n\n/**\n * A form component that provides context for coordinating field validation.\n *\n * Wraps a native `<form>` element and prevents the default submit behavior.\n * On submit, the `onSubmit` callback is called with the submit event and a\n * `validateAllFields` function that can be used to trigger validation on all\n * registered {@link Field} components and focus the first invalid field.\n */\nexport function Form({\n onSubmit,\n validationMode,\n debounceMs,\n ...props\n}: FormProps) {\n const fieldsRef = useRef<FieldMap>(new Map());\n\n const registerField = useCallback(\n (\n id: string,\n ref: HTMLElement | null,\n validate: () => Promise<boolean>,\n commitPendingValidation: () => void,\n ) => {\n fieldsRef.current.set(id, {\n ref,\n validate,\n commitPendingValidation,\n });\n },\n [],\n );\n\n const unregisterField = useCallback((id: string) => {\n fieldsRef.current.delete(id);\n }, []);\n\n const validateForm = useCallback(async () => {\n const fields = Array.from(fieldsRef.current.values());\n const validationPromises = fields.map(async (field) => ({\n isValid: await field.validate(),\n ref: field.ref,\n }));\n const results = await Promise.all(validationPromises);\n fields.forEach((field) => field.commitPendingValidation());\n const hasErrors = results.some((result) => !result.isValid);\n if (hasErrors) {\n focusFirstError(results);\n }\n return !hasErrors;\n }, []);\n\n return (\n <FormContext.Provider\n value={{\n registerField,\n unregisterField,\n validationMode,\n debounceMs,\n }}\n >\n <form onSubmit={(e) => onSubmit?.(e, validateForm)} {...props} />\n </FormContext.Provider>\n );\n}\n\nexport const FormContext = React.createContext<FormContextValue>({\n registerField: () => {},\n unregisterField: () => {},\n});\n\nexport function useFormContext() {\n return React.useContext(FormContext);\n}\n\nexport function focusFirstError(\n results: {\n isValid: boolean;\n ref: HTMLElement | null;\n }[],\n) {\n const firstInvalidField = results\n .filter((field) => !field.isValid && field.ref)\n .map((field) => field.ref!)\n .sort((a, b) =>\n a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_PRECEDING ? 1 : -1,\n )\n .at(0);\n\n if (!firstInvalidField) {\n return;\n }\n\n firstInvalidField.focus();\n const rect = firstInvalidField.getBoundingClientRect();\n window.scrollTo({\n top: rect.top + window.scrollY - 100,\n });\n}\n"],"names":[],"mappings":"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"}
@@ -1,6 +1,23 @@
1
1
  import { FieldState, SyncValidator, Validator } from "./types";
2
+ /** Creates the initial state for a field with the given initial value. */
2
3
  export declare function createFieldState<T>(initialValue: T): FieldState<T>;
4
+ /**
5
+ * Runs the provided synchronous validators against the current field value and returns an updated {@link FieldState}.
6
+ * Sets `isDirty` and `isTouched` to `true`. Stops at the first failing validator.
7
+ */
3
8
  export declare function validate<T>(state: FieldState<T>, ...validators: Array<SyncValidator<T> | undefined>): FieldState<T>;
9
+ /**
10
+ * Runs the provided synchronous and/or asynchronous validators against the current field value and returns an updated {@link FieldState}.
11
+ * Sets `isDirty` and `isTouched` to `true`. All validators run in parallel; the first truthy error message is used.
12
+ */
4
13
  export declare function validateAsync<T>(state: FieldState<T>, ...validators: Array<Validator<T> | undefined>): Promise<FieldState<T>>;
14
+ /**
15
+ * Runs the provided synchronous validators only if the field is dirty (i.e. the value has been changed).
16
+ * Returns the state unchanged if the field is not dirty.
17
+ */
5
18
  export declare function validateIfDirty<T>(state: FieldState<T>, ...validators: Array<SyncValidator<T> | undefined>): FieldState<T>;
19
+ /**
20
+ * Runs the provided synchronous and/or asynchronous validators only if the field is dirty (i.e. the value has been changed).
21
+ * Returns the state unchanged if the field is not dirty.
22
+ */
6
23
  export declare function validateIfDirtyAsync<T>(state: FieldState<T>, ...validators: Array<Validator<T> | undefined>): Promise<FieldState<T>>;
@@ -1,3 +1,4 @@
1
+ /** Creates the initial state for a field with the given initial value. */
1
2
  function createFieldState(initialValue) {
2
3
  return {
3
4
  value: initialValue,
@@ -8,6 +9,10 @@ function createFieldState(initialValue) {
8
9
  isValidating: false,
9
10
  };
10
11
  }
12
+ /**
13
+ * Runs the provided synchronous validators against the current field value and returns an updated {@link FieldState}.
14
+ * Sets `isDirty` and `isTouched` to `true`. Stops at the first failing validator.
15
+ */
11
16
  function validate(state, ...validators) {
12
17
  for (const validator of validators) {
13
18
  const errorMessage = validator?.(state.value);
@@ -31,6 +36,10 @@ function validate(state, ...validators) {
31
36
  isValidating: false,
32
37
  };
33
38
  }
39
+ /**
40
+ * Runs the provided synchronous and/or asynchronous validators against the current field value and returns an updated {@link FieldState}.
41
+ * Sets `isDirty` and `isTouched` to `true`. All validators run in parallel; the first truthy error message is used.
42
+ */
34
43
  async function validateAsync(state, ...validators) {
35
44
  const errorMessages = await Promise.all(validators.map((validator) => validator?.(state.value)));
36
45
  const errorMessage = errorMessages.find(Boolean);
@@ -53,12 +62,20 @@ async function validateAsync(state, ...validators) {
53
62
  isValidating: false,
54
63
  };
55
64
  }
65
+ /**
66
+ * Runs the provided synchronous validators only if the field is dirty (i.e. the value has been changed).
67
+ * Returns the state unchanged if the field is not dirty.
68
+ */
56
69
  function validateIfDirty(state, ...validators) {
57
70
  if (!state.isDirty) {
58
71
  return state;
59
72
  }
60
73
  return validate(state, ...validators);
61
74
  }
75
+ /**
76
+ * Runs the provided synchronous and/or asynchronous validators only if the field is dirty (i.e. the value has been changed).
77
+ * Returns the state unchanged if the field is not dirty.
78
+ */
62
79
  async function validateIfDirtyAsync(state, ...validators) {
63
80
  if (!state.isDirty) {
64
81
  return state;
@@ -1 +1 @@
1
- {"version":3,"file":"fieldState.js","sources":["../../../src/lib/fieldState.ts"],"sourcesContent":["import { FieldState, SyncValidator, Validator } from \"./types\";\n\nexport function createFieldState<T>(initialValue: T): FieldState<T> {\n return {\n value: initialValue,\n errorMessage: undefined,\n isTouched: false,\n isDirty: false,\n isValid: true,\n isValidating: false,\n };\n}\n\nexport function validate<T>(\n state: FieldState<T>,\n ...validators: Array<SyncValidator<T> | undefined>\n): FieldState<T> {\n for (const validator of validators) {\n const errorMessage = validator?.(state.value);\n if (errorMessage) {\n return {\n ...state,\n errorMessage,\n isDirty: true,\n isTouched: true,\n isValid: false,\n isValidating: false,\n };\n }\n }\n\n return {\n ...state,\n errorMessage: undefined,\n isDirty: true,\n isTouched: true,\n isValid: true,\n isValidating: false,\n };\n}\n\nexport async function validateAsync<T>(\n state: FieldState<T>,\n ...validators: Array<Validator<T> | undefined>\n): Promise<FieldState<T>> {\n const errorMessages = await Promise.all(\n validators.map((validator) => validator?.(state.value)),\n );\n const errorMessage = errorMessages.find(Boolean);\n\n if (errorMessage) {\n return {\n ...state,\n errorMessage,\n isDirty: true,\n isTouched: true,\n isValid: false,\n isValidating: false,\n };\n }\n\n return {\n ...state,\n errorMessage: undefined,\n isDirty: true,\n isTouched: true,\n isValid: true,\n isValidating: false,\n };\n}\n\nexport function validateIfDirty<T>(\n state: FieldState<T>,\n ...validators: Array<SyncValidator<T> | undefined>\n): FieldState<T> {\n if (!state.isDirty) {\n return state;\n }\n\n return validate(state, ...validators);\n}\n\nexport async function validateIfDirtyAsync<T>(\n state: FieldState<T>,\n ...validators: Array<Validator<T> | undefined>\n): Promise<FieldState<T>> {\n if (!state.isDirty) {\n return state;\n }\n\n return validateAsync(state, ...validators);\n}\n"],"names":[],"mappings":"AAEM,SAAU,gBAAgB,CAAI,YAAe,EAAA;IACjD,OAAO;AACL,QAAA,KAAK,EAAE,YAAY;AACnB,QAAA,YAAY,EAAE,SAAS;AACvB,QAAA,SAAS,EAAE,KAAK;AAChB,QAAA,OAAO,EAAE,KAAK;AACd,QAAA,OAAO,EAAE,IAAI;AACb,QAAA,YAAY,EAAE,KAAK;KACpB;AACH;SAEgB,QAAQ,CACtB,KAAoB,EACpB,GAAG,UAA+C,EAAA;AAElD,IAAA,KAAK,MAAM,SAAS,IAAI,UAAU,EAAE;QAClC,MAAM,YAAY,GAAG,SAAS,GAAG,KAAK,CAAC,KAAK,CAAC;QAC7C,IAAI,YAAY,EAAE;YAChB,OAAO;AACL,gBAAA,GAAG,KAAK;gBACR,YAAY;AACZ,gBAAA,OAAO,EAAE,IAAI;AACb,gBAAA,SAAS,EAAE,IAAI;AACf,gBAAA,OAAO,EAAE,KAAK;AACd,gBAAA,YAAY,EAAE,KAAK;aACpB;QACH;IACF;IAEA,OAAO;AACL,QAAA,GAAG,KAAK;AACR,QAAA,YAAY,EAAE,SAAS;AACvB,QAAA,OAAO,EAAE,IAAI;AACb,QAAA,SAAS,EAAE,IAAI;AACf,QAAA,OAAO,EAAE,IAAI;AACb,QAAA,YAAY,EAAE,KAAK;KACpB;AACH;AAEO,eAAe,aAAa,CACjC,KAAoB,EACpB,GAAG,UAA2C,EAAA;IAE9C,MAAM,aAAa,GAAG,MAAM,OAAO,CAAC,GAAG,CACrC,UAAU,CAAC,GAAG,CAAC,CAAC,SAAS,KAAK,SAAS,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,CACxD;IACD,MAAM,YAAY,GAAG,aAAa,CAAC,IAAI,CAAC,OAAO,CAAC;IAEhD,IAAI,YAAY,EAAE;QAChB,OAAO;AACL,YAAA,GAAG,KAAK;YACR,YAAY;AACZ,YAAA,OAAO,EAAE,IAAI;AACb,YAAA,SAAS,EAAE,IAAI;AACf,YAAA,OAAO,EAAE,KAAK;AACd,YAAA,YAAY,EAAE,KAAK;SACpB;IACH;IAEA,OAAO;AACL,QAAA,GAAG,KAAK;AACR,QAAA,YAAY,EAAE,SAAS;AACvB,QAAA,OAAO,EAAE,IAAI;AACb,QAAA,SAAS,EAAE,IAAI;AACf,QAAA,OAAO,EAAE,IAAI;AACb,QAAA,YAAY,EAAE,KAAK;KACpB;AACH;SAEgB,eAAe,CAC7B,KAAoB,EACpB,GAAG,UAA+C,EAAA;AAElD,IAAA,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE;AAClB,QAAA,OAAO,KAAK;IACd;AAEA,IAAA,OAAO,QAAQ,CAAC,KAAK,EAAE,GAAG,UAAU,CAAC;AACvC;AAEO,eAAe,oBAAoB,CACxC,KAAoB,EACpB,GAAG,UAA2C,EAAA;AAE9C,IAAA,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE;AAClB,QAAA,OAAO,KAAK;IACd;AAEA,IAAA,OAAO,aAAa,CAAC,KAAK,EAAE,GAAG,UAAU,CAAC;AAC5C;;;;"}
1
+ {"version":3,"file":"fieldState.js","sources":["../../../src/lib/fieldState.ts"],"sourcesContent":["import { FieldState, SyncValidator, Validator } from \"./types\";\n\n/** Creates the initial state for a field with the given initial value. */\nexport function createFieldState<T>(initialValue: T): FieldState<T> {\n return {\n value: initialValue,\n errorMessage: undefined,\n isTouched: false,\n isDirty: false,\n isValid: true,\n isValidating: false,\n };\n}\n\n/**\n * Runs the provided synchronous validators against the current field value and returns an updated {@link FieldState}.\n * Sets `isDirty` and `isTouched` to `true`. Stops at the first failing validator.\n */\nexport function validate<T>(\n state: FieldState<T>,\n ...validators: Array<SyncValidator<T> | undefined>\n): FieldState<T> {\n for (const validator of validators) {\n const errorMessage = validator?.(state.value);\n if (errorMessage) {\n return {\n ...state,\n errorMessage,\n isDirty: true,\n isTouched: true,\n isValid: false,\n isValidating: false,\n };\n }\n }\n\n return {\n ...state,\n errorMessage: undefined,\n isDirty: true,\n isTouched: true,\n isValid: true,\n isValidating: false,\n };\n}\n\n/**\n * Runs the provided synchronous and/or asynchronous validators against the current field value and returns an updated {@link FieldState}.\n * Sets `isDirty` and `isTouched` to `true`. All validators run in parallel; the first truthy error message is used.\n */\nexport async function validateAsync<T>(\n state: FieldState<T>,\n ...validators: Array<Validator<T> | undefined>\n): Promise<FieldState<T>> {\n const errorMessages = await Promise.all(\n validators.map((validator) => validator?.(state.value)),\n );\n const errorMessage = errorMessages.find(Boolean);\n\n if (errorMessage) {\n return {\n ...state,\n errorMessage,\n isDirty: true,\n isTouched: true,\n isValid: false,\n isValidating: false,\n };\n }\n\n return {\n ...state,\n errorMessage: undefined,\n isDirty: true,\n isTouched: true,\n isValid: true,\n isValidating: false,\n };\n}\n\n/**\n * Runs the provided synchronous validators only if the field is dirty (i.e. the value has been changed).\n * Returns the state unchanged if the field is not dirty.\n */\nexport function validateIfDirty<T>(\n state: FieldState<T>,\n ...validators: Array<SyncValidator<T> | undefined>\n): FieldState<T> {\n if (!state.isDirty) {\n return state;\n }\n\n return validate(state, ...validators);\n}\n\n/**\n * Runs the provided synchronous and/or asynchronous validators only if the field is dirty (i.e. the value has been changed).\n * Returns the state unchanged if the field is not dirty.\n */\nexport async function validateIfDirtyAsync<T>(\n state: FieldState<T>,\n ...validators: Array<Validator<T> | undefined>\n): Promise<FieldState<T>> {\n if (!state.isDirty) {\n return state;\n }\n\n return validateAsync(state, ...validators);\n}\n"],"names":[],"mappings":"AAEA;AACM,SAAU,gBAAgB,CAAI,YAAe,EAAA;IACjD,OAAO;AACL,QAAA,KAAK,EAAE,YAAY;AACnB,QAAA,YAAY,EAAE,SAAS;AACvB,QAAA,SAAS,EAAE,KAAK;AAChB,QAAA,OAAO,EAAE,KAAK;AACd,QAAA,OAAO,EAAE,IAAI;AACb,QAAA,YAAY,EAAE,KAAK;KACpB;AACH;AAEA;;;AAGG;SACa,QAAQ,CACtB,KAAoB,EACpB,GAAG,UAA+C,EAAA;AAElD,IAAA,KAAK,MAAM,SAAS,IAAI,UAAU,EAAE;QAClC,MAAM,YAAY,GAAG,SAAS,GAAG,KAAK,CAAC,KAAK,CAAC;QAC7C,IAAI,YAAY,EAAE;YAChB,OAAO;AACL,gBAAA,GAAG,KAAK;gBACR,YAAY;AACZ,gBAAA,OAAO,EAAE,IAAI;AACb,gBAAA,SAAS,EAAE,IAAI;AACf,gBAAA,OAAO,EAAE,KAAK;AACd,gBAAA,YAAY,EAAE,KAAK;aACpB;QACH;IACF;IAEA,OAAO;AACL,QAAA,GAAG,KAAK;AACR,QAAA,YAAY,EAAE,SAAS;AACvB,QAAA,OAAO,EAAE,IAAI;AACb,QAAA,SAAS,EAAE,IAAI;AACf,QAAA,OAAO,EAAE,IAAI;AACb,QAAA,YAAY,EAAE,KAAK;KACpB;AACH;AAEA;;;AAGG;AACI,eAAe,aAAa,CACjC,KAAoB,EACpB,GAAG,UAA2C,EAAA;IAE9C,MAAM,aAAa,GAAG,MAAM,OAAO,CAAC,GAAG,CACrC,UAAU,CAAC,GAAG,CAAC,CAAC,SAAS,KAAK,SAAS,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,CACxD;IACD,MAAM,YAAY,GAAG,aAAa,CAAC,IAAI,CAAC,OAAO,CAAC;IAEhD,IAAI,YAAY,EAAE;QAChB,OAAO;AACL,YAAA,GAAG,KAAK;YACR,YAAY;AACZ,YAAA,OAAO,EAAE,IAAI;AACb,YAAA,SAAS,EAAE,IAAI;AACf,YAAA,OAAO,EAAE,KAAK;AACd,YAAA,YAAY,EAAE,KAAK;SACpB;IACH;IAEA,OAAO;AACL,QAAA,GAAG,KAAK;AACR,QAAA,YAAY,EAAE,SAAS;AACvB,QAAA,OAAO,EAAE,IAAI;AACb,QAAA,SAAS,EAAE,IAAI;AACf,QAAA,OAAO,EAAE,IAAI;AACb,QAAA,YAAY,EAAE,KAAK;KACpB;AACH;AAEA;;;AAGG;SACa,eAAe,CAC7B,KAAoB,EACpB,GAAG,UAA+C,EAAA;AAElD,IAAA,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE;AAClB,QAAA,OAAO,KAAK;IACd;AAEA,IAAA,OAAO,QAAQ,CAAC,KAAK,EAAE,GAAG,UAAU,CAAC;AACvC;AAEA;;;AAGG;AACI,eAAe,oBAAoB,CACxC,KAAoB,EACpB,GAAG,UAA2C,EAAA;AAE9C,IAAA,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE;AAClB,QAAA,OAAO,KAAK;IACd;AAEA,IAAA,OAAO,aAAa,CAAC,KAAK,EAAE,GAAG,UAAU,CAAC;AAC5C;;;;"}
@@ -1,3 +1,4 @@
1
+ /** Represents the state of a single form field. */
1
2
  export interface FieldState<T> {
2
3
  /** The current value of the field. */
3
4
  value: T;
@@ -7,42 +8,77 @@ export interface FieldState<T> {
7
8
  isTouched: boolean;
8
9
  /** Whether the value of the field has been changed by the user. */
9
10
  isDirty: boolean;
10
- /** Whether the field is valid */
11
+ /** Whether the field is valid. */
11
12
  isValid: boolean;
12
- /** Whether the field is currently being validated */
13
+ /** Whether the field is currently being validated. */
13
14
  isValidating: boolean;
14
15
  }
16
+ /** Props passed to the render function of a {@link Field} component. */
15
17
  export interface FieldRenderProps<T> extends FieldState<T> {
18
+ /** Callback to update the field value. Should be called when the input value changes. */
16
19
  handleChange: (value: T) => void;
20
+ /** Callback to mark the field as touched. Should be called when the input loses focus. */
17
21
  handleBlur: () => void;
22
+ /** Ref callback to attach to the root element of the input. Used to focus the field on validation errors. */
18
23
  ref: (el: HTMLElement | null) => void;
19
24
  }
25
+ /** Validator functions to run on different field events. */
20
26
  export interface Validation<T> {
27
+ /** Synchronous validator to run when the field value changes. */
21
28
  onChange?: SyncValidator<T>;
29
+ /** Asynchronous validator to run when the field value changes (debounced). */
22
30
  onChangeAsync?: AsyncValidator<T>;
31
+ /** Synchronous validator to run when the field loses focus. */
23
32
  onBlur?: SyncValidator<T>;
33
+ /** Asynchronous validator to run when the field loses focus. */
24
34
  onBlurAsync?: AsyncValidator<T>;
35
+ /** Synchronous validator to run when the form is submitted. */
25
36
  onSubmit?: SyncValidator<T>;
37
+ /** Asynchronous validator to run when the form is submitted. */
26
38
  onSubmitAsync?: AsyncValidator<T>;
27
39
  }
40
+ /**
41
+ * Specifies when validation errors are displayed.
42
+ *
43
+ * - `"touched"` — Show errors after the field has been focused and blurred.
44
+ * - `"dirty"` — Show errors after the field value has been changed.
45
+ * - `"touchedAndDirty"` — Show errors only after the field has been both touched and changed.
46
+ * - `"touchedOrDirty"` — Show errors after the field has been either touched or changed.
47
+ */
28
48
  export type ValidationMode = "touched" | "dirty" | "touchedAndDirty" | "touchedOrDirty";
49
+ /** Props for the {@link Field} component. */
29
50
  export interface FieldProps<T> {
51
+ /** The current state of the field, created with {@link createFieldState}. */
30
52
  state: FieldState<T>;
53
+ /** Render function that receives field state and event handlers. */
31
54
  children: (props: FieldRenderProps<T>) => React.ReactNode;
55
+ /** Callback invoked with the updated field state whenever the state changes. */
32
56
  onChange: (newState: FieldState<T>) => void;
57
+ /** Callback invoked with the raw input value on every change, before validation. */
33
58
  onInput?: (value: T) => void;
59
+ /** Callback invoked when the field loses focus. */
34
60
  onBlur?: () => void;
61
+ /** Validator functions to run on various field events. */
35
62
  validation?: Validation<T>;
63
+ /** Overrides the validation mode set on the parent {@link Form}. Defaults to `"touchedAndDirty"`. */
36
64
  validationMode?: ValidationMode;
65
+ /** Debounce delay in milliseconds for async validators. Overrides the value set on the parent {@link Form}. Defaults to `500`. */
37
66
  debounceMs?: number;
38
67
  }
68
+ /** A synchronous validator function. Returns an error message if validation fails, or a falsy value if it passes. */
39
69
  export type SyncValidator<T> = (value: T) => React.ReactNode;
70
+ /** An asynchronous validator function. Resolves with an error message if validation fails, or a falsy value if it passes. */
40
71
  export type AsyncValidator<T> = (value: T) => Promise<React.ReactNode>;
72
+ /** A synchronous or asynchronous validator function. */
41
73
  export type Validator<T> = SyncValidator<T> | AsyncValidator<T>;
42
74
  export interface FormContextValue {
75
+ /** Default validation mode applied to all fields in the form. */
43
76
  validationMode?: ValidationMode;
77
+ /** Default debounce delay in milliseconds for async validators. */
44
78
  debounceMs?: number;
79
+ /** Registers a field with the form for submit validation. */
45
80
  registerField: (id: string, ref: HTMLElement | null, validate: () => Promise<boolean>, commitPendingValidation: () => void) => void;
81
+ /** Unregisters a field from the form. */
46
82
  unregisterField: (id: string) => void;
47
83
  }
48
84
  export type FieldMap = Map<string, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fransek/form",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "author": "Frans Ekman <fransedvinekman@gmail.com>",