remix-validated-form 5.0.2 → 5.1.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,197 +0,0 @@
1
- import { nanoid } from "nanoid";
2
- import React, { useMemo, useRef, useState } from "react";
3
- import { useCallback } from "react";
4
- import invariant from "tiny-invariant";
5
- import { InternalFormContextValue } from "../formContext";
6
- import {
7
- useFieldDefaultValue,
8
- useFieldError,
9
- useInternalFormContext,
10
- useInternalHasBeenSubmitted,
11
- useSmartValidate,
12
- } from "../hooks";
13
- import * as arrayUtil from "./arrayUtil";
14
- import { useRegisterControlledField } from "./controlledFields";
15
- import { useFormStore } from "./storeHooks";
16
-
17
- export type FieldArrayValidationBehavior = "onChange" | "onSubmit";
18
-
19
- export type FieldArrayValidationBehaviorOptions = {
20
- initial: FieldArrayValidationBehavior;
21
- whenSubmitted: FieldArrayValidationBehavior;
22
- };
23
-
24
- export type FieldArrayItem<T> = {
25
- /**
26
- * The default value of the item.
27
- * This does not update as the field is changed by the user.
28
- */
29
- defaultValue: T;
30
- /**
31
- * A unique key for the item.
32
- * Use this as the key prop when rendering the item.
33
- */
34
- key: string;
35
- };
36
-
37
- const useInternalFieldArray = (
38
- context: InternalFormContextValue,
39
- field: string,
40
- validationBehavior?: Partial<FieldArrayValidationBehaviorOptions>
41
- ) => {
42
- const value = useFieldDefaultValue(field, context);
43
- useRegisterControlledField(context, field);
44
- const hasBeenSubmitted = useInternalHasBeenSubmitted(context.formId);
45
- const validateField = useSmartValidate(context.formId);
46
- const error = useFieldError(field, context);
47
-
48
- const resolvedValidationBehavior: FieldArrayValidationBehaviorOptions = {
49
- initial: "onSubmit",
50
- whenSubmitted: "onChange",
51
- ...validationBehavior,
52
- };
53
-
54
- const behavior = hasBeenSubmitted
55
- ? resolvedValidationBehavior.whenSubmitted
56
- : resolvedValidationBehavior.initial;
57
-
58
- const maybeValidate = useCallback(() => {
59
- if (behavior === "onChange") {
60
- validateField({ alwaysIncludeErrorsFromFields: [field] });
61
- }
62
- }, [behavior, field, validateField]);
63
-
64
- invariant(
65
- value === undefined || value === null || Array.isArray(value),
66
- `FieldArray: defaultValue value for ${field} must be an array, null, or undefined`
67
- );
68
-
69
- const arr = useFormStore(
70
- context.formId,
71
- (state) => state.controlledFields.array
72
- );
73
-
74
- const arrayValue = useMemo<unknown[]>(() => value ?? [], [value]);
75
- const keyRef = useRef<string[]>([]);
76
-
77
- // If the lengths don't match up it means one of two things
78
- // 1. The array has been modified outside of this hook
79
- // 2. We're initializing the array
80
- if (keyRef.current.length !== arrayValue.length) {
81
- keyRef.current = arrayValue.map(() => nanoid());
82
- }
83
-
84
- const helpers = useMemo(
85
- () => ({
86
- push: (item: any) => {
87
- arr.push(field, item);
88
- keyRef.current.push(nanoid());
89
- maybeValidate();
90
- },
91
- swap: (indexA: number, indexB: number) => {
92
- arr.swap(field, indexA, indexB);
93
- arrayUtil.swap(keyRef.current, indexA, indexB);
94
- maybeValidate();
95
- },
96
- move: (from: number, to: number) => {
97
- arr.move(field, from, to);
98
- arrayUtil.move(keyRef.current, from, to);
99
- maybeValidate();
100
- },
101
- insert: (index: number, value: any) => {
102
- arr.insert(field, index, value);
103
- arrayUtil.insert(keyRef.current, index, nanoid());
104
- maybeValidate();
105
- },
106
- unshift: (value: any) => {
107
- arr.unshift(field, value);
108
- keyRef.current.unshift(nanoid());
109
- maybeValidate();
110
- },
111
- remove: (index: number) => {
112
- arr.remove(field, index);
113
- arrayUtil.remove(keyRef.current, index);
114
- maybeValidate();
115
- },
116
- pop: () => {
117
- arr.pop(field);
118
- keyRef.current.pop();
119
- maybeValidate();
120
- },
121
- replace: (index: number, value: any) => {
122
- arr.replace(field, index, value);
123
- keyRef.current[index] = nanoid();
124
- maybeValidate();
125
- },
126
- }),
127
- [arr, field, maybeValidate]
128
- );
129
-
130
- const valueWithKeys = useMemo(() => {
131
- const result: { defaultValue: any; key: string }[] = [];
132
- arrayValue.forEach((item, index) => {
133
- result[index] = {
134
- key: keyRef.current[index],
135
- defaultValue: item,
136
- };
137
- });
138
- return result;
139
- }, [arrayValue]);
140
-
141
- return [valueWithKeys, helpers, error] as const;
142
- };
143
-
144
- export type FieldArrayHelpers<Item = any> = {
145
- push: (item: Item) => void;
146
- swap: (indexA: number, indexB: number) => void;
147
- move: (from: number, to: number) => void;
148
- insert: (index: number, value: Item) => void;
149
- unshift: (value: Item) => void;
150
- remove: (index: number) => void;
151
- pop: () => void;
152
- replace: (index: number, value: Item) => void;
153
- };
154
-
155
- export type UseFieldArrayOptions = {
156
- formId?: string;
157
- validationBehavior?: Partial<FieldArrayValidationBehaviorOptions>;
158
- };
159
-
160
- export function useFieldArray<Item = any>(
161
- name: string,
162
- { formId, validationBehavior }: UseFieldArrayOptions = {}
163
- ) {
164
- const context = useInternalFormContext(formId, "FieldArray");
165
-
166
- return useInternalFieldArray(context, name, validationBehavior) as [
167
- items: FieldArrayItem<Item>[],
168
- helpers: FieldArrayHelpers,
169
- error: string | undefined
170
- ];
171
- }
172
-
173
- export type FieldArrayProps<Item> = {
174
- name: string;
175
- children: (
176
- items: FieldArrayItem<Item>[],
177
- helpers: FieldArrayHelpers<Item>,
178
- error: string | undefined
179
- ) => React.ReactNode;
180
- formId?: string;
181
- validationBehavior?: FieldArrayValidationBehaviorOptions;
182
- };
183
-
184
- export function FieldArray<Item = any>({
185
- name,
186
- children,
187
- formId,
188
- validationBehavior,
189
- }: FieldArrayProps<Item>) {
190
- const context = useInternalFormContext(formId, "FieldArray");
191
- const [value, helpers, error] = useInternalFieldArray(
192
- context,
193
- name,
194
- validationBehavior
195
- );
196
- return <>{children(value, helpers, error)}</>;
197
- }
@@ -1,9 +0,0 @@
1
- import { FormState, useRootFormStore } from "./createFormStore";
2
- import { InternalFormId } from "./types";
3
-
4
- export const useFormStore = <T>(
5
- formId: InternalFormId,
6
- selector: (state: FormState) => T
7
- ) => {
8
- return useRootFormStore((state) => selector(state.form(formId)));
9
- };
@@ -1 +0,0 @@
1
- export type InternalFormId = string | symbol;
@@ -1,15 +0,0 @@
1
- import { useEffect, useRef } from "react";
2
-
3
- export function useSubmitComplete(isSubmitting: boolean, callback: () => void) {
4
- const isPending = useRef(false);
5
- useEffect(() => {
6
- if (isSubmitting) {
7
- isPending.current = true;
8
- }
9
-
10
- if (!isSubmitting && isPending.current) {
11
- isPending.current = false;
12
- callback();
13
- }
14
- });
15
- }
@@ -1,39 +0,0 @@
1
- import type React from "react";
2
- import { useEffect, useLayoutEffect, useRef } from "react";
3
- import * as R from "remeda";
4
-
5
- export const omit = (obj: any, ...keys: string[]) => {
6
- const result = { ...obj };
7
- for (const key of keys) {
8
- delete result[key];
9
- }
10
- return result;
11
- };
12
-
13
- export const mergeRefs = <T = any>(
14
- refs: Array<React.MutableRefObject<T> | React.LegacyRef<T> | undefined>
15
- ): React.RefCallback<T> => {
16
- return (value: T) => {
17
- refs.filter(Boolean).forEach((ref) => {
18
- if (typeof ref === "function") {
19
- ref(value);
20
- } else if (ref != null) {
21
- (ref as React.MutableRefObject<T | null>).current = value;
22
- }
23
- });
24
- };
25
- };
26
-
27
- export const useIsomorphicLayoutEffect =
28
- typeof window !== "undefined" ? useLayoutEffect : useEffect;
29
-
30
- export const useDeepEqualsMemo = <T>(item: T): T => {
31
- const ref = useRef<T>(item);
32
- const areEqual = ref.current === item || R.equals(ref.current, item);
33
- useEffect(() => {
34
- if (!areEqual) {
35
- ref.current = item;
36
- }
37
- });
38
- return areEqual ? ref.current : item;
39
- };
package/src/server.ts DELETED
@@ -1,53 +0,0 @@
1
- import { json } from "@remix-run/server-runtime";
2
- import {
3
- formDefaultValuesKey,
4
- FORM_DEFAULTS_FIELD,
5
- } from "./internal/constants";
6
- import {
7
- ValidatorError,
8
- ValidationErrorResponseData,
9
- } from "./validation/types";
10
-
11
- /**
12
- * Takes the errors from a `Validator` and returns a `Response`.
13
- * When you return this from your action, `ValidatedForm` on the frontend will automatically
14
- * display the errors on the correct fields on the correct form.
15
- *
16
- * You can also provide a second argument to `validationError`
17
- * to specify how to repopulate the form when JS is disabled.
18
- *
19
- * @example
20
- * ```ts
21
- * const result = validator.validate(await request.formData());
22
- * if (result.error) return validationError(result.error, result.submittedData);
23
- * ```
24
- */
25
- export function validationError(
26
- error: ValidatorError,
27
- repopulateFields?: unknown,
28
- init?: ResponseInit
29
- ) {
30
- return json<ValidationErrorResponseData>(
31
- {
32
- fieldErrors: error.fieldErrors,
33
- subaction: error.subaction,
34
- repopulateFields,
35
- formId: error.formId,
36
- },
37
- { status: 422, ...init }
38
- );
39
- }
40
-
41
- export type FormDefaults = {
42
- [formDefaultsKey: `${typeof FORM_DEFAULTS_FIELD}_${string}`]: any;
43
- };
44
-
45
- // FIXME: Remove after https://github.com/egoist/tsup/issues/813 is fixed
46
- export type internal_FORM_DEFAULTS_FIELD = typeof FORM_DEFAULTS_FIELD;
47
-
48
- export const setFormDefaults = <DataType = any>(
49
- formId: string,
50
- defaultValues: Partial<DataType>
51
- ): FormDefaults => ({
52
- [formDefaultValuesKey(formId)]: defaultValues,
53
- });
@@ -1,170 +0,0 @@
1
- import { useMemo } from "react";
2
- import {} from "../internal/getInputProps";
3
- import {
4
- useInternalFormContext,
5
- useClearError,
6
- useSetTouched,
7
- useDefaultValuesForForm,
8
- useFieldErrorsForForm,
9
- useInternalIsSubmitting,
10
- useInternalHasBeenSubmitted,
11
- useTouchedFields,
12
- useInternalIsValid,
13
- useFieldErrors,
14
- useValidate,
15
- useSetFieldErrors,
16
- useResetFormElement,
17
- useSyncedDefaultValues,
18
- useFormActionProp,
19
- useFormSubactionProp,
20
- useSubmitForm,
21
- useFormValues,
22
- useSmartValidate,
23
- } from "../internal/hooks";
24
- import {
25
- FieldErrors,
26
- TouchedFields,
27
- ValidationResult,
28
- } from "../validation/types";
29
-
30
- export type FormState = {
31
- fieldErrors: FieldErrors;
32
- isSubmitting: boolean;
33
- hasBeenSubmitted: boolean;
34
- touchedFields: TouchedFields;
35
- defaultValues: { [fieldName: string]: any };
36
- action?: string;
37
- subaction?: string;
38
- isValid: boolean;
39
- };
40
-
41
- /**
42
- * Returns information about the form.
43
- *
44
- * @param formId the id of the form. Only necessary if being used outside a ValidatedForm.
45
- */
46
- export const useFormState = (formId?: string): FormState => {
47
- const formContext = useInternalFormContext(formId, "useFormState");
48
- const isSubmitting = useInternalIsSubmitting(formContext.formId);
49
- const hasBeenSubmitted = useInternalHasBeenSubmitted(formContext.formId);
50
- const touchedFields = useTouchedFields(formContext.formId);
51
- const isValid = useInternalIsValid(formContext.formId);
52
- const action = useFormActionProp(formContext.formId);
53
- const subaction = useFormSubactionProp(formContext.formId);
54
-
55
- const syncedDefaultValues = useSyncedDefaultValues(formContext.formId);
56
- const defaultValuesToUse = useDefaultValuesForForm(formContext);
57
- const hydratedDefaultValues =
58
- defaultValuesToUse.hydrateTo(syncedDefaultValues);
59
-
60
- const fieldErrorsFromState = useFieldErrors(formContext.formId);
61
- const fieldErrorsToUse = useFieldErrorsForForm(formContext);
62
- const hydratedFieldErrors = fieldErrorsToUse.hydrateTo(fieldErrorsFromState);
63
-
64
- return useMemo(
65
- () => ({
66
- action,
67
- subaction,
68
- defaultValues: hydratedDefaultValues,
69
- fieldErrors: hydratedFieldErrors ?? {},
70
- hasBeenSubmitted,
71
- isSubmitting,
72
- touchedFields,
73
- isValid,
74
- }),
75
- [
76
- action,
77
- hasBeenSubmitted,
78
- hydratedDefaultValues,
79
- hydratedFieldErrors,
80
- isSubmitting,
81
- isValid,
82
- subaction,
83
- touchedFields,
84
- ]
85
- );
86
- };
87
-
88
- export type FormHelpers = {
89
- /**
90
- * Clear the error of the specified field.
91
- */
92
- clearError: (fieldName: string) => void;
93
- /**
94
- * Validate the specified field.
95
- */
96
- validateField: (fieldName: string) => Promise<string | null>;
97
- /**
98
- * Change the touched state of the specified field.
99
- */
100
- setTouched: (fieldName: string, touched: boolean) => void;
101
- /**
102
- * Validate the whole form and populate any errors.
103
- */
104
- validate: () => Promise<ValidationResult<unknown>>;
105
- /**
106
- * Clears all errors on the form.
107
- */
108
- clearAllErrors: () => void;
109
- /**
110
- * Resets the form.
111
- *
112
- * _Note_: The equivalent behavior can be achieved by calling formElement.reset()
113
- * or clicking a button element with `type="reset"`.
114
- */
115
- reset: () => void;
116
- /**
117
- * Submits the form, running all validations first.
118
- *
119
- * _Note_: This is equivalent to clicking a button element with `type="submit"` or calling formElement.submit().
120
- */
121
- submit: () => void;
122
- /**
123
- * Returns the current form values as FormData
124
- */
125
- getValues: () => FormData;
126
- };
127
-
128
- /**
129
- * Returns helpers that can be used to update the form state.
130
- *
131
- * @param formId the id of the form. Only necessary if being used outside a ValidatedForm.
132
- */
133
- export const useFormHelpers = (formId?: string): FormHelpers => {
134
- const formContext = useInternalFormContext(formId, "useFormHelpers");
135
- const setTouched = useSetTouched(formContext);
136
- const validateField = useSmartValidate(formContext.formId);
137
- const validate = useValidate(formContext.formId);
138
- const clearError = useClearError(formContext);
139
- const setFieldErrors = useSetFieldErrors(formContext.formId);
140
- const reset = useResetFormElement(formContext.formId);
141
- const submit = useSubmitForm(formContext.formId);
142
- const getValues = useFormValues(formContext.formId);
143
- return useMemo(
144
- () => ({
145
- setTouched,
146
- validateField: async (fieldName: string) => {
147
- const res = await validateField({
148
- alwaysIncludeErrorsFromFields: [fieldName],
149
- });
150
- return res.error?.fieldErrors[fieldName] ?? null;
151
- },
152
- clearError,
153
- validate,
154
- clearAllErrors: () => setFieldErrors({}),
155
- reset,
156
- submit,
157
- getValues,
158
- }),
159
- [
160
- clearError,
161
- reset,
162
- setFieldErrors,
163
- setTouched,
164
- submit,
165
- validate,
166
- validateField,
167
- getValues,
168
- ]
169
- );
170
- };
@@ -1,147 +0,0 @@
1
- import { useCallback, useMemo } from "react";
2
- import {
3
- useInternalFormContext,
4
- useRegisterReceiveFocus,
5
- } from "./internal/hooks";
6
- import { useFormHelpers, useFormState } from "./unreleased/formStateHooks";
7
- import {
8
- FieldErrors,
9
- TouchedFields,
10
- ValidationResult,
11
- } from "./validation/types";
12
-
13
- export type FormContextValue = {
14
- /**
15
- * All the errors in all the fields in the form.
16
- */
17
- fieldErrors: FieldErrors;
18
- /**
19
- * Clear the errors of the specified fields.
20
- */
21
- clearError: (...names: string[]) => void;
22
- /**
23
- * Validate the specified field.
24
- */
25
- validateField: (fieldName: string) => Promise<string | null>;
26
- /**
27
- * The `action` prop of the form.
28
- */
29
- action?: string;
30
- /**
31
- * The `subaction` prop of the form.
32
- */
33
- subaction?: string;
34
- /**
35
- * Whether or not the form is submitting.
36
- */
37
- isSubmitting: boolean;
38
- /**
39
- * Whether or not a submission has been attempted.
40
- * This is true once the form has been submitted, even if there were validation errors.
41
- * Resets to false when the form is reset.
42
- */
43
- hasBeenSubmitted: boolean;
44
- /**
45
- * Whether or not the form is valid.
46
- */
47
- isValid: boolean;
48
- /**
49
- * The default values of the form.
50
- */
51
- defaultValues?: { [fieldName: string]: any };
52
- /**
53
- * Register a custom focus handler to be used when
54
- * the field needs to receive focus due to a validation error.
55
- */
56
- registerReceiveFocus: (fieldName: string, handler: () => void) => () => void;
57
- /**
58
- * Any fields that have been touched by the user.
59
- */
60
- touchedFields: TouchedFields;
61
- /**
62
- * Change the touched state of the specified field.
63
- */
64
- setFieldTouched: (fieldName: string, touched: boolean) => void;
65
- /**
66
- * Validate the whole form and populate any errors.
67
- */
68
- validate: () => Promise<ValidationResult<unknown>>;
69
- /**
70
- * Clears all errors on the form.
71
- */
72
- clearAllErrors: () => void;
73
- /**
74
- * Resets the form.
75
- *
76
- * _Note_: The equivalent behavior can be achieved by calling formElement.reset()
77
- * or clicking a button element with `type="reset"`.
78
- */
79
- reset: () => void;
80
- /**
81
- * Submits the form, running all validations first.
82
- *
83
- * _Note_: This is equivalent to clicking a button element with `type="submit"` or calling formElement.submit().
84
- */
85
- submit: () => void;
86
- /**
87
- * Returns the current form values as FormData
88
- */
89
- getValues: () => FormData;
90
- };
91
-
92
- /**
93
- * Provides access to some of the internal state of the form.
94
- */
95
- export const useFormContext = (formId?: string): FormContextValue => {
96
- // Try to access context so we get our error specific to this hook if it's not there
97
- const context = useInternalFormContext(formId, "useFormContext");
98
- const state = useFormState(formId);
99
- const {
100
- clearError: internalClearError,
101
- setTouched,
102
- validateField,
103
- clearAllErrors,
104
- validate,
105
- reset,
106
- submit,
107
- getValues,
108
- } = useFormHelpers(formId);
109
-
110
- const registerReceiveFocus = useRegisterReceiveFocus(context.formId);
111
-
112
- const clearError = useCallback(
113
- (...names: string[]) => {
114
- names.forEach((name) => {
115
- internalClearError(name);
116
- });
117
- },
118
- [internalClearError]
119
- );
120
-
121
- return useMemo(
122
- () => ({
123
- ...state,
124
- setFieldTouched: setTouched,
125
- validateField,
126
- clearError,
127
- registerReceiveFocus,
128
- clearAllErrors,
129
- validate,
130
- reset,
131
- submit,
132
- getValues,
133
- }),
134
- [
135
- clearAllErrors,
136
- clearError,
137
- registerReceiveFocus,
138
- reset,
139
- setTouched,
140
- state,
141
- submit,
142
- validate,
143
- validateField,
144
- getValues,
145
- ]
146
- );
147
- };
@@ -1,53 +0,0 @@
1
- import * as R from "remeda";
2
- import { CreateValidatorArg, GenericObject, Validator } from "..";
3
- import { FORM_ID_FIELD } from "../internal/constants";
4
- import { objectFromPathEntries } from "../internal/flatten";
5
-
6
- const preprocessFormData = (data: GenericObject | FormData): GenericObject => {
7
- // A slightly janky way of determining if the data is a FormData object
8
- // since node doesn't really have FormData
9
- if ("entries" in data && typeof data.entries === "function")
10
- return objectFromPathEntries([...data.entries()]);
11
- return objectFromPathEntries(Object.entries(data));
12
- };
13
-
14
- const omitInternalFields = (data: GenericObject): GenericObject =>
15
- R.omit(data, [FORM_ID_FIELD]);
16
-
17
- /**
18
- * Used to create a validator for a form.
19
- * It provides built-in handling for unflattening nested objects and
20
- * extracting the values from FormData.
21
- */
22
- export function createValidator<T>(
23
- validator: CreateValidatorArg<T>
24
- ): Validator<T> {
25
- return {
26
- validate: async (value) => {
27
- const data = preprocessFormData(value);
28
- const result = await validator.validate(omitInternalFields(data));
29
-
30
- if (result.error) {
31
- return {
32
- data: undefined,
33
- error: {
34
- fieldErrors: result.error,
35
- subaction: data.subaction,
36
- formId: data[FORM_ID_FIELD],
37
- },
38
- submittedData: data,
39
- formId: data[FORM_ID_FIELD],
40
- };
41
- }
42
-
43
- return {
44
- data: result.data,
45
- error: undefined,
46
- submittedData: data,
47
- formId: data[FORM_ID_FIELD],
48
- };
49
- },
50
- validateField: (data: GenericObject | FormData, field: string) =>
51
- validator.validateField(preprocessFormData(data), field),
52
- };
53
- }