remix-validated-form 5.0.2 → 5.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
- }