remix-validated-form 3.1.1 → 3.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.
@@ -1,9 +1,9 @@
1
1
  $ npm run build:browser && npm run build:main
2
2
 
3
- > remix-validated-form@3.1.0 build:browser
3
+ > remix-validated-form@3.1.1 build:browser
4
4
  > tsc --module ESNext --outDir ./browser
5
5
 
6
6
 
7
- > remix-validated-form@3.1.0 build:main
7
+ > remix-validated-form@3.1.1 build:main
8
8
  > tsc --module CommonJS --outDir ./build
9
9
 
package/README.md CHANGED
@@ -64,18 +64,14 @@ type MyInputProps = {
64
64
  };
65
65
 
66
66
  export const MyInput = ({ name, label }: InputProps) => {
67
- const { validate, clearError, defaultValue, error } = useField(name);
67
+ const { error, getInputProps } = useField(name);
68
68
  return (
69
69
  <div>
70
70
  <label htmlFor={name}>{label}</label>
71
- <input
72
- id={name}
73
- name={name}
74
- onBlur={validate}
75
- onChange={clearError}
76
- defaultValue={defaultValue}
77
- />
78
- {error && <span className="my-error-class">{error}</span>}
71
+ <input {...getInputProps({ id: name })} />
72
+ {error && (
73
+ <span className="my-error-class">{error}</span>
74
+ )}
79
75
  </div>
80
76
  );
81
77
  };
@@ -257,4 +253,8 @@ We recommend this approach since the validation will still work even if JS is di
257
253
  ## How do we trigger toast messages on success?
258
254
 
259
255
  Problem: how do we trigger a toast message on success if the action redirects away from the form route? The Remix solution is to flash a message in the session and pick this up in a loader function, probably in root.tsx
260
- See the [Remix](https://remix.run/docs/en/v1/api/remix#sessionflashkey-value) documentation for more information.
256
+ See the [Remix](https://remix.run/docs/en/v1/api/remix#sessionflashkey-value) documentation for more information.
257
+
258
+ ## Why is my cancel button triggering form submission?
259
+ Problem: the cancel button has an onClick handler to navigate away from the form route but instead it is submitting the form.
260
+ A button defaults to `type="submit"` in a form which will submit the form by default. If you want to prevent this you can add `type="reset"` or `type="button"` to the cancel button.
@@ -96,6 +96,8 @@ export function ValidatedForm({ validator, onSubmit, children, fetcher, action,
96
96
  const [fieldErrors, setFieldErrors] = useFieldErrors(fieldErrorsFromBackend);
97
97
  const isSubmitting = useIsSubmitting(action, subaction, fetcher);
98
98
  const defaultsToUse = useDefaultValues(fieldErrorsFromBackend, defaultValues);
99
+ const [touchedFields, setTouchedFields] = useState({});
100
+ const [hasBeenSubmitted, setHasBeenSubmitted] = useState(false);
99
101
  const formRef = useRef(null);
100
102
  useSubmitComplete(isSubmitting, () => {
101
103
  var _a;
@@ -110,6 +112,11 @@ export function ValidatedForm({ validator, onSubmit, children, fetcher, action,
110
112
  defaultValues: defaultsToUse,
111
113
  isSubmitting: isSubmitting !== null && isSubmitting !== void 0 ? isSubmitting : false,
112
114
  isValid: Object.keys(fieldErrors).length === 0,
115
+ touchedFields,
116
+ setFieldTouched: (fieldName, touched) => setTouchedFields((prev) => ({
117
+ ...prev,
118
+ [fieldName]: touched,
119
+ })),
113
120
  clearError: (fieldName) => {
114
121
  setFieldErrors((prev) => omit(prev, fieldName));
115
122
  },
@@ -122,6 +129,9 @@ export function ValidatedForm({ validator, onSubmit, children, fetcher, action,
122
129
  [fieldName]: error,
123
130
  }));
124
131
  }
132
+ else {
133
+ setFieldErrors((prev) => omit(prev, fieldName));
134
+ }
125
135
  },
126
136
  registerReceiveFocus: (fieldName, handler) => {
127
137
  customFocusHandlers().add(fieldName, handler);
@@ -129,17 +139,21 @@ export function ValidatedForm({ validator, onSubmit, children, fetcher, action,
129
139
  customFocusHandlers().remove(fieldName, handler);
130
140
  };
131
141
  },
142
+ hasBeenSubmitted,
132
143
  }), [
133
144
  fieldErrors,
134
145
  action,
135
146
  defaultsToUse,
136
147
  isSubmitting,
148
+ touchedFields,
149
+ hasBeenSubmitted,
137
150
  setFieldErrors,
138
151
  validator,
139
152
  customFocusHandlers,
140
153
  ]);
141
154
  const Form = (_a = fetcher === null || fetcher === void 0 ? void 0 : fetcher.Form) !== null && _a !== void 0 ? _a : RemixForm;
142
155
  return (_jsx(Form, { ref: mergeRefs([formRef, formRefProp]), ...rest, action: action, onSubmit: (event) => {
156
+ setHasBeenSubmitted(true);
143
157
  const result = validator.validate(getDataFromForm(event.currentTarget));
144
158
  if (result.error) {
145
159
  event.preventDefault();
@@ -156,5 +170,7 @@ export function ValidatedForm({ validator, onSubmit, children, fetcher, action,
156
170
  if (event.defaultPrevented)
157
171
  return;
158
172
  setFieldErrors({});
173
+ setTouchedFields({});
174
+ setHasBeenSubmitted(false);
159
175
  }, children: _jsxs(FormContext.Provider, { value: contextValue, children: [subaction && (_jsx("input", { type: "hidden", value: subaction, name: "subaction" }, void 0)), children] }, void 0) }, void 0));
160
176
  }
@@ -1,3 +1,4 @@
1
+ import { GetInputProps, ValidationBehaviorOptions } from "./internal/getInputProps";
1
2
  export declare type FieldProps = {
2
3
  /**
3
4
  * The validation error message if there is one.
@@ -15,6 +16,18 @@ export declare type FieldProps = {
15
16
  * The default value of the field, if there is one.
16
17
  */
17
18
  defaultValue?: any;
19
+ /**
20
+ * Whether or not the field has been touched.
21
+ */
22
+ touched: boolean;
23
+ /**
24
+ * Helper to set the touched state of the field.
25
+ */
26
+ setTouched: (touched: boolean) => void;
27
+ /**
28
+ * Helper to get all the props necessary for a regular input.
29
+ */
30
+ getInputProps: GetInputProps;
18
31
  };
19
32
  /**
20
33
  * Provides the data and helpers necessary to set up a field.
@@ -26,6 +39,10 @@ export declare const useField: (name: string, options?: {
26
39
  * This is useful for custom components that use a hidden input.
27
40
  */
28
41
  handleReceiveFocus?: (() => void) | undefined;
42
+ /**
43
+ * Allows you to specify when a field gets validated (when using getInputProps)
44
+ */
45
+ validationBehavior?: Partial<ValidationBehaviorOptions> | undefined;
29
46
  } | undefined) => FieldProps;
30
47
  /**
31
48
  * Provides access to the entire form context.
package/browser/hooks.js CHANGED
@@ -2,26 +2,52 @@ import get from "lodash/get";
2
2
  import toPath from "lodash/toPath";
3
3
  import { useContext, useEffect, useMemo } from "react";
4
4
  import { FormContext } from "./internal/formContext";
5
+ import { createGetInputProps, } from "./internal/getInputProps";
5
6
  /**
6
7
  * Provides the data and helpers necessary to set up a field.
7
8
  */
8
9
  export const useField = (name, options) => {
9
- const { fieldErrors, clearError, validateField, defaultValues, registerReceiveFocus, } = useContext(FormContext);
10
+ const { fieldErrors, clearError, validateField, defaultValues, registerReceiveFocus, touchedFields, setFieldTouched, hasBeenSubmitted, } = useContext(FormContext);
11
+ const isTouched = !!touchedFields[name];
10
12
  const { handleReceiveFocus } = options !== null && options !== void 0 ? options : {};
11
13
  useEffect(() => {
12
14
  if (handleReceiveFocus)
13
15
  return registerReceiveFocus(name, handleReceiveFocus);
14
16
  }, [handleReceiveFocus, name, registerReceiveFocus]);
15
- const field = useMemo(() => ({
16
- error: fieldErrors[name],
17
- clearError: () => {
18
- clearError(name);
19
- },
20
- validate: () => validateField(name),
21
- defaultValue: defaultValues
22
- ? get(defaultValues, toPath(name), undefined)
23
- : undefined,
24
- }), [clearError, defaultValues, fieldErrors, name, validateField]);
17
+ const field = useMemo(() => {
18
+ const helpers = {
19
+ error: fieldErrors[name],
20
+ clearError: () => {
21
+ clearError(name);
22
+ },
23
+ validate: () => validateField(name),
24
+ defaultValue: defaultValues
25
+ ? get(defaultValues, toPath(name), undefined)
26
+ : undefined,
27
+ touched: isTouched,
28
+ setTouched: (touched) => setFieldTouched(name, touched),
29
+ };
30
+ const getInputProps = createGetInputProps({
31
+ ...helpers,
32
+ name,
33
+ hasBeenSubmitted,
34
+ validationBehavior: options === null || options === void 0 ? void 0 : options.validationBehavior,
35
+ });
36
+ return {
37
+ ...helpers,
38
+ getInputProps,
39
+ };
40
+ }, [
41
+ fieldErrors,
42
+ name,
43
+ defaultValues,
44
+ isTouched,
45
+ hasBeenSubmitted,
46
+ options === null || options === void 0 ? void 0 : options.validationBehavior,
47
+ clearError,
48
+ validateField,
49
+ setFieldTouched,
50
+ ]);
25
51
  return field;
26
52
  };
27
53
  /**
@@ -1,5 +1,5 @@
1
1
  /// <reference types="react" />
2
- import { FieldErrors } from "../validation/types";
2
+ import { FieldErrors, TouchedFields } from "../validation/types";
3
3
  export declare type FormContextValue = {
4
4
  /**
5
5
  * All the errors in all the fields in the form.
@@ -21,6 +21,12 @@ export declare type FormContextValue = {
21
21
  * Whether or not the form is submitting.
22
22
  */
23
23
  isSubmitting: boolean;
24
+ /**
25
+ * Whether or not a submission has been attempted.
26
+ * This is true once the form has been submitted, even if there were validation errors.
27
+ * Resets to false when the form is reset.
28
+ */
29
+ hasBeenSubmitted: boolean;
24
30
  /**
25
31
  * Whether or not the form is valid.
26
32
  * This is a shortcut for `Object.keys(fieldErrors).length === 0`.
@@ -37,5 +43,13 @@ export declare type FormContextValue = {
37
43
  * the field needs to receive focus due to a validation error.
38
44
  */
39
45
  registerReceiveFocus: (fieldName: string, handler: () => void) => () => void;
46
+ /**
47
+ * Any fields that have been touched by the user.
48
+ */
49
+ touchedFields: TouchedFields;
50
+ /**
51
+ * Change the touched state of the specified field.
52
+ */
53
+ setFieldTouched: (fieldName: string, touched: boolean) => void;
40
54
  };
41
55
  export declare const FormContext: import("react").Context<FormContextValue>;
@@ -4,6 +4,9 @@ export const FormContext = createContext({
4
4
  clearError: () => { },
5
5
  validateField: () => { },
6
6
  isSubmitting: false,
7
+ hasBeenSubmitted: false,
7
8
  isValid: true,
8
9
  registerReceiveFocus: () => () => { },
10
+ touchedFields: {},
11
+ setFieldTouched: () => { },
9
12
  });
@@ -0,0 +1,28 @@
1
+ export declare type ValidationBehavior = "onBlur" | "onChange" | "onSubmit";
2
+ export declare type ValidationBehaviorOptions = {
3
+ initial: ValidationBehavior;
4
+ whenTouched: ValidationBehavior;
5
+ whenSubmitted: ValidationBehavior;
6
+ };
7
+ export declare type CreateGetInputPropsOptions = {
8
+ clearError: () => void;
9
+ validate: () => void;
10
+ defaultValue?: any;
11
+ touched: boolean;
12
+ setTouched: (touched: boolean) => void;
13
+ hasBeenSubmitted: boolean;
14
+ validationBehavior?: Partial<ValidationBehaviorOptions>;
15
+ name: string;
16
+ };
17
+ export declare type MinimalInputProps = {
18
+ onChange?: (...args: any[]) => void;
19
+ onBlur?: (...args: any[]) => void;
20
+ };
21
+ export declare type MinimalResult = {
22
+ name: string;
23
+ onChange: (...args: any[]) => void;
24
+ onBlur: (...args: any[]) => void;
25
+ defaultValue?: any;
26
+ };
27
+ export declare type GetInputProps = <T extends {}>(props?: T & MinimalInputProps) => T & MinimalResult;
28
+ export declare const createGetInputProps: ({ clearError, validate, defaultValue, touched, setTouched, hasBeenSubmitted, validationBehavior, name, }: CreateGetInputPropsOptions) => GetInputProps;
@@ -0,0 +1,38 @@
1
+ const defaultValidationBehavior = {
2
+ initial: "onBlur",
3
+ whenTouched: "onChange",
4
+ whenSubmitted: "onChange",
5
+ };
6
+ export const createGetInputProps = ({ clearError, validate, defaultValue, touched, setTouched, hasBeenSubmitted, validationBehavior, name, }) => {
7
+ const validationBehaviors = {
8
+ ...defaultValidationBehavior,
9
+ ...validationBehavior,
10
+ };
11
+ return (props = {}) => {
12
+ const behavior = hasBeenSubmitted
13
+ ? validationBehaviors.whenSubmitted
14
+ : touched
15
+ ? validationBehaviors.whenTouched
16
+ : validationBehaviors.initial;
17
+ return {
18
+ ...props,
19
+ onChange: (...args) => {
20
+ var _a;
21
+ if (behavior === "onChange")
22
+ validate();
23
+ else
24
+ clearError();
25
+ return (_a = props === null || props === void 0 ? void 0 : props.onChange) === null || _a === void 0 ? void 0 : _a.call(props, ...args);
26
+ },
27
+ onBlur: (...args) => {
28
+ var _a;
29
+ if (behavior === "onBlur")
30
+ validate();
31
+ setTouched(true);
32
+ return (_a = props === null || props === void 0 ? void 0 : props.onBlur) === null || _a === void 0 ? void 0 : _a.call(props, ...args);
33
+ },
34
+ defaultValue,
35
+ name,
36
+ };
37
+ };
38
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,10 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { createGetInputProps } from "./getInputProps";
3
+ const CompRequired = (props) => null;
4
+ const getProps = createGetInputProps({});
5
+ _jsx(CompRequired, { ...getProps({
6
+ temp: 21,
7
+ bob: "ross",
8
+ onBlur: () => { },
9
+ // onChange: () => {},
10
+ }) }, void 0);
@@ -1,4 +1,5 @@
1
1
  export declare type FieldErrors = Record<string, string>;
2
+ export declare type TouchedFields = Record<string, boolean>;
2
3
  export declare type FieldErrorsWithData = FieldErrors & {
3
4
  _submittedData: any;
4
5
  };
@@ -102,6 +102,8 @@ function ValidatedForm({ validator, onSubmit, children, fetcher, action, default
102
102
  const [fieldErrors, setFieldErrors] = useFieldErrors(fieldErrorsFromBackend);
103
103
  const isSubmitting = useIsSubmitting(action, subaction, fetcher);
104
104
  const defaultsToUse = useDefaultValues(fieldErrorsFromBackend, defaultValues);
105
+ const [touchedFields, setTouchedFields] = (0, react_2.useState)({});
106
+ const [hasBeenSubmitted, setHasBeenSubmitted] = (0, react_2.useState)(false);
105
107
  const formRef = (0, react_2.useRef)(null);
106
108
  (0, submissionCallbacks_1.useSubmitComplete)(isSubmitting, () => {
107
109
  var _a;
@@ -116,6 +118,11 @@ function ValidatedForm({ validator, onSubmit, children, fetcher, action, default
116
118
  defaultValues: defaultsToUse,
117
119
  isSubmitting: isSubmitting !== null && isSubmitting !== void 0 ? isSubmitting : false,
118
120
  isValid: Object.keys(fieldErrors).length === 0,
121
+ touchedFields,
122
+ setFieldTouched: (fieldName, touched) => setTouchedFields((prev) => ({
123
+ ...prev,
124
+ [fieldName]: touched,
125
+ })),
119
126
  clearError: (fieldName) => {
120
127
  setFieldErrors((prev) => (0, util_1.omit)(prev, fieldName));
121
128
  },
@@ -128,6 +135,9 @@ function ValidatedForm({ validator, onSubmit, children, fetcher, action, default
128
135
  [fieldName]: error,
129
136
  }));
130
137
  }
138
+ else {
139
+ setFieldErrors((prev) => (0, util_1.omit)(prev, fieldName));
140
+ }
131
141
  },
132
142
  registerReceiveFocus: (fieldName, handler) => {
133
143
  customFocusHandlers().add(fieldName, handler);
@@ -135,17 +145,21 @@ function ValidatedForm({ validator, onSubmit, children, fetcher, action, default
135
145
  customFocusHandlers().remove(fieldName, handler);
136
146
  };
137
147
  },
148
+ hasBeenSubmitted,
138
149
  }), [
139
150
  fieldErrors,
140
151
  action,
141
152
  defaultsToUse,
142
153
  isSubmitting,
154
+ touchedFields,
155
+ hasBeenSubmitted,
143
156
  setFieldErrors,
144
157
  validator,
145
158
  customFocusHandlers,
146
159
  ]);
147
160
  const Form = (_a = fetcher === null || fetcher === void 0 ? void 0 : fetcher.Form) !== null && _a !== void 0 ? _a : react_1.Form;
148
161
  return ((0, jsx_runtime_1.jsx)(Form, { ref: (0, util_1.mergeRefs)([formRef, formRefProp]), ...rest, action: action, onSubmit: (event) => {
162
+ setHasBeenSubmitted(true);
149
163
  const result = validator.validate(getDataFromForm(event.currentTarget));
150
164
  if (result.error) {
151
165
  event.preventDefault();
@@ -162,6 +176,8 @@ function ValidatedForm({ validator, onSubmit, children, fetcher, action, default
162
176
  if (event.defaultPrevented)
163
177
  return;
164
178
  setFieldErrors({});
179
+ setTouchedFields({});
180
+ setHasBeenSubmitted(false);
165
181
  }, children: (0, jsx_runtime_1.jsxs)(formContext_1.FormContext.Provider, { value: contextValue, children: [subaction && ((0, jsx_runtime_1.jsx)("input", { type: "hidden", value: subaction, name: "subaction" }, void 0)), children] }, void 0) }, void 0));
166
182
  }
167
183
  exports.ValidatedForm = ValidatedForm;
package/build/hooks.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { GetInputProps, ValidationBehaviorOptions } from "./internal/getInputProps";
1
2
  export declare type FieldProps = {
2
3
  /**
3
4
  * The validation error message if there is one.
@@ -15,6 +16,18 @@ export declare type FieldProps = {
15
16
  * The default value of the field, if there is one.
16
17
  */
17
18
  defaultValue?: any;
19
+ /**
20
+ * Whether or not the field has been touched.
21
+ */
22
+ touched: boolean;
23
+ /**
24
+ * Helper to set the touched state of the field.
25
+ */
26
+ setTouched: (touched: boolean) => void;
27
+ /**
28
+ * Helper to get all the props necessary for a regular input.
29
+ */
30
+ getInputProps: GetInputProps;
18
31
  };
19
32
  /**
20
33
  * Provides the data and helpers necessary to set up a field.
@@ -26,6 +39,10 @@ export declare const useField: (name: string, options?: {
26
39
  * This is useful for custom components that use a hidden input.
27
40
  */
28
41
  handleReceiveFocus?: (() => void) | undefined;
42
+ /**
43
+ * Allows you to specify when a field gets validated (when using getInputProps)
44
+ */
45
+ validationBehavior?: Partial<ValidationBehaviorOptions> | undefined;
29
46
  } | undefined) => FieldProps;
30
47
  /**
31
48
  * Provides access to the entire form context.
package/build/hooks.js CHANGED
@@ -8,26 +8,52 @@ const get_1 = __importDefault(require("lodash/get"));
8
8
  const toPath_1 = __importDefault(require("lodash/toPath"));
9
9
  const react_1 = require("react");
10
10
  const formContext_1 = require("./internal/formContext");
11
+ const getInputProps_1 = require("./internal/getInputProps");
11
12
  /**
12
13
  * Provides the data and helpers necessary to set up a field.
13
14
  */
14
15
  const useField = (name, options) => {
15
- const { fieldErrors, clearError, validateField, defaultValues, registerReceiveFocus, } = (0, react_1.useContext)(formContext_1.FormContext);
16
+ const { fieldErrors, clearError, validateField, defaultValues, registerReceiveFocus, touchedFields, setFieldTouched, hasBeenSubmitted, } = (0, react_1.useContext)(formContext_1.FormContext);
17
+ const isTouched = !!touchedFields[name];
16
18
  const { handleReceiveFocus } = options !== null && options !== void 0 ? options : {};
17
19
  (0, react_1.useEffect)(() => {
18
20
  if (handleReceiveFocus)
19
21
  return registerReceiveFocus(name, handleReceiveFocus);
20
22
  }, [handleReceiveFocus, name, registerReceiveFocus]);
21
- const field = (0, react_1.useMemo)(() => ({
22
- error: fieldErrors[name],
23
- clearError: () => {
24
- clearError(name);
25
- },
26
- validate: () => validateField(name),
27
- defaultValue: defaultValues
28
- ? (0, get_1.default)(defaultValues, (0, toPath_1.default)(name), undefined)
29
- : undefined,
30
- }), [clearError, defaultValues, fieldErrors, name, validateField]);
23
+ const field = (0, react_1.useMemo)(() => {
24
+ const helpers = {
25
+ error: fieldErrors[name],
26
+ clearError: () => {
27
+ clearError(name);
28
+ },
29
+ validate: () => validateField(name),
30
+ defaultValue: defaultValues
31
+ ? (0, get_1.default)(defaultValues, (0, toPath_1.default)(name), undefined)
32
+ : undefined,
33
+ touched: isTouched,
34
+ setTouched: (touched) => setFieldTouched(name, touched),
35
+ };
36
+ const getInputProps = (0, getInputProps_1.createGetInputProps)({
37
+ ...helpers,
38
+ name,
39
+ hasBeenSubmitted,
40
+ validationBehavior: options === null || options === void 0 ? void 0 : options.validationBehavior,
41
+ });
42
+ return {
43
+ ...helpers,
44
+ getInputProps,
45
+ };
46
+ }, [
47
+ fieldErrors,
48
+ name,
49
+ defaultValues,
50
+ isTouched,
51
+ hasBeenSubmitted,
52
+ options === null || options === void 0 ? void 0 : options.validationBehavior,
53
+ clearError,
54
+ validateField,
55
+ setFieldTouched,
56
+ ]);
31
57
  return field;
32
58
  };
33
59
  exports.useField = useField;
@@ -1,5 +1,5 @@
1
1
  /// <reference types="react" />
2
- import { FieldErrors } from "../validation/types";
2
+ import { FieldErrors, TouchedFields } from "../validation/types";
3
3
  export declare type FormContextValue = {
4
4
  /**
5
5
  * All the errors in all the fields in the form.
@@ -21,6 +21,12 @@ export declare type FormContextValue = {
21
21
  * Whether or not the form is submitting.
22
22
  */
23
23
  isSubmitting: boolean;
24
+ /**
25
+ * Whether or not a submission has been attempted.
26
+ * This is true once the form has been submitted, even if there were validation errors.
27
+ * Resets to false when the form is reset.
28
+ */
29
+ hasBeenSubmitted: boolean;
24
30
  /**
25
31
  * Whether or not the form is valid.
26
32
  * This is a shortcut for `Object.keys(fieldErrors).length === 0`.
@@ -37,5 +43,13 @@ export declare type FormContextValue = {
37
43
  * the field needs to receive focus due to a validation error.
38
44
  */
39
45
  registerReceiveFocus: (fieldName: string, handler: () => void) => () => void;
46
+ /**
47
+ * Any fields that have been touched by the user.
48
+ */
49
+ touchedFields: TouchedFields;
50
+ /**
51
+ * Change the touched state of the specified field.
52
+ */
53
+ setFieldTouched: (fieldName: string, touched: boolean) => void;
40
54
  };
41
55
  export declare const FormContext: import("react").Context<FormContextValue>;
@@ -7,6 +7,9 @@ exports.FormContext = (0, react_1.createContext)({
7
7
  clearError: () => { },
8
8
  validateField: () => { },
9
9
  isSubmitting: false,
10
+ hasBeenSubmitted: false,
10
11
  isValid: true,
11
12
  registerReceiveFocus: () => () => { },
13
+ touchedFields: {},
14
+ setFieldTouched: () => { },
12
15
  });
@@ -0,0 +1,28 @@
1
+ export declare type ValidationBehavior = "onBlur" | "onChange" | "onSubmit";
2
+ export declare type ValidationBehaviorOptions = {
3
+ initial: ValidationBehavior;
4
+ whenTouched: ValidationBehavior;
5
+ whenSubmitted: ValidationBehavior;
6
+ };
7
+ export declare type CreateGetInputPropsOptions = {
8
+ clearError: () => void;
9
+ validate: () => void;
10
+ defaultValue?: any;
11
+ touched: boolean;
12
+ setTouched: (touched: boolean) => void;
13
+ hasBeenSubmitted: boolean;
14
+ validationBehavior?: Partial<ValidationBehaviorOptions>;
15
+ name: string;
16
+ };
17
+ export declare type MinimalInputProps = {
18
+ onChange?: (...args: any[]) => void;
19
+ onBlur?: (...args: any[]) => void;
20
+ };
21
+ export declare type MinimalResult = {
22
+ name: string;
23
+ onChange: (...args: any[]) => void;
24
+ onBlur: (...args: any[]) => void;
25
+ defaultValue?: any;
26
+ };
27
+ export declare type GetInputProps = <T extends {}>(props?: T & MinimalInputProps) => T & MinimalResult;
28
+ export declare const createGetInputProps: ({ clearError, validate, defaultValue, touched, setTouched, hasBeenSubmitted, validationBehavior, name, }: CreateGetInputPropsOptions) => GetInputProps;
@@ -0,0 +1,42 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createGetInputProps = void 0;
4
+ const defaultValidationBehavior = {
5
+ initial: "onBlur",
6
+ whenTouched: "onChange",
7
+ whenSubmitted: "onChange",
8
+ };
9
+ const createGetInputProps = ({ clearError, validate, defaultValue, touched, setTouched, hasBeenSubmitted, validationBehavior, name, }) => {
10
+ const validationBehaviors = {
11
+ ...defaultValidationBehavior,
12
+ ...validationBehavior,
13
+ };
14
+ return (props = {}) => {
15
+ const behavior = hasBeenSubmitted
16
+ ? validationBehaviors.whenSubmitted
17
+ : touched
18
+ ? validationBehaviors.whenTouched
19
+ : validationBehaviors.initial;
20
+ return {
21
+ ...props,
22
+ onChange: (...args) => {
23
+ var _a;
24
+ if (behavior === "onChange")
25
+ validate();
26
+ else
27
+ clearError();
28
+ return (_a = props === null || props === void 0 ? void 0 : props.onChange) === null || _a === void 0 ? void 0 : _a.call(props, ...args);
29
+ },
30
+ onBlur: (...args) => {
31
+ var _a;
32
+ if (behavior === "onBlur")
33
+ validate();
34
+ setTouched(true);
35
+ return (_a = props === null || props === void 0 ? void 0 : props.onBlur) === null || _a === void 0 ? void 0 : _a.call(props, ...args);
36
+ },
37
+ defaultValue,
38
+ name,
39
+ };
40
+ };
41
+ };
42
+ exports.createGetInputProps = createGetInputProps;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const jsx_runtime_1 = require("react/jsx-runtime");
4
+ const getInputProps_1 = require("./getInputProps");
5
+ const CompRequired = (props) => null;
6
+ const getProps = (0, getInputProps_1.createGetInputProps)({});
7
+ (0, jsx_runtime_1.jsx)(CompRequired, { ...getProps({
8
+ temp: 21,
9
+ bob: "ross",
10
+ onBlur: () => { },
11
+ // onChange: () => {},
12
+ }) }, void 0);
@@ -1,4 +1,5 @@
1
1
  export declare type FieldErrors = Record<string, string>;
2
+ export declare type TouchedFields = Record<string, boolean>;
2
3
  export declare type FieldErrorsWithData = FieldErrors & {
3
4
  _submittedData: any;
4
5
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "remix-validated-form",
3
- "version": "3.1.1",
3
+ "version": "3.2.0",
4
4
  "description": "Form component and utils for easy form validation in remix",
5
5
  "browser": "./browser/index.js",
6
6
  "main": "./build/index.js",
@@ -24,6 +24,7 @@ import {
24
24
  FieldErrors,
25
25
  Validator,
26
26
  FieldErrorsWithData,
27
+ TouchedFields,
27
28
  } from "./validation/types";
28
29
 
29
30
  export type FormProps<DataType> = {
@@ -196,6 +197,8 @@ export function ValidatedForm<DataType>({
196
197
  const [fieldErrors, setFieldErrors] = useFieldErrors(fieldErrorsFromBackend);
197
198
  const isSubmitting = useIsSubmitting(action, subaction, fetcher);
198
199
  const defaultsToUse = useDefaultValues(fieldErrorsFromBackend, defaultValues);
200
+ const [touchedFields, setTouchedFields] = useState<TouchedFields>({});
201
+ const [hasBeenSubmitted, setHasBeenSubmitted] = useState(false);
199
202
  const formRef = useRef<HTMLFormElement>(null);
200
203
  useSubmitComplete(isSubmitting, () => {
201
204
  if (!fieldErrorsFromBackend && resetAfterSubmit) {
@@ -211,6 +214,12 @@ export function ValidatedForm<DataType>({
211
214
  defaultValues: defaultsToUse,
212
215
  isSubmitting: isSubmitting ?? false,
213
216
  isValid: Object.keys(fieldErrors).length === 0,
217
+ touchedFields,
218
+ setFieldTouched: (fieldName: string, touched: boolean) =>
219
+ setTouchedFields((prev) => ({
220
+ ...prev,
221
+ [fieldName]: touched,
222
+ })),
214
223
  clearError: (fieldName) => {
215
224
  setFieldErrors((prev) => omit(prev, fieldName));
216
225
  },
@@ -225,6 +234,8 @@ export function ValidatedForm<DataType>({
225
234
  ...prev,
226
235
  [fieldName]: error,
227
236
  }));
237
+ } else {
238
+ setFieldErrors((prev) => omit(prev, fieldName));
228
239
  }
229
240
  },
230
241
  registerReceiveFocus: (fieldName, handler) => {
@@ -233,12 +244,15 @@ export function ValidatedForm<DataType>({
233
244
  customFocusHandlers().remove(fieldName, handler);
234
245
  };
235
246
  },
247
+ hasBeenSubmitted,
236
248
  }),
237
249
  [
238
250
  fieldErrors,
239
251
  action,
240
252
  defaultsToUse,
241
253
  isSubmitting,
254
+ touchedFields,
255
+ hasBeenSubmitted,
242
256
  setFieldErrors,
243
257
  validator,
244
258
  customFocusHandlers,
@@ -253,6 +267,7 @@ export function ValidatedForm<DataType>({
253
267
  {...rest}
254
268
  action={action}
255
269
  onSubmit={(event) => {
270
+ setHasBeenSubmitted(true);
256
271
  const result = validator.validate(getDataFromForm(event.currentTarget));
257
272
  if (result.error) {
258
273
  event.preventDefault();
@@ -272,6 +287,8 @@ export function ValidatedForm<DataType>({
272
287
  onReset?.(event);
273
288
  if (event.defaultPrevented) return;
274
289
  setFieldErrors({});
290
+ setTouchedFields({});
291
+ setHasBeenSubmitted(false);
275
292
  }}
276
293
  >
277
294
  <FormContext.Provider value={contextValue}>
package/src/hooks.ts CHANGED
@@ -2,6 +2,11 @@ import get from "lodash/get";
2
2
  import toPath from "lodash/toPath";
3
3
  import { useContext, useEffect, useMemo } from "react";
4
4
  import { FormContext } from "./internal/formContext";
5
+ import {
6
+ createGetInputProps,
7
+ GetInputProps,
8
+ ValidationBehaviorOptions,
9
+ } from "./internal/getInputProps";
5
10
 
6
11
  export type FieldProps = {
7
12
  /**
@@ -20,6 +25,18 @@ export type FieldProps = {
20
25
  * The default value of the field, if there is one.
21
26
  */
22
27
  defaultValue?: any;
28
+ /**
29
+ * Whether or not the field has been touched.
30
+ */
31
+ touched: boolean;
32
+ /**
33
+ * Helper to set the touched state of the field.
34
+ */
35
+ setTouched: (touched: boolean) => void;
36
+ /**
37
+ * Helper to get all the props necessary for a regular input.
38
+ */
39
+ getInputProps: GetInputProps;
23
40
  };
24
41
 
25
42
  /**
@@ -34,6 +51,10 @@ export const useField = (
34
51
  * This is useful for custom components that use a hidden input.
35
52
  */
36
53
  handleReceiveFocus?: () => void;
54
+ /**
55
+ * Allows you to specify when a field gets validated (when using getInputProps)
56
+ */
57
+ validationBehavior?: Partial<ValidationBehaviorOptions>;
37
58
  }
38
59
  ): FieldProps => {
39
60
  const {
@@ -42,8 +63,12 @@ export const useField = (
42
63
  validateField,
43
64
  defaultValues,
44
65
  registerReceiveFocus,
66
+ touchedFields,
67
+ setFieldTouched,
68
+ hasBeenSubmitted,
45
69
  } = useContext(FormContext);
46
70
 
71
+ const isTouched = !!touchedFields[name];
47
72
  const { handleReceiveFocus } = options ?? {};
48
73
 
49
74
  useEffect(() => {
@@ -51,8 +76,8 @@ export const useField = (
51
76
  return registerReceiveFocus(name, handleReceiveFocus);
52
77
  }, [handleReceiveFocus, name, registerReceiveFocus]);
53
78
 
54
- const field = useMemo<FieldProps>(
55
- () => ({
79
+ const field = useMemo<FieldProps>(() => {
80
+ const helpers = {
56
81
  error: fieldErrors[name],
57
82
  clearError: () => {
58
83
  clearError(name);
@@ -61,9 +86,30 @@ export const useField = (
61
86
  defaultValue: defaultValues
62
87
  ? get(defaultValues, toPath(name), undefined)
63
88
  : undefined,
64
- }),
65
- [clearError, defaultValues, fieldErrors, name, validateField]
66
- );
89
+ touched: isTouched,
90
+ setTouched: (touched: boolean) => setFieldTouched(name, touched),
91
+ };
92
+ const getInputProps = createGetInputProps({
93
+ ...helpers,
94
+ name,
95
+ hasBeenSubmitted,
96
+ validationBehavior: options?.validationBehavior,
97
+ });
98
+ return {
99
+ ...helpers,
100
+ getInputProps,
101
+ };
102
+ }, [
103
+ fieldErrors,
104
+ name,
105
+ defaultValues,
106
+ isTouched,
107
+ hasBeenSubmitted,
108
+ options?.validationBehavior,
109
+ clearError,
110
+ validateField,
111
+ setFieldTouched,
112
+ ]);
67
113
 
68
114
  return field;
69
115
  };
@@ -1,5 +1,5 @@
1
1
  import { createContext } from "react";
2
- import { FieldErrors } from "../validation/types";
2
+ import { FieldErrors, TouchedFields } from "../validation/types";
3
3
 
4
4
  export type FormContextValue = {
5
5
  /**
@@ -22,6 +22,12 @@ export type FormContextValue = {
22
22
  * Whether or not the form is submitting.
23
23
  */
24
24
  isSubmitting: boolean;
25
+ /**
26
+ * Whether or not a submission has been attempted.
27
+ * This is true once the form has been submitted, even if there were validation errors.
28
+ * Resets to false when the form is reset.
29
+ */
30
+ hasBeenSubmitted: boolean;
25
31
  /**
26
32
  * Whether or not the form is valid.
27
33
  * This is a shortcut for `Object.keys(fieldErrors).length === 0`.
@@ -36,6 +42,14 @@ export type FormContextValue = {
36
42
  * the field needs to receive focus due to a validation error.
37
43
  */
38
44
  registerReceiveFocus: (fieldName: string, handler: () => void) => () => void;
45
+ /**
46
+ * Any fields that have been touched by the user.
47
+ */
48
+ touchedFields: TouchedFields;
49
+ /**
50
+ * Change the touched state of the specified field.
51
+ */
52
+ setFieldTouched: (fieldName: string, touched: boolean) => void;
39
53
  };
40
54
 
41
55
  export const FormContext = createContext<FormContextValue>({
@@ -43,6 +57,9 @@ export const FormContext = createContext<FormContextValue>({
43
57
  clearError: () => {},
44
58
  validateField: () => {},
45
59
  isSubmitting: false,
60
+ hasBeenSubmitted: false,
46
61
  isValid: true,
47
62
  registerReceiveFocus: () => () => {},
63
+ touchedFields: {},
64
+ setFieldTouched: () => {},
48
65
  });
@@ -0,0 +1,79 @@
1
+ export type ValidationBehavior = "onBlur" | "onChange" | "onSubmit";
2
+
3
+ export type ValidationBehaviorOptions = {
4
+ initial: ValidationBehavior;
5
+ whenTouched: ValidationBehavior;
6
+ whenSubmitted: ValidationBehavior;
7
+ };
8
+
9
+ export type CreateGetInputPropsOptions = {
10
+ clearError: () => void;
11
+ validate: () => void;
12
+ defaultValue?: any;
13
+ touched: boolean;
14
+ setTouched: (touched: boolean) => void;
15
+ hasBeenSubmitted: boolean;
16
+ validationBehavior?: Partial<ValidationBehaviorOptions>;
17
+ name: string;
18
+ };
19
+
20
+ export type MinimalInputProps = {
21
+ onChange?: (...args: any[]) => void;
22
+ onBlur?: (...args: any[]) => void;
23
+ };
24
+
25
+ export type MinimalResult = {
26
+ name: string;
27
+ onChange: (...args: any[]) => void;
28
+ onBlur: (...args: any[]) => void;
29
+ defaultValue?: any;
30
+ };
31
+
32
+ export type GetInputProps = <T extends {}>(
33
+ props?: T & MinimalInputProps
34
+ ) => T & MinimalResult;
35
+
36
+ const defaultValidationBehavior: ValidationBehaviorOptions = {
37
+ initial: "onBlur",
38
+ whenTouched: "onChange",
39
+ whenSubmitted: "onChange",
40
+ };
41
+
42
+ export const createGetInputProps = ({
43
+ clearError,
44
+ validate,
45
+ defaultValue,
46
+ touched,
47
+ setTouched,
48
+ hasBeenSubmitted,
49
+ validationBehavior,
50
+ name,
51
+ }: CreateGetInputPropsOptions): GetInputProps => {
52
+ const validationBehaviors = {
53
+ ...defaultValidationBehavior,
54
+ ...validationBehavior,
55
+ };
56
+
57
+ return (props = {} as any) => {
58
+ const behavior = hasBeenSubmitted
59
+ ? validationBehaviors.whenSubmitted
60
+ : touched
61
+ ? validationBehaviors.whenTouched
62
+ : validationBehaviors.initial;
63
+ return {
64
+ ...props,
65
+ onChange: (...args) => {
66
+ if (behavior === "onChange") validate();
67
+ else clearError();
68
+ return props?.onChange?.(...args);
69
+ },
70
+ onBlur: (...args) => {
71
+ if (behavior === "onBlur") validate();
72
+ setTouched(true);
73
+ return props?.onBlur?.(...args);
74
+ },
75
+ defaultValue,
76
+ name,
77
+ };
78
+ };
79
+ };
@@ -1,5 +1,7 @@
1
1
  export type FieldErrors = Record<string, string>;
2
2
 
3
+ export type TouchedFields = Record<string, boolean>;
4
+
3
5
  export type FieldErrorsWithData = FieldErrors & { _submittedData: any };
4
6
 
5
7
  export type GenericObject = { [key: string]: any };