@fransek/form 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/dist/cjs/index.d.ts +3 -3
  2. package/dist/cjs/index.js +8 -10
  3. package/dist/cjs/index.js.map +1 -1
  4. package/dist/cjs/lib/{Field.js → field.js} +48 -41
  5. package/dist/cjs/lib/field.js.map +1 -0
  6. package/dist/cjs/lib/focus-first-error.d.ts +4 -0
  7. package/dist/cjs/lib/focus-first-error.js +20 -0
  8. package/dist/cjs/lib/focus-first-error.js.map +1 -0
  9. package/dist/cjs/lib/{Form.d.ts → form.d.ts} +0 -4
  10. package/dist/cjs/lib/{Form.js → form.js} +3 -18
  11. package/dist/cjs/lib/form.js.map +1 -0
  12. package/dist/cjs/lib/state-utils.d.ts +20 -0
  13. package/dist/cjs/lib/state-utils.js +95 -0
  14. package/dist/cjs/lib/state-utils.js.map +1 -0
  15. package/dist/cjs/lib/test/test-utils.d.ts +5 -6
  16. package/dist/esm/index.d.ts +3 -3
  17. package/dist/esm/index.js +3 -3
  18. package/dist/esm/lib/{Field.js → field.js} +47 -40
  19. package/dist/esm/lib/field.js.map +1 -0
  20. package/dist/esm/lib/focus-first-error.d.ts +4 -0
  21. package/dist/esm/lib/focus-first-error.js +18 -0
  22. package/dist/esm/lib/focus-first-error.js.map +1 -0
  23. package/dist/esm/lib/{Form.d.ts → form.d.ts} +0 -4
  24. package/dist/esm/lib/{Form.js → form.js} +3 -17
  25. package/dist/esm/lib/form.js.map +1 -0
  26. package/dist/esm/lib/state-utils.d.ts +20 -0
  27. package/dist/esm/lib/state-utils.js +90 -0
  28. package/dist/esm/lib/state-utils.js.map +1 -0
  29. package/dist/esm/lib/test/test-utils.d.ts +5 -6
  30. package/package.json +2 -1
  31. package/dist/cjs/lib/Field.js.map +0 -1
  32. package/dist/cjs/lib/Form.js.map +0 -1
  33. package/dist/cjs/lib/fieldState.d.ts +0 -23
  34. package/dist/cjs/lib/fieldState.js +0 -93
  35. package/dist/cjs/lib/fieldState.js.map +0 -1
  36. package/dist/esm/lib/Field.js.map +0 -1
  37. package/dist/esm/lib/Form.js.map +0 -1
  38. package/dist/esm/lib/fieldState.d.ts +0 -23
  39. package/dist/esm/lib/fieldState.js +0 -87
  40. package/dist/esm/lib/fieldState.js.map +0 -1
  41. /package/dist/cjs/lib/{Field.d.ts → field.d.ts} +0 -0
  42. /package/dist/cjs/lib/{Field.test.d.ts → field.test.d.ts} +0 -0
  43. /package/dist/cjs/lib/{Form.test.d.ts → form.test.d.ts} +0 -0
  44. /package/dist/esm/lib/{Field.d.ts → field.d.ts} +0 -0
  45. /package/dist/esm/lib/{Field.test.d.ts → field.test.d.ts} +0 -0
  46. /package/dist/esm/lib/{Form.test.d.ts → form.test.d.ts} +0 -0
@@ -1,6 +1,6 @@
1
1
  import { useRef, useId, useEffect } from 'react';
2
- import { validate, validateAsync } from './fieldState.js';
3
- import { useFormContext } from './Form.js';
2
+ import { useFormContext } from './form.js';
3
+ import { validate, validateAsync } from './state-utils.js';
4
4
 
5
5
  /**
6
6
  * A headless form field component that manages validation state using a render prop pattern.
@@ -20,6 +20,26 @@ function Field(props) {
20
20
  const pendingValidationRef = useRef(null);
21
21
  const fieldRef = useRef(null);
22
22
  const id = useId();
23
+ const clearValidationTimeout = () => {
24
+ if (!validationTimeoutRef.current) {
25
+ return;
26
+ }
27
+ clearTimeout(validationTimeoutRef.current);
28
+ validationTimeoutRef.current = null;
29
+ };
30
+ const updateState = (overrides) => {
31
+ onChange({ ...stateRef.current, ...overrides });
32
+ };
33
+ const shouldValidateOnChange = () => validationMode === "dirty" ||
34
+ validationMode === "touchedOrDirty" ||
35
+ stateRef.current.isTouched;
36
+ const shouldValidateOnBlur = () => validationMode === "touched" ||
37
+ validationMode === "touchedOrDirty" ||
38
+ stateRef.current.isDirty;
39
+ const shouldValidateChangeOnBlur = () => !stateRef.current.isTouched &&
40
+ (validationMode === "touched" ||
41
+ (validationMode === "touchedOrDirty" && !stateRef.current.isDirty) ||
42
+ (validationMode === "touchedAndDirty" && stateRef.current.isDirty));
23
43
  useEffect(() => {
24
44
  async function performValidation() {
25
45
  if (!validation?.onSubmit &&
@@ -31,9 +51,17 @@ function Field(props) {
31
51
  return stateRef.current.isValid;
32
52
  }
33
53
  validationIdRef.current++;
34
- pendingValidationRef.current = validate(stateRef.current, validation?.onChange, validation?.onBlur, validation?.onSubmit);
54
+ pendingValidationRef.current = validate(stateRef.current, [
55
+ validation?.onChange,
56
+ validation?.onBlur,
57
+ validation?.onSubmit,
58
+ ]);
35
59
  if (pendingValidationRef.current.isValid) {
36
- pendingValidationRef.current = await validateAsync(stateRef.current, validation?.onChangeAsync, validation?.onBlurAsync, validation?.onSubmitAsync);
60
+ pendingValidationRef.current = await validateAsync(stateRef.current, [
61
+ validation?.onChangeAsync,
62
+ validation?.onBlurAsync,
63
+ validation?.onSubmitAsync,
64
+ ]);
37
65
  }
38
66
  return pendingValidationRef.current.isValid;
39
67
  }
@@ -58,17 +86,13 @@ function Field(props) {
58
86
  function handleChange(value) {
59
87
  onInput?.(value);
60
88
  if (validationTimeoutRef.current) {
61
- clearTimeout(validationTimeoutRef.current);
89
+ clearValidationTimeout();
62
90
  }
63
91
  const currentValidation = ++validationIdRef.current;
64
- const shouldValidate = validationMode === "dirty" ||
65
- validationMode === "touchedOrDirty" ||
66
- stateRef.current.isTouched;
67
- if (shouldValidate) {
92
+ if (shouldValidateOnChange()) {
68
93
  const errorMessage = validation?.onChange?.(value);
69
94
  const willValidateAsync = Boolean(validation?.onChangeAsync && !errorMessage);
70
- onChange({
71
- ...stateRef.current,
95
+ updateState({
72
96
  value,
73
97
  errorMessage,
74
98
  isDirty: true,
@@ -81,8 +105,7 @@ function Field(props) {
81
105
  const asyncErrorMessage = await validation?.onChangeAsync?.(value);
82
106
  isValidatingOnChangeRef.current = false;
83
107
  if (currentValidation === validationIdRef.current) {
84
- onChange({
85
- ...stateRef.current,
108
+ updateState({
86
109
  errorMessage: asyncErrorMessage,
87
110
  isValid: !asyncErrorMessage,
88
111
  isValidating: isValidatingOnBlurRef.current,
@@ -92,8 +115,7 @@ function Field(props) {
92
115
  }
93
116
  }
94
117
  else {
95
- onChange({
96
- ...stateRef.current,
118
+ updateState({
97
119
  value,
98
120
  isDirty: true,
99
121
  isValidating: false,
@@ -102,39 +124,25 @@ function Field(props) {
102
124
  }
103
125
  async function handleBlur() {
104
126
  onBlur?.();
105
- const shouldValidateOnBlur = validationMode === "touched" ||
106
- validationMode === "touchedOrDirty" ||
107
- stateRef.current.isDirty;
108
- if (!shouldValidateOnBlur) {
109
- onChange({
110
- ...stateRef.current,
111
- isTouched: true,
112
- });
127
+ if (!shouldValidateOnBlur()) {
128
+ updateState({ isTouched: true });
113
129
  return;
114
130
  }
115
131
  const currentValidation = validationIdRef.current;
116
132
  let errorMessage = stateRef.current.errorMessage ||
117
133
  validation?.onBlur?.(stateRef.current.value);
118
- const shouldValidateOnChange = !stateRef.current.isTouched &&
119
- (validationMode === "touched" ||
120
- (validationMode === "touchedOrDirty" && !stateRef.current.isDirty) ||
121
- (validationMode === "touchedAndDirty" && stateRef.current.isDirty));
122
- if (!errorMessage && shouldValidateOnChange && validation?.onChange) {
134
+ if (!errorMessage && shouldValidateChangeOnBlur() && validation?.onChange) {
123
135
  errorMessage = validation.onChange(stateRef.current.value);
124
136
  }
125
137
  if (!errorMessage &&
126
138
  (validation?.onBlurAsync || validation?.onChangeAsync)) {
127
139
  isValidatingOnBlurRef.current = true;
128
- onChange({
129
- ...stateRef.current,
130
- isValidating: true,
131
- isTouched: true,
132
- });
140
+ updateState({ isValidating: true, isTouched: true });
133
141
  const asyncValidations = [];
134
142
  if (validation?.onBlurAsync) {
135
143
  asyncValidations.push(validation.onBlurAsync(stateRef.current.value));
136
144
  }
137
- if (shouldValidateOnChange && validation?.onChangeAsync) {
145
+ if (shouldValidateChangeOnBlur() && validation?.onChangeAsync) {
138
146
  asyncValidations.push(validation.onChangeAsync(stateRef.current.value));
139
147
  }
140
148
  const [blurError, changeError] = await Promise.all(asyncValidations);
@@ -142,17 +150,16 @@ function Field(props) {
142
150
  }
143
151
  isValidatingOnBlurRef.current = false;
144
152
  if (errorMessage && validationTimeoutRef.current) {
145
- clearTimeout(validationTimeoutRef.current);
153
+ clearValidationTimeout();
146
154
  }
147
155
  if (currentValidation !== validationIdRef.current) {
148
- onChange({
149
- ...stateRef.current,
156
+ updateState({
150
157
  isTouched: true,
158
+ isValidating: isValidatingOnChangeRef.current,
151
159
  });
152
160
  return;
153
161
  }
154
- onChange({
155
- ...stateRef.current,
162
+ updateState({
156
163
  errorMessage,
157
164
  isTouched: true,
158
165
  isValid: !errorMessage,
@@ -171,4 +178,4 @@ function Field(props) {
171
178
  }
172
179
 
173
180
  export { Field };
174
- //# sourceMappingURL=Field.js.map
181
+ //# sourceMappingURL=field.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"field.js","sources":["../../../src/lib/field.tsx"],"sourcesContent":["import { useEffect, useId, useRef } from \"react\";\nimport { useFormContext } from \"./form\";\nimport { validate, validateAsync } from \"./state-utils\";\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 const clearValidationTimeout = () => {\n if (!validationTimeoutRef.current) {\n return;\n }\n clearTimeout(validationTimeoutRef.current);\n validationTimeoutRef.current = null;\n };\n\n const updateState = (overrides: Partial<FieldState<T>>) => {\n onChange({ ...stateRef.current, ...overrides });\n };\n\n const shouldValidateOnChange = () =>\n validationMode === \"dirty\" ||\n validationMode === \"touchedOrDirty\" ||\n stateRef.current.isTouched;\n\n const shouldValidateOnBlur = () =>\n validationMode === \"touched\" ||\n validationMode === \"touchedOrDirty\" ||\n stateRef.current.isDirty;\n\n const shouldValidateChangeOnBlur = () =>\n !stateRef.current.isTouched &&\n (validationMode === \"touched\" ||\n (validationMode === \"touchedOrDirty\" && !stateRef.current.isDirty) ||\n (validationMode === \"touchedAndDirty\" && stateRef.current.isDirty));\n\n useEffect(() => {\n async function performValidation() {\n if (\n !validation?.onSubmit &&\n !validation?.onSubmitAsync &&\n !stateRef.current.isValidating &&\n stateRef.current.isTouched &&\n stateRef.current.isDirty &&\n document.activeElement !== fieldRef.current\n ) {\n return stateRef.current.isValid;\n }\n validationIdRef.current++;\n pendingValidationRef.current = validate(stateRef.current, [\n validation?.onChange,\n validation?.onBlur,\n validation?.onSubmit,\n ]);\n if (pendingValidationRef.current.isValid) {\n pendingValidationRef.current = await validateAsync(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 clearValidationTimeout();\n }\n\n const currentValidation = ++validationIdRef.current;\n\n if (shouldValidateOnChange()) {\n const errorMessage = validation?.onChange?.(value);\n const willValidateAsync = Boolean(\n validation?.onChangeAsync && !errorMessage,\n );\n\n updateState({\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 updateState({\n errorMessage: asyncErrorMessage,\n isValid: !asyncErrorMessage,\n isValidating: isValidatingOnBlurRef.current,\n });\n }\n }, debounceMs);\n }\n } else {\n updateState({\n value,\n isDirty: true,\n isValidating: false,\n });\n }\n }\n\n async function handleBlur() {\n onBlur?.();\n\n if (!shouldValidateOnBlur()) {\n updateState({ isTouched: true });\n return;\n }\n\n const currentValidation = validationIdRef.current;\n\n let errorMessage =\n stateRef.current.errorMessage ||\n validation?.onBlur?.(stateRef.current.value);\n\n if (!errorMessage && shouldValidateChangeOnBlur() && 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 updateState({ isValidating: true, isTouched: true });\n\n const asyncValidations: Promise<React.ReactNode>[] = [];\n\n if (validation?.onBlurAsync) {\n asyncValidations.push(validation.onBlurAsync(stateRef.current.value));\n }\n\n if (shouldValidateChangeOnBlur() && 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 clearValidationTimeout();\n }\n\n if (currentValidation !== validationIdRef.current) {\n updateState({\n isTouched: true,\n isValidating: isValidatingOnChangeRef.current,\n });\n return;\n }\n\n updateState({\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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"}
@@ -0,0 +1,4 @@
1
+ export declare function focusFirstError(results: {
2
+ isValid: boolean;
3
+ ref: HTMLElement | null;
4
+ }[], scrollOffset: number): void;
@@ -0,0 +1,18 @@
1
+ function focusFirstError(results, scrollOffset) {
2
+ const firstInvalidField = results
3
+ .filter((field) => !field.isValid && field.ref)
4
+ .map((field) => field.ref)
5
+ .sort((a, b) => a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_PRECEDING ? 1 : -1)
6
+ .at(0);
7
+ if (!firstInvalidField) {
8
+ return;
9
+ }
10
+ firstInvalidField.focus();
11
+ const rect = firstInvalidField.getBoundingClientRect();
12
+ window.scrollTo({
13
+ top: rect.top + window.scrollY - scrollOffset,
14
+ });
15
+ }
16
+
17
+ export { focusFirstError };
18
+ //# sourceMappingURL=focus-first-error.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"focus-first-error.js","sources":["../../../src/lib/focus-first-error.ts"],"sourcesContent":["export function focusFirstError(\n results: {\n isValid: boolean;\n ref: HTMLElement | null;\n }[],\n scrollOffset: number,\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 - scrollOffset,\n });\n}\n"],"names":[],"mappings":"AAAM,SAAU,eAAe,CAC7B,OAGG,EACH,YAAoB,EAAA;IAEpB,MAAM,iBAAiB,GAAG;AACvB,SAAA,MAAM,CAAC,CAAC,KAAK,KAAK,CAAC,KAAK,CAAC,OAAO,IAAI,KAAK,CAAC,GAAG;SAC7C,GAAG,CAAC,CAAC,KAAK,KAAK,KAAK,CAAC,GAAI;SACzB,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,KACT,CAAC,CAAC,uBAAuB,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,2BAA2B,GAAG,CAAC,GAAG,EAAE;SAEzE,EAAE,CAAC,CAAC,CAAC;IAER,IAAI,CAAC,iBAAiB,EAAE;QACtB;IACF;IAEA,iBAAiB,CAAC,KAAK,EAAE;AACzB,IAAA,MAAM,IAAI,GAAG,iBAAiB,CAAC,qBAAqB,EAAE;IACtD,MAAM,CAAC,QAAQ,CAAC;QACd,GAAG,EAAE,IAAI,CAAC,GAAG,GAAG,MAAM,CAAC,OAAO,GAAG,YAAY;AAC9C,KAAA,CAAC;AACJ;;;;"}
@@ -24,8 +24,4 @@ interface FormProps extends Omit<React.ComponentProps<"form">, "onSubmit"> {
24
24
  export declare function Form({ onSubmit, validationMode, debounceMs, ...props }: FormProps): React.JSX.Element;
25
25
  export declare const FormContext: React.Context<FormContextValue>;
26
26
  export declare function useFormContext(): FormContextValue;
27
- export declare function focusFirstError(results: {
28
- isValid: boolean;
29
- ref: HTMLElement | null;
30
- }[], scrollOffset: number): void;
31
27
  export {};
@@ -1,4 +1,5 @@
1
1
  import React, { useRef, useCallback } from 'react';
2
+ import { focusFirstError } from './focus-first-error.js';
2
3
 
3
4
  /**
4
5
  * A form component that provides context for coordinating field validation.
@@ -50,21 +51,6 @@ const FormContext = React.createContext({
50
51
  function useFormContext() {
51
52
  return React.useContext(FormContext);
52
53
  }
53
- function focusFirstError(results, scrollOffset) {
54
- const firstInvalidField = results
55
- .filter((field) => !field.isValid && field.ref)
56
- .map((field) => field.ref)
57
- .sort((a, b) => a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_PRECEDING ? 1 : -1)
58
- .at(0);
59
- if (!firstInvalidField) {
60
- return;
61
- }
62
- firstInvalidField.focus();
63
- const rect = firstInvalidField.getBoundingClientRect();
64
- window.scrollTo({
65
- top: rect.top + window.scrollY - scrollOffset,
66
- });
67
- }
68
54
 
69
- export { Form, FormContext, focusFirstError, useFormContext };
70
- //# sourceMappingURL=Form.js.map
55
+ export { Form, FormContext, useFormContext };
56
+ //# sourceMappingURL=form.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"form.js","sources":["../../../src/lib/form.tsx"],"sourcesContent":["import React, { useCallback, useRef } from \"react\";\nimport { focusFirstError } from \"./focus-first-error\";\nimport {\n FieldMap,\n FormContextValue,\n ValidateFormOptions,\n ValidationMode,\n} 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(\n async (options: ValidateFormOptions = {}) => {\n const {\n focusFirstError: shouldFocusFirstError = true,\n scrollOffset = 100,\n } = options;\n\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\n if (hasErrors && shouldFocusFirstError) {\n focusFirstError(results, scrollOffset);\n }\n return !hasErrors;\n },\n [],\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"],"names":[],"mappings":";;;AA0BA;;;;;;;AAOG;AACG,SAAU,IAAI,CAAC,EACnB,QAAQ,EACR,cAAc,EACd,UAAU,EACV,GAAG,KAAK,EACE,EAAA;IACV,MAAM,SAAS,GAAG,MAAM,CAAW,IAAI,GAAG,EAAE,CAAC;AAE7C,IAAA,MAAM,aAAa,GAAG,WAAW,CAC/B,CACE,EAAU,EACV,GAAuB,EACvB,QAAgC,EAChC,uBAAmC,KACjC;AACF,QAAA,SAAS,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,EAAE;YACxB,GAAG;YACH,QAAQ;YACR,uBAAuB;AACxB,SAAA,CAAC;IACJ,CAAC,EACD,EAAE,CACH;AAED,IAAA,MAAM,eAAe,GAAG,WAAW,CAAC,CAAC,EAAU,KAAI;AACjD,QAAA,SAAS,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;IAC9B,CAAC,EAAE,EAAE,CAAC;IAEN,MAAM,YAAY,GAAG,WAAW,CAC9B,OAAO,OAAA,GAA+B,EAAE,KAAI;AAC1C,QAAA,MAAM,EACJ,eAAe,EAAE,qBAAqB,GAAG,IAAI,EAC7C,YAAY,GAAG,GAAG,GACnB,GAAG,OAAO;AAEX,QAAA,MAAM,MAAM,GAAG,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC;AACrD,QAAA,MAAM,kBAAkB,GAAG,MAAM,CAAC,GAAG,CAAC,OAAO,KAAK,MAAM;AACtD,YAAA,OAAO,EAAE,MAAM,KAAK,CAAC,QAAQ,EAAE;YAC/B,GAAG,EAAE,KAAK,CAAC,GAAG;AACf,SAAA,CAAC,CAAC;QACH,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC,kBAAkB,CAAC;AACrD,QAAA,MAAM,CAAC,OAAO,CAAC,CAAC,KAAK,KAAK,KAAK,CAAC,uBAAuB,EAAE,CAAC;AAC1D,QAAA,MAAM,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,MAAM,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC;AAE3D,QAAA,IAAI,SAAS,IAAI,qBAAqB,EAAE;AACtC,YAAA,eAAe,CAAC,OAAO,EAAE,YAAY,CAAC;QACxC;QACA,OAAO,CAAC,SAAS;IACnB,CAAC,EACD,EAAE,CACH;AAED,IAAA,QACE,KAAA,CAAA,aAAA,CAAC,WAAW,CAAC,QAAQ,EAAA,EACnB,KAAK,EAAE;YACL,aAAa;YACb,eAAe;YACf,cAAc;YACd,UAAU;AACX,SAAA,EAAA;AAED,QAAA,KAAA,CAAA,aAAA,CAAA,MAAA,EAAA,EAAM,QAAQ,EAAE,CAAC,CAAC,KAAK,QAAQ,GAAG,CAAC,EAAE,YAAY,CAAC,EAAA,GAAM,KAAK,EAAA,CAAI,CAC5C;AAE3B;AAEO,MAAM,WAAW,GAAG,KAAK,CAAC,aAAa,CAAmB;AAC/D,IAAA,aAAa,EAAE,MAAK,EAAE,CAAC;AACvB,IAAA,eAAe,EAAE,MAAK,EAAE,CAAC;AAC1B,CAAA;SAEe,cAAc,GAAA;AAC5B,IAAA,OAAO,KAAK,CAAC,UAAU,CAAC,WAAW,CAAC;AACtC;;;;"}
@@ -0,0 +1,20 @@
1
+ import { FieldState, SyncValidator, ValidationMode, Validator } from "./types";
2
+ /** Creates the initial state for a field with the provided value. */
3
+ export declare function createFieldState<T>(initialValue: T): FieldState<T>;
4
+ /**
5
+ * Validates the field with synchronous validators and returns the next {@link FieldState}.
6
+ *
7
+ * If `validationMode` blocks validation, the original state is returned unchanged.
8
+ * When validation runs, `isDirty` and `isTouched` are set to `true`, and evaluation
9
+ * stops at the first validator that returns a truthy error message.
10
+ */
11
+ export declare function validate<T>(state: FieldState<T>, validators: Array<SyncValidator<T> | undefined> | SyncValidator<T> | undefined, validationMode?: ValidationMode | undefined): FieldState<T>;
12
+ /**
13
+ * Validates the field with synchronous and/or asynchronous validators and returns the next {@link FieldState}.
14
+ *
15
+ * If `validationMode` blocks validation, the original state is returned unchanged.
16
+ * When validation runs, all validators are awaited in parallel and the first truthy
17
+ * error in validator-list order is used.
18
+ */
19
+ export declare function validateAsync<T>(state: FieldState<T>, validators: Array<Validator<T> | undefined> | Validator<T> | undefined, validationMode?: ValidationMode | undefined): Promise<FieldState<T>>;
20
+ export declare function shouldValidate<T>(state: FieldState<T>, validationMode: ValidationMode | undefined): boolean;
@@ -0,0 +1,90 @@
1
+ /** Creates the initial state for a field with the provided value. */
2
+ function createFieldState(initialValue) {
3
+ return {
4
+ value: initialValue,
5
+ errorMessage: undefined,
6
+ isTouched: false,
7
+ isDirty: false,
8
+ isValid: true,
9
+ isValidating: false,
10
+ };
11
+ }
12
+ /**
13
+ * Validates the field with synchronous validators and returns the next {@link FieldState}.
14
+ *
15
+ * If `validationMode` blocks validation, the original state is returned unchanged.
16
+ * When validation runs, `isDirty` and `isTouched` are set to `true`, and evaluation
17
+ * stops at the first validator that returns a truthy error message.
18
+ */
19
+ function validate(state, validators, validationMode) {
20
+ if (!shouldValidate(state, validationMode)) {
21
+ return state;
22
+ }
23
+ const _validators = Array.isArray(validators) ? validators : [validators];
24
+ for (const validator of _validators) {
25
+ const errorMessage = validator?.(state.value);
26
+ if (errorMessage) {
27
+ return {
28
+ ...state,
29
+ errorMessage,
30
+ isDirty: true,
31
+ isTouched: true,
32
+ isValid: false,
33
+ isValidating: false,
34
+ };
35
+ }
36
+ }
37
+ return {
38
+ ...state,
39
+ errorMessage: undefined,
40
+ isDirty: true,
41
+ isTouched: true,
42
+ isValid: true,
43
+ isValidating: false,
44
+ };
45
+ }
46
+ /**
47
+ * Validates the field with synchronous and/or asynchronous validators and returns the next {@link FieldState}.
48
+ *
49
+ * If `validationMode` blocks validation, the original state is returned unchanged.
50
+ * When validation runs, all validators are awaited in parallel and the first truthy
51
+ * error in validator-list order is used.
52
+ */
53
+ async function validateAsync(state, validators, validationMode) {
54
+ if (!shouldValidate(state, validationMode)) {
55
+ return state;
56
+ }
57
+ const _validators = Array.isArray(validators) ? validators : [validators];
58
+ const errorMessages = await Promise.all(_validators.map((validator) => validator?.(state.value)));
59
+ const errorMessage = errorMessages.find(Boolean);
60
+ if (errorMessage) {
61
+ return {
62
+ ...state,
63
+ errorMessage,
64
+ isDirty: true,
65
+ isTouched: true,
66
+ isValid: false,
67
+ isValidating: false,
68
+ };
69
+ }
70
+ return {
71
+ ...state,
72
+ errorMessage: undefined,
73
+ isDirty: true,
74
+ isTouched: true,
75
+ isValid: true,
76
+ isValidating: false,
77
+ };
78
+ }
79
+ function shouldValidate(state, validationMode) {
80
+ return (validationMode === undefined ||
81
+ (validationMode === "touched" && state.isTouched) ||
82
+ (validationMode === "dirty" && state.isDirty) ||
83
+ (validationMode === "touchedAndDirty" &&
84
+ state.isTouched &&
85
+ state.isDirty) ||
86
+ (validationMode === "touchedOrDirty" && (state.isTouched || state.isDirty)));
87
+ }
88
+
89
+ export { createFieldState, shouldValidate, validate, validateAsync };
90
+ //# sourceMappingURL=state-utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"state-utils.js","sources":["../../../src/lib/state-utils.ts"],"sourcesContent":["import { FieldState, SyncValidator, ValidationMode, Validator } from \"./types\";\n\n/** Creates the initial state for a field with the provided 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 * Validates the field with synchronous validators and returns the next {@link FieldState}.\n *\n * If `validationMode` blocks validation, the original state is returned unchanged.\n * When validation runs, `isDirty` and `isTouched` are set to `true`, and evaluation\n * stops at the first validator that returns a truthy error message.\n */\nexport function validate<T>(\n state: FieldState<T>,\n validators:\n | Array<SyncValidator<T> | undefined>\n | SyncValidator<T>\n | undefined,\n validationMode?: ValidationMode | undefined,\n): FieldState<T> {\n if (!shouldValidate(state, validationMode)) {\n return state;\n }\n\n const _validators = Array.isArray(validators) ? validators : [validators];\n\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 * Validates the field with synchronous and/or asynchronous validators and returns the next {@link FieldState}.\n *\n * If `validationMode` blocks validation, the original state is returned unchanged.\n * When validation runs, all validators are awaited in parallel and the first truthy\n * error in validator-list order is used.\n */\nexport async function validateAsync<T>(\n state: FieldState<T>,\n validators: Array<Validator<T> | undefined> | Validator<T> | undefined,\n validationMode?: ValidationMode | undefined,\n): Promise<FieldState<T>> {\n if (!shouldValidate(state, validationMode)) {\n return state;\n }\n\n const _validators = Array.isArray(validators) ? validators : [validators];\n\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 shouldValidate<T>(\n state: FieldState<T>,\n validationMode: ValidationMode | undefined,\n) {\n return (\n validationMode === undefined ||\n (validationMode === \"touched\" && state.isTouched) ||\n (validationMode === \"dirty\" && state.isDirty) ||\n (validationMode === \"touchedAndDirty\" &&\n state.isTouched &&\n state.isDirty) ||\n (validationMode === \"touchedOrDirty\" && (state.isTouched || state.isDirty))\n );\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;;;;;;AAMG;SACa,QAAQ,CACtB,KAAoB,EACpB,UAGa,EACb,cAA2C,EAAA;IAE3C,IAAI,CAAC,cAAc,CAAC,KAAK,EAAE,cAAc,CAAC,EAAE;AAC1C,QAAA,OAAO,KAAK;IACd;AAEA,IAAA,MAAM,WAAW,GAAG,KAAK,CAAC,OAAO,CAAC,UAAU,CAAC,GAAG,UAAU,GAAG,CAAC,UAAU,CAAC;AAEzE,IAAA,KAAK,MAAM,SAAS,IAAI,WAAW,EAAE;QACnC,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;;;;;;AAMG;AACI,eAAe,aAAa,CACjC,KAAoB,EACpB,UAAsE,EACtE,cAA2C,EAAA;IAE3C,IAAI,CAAC,cAAc,CAAC,KAAK,EAAE,cAAc,CAAC,EAAE;AAC1C,QAAA,OAAO,KAAK;IACd;AAEA,IAAA,MAAM,WAAW,GAAG,KAAK,CAAC,OAAO,CAAC,UAAU,CAAC,GAAG,UAAU,GAAG,CAAC,UAAU,CAAC;IAEzE,MAAM,aAAa,GAAG,MAAM,OAAO,CAAC,GAAG,CACrC,WAAW,CAAC,GAAG,CAAC,CAAC,SAAS,KAAK,SAAS,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,CACzD;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;AAEM,SAAU,cAAc,CAC5B,KAAoB,EACpB,cAA0C,EAAA;IAE1C,QACE,cAAc,KAAK,SAAS;AAC5B,SAAC,cAAc,KAAK,SAAS,IAAI,KAAK,CAAC,SAAS,CAAC;AACjD,SAAC,cAAc,KAAK,OAAO,IAAI,KAAK,CAAC,OAAO,CAAC;SAC5C,cAAc,KAAK,iBAAiB;AACnC,YAAA,KAAK,CAAC,SAAS;YACf,KAAK,CAAC,OAAO,CAAC;AAChB,SAAC,cAAc,KAAK,gBAAgB,KAAK,KAAK,CAAC,SAAS,IAAI,KAAK,CAAC,OAAO,CAAC,CAAC;AAE/E;;;;"}
@@ -1,13 +1,12 @@
1
1
  import userEvent from "@testing-library/user-event";
2
- import React from "react";
3
- import { ValidationMode } from "../types";
2
+ import { Validation, ValidationMode } from "../types";
4
3
  interface InputProps {
5
- validateOnChange?: (value: string) => React.ReactNode;
6
- validateOnChangeAsync?: (value: string) => Promise<React.ReactNode>;
7
- validateOnBlur?: (value: string) => React.ReactNode;
8
- validateOnBlurAsync?: (value: string) => Promise<React.ReactNode>;
4
+ validation?: Validation<string>;
9
5
  debounceMs?: number;
10
6
  validationMode?: ValidationMode;
7
+ onInput?: (value: string) => void;
8
+ onBlur?: () => void;
9
+ initialValue?: string;
11
10
  }
12
11
  export declare const setupTest: (props?: InputProps) => {
13
12
  user: import("@testing-library/user-event").UserEvent;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fransek/form",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "author": "Frans Ekman <fransedvinekman@gmail.com>",
@@ -59,6 +59,7 @@
59
59
  "prettier-plugin-organize-imports": "^4.3.0",
60
60
  "prettier-plugin-tailwindcss": "^0.7.2",
61
61
  "react": "19.2.3",
62
+ "react-dom": "19.2.3",
62
63
  "rollup": "^4.56.0",
63
64
  "semantic-release": "^25.0.2",
64
65
  "typescript": "^5.9.3",
@@ -1 +0,0 @@
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 if (\n !validation?.onSubmit &&\n !validation?.onSubmitAsync &&\n !stateRef.current.isValidating &&\n stateRef.current.isTouched &&\n stateRef.current.isDirty &&\n document.activeElement !== fieldRef.current\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) {\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 shouldValidateOnBlur =\n validationMode === \"touched\" ||\n validationMode === \"touchedOrDirty\" ||\n stateRef.current.isDirty;\n\n if (!shouldValidateOnBlur) {\n onChange({\n ...stateRef.current,\n isTouched: true,\n });\n return;\n }\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 +0,0 @@
1
- {"version":3,"file":"Form.js","sources":["../../../src/lib/Form.tsx"],"sourcesContent":["import React, { useCallback, useRef } from \"react\";\nimport {\n FieldMap,\n FormContextValue,\n ValidateFormOptions,\n ValidationMode,\n} 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(\n async (options: ValidateFormOptions = {}) => {\n const {\n focusFirstError: shouldFocusFirstError = true,\n scrollOffset = 100,\n } = options;\n\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\n if (hasErrors && shouldFocusFirstError) {\n focusFirstError(results, scrollOffset);\n }\n return !hasErrors;\n },\n [],\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 scrollOffset: number,\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 - scrollOffset,\n });\n}\n"],"names":["useRef","useCallback"],"mappings":"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"}
@@ -1,23 +0,0 @@
1
- import { FieldState, SyncValidator, Validator } from "./types";
2
- /** Creates the initial state for a field with the given initial value. */
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
- */
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
- */
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
- */
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
- */
23
- export declare function validateIfDirtyAsync<T>(state: FieldState<T>, ...validators: Array<Validator<T> | undefined>): Promise<FieldState<T>>;
@@ -1,93 +0,0 @@
1
- 'use strict';
2
-
3
- /** Creates the initial state for a field with the given initial value. */
4
- function createFieldState(initialValue) {
5
- return {
6
- value: initialValue,
7
- errorMessage: undefined,
8
- isTouched: false,
9
- isDirty: false,
10
- isValid: true,
11
- isValidating: false,
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
- */
18
- function validate(state, ...validators) {
19
- for (const validator of validators) {
20
- const errorMessage = validator?.(state.value);
21
- if (errorMessage) {
22
- return {
23
- ...state,
24
- errorMessage,
25
- isDirty: true,
26
- isTouched: true,
27
- isValid: false,
28
- isValidating: false,
29
- };
30
- }
31
- }
32
- return {
33
- ...state,
34
- errorMessage: undefined,
35
- isDirty: true,
36
- isTouched: true,
37
- isValid: true,
38
- isValidating: false,
39
- };
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
- */
45
- async function validateAsync(state, ...validators) {
46
- const errorMessages = await Promise.all(validators.map((validator) => validator?.(state.value)));
47
- const errorMessage = errorMessages.find(Boolean);
48
- if (errorMessage) {
49
- return {
50
- ...state,
51
- errorMessage,
52
- isDirty: true,
53
- isTouched: true,
54
- isValid: false,
55
- isValidating: false,
56
- };
57
- }
58
- return {
59
- ...state,
60
- errorMessage: undefined,
61
- isDirty: true,
62
- isTouched: true,
63
- isValid: true,
64
- isValidating: false,
65
- };
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
- */
71
- function validateIfDirty(state, ...validators) {
72
- if (!state.isDirty) {
73
- return state;
74
- }
75
- return validate(state, ...validators);
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
- */
81
- async function validateIfDirtyAsync(state, ...validators) {
82
- if (!state.isDirty) {
83
- return state;
84
- }
85
- return validateAsync(state, ...validators);
86
- }
87
-
88
- exports.createFieldState = createFieldState;
89
- exports.validate = validate;
90
- exports.validateAsync = validateAsync;
91
- exports.validateIfDirty = validateIfDirty;
92
- exports.validateIfDirtyAsync = validateIfDirtyAsync;
93
- //# sourceMappingURL=fieldState.js.map
@@ -1 +0,0 @@
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;;;;;;;;"}