@fransek/form 0.2.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.
package/README.md ADDED
@@ -0,0 +1,3 @@
1
+ # @fransek/form
2
+
3
+ Simple form management without sacrificing control.
@@ -0,0 +1,4 @@
1
+ export * from "./lib/Field";
2
+ export * from "./lib/fieldState";
3
+ export * from "./lib/Form";
4
+ export * from "./lib/types";
@@ -0,0 +1,19 @@
1
+ 'use strict';
2
+
3
+ var Field = require('./lib/Field.js');
4
+ var fieldState = require('./lib/fieldState.js');
5
+ var Form = require('./lib/Form.js');
6
+
7
+
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;
16
+ exports.FormContext = Form.FormContext;
17
+ exports.focusFirstError = Form.focusFirstError;
18
+ exports.useFormContext = Form.useFormContext;
19
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;"}
@@ -0,0 +1,2 @@
1
+ import { FieldProps } from "./types";
2
+ export declare function Field<T>(props: FieldProps<T>): import("react").ReactNode;
@@ -0,0 +1,156 @@
1
+ 'use strict';
2
+
3
+ var React = require('react');
4
+ var fieldState = require('./fieldState.js');
5
+ var Form = require('./Form.js');
6
+
7
+ function Field(props) {
8
+ const { registerField, unregisterField, validationMode: formValidationMode, debounceMs: formDebounceMs, } = Form.useFormContext();
9
+ const { children, state, onChange, onInput, onBlur, validation, debounceMs = formDebounceMs || 500, validationMode = formValidationMode || "touchedAndDirty", } = props;
10
+ const stateRef = React.useRef(state);
11
+ const validationTimeoutRef = React.useRef(null);
12
+ const validationIdRef = React.useRef(0);
13
+ const isValidatingOnBlurRef = React.useRef(false);
14
+ const isValidatingOnChangeRef = React.useRef(false);
15
+ const pendingValidationRef = React.useRef(null);
16
+ const fieldRef = React.useRef(null);
17
+ const id = React.useId();
18
+ stateRef.current = state;
19
+ React.useEffect(() => {
20
+ async function performValidation() {
21
+ if (!stateRef.current.isValidating &&
22
+ stateRef.current.isDirty &&
23
+ stateRef.current.isTouched) {
24
+ return stateRef.current.isValid;
25
+ }
26
+ validationIdRef.current++;
27
+ pendingValidationRef.current = fieldState.validate(stateRef.current, validation?.onChange, validation?.onBlur, validation?.onSubmit);
28
+ if (pendingValidationRef.current.isValid) {
29
+ pendingValidationRef.current = await fieldState.validateAsync(stateRef.current, validation?.onChangeAsync, validation?.onBlurAsync, validation?.onSubmitAsync);
30
+ }
31
+ return pendingValidationRef.current.isValid;
32
+ }
33
+ function commitPendingValidation() {
34
+ if (pendingValidationRef.current) {
35
+ onChange(pendingValidationRef.current);
36
+ pendingValidationRef.current = null;
37
+ }
38
+ }
39
+ registerField(id, fieldRef.current, performValidation, commitPendingValidation);
40
+ return () => {
41
+ unregisterField(id);
42
+ };
43
+ }, [id, registerField, validation, onChange, unregisterField]);
44
+ React.useEffect(() => {
45
+ return () => {
46
+ if (validationTimeoutRef.current) {
47
+ clearTimeout(validationTimeoutRef.current);
48
+ }
49
+ };
50
+ }, []);
51
+ function handleChange(value) {
52
+ onInput?.(value);
53
+ if (validationTimeoutRef.current) {
54
+ clearTimeout(validationTimeoutRef.current);
55
+ }
56
+ const currentValidation = ++validationIdRef.current;
57
+ const shouldValidate = validationMode === "dirty" ||
58
+ validationMode === "touchedOrDirty" ||
59
+ stateRef.current.isTouched;
60
+ if (shouldValidate && (validation?.onChange || validation?.onChangeAsync)) {
61
+ const errorMessage = validation?.onChange?.(value);
62
+ const willValidateAsync = Boolean(validation?.onChangeAsync && !errorMessage);
63
+ onChange({
64
+ ...stateRef.current,
65
+ value,
66
+ errorMessage,
67
+ isDirty: true,
68
+ isValid: !errorMessage,
69
+ isValidating: willValidateAsync,
70
+ });
71
+ if (willValidateAsync) {
72
+ isValidatingOnChangeRef.current = true;
73
+ validationTimeoutRef.current = setTimeout(async () => {
74
+ const asyncErrorMessage = await validation?.onChangeAsync?.(value);
75
+ isValidatingOnChangeRef.current = false;
76
+ if (currentValidation === validationIdRef.current) {
77
+ onChange({
78
+ ...stateRef.current,
79
+ errorMessage: asyncErrorMessage,
80
+ isValid: !asyncErrorMessage,
81
+ isValidating: isValidatingOnBlurRef.current,
82
+ });
83
+ }
84
+ }, debounceMs);
85
+ }
86
+ }
87
+ else {
88
+ onChange({
89
+ ...stateRef.current,
90
+ value,
91
+ isDirty: true,
92
+ isValidating: false,
93
+ });
94
+ }
95
+ }
96
+ async function handleBlur() {
97
+ onBlur?.();
98
+ const currentValidation = validationIdRef.current;
99
+ let errorMessage = stateRef.current.errorMessage ||
100
+ validation?.onBlur?.(stateRef.current.value);
101
+ const shouldValidateOnChange = validationMode === "touched" ||
102
+ validationMode === "touchedOrDirty" ||
103
+ (validationMode === "touchedAndDirty" && stateRef.current.isDirty);
104
+ if (!errorMessage && shouldValidateOnChange && validation?.onChange) {
105
+ errorMessage = validation.onChange(stateRef.current.value);
106
+ }
107
+ if (!errorMessage &&
108
+ (validation?.onBlurAsync || validation?.onChangeAsync)) {
109
+ isValidatingOnBlurRef.current = true;
110
+ onChange({
111
+ ...stateRef.current,
112
+ isValidating: true,
113
+ isTouched: true,
114
+ });
115
+ const asyncValidations = [];
116
+ if (validation?.onBlurAsync) {
117
+ asyncValidations.push(validation.onBlurAsync(stateRef.current.value));
118
+ }
119
+ if (shouldValidateOnChange && validation?.onChangeAsync) {
120
+ asyncValidations.push(validation.onChangeAsync(stateRef.current.value));
121
+ }
122
+ const [blurError, changeError] = await Promise.all(asyncValidations);
123
+ errorMessage = blurError || changeError;
124
+ }
125
+ isValidatingOnBlurRef.current = false;
126
+ if (errorMessage && validationTimeoutRef.current) {
127
+ clearTimeout(validationTimeoutRef.current);
128
+ }
129
+ if (currentValidation !== validationIdRef.current) {
130
+ onChange({
131
+ ...stateRef.current,
132
+ isTouched: true,
133
+ });
134
+ return;
135
+ }
136
+ onChange({
137
+ ...stateRef.current,
138
+ errorMessage,
139
+ isTouched: true,
140
+ isValid: !errorMessage,
141
+ isValidating: isValidatingOnChangeRef.current,
142
+ });
143
+ }
144
+ const ref = (el) => {
145
+ fieldRef.current = el;
146
+ };
147
+ return children({
148
+ ...stateRef.current,
149
+ handleChange,
150
+ handleBlur,
151
+ ref,
152
+ });
153
+ }
154
+
155
+ exports.Field = Field;
156
+ //# 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 { validate, validateAsync } from \"./fieldState\";\nimport { useFormContext } from \"./Form\";\nimport { FieldProps, FieldState } from \"./types\";\n\nexport function Field<T>(props: FieldProps<T>) {\n const {\n registerField,\n unregisterField,\n validationMode: formValidationMode,\n debounceMs: formDebounceMs,\n } = useFormContext();\n\n const {\n children,\n state,\n onChange,\n onInput,\n onBlur,\n validation,\n debounceMs = formDebounceMs || 500,\n validationMode = formValidationMode || \"touchedAndDirty\",\n } = props;\n\n const stateRef = useRef(state);\n const validationTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(\n null,\n );\n const validationIdRef = useRef(0);\n const isValidatingOnBlurRef = useRef(false);\n const isValidatingOnChangeRef = useRef(false);\n const pendingValidationRef = useRef<FieldState<T> | null>(null);\n const fieldRef = useRef<HTMLElement | null>(null);\n const id = useId();\n\n stateRef.current = state;\n\n useEffect(() => {\n async function performValidation() {\n if (\n !stateRef.current.isValidating &&\n stateRef.current.isDirty &&\n stateRef.current.isTouched\n ) {\n return stateRef.current.isValid;\n }\n validationIdRef.current++;\n pendingValidationRef.current = validate(\n stateRef.current,\n validation?.onChange,\n validation?.onBlur,\n validation?.onSubmit,\n );\n if (pendingValidationRef.current.isValid) {\n pendingValidationRef.current = await validateAsync(\n stateRef.current,\n validation?.onChangeAsync,\n validation?.onBlurAsync,\n validation?.onSubmitAsync,\n );\n }\n return pendingValidationRef.current.isValid;\n }\n\n function commitPendingValidation() {\n if (pendingValidationRef.current) {\n onChange(pendingValidationRef.current);\n pendingValidationRef.current = null;\n }\n }\n\n registerField(\n id,\n fieldRef.current,\n performValidation,\n commitPendingValidation,\n );\n\n return () => {\n unregisterField(id);\n };\n }, [id, registerField, validation, onChange, unregisterField]);\n\n useEffect(() => {\n return () => {\n if (validationTimeoutRef.current) {\n clearTimeout(validationTimeoutRef.current);\n }\n };\n }, []);\n\n function handleChange(value: T) {\n onInput?.(value);\n\n if (validationTimeoutRef.current) {\n clearTimeout(validationTimeoutRef.current);\n }\n\n const currentValidation = ++validationIdRef.current;\n\n const shouldValidate =\n validationMode === \"dirty\" ||\n validationMode === \"touchedOrDirty\" ||\n stateRef.current.isTouched;\n\n if (shouldValidate && (validation?.onChange || validation?.onChangeAsync)) {\n const errorMessage = validation?.onChange?.(value);\n const willValidateAsync = Boolean(\n validation?.onChangeAsync && !errorMessage,\n );\n\n onChange({\n ...stateRef.current,\n value,\n errorMessage,\n isDirty: true,\n isValid: !errorMessage,\n isValidating: willValidateAsync,\n });\n\n if (willValidateAsync) {\n isValidatingOnChangeRef.current = true;\n validationTimeoutRef.current = setTimeout(async () => {\n const asyncErrorMessage = await validation?.onChangeAsync?.(value);\n\n isValidatingOnChangeRef.current = false;\n if (currentValidation === validationIdRef.current) {\n onChange({\n ...stateRef.current,\n errorMessage: asyncErrorMessage,\n isValid: !asyncErrorMessage,\n isValidating: isValidatingOnBlurRef.current,\n });\n }\n }, debounceMs);\n }\n } else {\n onChange({\n ...stateRef.current,\n value,\n isDirty: true,\n isValidating: false,\n });\n }\n }\n\n async function handleBlur() {\n onBlur?.();\n\n const currentValidation = validationIdRef.current;\n\n let errorMessage =\n stateRef.current.errorMessage ||\n validation?.onBlur?.(stateRef.current.value);\n\n const shouldValidateOnChange =\n validationMode === \"touched\" ||\n validationMode === \"touchedOrDirty\" ||\n (validationMode === \"touchedAndDirty\" && stateRef.current.isDirty);\n\n if (!errorMessage && shouldValidateOnChange && validation?.onChange) {\n errorMessage = validation.onChange(stateRef.current.value);\n }\n\n if (\n !errorMessage &&\n (validation?.onBlurAsync || validation?.onChangeAsync)\n ) {\n isValidatingOnBlurRef.current = true;\n onChange({\n ...stateRef.current,\n isValidating: true,\n isTouched: true,\n });\n\n const asyncValidations: Promise<React.ReactNode>[] = [];\n\n if (validation?.onBlurAsync) {\n asyncValidations.push(validation.onBlurAsync(stateRef.current.value));\n }\n\n if (shouldValidateOnChange && validation?.onChangeAsync) {\n asyncValidations.push(validation.onChangeAsync(stateRef.current.value));\n }\n\n const [blurError, changeError] = await Promise.all(asyncValidations);\n errorMessage = blurError || changeError;\n }\n\n isValidatingOnBlurRef.current = false;\n\n if (errorMessage && validationTimeoutRef.current) {\n clearTimeout(validationTimeoutRef.current);\n }\n\n if (currentValidation !== validationIdRef.current) {\n onChange({\n ...stateRef.current,\n isTouched: true,\n });\n return;\n }\n\n onChange({\n ...stateRef.current,\n errorMessage,\n isTouched: true,\n isValid: !errorMessage,\n isValidating: isValidatingOnChangeRef.current,\n });\n }\n\n const ref = (el: HTMLElement | null) => {\n fieldRef.current = el;\n };\n\n return children({\n ...stateRef.current,\n handleChange,\n handleBlur,\n ref,\n });\n}\n"],"names":["useFormContext","useRef","useId","useEffect","validate","validateAsync"],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,15 @@
1
+ import React from "react";
2
+ import { FormContextValue, ValidationMode } from "./types";
3
+ interface FormProps extends Omit<React.ComponentProps<"form">, "onSubmit"> {
4
+ validationMode?: ValidationMode;
5
+ debounceMs?: number;
6
+ onSubmit?: (e: React.SubmitEvent<HTMLFormElement>, validateAllFields: () => Promise<boolean>) => void;
7
+ }
8
+ export declare function Form({ onSubmit, validationMode, debounceMs, ...props }: FormProps): React.JSX.Element;
9
+ export declare const FormContext: React.Context<FormContextValue>;
10
+ export declare function useFormContext(): FormContextValue;
11
+ export declare function focusFirstError(results: {
12
+ isValid: boolean;
13
+ ref: HTMLElement | null;
14
+ }[]): void;
15
+ export {};
@@ -0,0 +1,81 @@
1
+ 'use strict';
2
+
3
+ var React = require('react');
4
+
5
+ function Form({ onSubmit, validationMode, debounceMs, ...props }) {
6
+ const fieldsRef = React.useRef(new Map());
7
+ const registerField = React.useCallback((id, ref, validate, commitPendingValidation) => {
8
+ fieldsRef.current.set(id, {
9
+ ref,
10
+ validate,
11
+ commitPendingValidation,
12
+ });
13
+ }, []);
14
+ const unregisterField = React.useCallback((id) => {
15
+ fieldsRef.current.delete(id);
16
+ }, []);
17
+ const validateAllFields = React.useCallback(async () => {
18
+ const fields = Array.from(fieldsRef.current.values());
19
+ const validationPromises = fields.map(async (field) => ({
20
+ isValid: await field.validate(),
21
+ ref: field.ref,
22
+ }));
23
+ const results = await Promise.all(validationPromises);
24
+ fields.forEach((field) => field.commitPendingValidation());
25
+ const hasErrors = results.some((result) => !result.isValid);
26
+ if (hasErrors) {
27
+ focusFirstError(results);
28
+ }
29
+ return !hasErrors;
30
+ }, []);
31
+ return (React.createElement(FormContext.Provider, { value: {
32
+ registerField,
33
+ unregisterField,
34
+ validationMode,
35
+ debounceMs,
36
+ } },
37
+ React.createElement("form", { onSubmit: (e) => onSubmit?.(e, validateAllFields), ...props })));
38
+ }
39
+ const FormContext = React.createContext({
40
+ registerField: () => { },
41
+ unregisterField: () => { },
42
+ });
43
+ function useFormContext() {
44
+ return React.useContext(FormContext);
45
+ }
46
+ function focusFirstError(results) {
47
+ const firstInvalidField = results
48
+ .filter((field) => !field.isValid && field.ref)
49
+ .map((field) => field.ref)
50
+ .sort((a, b) => a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_PRECEDING ? 1 : -1)
51
+ .at(0);
52
+ if (!firstInvalidField) {
53
+ return;
54
+ }
55
+ let firstInvalid = firstInvalidField;
56
+ if (firstInvalid.role === "radiogroup") {
57
+ const radio = firstInvalid.querySelector('[role="radio"]');
58
+ if (radio) {
59
+ firstInvalid = radio;
60
+ }
61
+ }
62
+ if (firstInvalid.role === "group") {
63
+ const checkbox = firstInvalid.querySelector('[role="checkbox"]');
64
+ if (checkbox) {
65
+ firstInvalid = checkbox;
66
+ }
67
+ }
68
+ firstInvalid.focus();
69
+ const rect = firstInvalid.getBoundingClientRect();
70
+ if (rect) {
71
+ window.scrollTo({
72
+ top: rect.top + window.scrollY - 100,
73
+ });
74
+ }
75
+ }
76
+
77
+ exports.Form = Form;
78
+ exports.FormContext = FormContext;
79
+ exports.focusFirstError = focusFirstError;
80
+ exports.useFormContext = useFormContext;
81
+ //# 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 { FieldMap, FormContextValue, ValidationMode } from \"./types\";\n\ninterface FormProps extends Omit<React.ComponentProps<\"form\">, \"onSubmit\"> {\n validationMode?: ValidationMode;\n debounceMs?: number;\n onSubmit?: (\n e: React.SubmitEvent<HTMLFormElement>,\n validateAllFields: () => Promise<boolean>,\n ) => void;\n}\n\nexport function Form({\n onSubmit,\n validationMode,\n debounceMs,\n ...props\n}: FormProps) {\n const fieldsRef = useRef<FieldMap>(new Map());\n\n const registerField = useCallback(\n (\n id: string,\n ref: HTMLElement | null,\n validate: () => Promise<boolean>,\n commitPendingValidation: () => void,\n ) => {\n fieldsRef.current.set(id, {\n ref,\n validate,\n commitPendingValidation,\n });\n },\n [],\n );\n\n const unregisterField = useCallback((id: string) => {\n fieldsRef.current.delete(id);\n }, []);\n\n const validateAllFields = useCallback(async () => {\n const fields = Array.from(fieldsRef.current.values());\n const validationPromises = fields.map(async (field) => ({\n isValid: await field.validate(),\n ref: field.ref,\n }));\n const results = await Promise.all(validationPromises);\n fields.forEach((field) => field.commitPendingValidation());\n const hasErrors = results.some((result) => !result.isValid);\n if (hasErrors) {\n focusFirstError(results);\n }\n return !hasErrors;\n }, []);\n\n return (\n <FormContext.Provider\n value={{\n registerField,\n unregisterField,\n validationMode,\n debounceMs,\n }}\n >\n <form onSubmit={(e) => onSubmit?.(e, validateAllFields)} {...props} />\n </FormContext.Provider>\n );\n}\n\nexport const FormContext = React.createContext<FormContextValue>({\n registerField: () => {},\n unregisterField: () => {},\n});\n\nexport function useFormContext() {\n return React.useContext(FormContext);\n}\n\nexport function focusFirstError(\n results: {\n isValid: boolean;\n ref: HTMLElement | null;\n }[],\n) {\n const firstInvalidField = results\n .filter((field) => !field.isValid && field.ref)\n .map((field) => field.ref!)\n .sort((a, b) =>\n a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_PRECEDING ? 1 : -1,\n )\n .at(0);\n\n if (!firstInvalidField) {\n return;\n }\n\n let firstInvalid = firstInvalidField;\n\n if (firstInvalid.role === \"radiogroup\") {\n const radio = firstInvalid.querySelector<HTMLElement>('[role=\"radio\"]');\n if (radio) {\n firstInvalid = radio;\n }\n }\n\n if (firstInvalid.role === \"group\") {\n const checkbox =\n firstInvalid.querySelector<HTMLElement>('[role=\"checkbox\"]');\n if (checkbox) {\n firstInvalid = checkbox;\n }\n }\n\n firstInvalid.focus();\n const rect = firstInvalid.getBoundingClientRect();\n if (rect) {\n window.scrollTo({\n top: rect.top + window.scrollY - 100,\n });\n }\n}\n"],"names":["useRef","useCallback"],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,6 @@
1
+ import { FieldState, SyncValidator, Validator } from "./types";
2
+ export declare function createFieldState<T>(initialValue: T): FieldState<T>;
3
+ export declare function validate<T>(state: FieldState<T>, ...validators: Array<SyncValidator<T> | undefined>): FieldState<T>;
4
+ export declare function validateAsync<T>(state: FieldState<T>, ...validators: Array<Validator<T> | undefined>): Promise<FieldState<T>>;
5
+ export declare function validateIfDirty<T>(state: FieldState<T>, ...validators: Array<SyncValidator<T> | undefined>): FieldState<T>;
6
+ export declare function validateIfDirtyAsync<T>(state: FieldState<T>, ...validators: Array<Validator<T> | undefined>): Promise<FieldState<T>>;
@@ -0,0 +1,76 @@
1
+ 'use strict';
2
+
3
+ function createFieldState(initialValue) {
4
+ return {
5
+ value: initialValue,
6
+ errorMessage: undefined,
7
+ isTouched: false,
8
+ isDirty: false,
9
+ isValid: true,
10
+ isValidating: false,
11
+ };
12
+ }
13
+ function validate(state, ...validators) {
14
+ for (const validator of validators) {
15
+ const errorMessage = validator?.(state.value);
16
+ if (errorMessage) {
17
+ return {
18
+ ...state,
19
+ errorMessage,
20
+ isDirty: true,
21
+ isTouched: true,
22
+ isValid: false,
23
+ isValidating: false,
24
+ };
25
+ }
26
+ }
27
+ return {
28
+ ...state,
29
+ errorMessage: undefined,
30
+ isDirty: true,
31
+ isTouched: true,
32
+ isValid: true,
33
+ isValidating: false,
34
+ };
35
+ }
36
+ async function validateAsync(state, ...validators) {
37
+ const errorMessages = await Promise.all(validators.map((validator) => validator?.(state.value)));
38
+ const errorMessage = errorMessages.find(Boolean);
39
+ if (errorMessage) {
40
+ return {
41
+ ...state,
42
+ errorMessage,
43
+ isDirty: true,
44
+ isTouched: true,
45
+ isValid: false,
46
+ isValidating: false,
47
+ };
48
+ }
49
+ return {
50
+ ...state,
51
+ errorMessage: undefined,
52
+ isDirty: true,
53
+ isTouched: true,
54
+ isValid: true,
55
+ isValidating: false,
56
+ };
57
+ }
58
+ function validateIfDirty(state, ...validators) {
59
+ if (!state.isDirty) {
60
+ return state;
61
+ }
62
+ return validate(state, ...validators);
63
+ }
64
+ async function validateIfDirtyAsync(state, ...validators) {
65
+ if (!state.isDirty) {
66
+ return state;
67
+ }
68
+ return validateAsync(state, ...validators);
69
+ }
70
+
71
+ exports.createFieldState = createFieldState;
72
+ exports.validate = validate;
73
+ exports.validateAsync = validateAsync;
74
+ exports.validateIfDirty = validateIfDirty;
75
+ exports.validateIfDirtyAsync = validateIfDirtyAsync;
76
+ //# sourceMappingURL=fieldState.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fieldState.js","sources":["../../../src/lib/fieldState.ts"],"sourcesContent":["import { FieldState, SyncValidator, Validator } from \"./types\";\n\nexport function createFieldState<T>(initialValue: T): FieldState<T> {\n return {\n value: initialValue,\n errorMessage: undefined,\n isTouched: false,\n isDirty: false,\n isValid: true,\n isValidating: false,\n };\n}\n\nexport function validate<T>(\n state: FieldState<T>,\n ...validators: Array<SyncValidator<T> | undefined>\n): FieldState<T> {\n for (const validator of validators) {\n const errorMessage = validator?.(state.value);\n if (errorMessage) {\n return {\n ...state,\n errorMessage,\n isDirty: true,\n isTouched: true,\n isValid: false,\n isValidating: false,\n };\n }\n }\n\n return {\n ...state,\n errorMessage: undefined,\n isDirty: true,\n isTouched: true,\n isValid: true,\n isValidating: false,\n };\n}\n\nexport async function validateAsync<T>(\n state: FieldState<T>,\n ...validators: Array<Validator<T> | undefined>\n): Promise<FieldState<T>> {\n const errorMessages = await Promise.all(\n validators.map((validator) => validator?.(state.value)),\n );\n const errorMessage = errorMessages.find(Boolean);\n\n if (errorMessage) {\n return {\n ...state,\n errorMessage,\n isDirty: true,\n isTouched: true,\n isValid: false,\n isValidating: false,\n };\n }\n\n return {\n ...state,\n errorMessage: undefined,\n isDirty: true,\n isTouched: true,\n isValid: true,\n isValidating: false,\n };\n}\n\nexport function validateIfDirty<T>(\n state: FieldState<T>,\n ...validators: Array<SyncValidator<T> | undefined>\n): FieldState<T> {\n if (!state.isDirty) {\n return state;\n }\n\n return validate(state, ...validators);\n}\n\nexport async function validateIfDirtyAsync<T>(\n state: FieldState<T>,\n ...validators: Array<Validator<T> | undefined>\n): Promise<FieldState<T>> {\n if (!state.isDirty) {\n return state;\n }\n\n return validateAsync(state, ...validators);\n}\n"],"names":[],"mappings":";;AAEM,SAAU,gBAAgB,CAAI,YAAe,EAAA;IACjD,OAAO;AACL,QAAA,KAAK,EAAE,YAAY;AACnB,QAAA,YAAY,EAAE,SAAS;AACvB,QAAA,SAAS,EAAE,KAAK;AAChB,QAAA,OAAO,EAAE,KAAK;AACd,QAAA,OAAO,EAAE,IAAI;AACb,QAAA,YAAY,EAAE,KAAK;KACpB;AACH;SAEgB,QAAQ,CACtB,KAAoB,EACpB,GAAG,UAA+C,EAAA;AAElD,IAAA,KAAK,MAAM,SAAS,IAAI,UAAU,EAAE;QAClC,MAAM,YAAY,GAAG,SAAS,GAAG,KAAK,CAAC,KAAK,CAAC;QAC7C,IAAI,YAAY,EAAE;YAChB,OAAO;AACL,gBAAA,GAAG,KAAK;gBACR,YAAY;AACZ,gBAAA,OAAO,EAAE,IAAI;AACb,gBAAA,SAAS,EAAE,IAAI;AACf,gBAAA,OAAO,EAAE,KAAK;AACd,gBAAA,YAAY,EAAE,KAAK;aACpB;QACH;IACF;IAEA,OAAO;AACL,QAAA,GAAG,KAAK;AACR,QAAA,YAAY,EAAE,SAAS;AACvB,QAAA,OAAO,EAAE,IAAI;AACb,QAAA,SAAS,EAAE,IAAI;AACf,QAAA,OAAO,EAAE,IAAI;AACb,QAAA,YAAY,EAAE,KAAK;KACpB;AACH;AAEO,eAAe,aAAa,CACjC,KAAoB,EACpB,GAAG,UAA2C,EAAA;IAE9C,MAAM,aAAa,GAAG,MAAM,OAAO,CAAC,GAAG,CACrC,UAAU,CAAC,GAAG,CAAC,CAAC,SAAS,KAAK,SAAS,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,CACxD;IACD,MAAM,YAAY,GAAG,aAAa,CAAC,IAAI,CAAC,OAAO,CAAC;IAEhD,IAAI,YAAY,EAAE;QAChB,OAAO;AACL,YAAA,GAAG,KAAK;YACR,YAAY;AACZ,YAAA,OAAO,EAAE,IAAI;AACb,YAAA,SAAS,EAAE,IAAI;AACf,YAAA,OAAO,EAAE,KAAK;AACd,YAAA,YAAY,EAAE,KAAK;SACpB;IACH;IAEA,OAAO;AACL,QAAA,GAAG,KAAK;AACR,QAAA,YAAY,EAAE,SAAS;AACvB,QAAA,OAAO,EAAE,IAAI;AACb,QAAA,SAAS,EAAE,IAAI;AACf,QAAA,OAAO,EAAE,IAAI;AACb,QAAA,YAAY,EAAE,KAAK;KACpB;AACH;SAEgB,eAAe,CAC7B,KAAoB,EACpB,GAAG,UAA+C,EAAA;AAElD,IAAA,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE;AAClB,QAAA,OAAO,KAAK;IACd;AAEA,IAAA,OAAO,QAAQ,CAAC,KAAK,EAAE,GAAG,UAAU,CAAC;AACvC;AAEO,eAAe,oBAAoB,CACxC,KAAoB,EACpB,GAAG,UAA2C,EAAA;AAE9C,IAAA,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE;AAClB,QAAA,OAAO,KAAK;IACd;AAEA,IAAA,OAAO,aAAa,CAAC,KAAK,EAAE,GAAG,UAAU,CAAC;AAC5C;;;;;;;;"}
@@ -0,0 +1,24 @@
1
+ import userEvent from "@testing-library/user-event";
2
+ import React from "react";
3
+ import { ValidationMode } from "../types";
4
+ 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>;
9
+ debounceMs?: number;
10
+ validationMode?: ValidationMode;
11
+ }
12
+ export declare const setupTest: (props?: InputProps) => {
13
+ user: import("@testing-library/user-event").UserEvent;
14
+ input: HTMLElement;
15
+ };
16
+ export declare const blurInput: (input: HTMLElement) => Promise<void>;
17
+ export declare const makeFieldDirty: (user: ReturnType<typeof userEvent.setup>, input: HTMLElement) => Promise<void>;
18
+ export declare const expectAttribute: (input: HTMLElement, attr: string, value: string) => void;
19
+ export declare const expectErrorMessage: (input: HTMLElement, message: string | null) => void;
20
+ export declare const minLengthValidator: (min: number) => (value: string) => string | undefined;
21
+ export declare const asyncMinLengthValidator: (min: number, delay?: number) => (value: string) => Promise<string | undefined>;
22
+ export declare const specificValueValidator: (invalid: string, message: string) => (value: string) => string | undefined;
23
+ export declare const asyncSpecificValueValidator: (invalid: string, message: string, delay?: number) => (value: string) => Promise<string | undefined>;
24
+ export {};
@@ -0,0 +1,52 @@
1
+ export interface FieldState<T> {
2
+ /** The current value of the field. */
3
+ value: T;
4
+ /** The error message for the field. */
5
+ errorMessage: React.ReactNode;
6
+ /** Whether the field has been touched by the user. */
7
+ isTouched: boolean;
8
+ /** Whether the value of the field has been changed by the user. */
9
+ isDirty: boolean;
10
+ /** Whether the field is valid */
11
+ isValid: boolean;
12
+ /** Whether the field is currently being validated */
13
+ isValidating: boolean;
14
+ }
15
+ export interface FieldRenderProps<T> extends FieldState<T> {
16
+ handleChange: (value: T) => void;
17
+ handleBlur: () => void;
18
+ ref: (el: HTMLElement | null) => void;
19
+ }
20
+ export interface Validation<T> {
21
+ onChange?: SyncValidator<T>;
22
+ onChangeAsync?: AsyncValidator<T>;
23
+ onBlur?: SyncValidator<T>;
24
+ onBlurAsync?: AsyncValidator<T>;
25
+ onSubmit?: SyncValidator<T>;
26
+ onSubmitAsync?: AsyncValidator<T>;
27
+ }
28
+ export type ValidationMode = "touched" | "dirty" | "touchedAndDirty" | "touchedOrDirty";
29
+ export interface FieldProps<T> {
30
+ state: FieldState<T>;
31
+ children: (props: FieldRenderProps<T>) => React.ReactNode;
32
+ onChange: (newState: FieldState<T>) => void;
33
+ onInput?: (value: T) => void;
34
+ onBlur?: () => void;
35
+ validation?: Validation<T>;
36
+ validationMode?: ValidationMode;
37
+ debounceMs?: number;
38
+ }
39
+ export type SyncValidator<T> = (value: T) => React.ReactNode;
40
+ export type AsyncValidator<T> = (value: T) => Promise<React.ReactNode>;
41
+ export type Validator<T> = SyncValidator<T> | AsyncValidator<T>;
42
+ export interface FormContextValue {
43
+ validationMode?: ValidationMode;
44
+ debounceMs?: number;
45
+ registerField: (id: string, ref: HTMLElement | null, validate: () => Promise<boolean>, commitPendingValidation: () => void) => void;
46
+ unregisterField: (id: string) => void;
47
+ }
48
+ export type FieldMap = Map<string, {
49
+ ref: HTMLElement | null;
50
+ validate: () => Promise<boolean>;
51
+ commitPendingValidation: () => void;
52
+ }>;
@@ -0,0 +1,4 @@
1
+ export * from "./lib/Field";
2
+ export * from "./lib/fieldState";
3
+ export * from "./lib/Form";
4
+ export * from "./lib/types";
@@ -0,0 +1,4 @@
1
+ export { Field } from './lib/Field.js';
2
+ export { createFieldState, validate, validateAsync, validateIfDirty, validateIfDirtyAsync } from './lib/fieldState.js';
3
+ export { Form, FormContext, focusFirstError, useFormContext } from './lib/Form.js';
4
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;"}
@@ -0,0 +1,2 @@
1
+ import { FieldProps } from "./types";
2
+ export declare function Field<T>(props: FieldProps<T>): import("react").ReactNode;
@@ -0,0 +1,154 @@
1
+ import { useRef, useId, useEffect } from 'react';
2
+ import { validate, validateAsync } from './fieldState.js';
3
+ import { useFormContext } from './Form.js';
4
+
5
+ function Field(props) {
6
+ const { registerField, unregisterField, validationMode: formValidationMode, debounceMs: formDebounceMs, } = useFormContext();
7
+ const { children, state, onChange, onInput, onBlur, validation, debounceMs = formDebounceMs || 500, validationMode = formValidationMode || "touchedAndDirty", } = props;
8
+ const stateRef = useRef(state);
9
+ const validationTimeoutRef = useRef(null);
10
+ const validationIdRef = useRef(0);
11
+ const isValidatingOnBlurRef = useRef(false);
12
+ const isValidatingOnChangeRef = useRef(false);
13
+ const pendingValidationRef = useRef(null);
14
+ const fieldRef = useRef(null);
15
+ const id = useId();
16
+ stateRef.current = state;
17
+ useEffect(() => {
18
+ async function performValidation() {
19
+ if (!stateRef.current.isValidating &&
20
+ stateRef.current.isDirty &&
21
+ stateRef.current.isTouched) {
22
+ return stateRef.current.isValid;
23
+ }
24
+ validationIdRef.current++;
25
+ pendingValidationRef.current = validate(stateRef.current, validation?.onChange, validation?.onBlur, validation?.onSubmit);
26
+ if (pendingValidationRef.current.isValid) {
27
+ pendingValidationRef.current = await validateAsync(stateRef.current, validation?.onChangeAsync, validation?.onBlurAsync, validation?.onSubmitAsync);
28
+ }
29
+ return pendingValidationRef.current.isValid;
30
+ }
31
+ function commitPendingValidation() {
32
+ if (pendingValidationRef.current) {
33
+ onChange(pendingValidationRef.current);
34
+ pendingValidationRef.current = null;
35
+ }
36
+ }
37
+ registerField(id, fieldRef.current, performValidation, commitPendingValidation);
38
+ return () => {
39
+ unregisterField(id);
40
+ };
41
+ }, [id, registerField, validation, onChange, unregisterField]);
42
+ useEffect(() => {
43
+ return () => {
44
+ if (validationTimeoutRef.current) {
45
+ clearTimeout(validationTimeoutRef.current);
46
+ }
47
+ };
48
+ }, []);
49
+ function handleChange(value) {
50
+ onInput?.(value);
51
+ if (validationTimeoutRef.current) {
52
+ clearTimeout(validationTimeoutRef.current);
53
+ }
54
+ const currentValidation = ++validationIdRef.current;
55
+ const shouldValidate = validationMode === "dirty" ||
56
+ validationMode === "touchedOrDirty" ||
57
+ stateRef.current.isTouched;
58
+ if (shouldValidate && (validation?.onChange || validation?.onChangeAsync)) {
59
+ const errorMessage = validation?.onChange?.(value);
60
+ const willValidateAsync = Boolean(validation?.onChangeAsync && !errorMessage);
61
+ onChange({
62
+ ...stateRef.current,
63
+ value,
64
+ errorMessage,
65
+ isDirty: true,
66
+ isValid: !errorMessage,
67
+ isValidating: willValidateAsync,
68
+ });
69
+ if (willValidateAsync) {
70
+ isValidatingOnChangeRef.current = true;
71
+ validationTimeoutRef.current = setTimeout(async () => {
72
+ const asyncErrorMessage = await validation?.onChangeAsync?.(value);
73
+ isValidatingOnChangeRef.current = false;
74
+ if (currentValidation === validationIdRef.current) {
75
+ onChange({
76
+ ...stateRef.current,
77
+ errorMessage: asyncErrorMessage,
78
+ isValid: !asyncErrorMessage,
79
+ isValidating: isValidatingOnBlurRef.current,
80
+ });
81
+ }
82
+ }, debounceMs);
83
+ }
84
+ }
85
+ else {
86
+ onChange({
87
+ ...stateRef.current,
88
+ value,
89
+ isDirty: true,
90
+ isValidating: false,
91
+ });
92
+ }
93
+ }
94
+ async function handleBlur() {
95
+ onBlur?.();
96
+ const currentValidation = validationIdRef.current;
97
+ let errorMessage = stateRef.current.errorMessage ||
98
+ validation?.onBlur?.(stateRef.current.value);
99
+ const shouldValidateOnChange = validationMode === "touched" ||
100
+ validationMode === "touchedOrDirty" ||
101
+ (validationMode === "touchedAndDirty" && stateRef.current.isDirty);
102
+ if (!errorMessage && shouldValidateOnChange && validation?.onChange) {
103
+ errorMessage = validation.onChange(stateRef.current.value);
104
+ }
105
+ if (!errorMessage &&
106
+ (validation?.onBlurAsync || validation?.onChangeAsync)) {
107
+ isValidatingOnBlurRef.current = true;
108
+ onChange({
109
+ ...stateRef.current,
110
+ isValidating: true,
111
+ isTouched: true,
112
+ });
113
+ const asyncValidations = [];
114
+ if (validation?.onBlurAsync) {
115
+ asyncValidations.push(validation.onBlurAsync(stateRef.current.value));
116
+ }
117
+ if (shouldValidateOnChange && validation?.onChangeAsync) {
118
+ asyncValidations.push(validation.onChangeAsync(stateRef.current.value));
119
+ }
120
+ const [blurError, changeError] = await Promise.all(asyncValidations);
121
+ errorMessage = blurError || changeError;
122
+ }
123
+ isValidatingOnBlurRef.current = false;
124
+ if (errorMessage && validationTimeoutRef.current) {
125
+ clearTimeout(validationTimeoutRef.current);
126
+ }
127
+ if (currentValidation !== validationIdRef.current) {
128
+ onChange({
129
+ ...stateRef.current,
130
+ isTouched: true,
131
+ });
132
+ return;
133
+ }
134
+ onChange({
135
+ ...stateRef.current,
136
+ errorMessage,
137
+ isTouched: true,
138
+ isValid: !errorMessage,
139
+ isValidating: isValidatingOnChangeRef.current,
140
+ });
141
+ }
142
+ const ref = (el) => {
143
+ fieldRef.current = el;
144
+ };
145
+ return children({
146
+ ...stateRef.current,
147
+ handleChange,
148
+ handleBlur,
149
+ ref,
150
+ });
151
+ }
152
+
153
+ export { Field };
154
+ //# 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 { validate, validateAsync } from \"./fieldState\";\nimport { useFormContext } from \"./Form\";\nimport { FieldProps, FieldState } from \"./types\";\n\nexport function Field<T>(props: FieldProps<T>) {\n const {\n registerField,\n unregisterField,\n validationMode: formValidationMode,\n debounceMs: formDebounceMs,\n } = useFormContext();\n\n const {\n children,\n state,\n onChange,\n onInput,\n onBlur,\n validation,\n debounceMs = formDebounceMs || 500,\n validationMode = formValidationMode || \"touchedAndDirty\",\n } = props;\n\n const stateRef = useRef(state);\n const validationTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(\n null,\n );\n const validationIdRef = useRef(0);\n const isValidatingOnBlurRef = useRef(false);\n const isValidatingOnChangeRef = useRef(false);\n const pendingValidationRef = useRef<FieldState<T> | null>(null);\n const fieldRef = useRef<HTMLElement | null>(null);\n const id = useId();\n\n stateRef.current = state;\n\n useEffect(() => {\n async function performValidation() {\n if (\n !stateRef.current.isValidating &&\n stateRef.current.isDirty &&\n stateRef.current.isTouched\n ) {\n return stateRef.current.isValid;\n }\n validationIdRef.current++;\n pendingValidationRef.current = validate(\n stateRef.current,\n validation?.onChange,\n validation?.onBlur,\n validation?.onSubmit,\n );\n if (pendingValidationRef.current.isValid) {\n pendingValidationRef.current = await validateAsync(\n stateRef.current,\n validation?.onChangeAsync,\n validation?.onBlurAsync,\n validation?.onSubmitAsync,\n );\n }\n return pendingValidationRef.current.isValid;\n }\n\n function commitPendingValidation() {\n if (pendingValidationRef.current) {\n onChange(pendingValidationRef.current);\n pendingValidationRef.current = null;\n }\n }\n\n registerField(\n id,\n fieldRef.current,\n performValidation,\n commitPendingValidation,\n );\n\n return () => {\n unregisterField(id);\n };\n }, [id, registerField, validation, onChange, unregisterField]);\n\n useEffect(() => {\n return () => {\n if (validationTimeoutRef.current) {\n clearTimeout(validationTimeoutRef.current);\n }\n };\n }, []);\n\n function handleChange(value: T) {\n onInput?.(value);\n\n if (validationTimeoutRef.current) {\n clearTimeout(validationTimeoutRef.current);\n }\n\n const currentValidation = ++validationIdRef.current;\n\n const shouldValidate =\n validationMode === \"dirty\" ||\n validationMode === \"touchedOrDirty\" ||\n stateRef.current.isTouched;\n\n if (shouldValidate && (validation?.onChange || validation?.onChangeAsync)) {\n const errorMessage = validation?.onChange?.(value);\n const willValidateAsync = Boolean(\n validation?.onChangeAsync && !errorMessage,\n );\n\n onChange({\n ...stateRef.current,\n value,\n errorMessage,\n isDirty: true,\n isValid: !errorMessage,\n isValidating: willValidateAsync,\n });\n\n if (willValidateAsync) {\n isValidatingOnChangeRef.current = true;\n validationTimeoutRef.current = setTimeout(async () => {\n const asyncErrorMessage = await validation?.onChangeAsync?.(value);\n\n isValidatingOnChangeRef.current = false;\n if (currentValidation === validationIdRef.current) {\n onChange({\n ...stateRef.current,\n errorMessage: asyncErrorMessage,\n isValid: !asyncErrorMessage,\n isValidating: isValidatingOnBlurRef.current,\n });\n }\n }, debounceMs);\n }\n } else {\n onChange({\n ...stateRef.current,\n value,\n isDirty: true,\n isValidating: false,\n });\n }\n }\n\n async function handleBlur() {\n onBlur?.();\n\n const currentValidation = validationIdRef.current;\n\n let errorMessage =\n stateRef.current.errorMessage ||\n validation?.onBlur?.(stateRef.current.value);\n\n const shouldValidateOnChange =\n validationMode === \"touched\" ||\n validationMode === \"touchedOrDirty\" ||\n (validationMode === \"touchedAndDirty\" && stateRef.current.isDirty);\n\n if (!errorMessage && shouldValidateOnChange && validation?.onChange) {\n errorMessage = validation.onChange(stateRef.current.value);\n }\n\n if (\n !errorMessage &&\n (validation?.onBlurAsync || validation?.onChangeAsync)\n ) {\n isValidatingOnBlurRef.current = true;\n onChange({\n ...stateRef.current,\n isValidating: true,\n isTouched: true,\n });\n\n const asyncValidations: Promise<React.ReactNode>[] = [];\n\n if (validation?.onBlurAsync) {\n asyncValidations.push(validation.onBlurAsync(stateRef.current.value));\n }\n\n if (shouldValidateOnChange && validation?.onChangeAsync) {\n asyncValidations.push(validation.onChangeAsync(stateRef.current.value));\n }\n\n const [blurError, changeError] = await Promise.all(asyncValidations);\n errorMessage = blurError || changeError;\n }\n\n isValidatingOnBlurRef.current = false;\n\n if (errorMessage && validationTimeoutRef.current) {\n clearTimeout(validationTimeoutRef.current);\n }\n\n if (currentValidation !== validationIdRef.current) {\n onChange({\n ...stateRef.current,\n isTouched: true,\n });\n return;\n }\n\n onChange({\n ...stateRef.current,\n errorMessage,\n isTouched: true,\n isValid: !errorMessage,\n isValidating: isValidatingOnChangeRef.current,\n });\n }\n\n const ref = (el: HTMLElement | null) => {\n fieldRef.current = el;\n };\n\n return children({\n ...stateRef.current,\n handleChange,\n handleBlur,\n ref,\n });\n}\n"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,15 @@
1
+ import React from "react";
2
+ import { FormContextValue, ValidationMode } from "./types";
3
+ interface FormProps extends Omit<React.ComponentProps<"form">, "onSubmit"> {
4
+ validationMode?: ValidationMode;
5
+ debounceMs?: number;
6
+ onSubmit?: (e: React.SubmitEvent<HTMLFormElement>, validateAllFields: () => Promise<boolean>) => void;
7
+ }
8
+ export declare function Form({ onSubmit, validationMode, debounceMs, ...props }: FormProps): React.JSX.Element;
9
+ export declare const FormContext: React.Context<FormContextValue>;
10
+ export declare function useFormContext(): FormContextValue;
11
+ export declare function focusFirstError(results: {
12
+ isValid: boolean;
13
+ ref: HTMLElement | null;
14
+ }[]): void;
15
+ export {};
@@ -0,0 +1,76 @@
1
+ import React, { useRef, useCallback } from 'react';
2
+
3
+ function Form({ onSubmit, validationMode, debounceMs, ...props }) {
4
+ const fieldsRef = useRef(new Map());
5
+ const registerField = useCallback((id, ref, validate, commitPendingValidation) => {
6
+ fieldsRef.current.set(id, {
7
+ ref,
8
+ validate,
9
+ commitPendingValidation,
10
+ });
11
+ }, []);
12
+ const unregisterField = useCallback((id) => {
13
+ fieldsRef.current.delete(id);
14
+ }, []);
15
+ const validateAllFields = useCallback(async () => {
16
+ const fields = Array.from(fieldsRef.current.values());
17
+ const validationPromises = fields.map(async (field) => ({
18
+ isValid: await field.validate(),
19
+ ref: field.ref,
20
+ }));
21
+ const results = await Promise.all(validationPromises);
22
+ fields.forEach((field) => field.commitPendingValidation());
23
+ const hasErrors = results.some((result) => !result.isValid);
24
+ if (hasErrors) {
25
+ focusFirstError(results);
26
+ }
27
+ return !hasErrors;
28
+ }, []);
29
+ return (React.createElement(FormContext.Provider, { value: {
30
+ registerField,
31
+ unregisterField,
32
+ validationMode,
33
+ debounceMs,
34
+ } },
35
+ React.createElement("form", { onSubmit: (e) => onSubmit?.(e, validateAllFields), ...props })));
36
+ }
37
+ const FormContext = React.createContext({
38
+ registerField: () => { },
39
+ unregisterField: () => { },
40
+ });
41
+ function useFormContext() {
42
+ return React.useContext(FormContext);
43
+ }
44
+ function focusFirstError(results) {
45
+ const firstInvalidField = results
46
+ .filter((field) => !field.isValid && field.ref)
47
+ .map((field) => field.ref)
48
+ .sort((a, b) => a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_PRECEDING ? 1 : -1)
49
+ .at(0);
50
+ if (!firstInvalidField) {
51
+ return;
52
+ }
53
+ let firstInvalid = firstInvalidField;
54
+ if (firstInvalid.role === "radiogroup") {
55
+ const radio = firstInvalid.querySelector('[role="radio"]');
56
+ if (radio) {
57
+ firstInvalid = radio;
58
+ }
59
+ }
60
+ if (firstInvalid.role === "group") {
61
+ const checkbox = firstInvalid.querySelector('[role="checkbox"]');
62
+ if (checkbox) {
63
+ firstInvalid = checkbox;
64
+ }
65
+ }
66
+ firstInvalid.focus();
67
+ const rect = firstInvalid.getBoundingClientRect();
68
+ if (rect) {
69
+ window.scrollTo({
70
+ top: rect.top + window.scrollY - 100,
71
+ });
72
+ }
73
+ }
74
+
75
+ export { Form, FormContext, focusFirstError, useFormContext };
76
+ //# 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 { FieldMap, FormContextValue, ValidationMode } from \"./types\";\n\ninterface FormProps extends Omit<React.ComponentProps<\"form\">, \"onSubmit\"> {\n validationMode?: ValidationMode;\n debounceMs?: number;\n onSubmit?: (\n e: React.SubmitEvent<HTMLFormElement>,\n validateAllFields: () => Promise<boolean>,\n ) => void;\n}\n\nexport function Form({\n onSubmit,\n validationMode,\n debounceMs,\n ...props\n}: FormProps) {\n const fieldsRef = useRef<FieldMap>(new Map());\n\n const registerField = useCallback(\n (\n id: string,\n ref: HTMLElement | null,\n validate: () => Promise<boolean>,\n commitPendingValidation: () => void,\n ) => {\n fieldsRef.current.set(id, {\n ref,\n validate,\n commitPendingValidation,\n });\n },\n [],\n );\n\n const unregisterField = useCallback((id: string) => {\n fieldsRef.current.delete(id);\n }, []);\n\n const validateAllFields = useCallback(async () => {\n const fields = Array.from(fieldsRef.current.values());\n const validationPromises = fields.map(async (field) => ({\n isValid: await field.validate(),\n ref: field.ref,\n }));\n const results = await Promise.all(validationPromises);\n fields.forEach((field) => field.commitPendingValidation());\n const hasErrors = results.some((result) => !result.isValid);\n if (hasErrors) {\n focusFirstError(results);\n }\n return !hasErrors;\n }, []);\n\n return (\n <FormContext.Provider\n value={{\n registerField,\n unregisterField,\n validationMode,\n debounceMs,\n }}\n >\n <form onSubmit={(e) => onSubmit?.(e, validateAllFields)} {...props} />\n </FormContext.Provider>\n );\n}\n\nexport const FormContext = React.createContext<FormContextValue>({\n registerField: () => {},\n unregisterField: () => {},\n});\n\nexport function useFormContext() {\n return React.useContext(FormContext);\n}\n\nexport function focusFirstError(\n results: {\n isValid: boolean;\n ref: HTMLElement | null;\n }[],\n) {\n const firstInvalidField = results\n .filter((field) => !field.isValid && field.ref)\n .map((field) => field.ref!)\n .sort((a, b) =>\n a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_PRECEDING ? 1 : -1,\n )\n .at(0);\n\n if (!firstInvalidField) {\n return;\n }\n\n let firstInvalid = firstInvalidField;\n\n if (firstInvalid.role === \"radiogroup\") {\n const radio = firstInvalid.querySelector<HTMLElement>('[role=\"radio\"]');\n if (radio) {\n firstInvalid = radio;\n }\n }\n\n if (firstInvalid.role === \"group\") {\n const checkbox =\n firstInvalid.querySelector<HTMLElement>('[role=\"checkbox\"]');\n if (checkbox) {\n firstInvalid = checkbox;\n }\n }\n\n firstInvalid.focus();\n const rect = firstInvalid.getBoundingClientRect();\n if (rect) {\n window.scrollTo({\n top: rect.top + window.scrollY - 100,\n });\n }\n}\n"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,6 @@
1
+ import { FieldState, SyncValidator, Validator } from "./types";
2
+ export declare function createFieldState<T>(initialValue: T): FieldState<T>;
3
+ export declare function validate<T>(state: FieldState<T>, ...validators: Array<SyncValidator<T> | undefined>): FieldState<T>;
4
+ export declare function validateAsync<T>(state: FieldState<T>, ...validators: Array<Validator<T> | undefined>): Promise<FieldState<T>>;
5
+ export declare function validateIfDirty<T>(state: FieldState<T>, ...validators: Array<SyncValidator<T> | undefined>): FieldState<T>;
6
+ export declare function validateIfDirtyAsync<T>(state: FieldState<T>, ...validators: Array<Validator<T> | undefined>): Promise<FieldState<T>>;
@@ -0,0 +1,70 @@
1
+ function createFieldState(initialValue) {
2
+ return {
3
+ value: initialValue,
4
+ errorMessage: undefined,
5
+ isTouched: false,
6
+ isDirty: false,
7
+ isValid: true,
8
+ isValidating: false,
9
+ };
10
+ }
11
+ function validate(state, ...validators) {
12
+ for (const validator of validators) {
13
+ const errorMessage = validator?.(state.value);
14
+ if (errorMessage) {
15
+ return {
16
+ ...state,
17
+ errorMessage,
18
+ isDirty: true,
19
+ isTouched: true,
20
+ isValid: false,
21
+ isValidating: false,
22
+ };
23
+ }
24
+ }
25
+ return {
26
+ ...state,
27
+ errorMessage: undefined,
28
+ isDirty: true,
29
+ isTouched: true,
30
+ isValid: true,
31
+ isValidating: false,
32
+ };
33
+ }
34
+ async function validateAsync(state, ...validators) {
35
+ const errorMessages = await Promise.all(validators.map((validator) => validator?.(state.value)));
36
+ const errorMessage = errorMessages.find(Boolean);
37
+ if (errorMessage) {
38
+ return {
39
+ ...state,
40
+ errorMessage,
41
+ isDirty: true,
42
+ isTouched: true,
43
+ isValid: false,
44
+ isValidating: false,
45
+ };
46
+ }
47
+ return {
48
+ ...state,
49
+ errorMessage: undefined,
50
+ isDirty: true,
51
+ isTouched: true,
52
+ isValid: true,
53
+ isValidating: false,
54
+ };
55
+ }
56
+ function validateIfDirty(state, ...validators) {
57
+ if (!state.isDirty) {
58
+ return state;
59
+ }
60
+ return validate(state, ...validators);
61
+ }
62
+ async function validateIfDirtyAsync(state, ...validators) {
63
+ if (!state.isDirty) {
64
+ return state;
65
+ }
66
+ return validateAsync(state, ...validators);
67
+ }
68
+
69
+ export { createFieldState, validate, validateAsync, validateIfDirty, validateIfDirtyAsync };
70
+ //# sourceMappingURL=fieldState.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fieldState.js","sources":["../../../src/lib/fieldState.ts"],"sourcesContent":["import { FieldState, SyncValidator, Validator } from \"./types\";\n\nexport function createFieldState<T>(initialValue: T): FieldState<T> {\n return {\n value: initialValue,\n errorMessage: undefined,\n isTouched: false,\n isDirty: false,\n isValid: true,\n isValidating: false,\n };\n}\n\nexport function validate<T>(\n state: FieldState<T>,\n ...validators: Array<SyncValidator<T> | undefined>\n): FieldState<T> {\n for (const validator of validators) {\n const errorMessage = validator?.(state.value);\n if (errorMessage) {\n return {\n ...state,\n errorMessage,\n isDirty: true,\n isTouched: true,\n isValid: false,\n isValidating: false,\n };\n }\n }\n\n return {\n ...state,\n errorMessage: undefined,\n isDirty: true,\n isTouched: true,\n isValid: true,\n isValidating: false,\n };\n}\n\nexport async function validateAsync<T>(\n state: FieldState<T>,\n ...validators: Array<Validator<T> | undefined>\n): Promise<FieldState<T>> {\n const errorMessages = await Promise.all(\n validators.map((validator) => validator?.(state.value)),\n );\n const errorMessage = errorMessages.find(Boolean);\n\n if (errorMessage) {\n return {\n ...state,\n errorMessage,\n isDirty: true,\n isTouched: true,\n isValid: false,\n isValidating: false,\n };\n }\n\n return {\n ...state,\n errorMessage: undefined,\n isDirty: true,\n isTouched: true,\n isValid: true,\n isValidating: false,\n };\n}\n\nexport function validateIfDirty<T>(\n state: FieldState<T>,\n ...validators: Array<SyncValidator<T> | undefined>\n): FieldState<T> {\n if (!state.isDirty) {\n return state;\n }\n\n return validate(state, ...validators);\n}\n\nexport async function validateIfDirtyAsync<T>(\n state: FieldState<T>,\n ...validators: Array<Validator<T> | undefined>\n): Promise<FieldState<T>> {\n if (!state.isDirty) {\n return state;\n }\n\n return validateAsync(state, ...validators);\n}\n"],"names":[],"mappings":"AAEM,SAAU,gBAAgB,CAAI,YAAe,EAAA;IACjD,OAAO;AACL,QAAA,KAAK,EAAE,YAAY;AACnB,QAAA,YAAY,EAAE,SAAS;AACvB,QAAA,SAAS,EAAE,KAAK;AAChB,QAAA,OAAO,EAAE,KAAK;AACd,QAAA,OAAO,EAAE,IAAI;AACb,QAAA,YAAY,EAAE,KAAK;KACpB;AACH;SAEgB,QAAQ,CACtB,KAAoB,EACpB,GAAG,UAA+C,EAAA;AAElD,IAAA,KAAK,MAAM,SAAS,IAAI,UAAU,EAAE;QAClC,MAAM,YAAY,GAAG,SAAS,GAAG,KAAK,CAAC,KAAK,CAAC;QAC7C,IAAI,YAAY,EAAE;YAChB,OAAO;AACL,gBAAA,GAAG,KAAK;gBACR,YAAY;AACZ,gBAAA,OAAO,EAAE,IAAI;AACb,gBAAA,SAAS,EAAE,IAAI;AACf,gBAAA,OAAO,EAAE,KAAK;AACd,gBAAA,YAAY,EAAE,KAAK;aACpB;QACH;IACF;IAEA,OAAO;AACL,QAAA,GAAG,KAAK;AACR,QAAA,YAAY,EAAE,SAAS;AACvB,QAAA,OAAO,EAAE,IAAI;AACb,QAAA,SAAS,EAAE,IAAI;AACf,QAAA,OAAO,EAAE,IAAI;AACb,QAAA,YAAY,EAAE,KAAK;KACpB;AACH;AAEO,eAAe,aAAa,CACjC,KAAoB,EACpB,GAAG,UAA2C,EAAA;IAE9C,MAAM,aAAa,GAAG,MAAM,OAAO,CAAC,GAAG,CACrC,UAAU,CAAC,GAAG,CAAC,CAAC,SAAS,KAAK,SAAS,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,CACxD;IACD,MAAM,YAAY,GAAG,aAAa,CAAC,IAAI,CAAC,OAAO,CAAC;IAEhD,IAAI,YAAY,EAAE;QAChB,OAAO;AACL,YAAA,GAAG,KAAK;YACR,YAAY;AACZ,YAAA,OAAO,EAAE,IAAI;AACb,YAAA,SAAS,EAAE,IAAI;AACf,YAAA,OAAO,EAAE,KAAK;AACd,YAAA,YAAY,EAAE,KAAK;SACpB;IACH;IAEA,OAAO;AACL,QAAA,GAAG,KAAK;AACR,QAAA,YAAY,EAAE,SAAS;AACvB,QAAA,OAAO,EAAE,IAAI;AACb,QAAA,SAAS,EAAE,IAAI;AACf,QAAA,OAAO,EAAE,IAAI;AACb,QAAA,YAAY,EAAE,KAAK;KACpB;AACH;SAEgB,eAAe,CAC7B,KAAoB,EACpB,GAAG,UAA+C,EAAA;AAElD,IAAA,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE;AAClB,QAAA,OAAO,KAAK;IACd;AAEA,IAAA,OAAO,QAAQ,CAAC,KAAK,EAAE,GAAG,UAAU,CAAC;AACvC;AAEO,eAAe,oBAAoB,CACxC,KAAoB,EACpB,GAAG,UAA2C,EAAA;AAE9C,IAAA,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE;AAClB,QAAA,OAAO,KAAK;IACd;AAEA,IAAA,OAAO,aAAa,CAAC,KAAK,EAAE,GAAG,UAAU,CAAC;AAC5C;;;;"}
@@ -0,0 +1,24 @@
1
+ import userEvent from "@testing-library/user-event";
2
+ import React from "react";
3
+ import { ValidationMode } from "../types";
4
+ 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>;
9
+ debounceMs?: number;
10
+ validationMode?: ValidationMode;
11
+ }
12
+ export declare const setupTest: (props?: InputProps) => {
13
+ user: import("@testing-library/user-event").UserEvent;
14
+ input: HTMLElement;
15
+ };
16
+ export declare const blurInput: (input: HTMLElement) => Promise<void>;
17
+ export declare const makeFieldDirty: (user: ReturnType<typeof userEvent.setup>, input: HTMLElement) => Promise<void>;
18
+ export declare const expectAttribute: (input: HTMLElement, attr: string, value: string) => void;
19
+ export declare const expectErrorMessage: (input: HTMLElement, message: string | null) => void;
20
+ export declare const minLengthValidator: (min: number) => (value: string) => string | undefined;
21
+ export declare const asyncMinLengthValidator: (min: number, delay?: number) => (value: string) => Promise<string | undefined>;
22
+ export declare const specificValueValidator: (invalid: string, message: string) => (value: string) => string | undefined;
23
+ export declare const asyncSpecificValueValidator: (invalid: string, message: string, delay?: number) => (value: string) => Promise<string | undefined>;
24
+ export {};
@@ -0,0 +1,52 @@
1
+ export interface FieldState<T> {
2
+ /** The current value of the field. */
3
+ value: T;
4
+ /** The error message for the field. */
5
+ errorMessage: React.ReactNode;
6
+ /** Whether the field has been touched by the user. */
7
+ isTouched: boolean;
8
+ /** Whether the value of the field has been changed by the user. */
9
+ isDirty: boolean;
10
+ /** Whether the field is valid */
11
+ isValid: boolean;
12
+ /** Whether the field is currently being validated */
13
+ isValidating: boolean;
14
+ }
15
+ export interface FieldRenderProps<T> extends FieldState<T> {
16
+ handleChange: (value: T) => void;
17
+ handleBlur: () => void;
18
+ ref: (el: HTMLElement | null) => void;
19
+ }
20
+ export interface Validation<T> {
21
+ onChange?: SyncValidator<T>;
22
+ onChangeAsync?: AsyncValidator<T>;
23
+ onBlur?: SyncValidator<T>;
24
+ onBlurAsync?: AsyncValidator<T>;
25
+ onSubmit?: SyncValidator<T>;
26
+ onSubmitAsync?: AsyncValidator<T>;
27
+ }
28
+ export type ValidationMode = "touched" | "dirty" | "touchedAndDirty" | "touchedOrDirty";
29
+ export interface FieldProps<T> {
30
+ state: FieldState<T>;
31
+ children: (props: FieldRenderProps<T>) => React.ReactNode;
32
+ onChange: (newState: FieldState<T>) => void;
33
+ onInput?: (value: T) => void;
34
+ onBlur?: () => void;
35
+ validation?: Validation<T>;
36
+ validationMode?: ValidationMode;
37
+ debounceMs?: number;
38
+ }
39
+ export type SyncValidator<T> = (value: T) => React.ReactNode;
40
+ export type AsyncValidator<T> = (value: T) => Promise<React.ReactNode>;
41
+ export type Validator<T> = SyncValidator<T> | AsyncValidator<T>;
42
+ export interface FormContextValue {
43
+ validationMode?: ValidationMode;
44
+ debounceMs?: number;
45
+ registerField: (id: string, ref: HTMLElement | null, validate: () => Promise<boolean>, commitPendingValidation: () => void) => void;
46
+ unregisterField: (id: string) => void;
47
+ }
48
+ export type FieldMap = Map<string, {
49
+ ref: HTMLElement | null;
50
+ validate: () => Promise<boolean>;
51
+ commitPendingValidation: () => void;
52
+ }>;
package/package.json ADDED
@@ -0,0 +1,88 @@
1
+ {
2
+ "name": "@fransek/form",
3
+ "version": "0.2.0",
4
+ "type": "module",
5
+ "private": false,
6
+ "author": "Frans Ekman <fransedvinekman@gmail.com>",
7
+ "license": "MIT",
8
+ "description": "Simple form management without sacrificing control.",
9
+ "keywords": [],
10
+ "homepage": "https://github.com/fransek/form#readme",
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "git+https://github.com/fransek/form"
14
+ },
15
+ "bugs": {
16
+ "url": "https://github.com/fransek/form/issues"
17
+ },
18
+ "files": [
19
+ "dist"
20
+ ],
21
+ "main": "dist/cjs/index.js",
22
+ "module": "dist/esm/index.js",
23
+ "exports": {
24
+ ".": {
25
+ "import": "./dist/esm/index.js",
26
+ "require": "./dist/cjs/index.js"
27
+ }
28
+ },
29
+ "sideEffects": false,
30
+ "devDependencies": {
31
+ "@commitlint/cli": "^20.3.1",
32
+ "@commitlint/config-conventional": "^20.3.1",
33
+ "@eslint/eslintrc": "^3.3.3",
34
+ "@eslint/js": "^9.39.2",
35
+ "@rollup/plugin-typescript": "^12.3.0",
36
+ "@semantic-release/changelog": "^6.0.3",
37
+ "@semantic-release/git": "^10.0.1",
38
+ "@semantic-release/github": "^12.0.2",
39
+ "@testing-library/dom": "^10.4.1",
40
+ "@testing-library/jest-dom": "^6.9.1",
41
+ "@testing-library/react": "^16.3.2",
42
+ "@testing-library/user-event": "^14.6.1",
43
+ "@types/node": "^25.0.10",
44
+ "@types/react": "^19.2.13",
45
+ "@types/react-dom": "^19.2.3",
46
+ "@typescript-eslint/eslint-plugin": "^8.53.1",
47
+ "@typescript-eslint/parser": "^8.53.1",
48
+ "@vitest/coverage-v8": "4.0.18",
49
+ "@vitest/ui": "^4.0.18",
50
+ "concurrently": "^9.2.1",
51
+ "eslint": "^9.39.2",
52
+ "eslint-config-prettier": "^10.1.8",
53
+ "eslint-plugin-prettier": "^5.5.5",
54
+ "husky": "^9.1.7",
55
+ "jiti": "^2.6.1",
56
+ "jsdom": "^28.1.0",
57
+ "lint-staged": "^16.2.7",
58
+ "prettier": "^3.8.1",
59
+ "prettier-plugin-organize-imports": "^4.3.0",
60
+ "prettier-plugin-tailwindcss": "^0.7.2",
61
+ "react": "19.2.3",
62
+ "rollup": "^4.56.0",
63
+ "semantic-release": "^25.0.2",
64
+ "typescript": "^5.9.3",
65
+ "vitest": "^4.0.18"
66
+ },
67
+ "lint-staged": {
68
+ "*.{ts,tsx}": [
69
+ "prettier --write",
70
+ "eslint --fix",
71
+ "bash -c tsc --noEmit"
72
+ ]
73
+ },
74
+ "publishConfig": {
75
+ "access": "public"
76
+ },
77
+ "scripts": {
78
+ "build": "rollup -c",
79
+ "dev": "concurrently -n rollup,sandbox \"rollup -c -w\" \"pnpm -F sandbox dev\"",
80
+ "test": "vitest --run --passWithNoTests",
81
+ "test:watch": "vitest",
82
+ "test:ui": "vitest --ui --coverage",
83
+ "coverage": "vitest --run --coverage --passWithNoTests",
84
+ "lint": "eslint --fix src",
85
+ "format": "prettier --write src",
86
+ "validate": "tsc --noEmit && pnpm lint && pnpm format && pnpm test"
87
+ }
88
+ }