@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,4 +1,4 @@
1
- export { Field } from "./lib/Field";
2
- export { createFieldState, validate, validateAsync, validateIfDirty, validateIfDirtyAsync, } from "./lib/fieldState";
3
- export { Form } from "./lib/Form";
1
+ export { Field } from "./lib/field";
2
+ export { Form } from "./lib/form";
3
+ export { createFieldState, validate, validateAsync } from "./lib/state-utils";
4
4
  export type { AsyncValidator, FieldProps, FieldRenderProps, FieldState, SyncValidator, Validation, ValidationMode, Validator, } from "./lib/types";
package/dist/cjs/index.js CHANGED
@@ -1,16 +1,14 @@
1
1
  'use strict';
2
2
 
3
- var Field = require('./lib/Field.js');
4
- var fieldState = require('./lib/fieldState.js');
5
- var Form = require('./lib/Form.js');
3
+ var field = require('./lib/field.js');
4
+ var form = require('./lib/form.js');
5
+ var stateUtils = require('./lib/state-utils.js');
6
6
 
7
7
 
8
8
 
9
- exports.Field = Field.Field;
10
- exports.createFieldState = fieldState.createFieldState;
11
- exports.validate = fieldState.validate;
12
- exports.validateAsync = fieldState.validateAsync;
13
- exports.validateIfDirty = fieldState.validateIfDirty;
14
- exports.validateIfDirtyAsync = fieldState.validateIfDirtyAsync;
15
- exports.Form = Form.Form;
9
+ exports.Field = field.Field;
10
+ exports.Form = form.Form;
11
+ exports.createFieldState = stateUtils.createFieldState;
12
+ exports.validate = stateUtils.validate;
13
+ exports.validateAsync = stateUtils.validateAsync;
16
14
  //# 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,8 +1,8 @@
1
1
  'use strict';
2
2
 
3
3
  var React = require('react');
4
- var fieldState = require('./fieldState.js');
5
- var Form = require('./Form.js');
4
+ var form = require('./form.js');
5
+ var stateUtils = require('./state-utils.js');
6
6
 
7
7
  /**
8
8
  * A headless form field component that manages validation state using a render prop pattern.
@@ -11,7 +11,7 @@ var Form = require('./Form.js');
11
11
  * Passes current field state and event handlers to the `children` render function.
12
12
  */
13
13
  function Field(props) {
14
- const { registerField, unregisterField, validationMode: formValidationMode, debounceMs: formDebounceMs, } = Form.useFormContext();
14
+ const { registerField, unregisterField, validationMode: formValidationMode, debounceMs: formDebounceMs, } = form.useFormContext();
15
15
  const { children, state, onChange, onInput, onBlur, validation, debounceMs = formDebounceMs || 500, validationMode = formValidationMode || "touchedAndDirty", } = props;
16
16
  const stateRef = React.useRef(state);
17
17
  stateRef.current = state;
@@ -22,6 +22,26 @@ function Field(props) {
22
22
  const pendingValidationRef = React.useRef(null);
23
23
  const fieldRef = React.useRef(null);
24
24
  const id = React.useId();
25
+ const clearValidationTimeout = () => {
26
+ if (!validationTimeoutRef.current) {
27
+ return;
28
+ }
29
+ clearTimeout(validationTimeoutRef.current);
30
+ validationTimeoutRef.current = null;
31
+ };
32
+ const updateState = (overrides) => {
33
+ onChange({ ...stateRef.current, ...overrides });
34
+ };
35
+ const shouldValidateOnChange = () => validationMode === "dirty" ||
36
+ validationMode === "touchedOrDirty" ||
37
+ stateRef.current.isTouched;
38
+ const shouldValidateOnBlur = () => validationMode === "touched" ||
39
+ validationMode === "touchedOrDirty" ||
40
+ stateRef.current.isDirty;
41
+ const shouldValidateChangeOnBlur = () => !stateRef.current.isTouched &&
42
+ (validationMode === "touched" ||
43
+ (validationMode === "touchedOrDirty" && !stateRef.current.isDirty) ||
44
+ (validationMode === "touchedAndDirty" && stateRef.current.isDirty));
25
45
  React.useEffect(() => {
26
46
  async function performValidation() {
27
47
  if (!validation?.onSubmit &&
@@ -33,9 +53,17 @@ function Field(props) {
33
53
  return stateRef.current.isValid;
34
54
  }
35
55
  validationIdRef.current++;
36
- pendingValidationRef.current = fieldState.validate(stateRef.current, validation?.onChange, validation?.onBlur, validation?.onSubmit);
56
+ pendingValidationRef.current = stateUtils.validate(stateRef.current, [
57
+ validation?.onChange,
58
+ validation?.onBlur,
59
+ validation?.onSubmit,
60
+ ]);
37
61
  if (pendingValidationRef.current.isValid) {
38
- pendingValidationRef.current = await fieldState.validateAsync(stateRef.current, validation?.onChangeAsync, validation?.onBlurAsync, validation?.onSubmitAsync);
62
+ pendingValidationRef.current = await stateUtils.validateAsync(stateRef.current, [
63
+ validation?.onChangeAsync,
64
+ validation?.onBlurAsync,
65
+ validation?.onSubmitAsync,
66
+ ]);
39
67
  }
40
68
  return pendingValidationRef.current.isValid;
41
69
  }
@@ -60,17 +88,13 @@ function Field(props) {
60
88
  function handleChange(value) {
61
89
  onInput?.(value);
62
90
  if (validationTimeoutRef.current) {
63
- clearTimeout(validationTimeoutRef.current);
91
+ clearValidationTimeout();
64
92
  }
65
93
  const currentValidation = ++validationIdRef.current;
66
- const shouldValidate = validationMode === "dirty" ||
67
- validationMode === "touchedOrDirty" ||
68
- stateRef.current.isTouched;
69
- if (shouldValidate) {
94
+ if (shouldValidateOnChange()) {
70
95
  const errorMessage = validation?.onChange?.(value);
71
96
  const willValidateAsync = Boolean(validation?.onChangeAsync && !errorMessage);
72
- onChange({
73
- ...stateRef.current,
97
+ updateState({
74
98
  value,
75
99
  errorMessage,
76
100
  isDirty: true,
@@ -83,8 +107,7 @@ function Field(props) {
83
107
  const asyncErrorMessage = await validation?.onChangeAsync?.(value);
84
108
  isValidatingOnChangeRef.current = false;
85
109
  if (currentValidation === validationIdRef.current) {
86
- onChange({
87
- ...stateRef.current,
110
+ updateState({
88
111
  errorMessage: asyncErrorMessage,
89
112
  isValid: !asyncErrorMessage,
90
113
  isValidating: isValidatingOnBlurRef.current,
@@ -94,8 +117,7 @@ function Field(props) {
94
117
  }
95
118
  }
96
119
  else {
97
- onChange({
98
- ...stateRef.current,
120
+ updateState({
99
121
  value,
100
122
  isDirty: true,
101
123
  isValidating: false,
@@ -104,39 +126,25 @@ function Field(props) {
104
126
  }
105
127
  async function handleBlur() {
106
128
  onBlur?.();
107
- const shouldValidateOnBlur = validationMode === "touched" ||
108
- validationMode === "touchedOrDirty" ||
109
- stateRef.current.isDirty;
110
- if (!shouldValidateOnBlur) {
111
- onChange({
112
- ...stateRef.current,
113
- isTouched: true,
114
- });
129
+ if (!shouldValidateOnBlur()) {
130
+ updateState({ isTouched: true });
115
131
  return;
116
132
  }
117
133
  const currentValidation = validationIdRef.current;
118
134
  let errorMessage = stateRef.current.errorMessage ||
119
135
  validation?.onBlur?.(stateRef.current.value);
120
- const shouldValidateOnChange = !stateRef.current.isTouched &&
121
- (validationMode === "touched" ||
122
- (validationMode === "touchedOrDirty" && !stateRef.current.isDirty) ||
123
- (validationMode === "touchedAndDirty" && stateRef.current.isDirty));
124
- if (!errorMessage && shouldValidateOnChange && validation?.onChange) {
136
+ if (!errorMessage && shouldValidateChangeOnBlur() && validation?.onChange) {
125
137
  errorMessage = validation.onChange(stateRef.current.value);
126
138
  }
127
139
  if (!errorMessage &&
128
140
  (validation?.onBlurAsync || validation?.onChangeAsync)) {
129
141
  isValidatingOnBlurRef.current = true;
130
- onChange({
131
- ...stateRef.current,
132
- isValidating: true,
133
- isTouched: true,
134
- });
142
+ updateState({ isValidating: true, isTouched: true });
135
143
  const asyncValidations = [];
136
144
  if (validation?.onBlurAsync) {
137
145
  asyncValidations.push(validation.onBlurAsync(stateRef.current.value));
138
146
  }
139
- if (shouldValidateOnChange && validation?.onChangeAsync) {
147
+ if (shouldValidateChangeOnBlur() && validation?.onChangeAsync) {
140
148
  asyncValidations.push(validation.onChangeAsync(stateRef.current.value));
141
149
  }
142
150
  const [blurError, changeError] = await Promise.all(asyncValidations);
@@ -144,17 +152,16 @@ function Field(props) {
144
152
  }
145
153
  isValidatingOnBlurRef.current = false;
146
154
  if (errorMessage && validationTimeoutRef.current) {
147
- clearTimeout(validationTimeoutRef.current);
155
+ clearValidationTimeout();
148
156
  }
149
157
  if (currentValidation !== validationIdRef.current) {
150
- onChange({
151
- ...stateRef.current,
158
+ updateState({
152
159
  isTouched: true,
160
+ isValidating: isValidatingOnChangeRef.current,
153
161
  });
154
162
  return;
155
163
  }
156
- onChange({
157
- ...stateRef.current,
164
+ updateState({
158
165
  errorMessage,
159
166
  isTouched: true,
160
167
  isValid: !errorMessage,
@@ -173,4 +180,4 @@ function Field(props) {
173
180
  }
174
181
 
175
182
  exports.Field = Field;
176
- //# sourceMappingURL=Field.js.map
183
+ //# 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":["useFormContext","useRef","useId","useEffect","validate","validateAsync"],"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,20 @@
1
+ 'use strict';
2
+
3
+ function focusFirstError(results, scrollOffset) {
4
+ const firstInvalidField = results
5
+ .filter((field) => !field.isValid && field.ref)
6
+ .map((field) => field.ref)
7
+ .sort((a, b) => a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_PRECEDING ? 1 : -1)
8
+ .at(0);
9
+ if (!firstInvalidField) {
10
+ return;
11
+ }
12
+ firstInvalidField.focus();
13
+ const rect = firstInvalidField.getBoundingClientRect();
14
+ window.scrollTo({
15
+ top: rect.top + window.scrollY - scrollOffset,
16
+ });
17
+ }
18
+
19
+ exports.focusFirstError = focusFirstError;
20
+ //# 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,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  var React = require('react');
4
+ var focusFirstError = require('./focus-first-error.js');
4
5
 
5
6
  /**
6
7
  * A form component that provides context for coordinating field validation.
@@ -33,7 +34,7 @@ function Form({ onSubmit, validationMode, debounceMs, ...props }) {
33
34
  fields.forEach((field) => field.commitPendingValidation());
34
35
  const hasErrors = results.some((result) => !result.isValid);
35
36
  if (hasErrors && shouldFocusFirstError) {
36
- focusFirstError(results, scrollOffset);
37
+ focusFirstError.focusFirstError(results, scrollOffset);
37
38
  }
38
39
  return !hasErrors;
39
40
  }, []);
@@ -52,24 +53,8 @@ const FormContext = React.createContext({
52
53
  function useFormContext() {
53
54
  return React.useContext(FormContext);
54
55
  }
55
- function focusFirstError(results, scrollOffset) {
56
- const firstInvalidField = results
57
- .filter((field) => !field.isValid && field.ref)
58
- .map((field) => field.ref)
59
- .sort((a, b) => a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_PRECEDING ? 1 : -1)
60
- .at(0);
61
- if (!firstInvalidField) {
62
- return;
63
- }
64
- firstInvalidField.focus();
65
- const rect = firstInvalidField.getBoundingClientRect();
66
- window.scrollTo({
67
- top: rect.top + window.scrollY - scrollOffset,
68
- });
69
- }
70
56
 
71
57
  exports.Form = Form;
72
58
  exports.FormContext = FormContext;
73
- exports.focusFirstError = focusFirstError;
74
59
  exports.useFormContext = useFormContext;
75
- //# sourceMappingURL=Form.js.map
60
+ //# 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":["useRef","useCallback","focusFirstError"],"mappings":";;;;;AA0BA;;;;;;;AAOG;AACG,SAAU,IAAI,CAAC,EACnB,QAAQ,EACR,cAAc,EACd,UAAU,EACV,GAAG,KAAK,EACE,EAAA;IACV,MAAM,SAAS,GAAGA,YAAM,CAAW,IAAI,GAAG,EAAE,CAAC;AAE7C,IAAA,MAAM,aAAa,GAAGC,iBAAW,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,GAAGA,iBAAW,CAAC,CAAC,EAAU,KAAI;AACjD,QAAA,SAAS,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;IAC9B,CAAC,EAAE,EAAE,CAAC;IAEN,MAAM,YAAY,GAAGA,iBAAW,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,YAAAC,+BAAe,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,95 @@
1
+ 'use strict';
2
+
3
+ /** Creates the initial state for a field with the provided 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
+ * Validates the field with synchronous validators and returns the next {@link FieldState}.
16
+ *
17
+ * If `validationMode` blocks validation, the original state is returned unchanged.
18
+ * When validation runs, `isDirty` and `isTouched` are set to `true`, and evaluation
19
+ * stops at the first validator that returns a truthy error message.
20
+ */
21
+ function validate(state, validators, validationMode) {
22
+ if (!shouldValidate(state, validationMode)) {
23
+ return state;
24
+ }
25
+ const _validators = Array.isArray(validators) ? validators : [validators];
26
+ for (const validator of _validators) {
27
+ const errorMessage = validator?.(state.value);
28
+ if (errorMessage) {
29
+ return {
30
+ ...state,
31
+ errorMessage,
32
+ isDirty: true,
33
+ isTouched: true,
34
+ isValid: false,
35
+ isValidating: false,
36
+ };
37
+ }
38
+ }
39
+ return {
40
+ ...state,
41
+ errorMessage: undefined,
42
+ isDirty: true,
43
+ isTouched: true,
44
+ isValid: true,
45
+ isValidating: false,
46
+ };
47
+ }
48
+ /**
49
+ * Validates the field with synchronous and/or asynchronous validators and returns the next {@link FieldState}.
50
+ *
51
+ * If `validationMode` blocks validation, the original state is returned unchanged.
52
+ * When validation runs, all validators are awaited in parallel and the first truthy
53
+ * error in validator-list order is used.
54
+ */
55
+ async function validateAsync(state, validators, validationMode) {
56
+ if (!shouldValidate(state, validationMode)) {
57
+ return state;
58
+ }
59
+ const _validators = Array.isArray(validators) ? validators : [validators];
60
+ const errorMessages = await Promise.all(_validators.map((validator) => validator?.(state.value)));
61
+ const errorMessage = errorMessages.find(Boolean);
62
+ if (errorMessage) {
63
+ return {
64
+ ...state,
65
+ errorMessage,
66
+ isDirty: true,
67
+ isTouched: true,
68
+ isValid: false,
69
+ isValidating: false,
70
+ };
71
+ }
72
+ return {
73
+ ...state,
74
+ errorMessage: undefined,
75
+ isDirty: true,
76
+ isTouched: true,
77
+ isValid: true,
78
+ isValidating: false,
79
+ };
80
+ }
81
+ function shouldValidate(state, validationMode) {
82
+ return (validationMode === undefined ||
83
+ (validationMode === "touched" && state.isTouched) ||
84
+ (validationMode === "dirty" && state.isDirty) ||
85
+ (validationMode === "touchedAndDirty" &&
86
+ state.isTouched &&
87
+ state.isDirty) ||
88
+ (validationMode === "touchedOrDirty" && (state.isTouched || state.isDirty)));
89
+ }
90
+
91
+ exports.createFieldState = createFieldState;
92
+ exports.shouldValidate = shouldValidate;
93
+ exports.validate = validate;
94
+ exports.validateAsync = validateAsync;
95
+ //# 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;
@@ -1,4 +1,4 @@
1
- export { Field } from "./lib/Field";
2
- export { createFieldState, validate, validateAsync, validateIfDirty, validateIfDirtyAsync, } from "./lib/fieldState";
3
- export { Form } from "./lib/Form";
1
+ export { Field } from "./lib/field";
2
+ export { Form } from "./lib/form";
3
+ export { createFieldState, validate, validateAsync } from "./lib/state-utils";
4
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
- export { Field } from './lib/Field.js';
2
- export { createFieldState, validate, validateAsync, validateIfDirty, validateIfDirtyAsync } from './lib/fieldState.js';
3
- export { Form } from './lib/Form.js';
1
+ export { Field } from './lib/field.js';
2
+ export { Form } from './lib/form.js';
3
+ export { createFieldState, validate, validateAsync } from './lib/state-utils.js';
4
4
  //# sourceMappingURL=index.js.map