remix-validated-form 4.6.12 → 5.0.1

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,4 +1,5 @@
1
- import React, { useMemo } from "react";
1
+ import { nanoid } from "nanoid";
2
+ import React, { useMemo, useRef, useState } from "react";
2
3
  import { useCallback } from "react";
3
4
  import invariant from "tiny-invariant";
4
5
  import { InternalFormContextValue } from "../formContext";
@@ -7,8 +8,9 @@ import {
7
8
  useFieldError,
8
9
  useInternalFormContext,
9
10
  useInternalHasBeenSubmitted,
10
- useValidateField,
11
+ useSmartValidate,
11
12
  } from "../hooks";
13
+ import * as arrayUtil from "./arrayUtil";
12
14
  import { useRegisterControlledField } from "./controlledFields";
13
15
  import { useFormStore } from "./storeHooks";
14
16
 
@@ -19,6 +21,19 @@ export type FieldArrayValidationBehaviorOptions = {
19
21
  whenSubmitted: FieldArrayValidationBehavior;
20
22
  };
21
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
+
22
37
  const useInternalFieldArray = (
23
38
  context: InternalFormContextValue,
24
39
  field: string,
@@ -27,7 +42,7 @@ const useInternalFieldArray = (
27
42
  const value = useFieldDefaultValue(field, context);
28
43
  useRegisterControlledField(context, field);
29
44
  const hasBeenSubmitted = useInternalHasBeenSubmitted(context.formId);
30
- const validateField = useValidateField(context.formId);
45
+ const validateField = useSmartValidate(context.formId);
31
46
  const error = useFieldError(field, context);
32
47
 
33
48
  const resolvedValidationBehavior: FieldArrayValidationBehaviorOptions = {
@@ -42,7 +57,7 @@ const useInternalFieldArray = (
42
57
 
43
58
  const maybeValidate = useCallback(() => {
44
59
  if (behavior === "onChange") {
45
- validateField(field);
60
+ validateField({ alwaysIncludeErrorsFromFields: [field] });
46
61
  }
47
62
  }, [behavior, field, validateField]);
48
63
 
@@ -56,47 +71,74 @@ const useInternalFieldArray = (
56
71
  (state) => state.controlledFields.array
57
72
  );
58
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
+
59
84
  const helpers = useMemo(
60
85
  () => ({
61
86
  push: (item: any) => {
62
87
  arr.push(field, item);
88
+ keyRef.current.push(nanoid());
63
89
  maybeValidate();
64
90
  },
65
91
  swap: (indexA: number, indexB: number) => {
66
92
  arr.swap(field, indexA, indexB);
93
+ arrayUtil.swap(keyRef.current, indexA, indexB);
67
94
  maybeValidate();
68
95
  },
69
96
  move: (from: number, to: number) => {
70
97
  arr.move(field, from, to);
98
+ arrayUtil.move(keyRef.current, from, to);
71
99
  maybeValidate();
72
100
  },
73
101
  insert: (index: number, value: any) => {
74
102
  arr.insert(field, index, value);
103
+ arrayUtil.insert(keyRef.current, index, nanoid());
75
104
  maybeValidate();
76
105
  },
77
106
  unshift: (value: any) => {
78
107
  arr.unshift(field, value);
108
+ keyRef.current.unshift(nanoid());
79
109
  maybeValidate();
80
110
  },
81
111
  remove: (index: number) => {
82
112
  arr.remove(field, index);
113
+ arrayUtil.remove(keyRef.current, index);
83
114
  maybeValidate();
84
115
  },
85
116
  pop: () => {
86
117
  arr.pop(field);
118
+ keyRef.current.pop();
87
119
  maybeValidate();
88
120
  },
89
121
  replace: (index: number, value: any) => {
90
122
  arr.replace(field, index, value);
123
+ keyRef.current[index] = nanoid();
91
124
  maybeValidate();
92
125
  },
93
126
  }),
94
127
  [arr, field, maybeValidate]
95
128
  );
96
129
 
97
- const arrayValue = useMemo(() => value ?? [], [value]);
98
-
99
- return [arrayValue, helpers, error] as const;
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;
100
142
  };
101
143
 
102
144
  export type FieldArrayHelpers<Item = any> = {
@@ -122,29 +164,29 @@ export function useFieldArray<Item = any>(
122
164
  const context = useInternalFormContext(formId, "FieldArray");
123
165
 
124
166
  return useInternalFieldArray(context, name, validationBehavior) as [
125
- itemDefaults: Item[],
167
+ items: FieldArrayItem<Item>[],
126
168
  helpers: FieldArrayHelpers,
127
169
  error: string | undefined
128
170
  ];
129
171
  }
130
172
 
131
- export type FieldArrayProps = {
173
+ export type FieldArrayProps<Item> = {
132
174
  name: string;
133
175
  children: (
134
- itemDefaults: any[],
135
- helpers: FieldArrayHelpers,
176
+ items: FieldArrayItem<Item>[],
177
+ helpers: FieldArrayHelpers<Item>,
136
178
  error: string | undefined
137
179
  ) => React.ReactNode;
138
180
  formId?: string;
139
181
  validationBehavior?: FieldArrayValidationBehaviorOptions;
140
182
  };
141
183
 
142
- export const FieldArray = ({
184
+ export function FieldArray<Item = any>({
143
185
  name,
144
186
  children,
145
187
  formId,
146
188
  validationBehavior,
147
- }: FieldArrayProps) => {
189
+ }: FieldArrayProps<Item>) {
148
190
  const context = useInternalFormContext(formId, "FieldArray");
149
191
  const [value, helpers, error] = useInternalFieldArray(
150
192
  context,
@@ -152,4 +194,4 @@ export const FieldArray = ({
152
194
  validationBehavior
153
195
  );
154
196
  return <>{children(value, helpers, error)}</>;
155
- };
197
+ }
package/src/server.ts CHANGED
@@ -42,6 +42,9 @@ export type FormDefaults = {
42
42
  [formDefaultsKey: `${typeof FORM_DEFAULTS_FIELD}_${string}`]: any;
43
43
  };
44
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
+
45
48
  export const setFormDefaults = <DataType = any>(
46
49
  formId: string,
47
50
  defaultValues: Partial<DataType>
@@ -11,7 +11,6 @@ import {
11
11
  useTouchedFields,
12
12
  useInternalIsValid,
13
13
  useFieldErrors,
14
- useValidateField,
15
14
  useValidate,
16
15
  useSetFieldErrors,
17
16
  useResetFormElement,
@@ -20,6 +19,7 @@ import {
20
19
  useFormSubactionProp,
21
20
  useSubmitForm,
22
21
  useFormValues,
22
+ useSmartValidate,
23
23
  } from "../internal/hooks";
24
24
  import {
25
25
  FieldErrors,
@@ -133,7 +133,7 @@ export type FormHelpers = {
133
133
  export const useFormHelpers = (formId?: string): FormHelpers => {
134
134
  const formContext = useInternalFormContext(formId, "useFormHelpers");
135
135
  const setTouched = useSetTouched(formContext);
136
- const validateField = useValidateField(formContext.formId);
136
+ const validateField = useSmartValidate(formContext.formId);
137
137
  const validate = useValidate(formContext.formId);
138
138
  const clearError = useClearError(formContext);
139
139
  const setFieldErrors = useSetFieldErrors(formContext.formId);
@@ -143,7 +143,12 @@ export const useFormHelpers = (formId?: string): FormHelpers => {
143
143
  return useMemo(
144
144
  () => ({
145
145
  setTouched,
146
- validateField,
146
+ validateField: async (fieldName: string) => {
147
+ const res = await validateField({
148
+ alwaysIncludeErrorsFromFields: [fieldName],
149
+ });
150
+ return res.error?.fieldErrors[fieldName] ?? null;
151
+ },
147
152
  clearError,
148
153
  validate,
149
154
  clearAllErrors: () => setFieldErrors({}),
@@ -44,7 +44,10 @@ export type Validator<DataType> = {
44
44
  validate: (
45
45
  unvalidatedData: GenericObject
46
46
  ) => Promise<ValidationResult<DataType>>;
47
- validateField: (
47
+ /**
48
+ * @deprecated Will be removed in a future version of remix-validated-form
49
+ */
50
+ validateField?: (
48
51
  unvalidatedData: GenericObject,
49
52
  field: string
50
53
  ) => Promise<ValidateFieldResult>;
package/tsconfig.json CHANGED
@@ -4,5 +4,5 @@
4
4
  "module": "esnext"
5
5
  },
6
6
  "include": ["src/**/*.ts", "src/**/*.tsx"],
7
- "exclude": ["node_modules", "**/*.test.ts", "**/*.test.tsx"]
7
+ "exclude": ["node_modules"]
8
8
  }
@@ -1,330 +0,0 @@
1
- import { anyString, TestFormData } from "@remix-validated-form/test-utils";
2
- import { withYup } from "@remix-validated-form/with-yup/src";
3
- import { withZod } from "@remix-validated-form/with-zod";
4
- import * as R from "remeda";
5
- import { Validator } from "remix-validated-form/src";
6
- import { objectFromPathEntries } from "remix-validated-form/src/internal/flatten";
7
- import { describe, it, expect } from "vitest";
8
- import * as yup from "yup";
9
- import { z } from "zod";
10
- import { FORM_ID_FIELD } from "../internal/constants";
11
-
12
- // If adding an adapter, write a validator that validates this shape
13
- type Person = {
14
- firstName: string;
15
- lastName: string;
16
- age?: number;
17
- address: {
18
- streetAddress: string;
19
- city: string;
20
- country: string;
21
- };
22
- pets?: {
23
- animal: string;
24
- name: string;
25
- }[];
26
- };
27
-
28
- type ValidationTestCase = {
29
- name: string;
30
- validator: Validator<Person>;
31
- };
32
-
33
- const validationTestCases: ValidationTestCase[] = [
34
- {
35
- name: "yup",
36
- validator: withYup(
37
- yup.object({
38
- firstName: yup.string().required(),
39
- lastName: yup.string().required(),
40
- age: yup.number(),
41
- address: yup
42
- .object({
43
- streetAddress: yup.string().required(),
44
- city: yup.string().required(),
45
- country: yup.string().required(),
46
- })
47
- .required(),
48
- pets: yup.array().of(
49
- yup.object({
50
- animal: yup.string().required(),
51
- name: yup.string().required(),
52
- })
53
- ),
54
- })
55
- ),
56
- },
57
- {
58
- name: "zod",
59
- validator: withZod(
60
- z.object({
61
- firstName: z.string().min(1),
62
- lastName: z.string().min(1),
63
- age: z.optional(z.number()),
64
- address: z.preprocess(
65
- (value) => (value == null ? {} : value),
66
- z.object({
67
- streetAddress: z.string().min(1),
68
- city: z.string().min(1),
69
- country: z.string().min(1),
70
- })
71
- ),
72
- pets: z
73
- .object({
74
- animal: z.string().min(1),
75
- name: z.string().min(1),
76
- })
77
- .array()
78
- .optional(),
79
- })
80
- ),
81
- },
82
- ];
83
-
84
- describe("Validation", () => {
85
- describe.each(validationTestCases)("Adapter for $name", ({ validator }) => {
86
- describe("validate", () => {
87
- it("should return the data when valid", async () => {
88
- const person: Person = {
89
- firstName: "John",
90
- lastName: "Doe",
91
- age: 30,
92
- address: {
93
- streetAddress: "123 Main St",
94
- city: "Anytown",
95
- country: "USA",
96
- },
97
- pets: [{ animal: "dog", name: "Fido" }],
98
- };
99
- expect(await validator.validate(person)).toEqual({
100
- data: person,
101
- error: undefined,
102
- submittedData: person,
103
- });
104
- });
105
-
106
- it("should omit internal fields", async () => {
107
- const person: Person = {
108
- firstName: "John",
109
- lastName: "Doe",
110
- age: 30,
111
- address: {
112
- streetAddress: "123 Main St",
113
- city: "Anytown",
114
- country: "USA",
115
- },
116
- pets: [{ animal: "dog", name: "Fido" }],
117
-
118
- // @ts-expect-error
119
- // internal filed technically not part of person type
120
- [FORM_ID_FIELD]: "something",
121
- };
122
- expect(await validator.validate(person)).toEqual({
123
- data: R.omit(person as any, [FORM_ID_FIELD]),
124
- error: undefined,
125
- submittedData: person,
126
- formId: "something",
127
- });
128
- });
129
-
130
- it("should return field errors when invalid", async () => {
131
- const obj = { age: "hi!", pets: [{ animal: "dog" }] };
132
- expect(await validator.validate(obj)).toEqual({
133
- data: undefined,
134
- error: {
135
- fieldErrors: {
136
- firstName: anyString,
137
- lastName: anyString,
138
- age: anyString,
139
- "address.city": anyString,
140
- "address.country": anyString,
141
- "address.streetAddress": anyString,
142
- "pets[0].name": anyString,
143
- },
144
- subaction: undefined,
145
- },
146
- submittedData: obj,
147
- });
148
- });
149
-
150
- it("should unflatten data when validating", async () => {
151
- const data = {
152
- firstName: "John",
153
- lastName: "Doe",
154
- age: 30,
155
- "address.streetAddress": "123 Main St",
156
- "address.city": "Anytown",
157
- "address.country": "USA",
158
- "pets[0].animal": "dog",
159
- "pets[0].name": "Fido",
160
- };
161
- expect(await validator.validate(data)).toEqual({
162
- data: {
163
- firstName: "John",
164
- lastName: "Doe",
165
- age: 30,
166
- address: {
167
- streetAddress: "123 Main St",
168
- city: "Anytown",
169
- country: "USA",
170
- },
171
- pets: [{ animal: "dog", name: "Fido" }],
172
- },
173
- error: undefined,
174
- submittedData: objectFromPathEntries(Object.entries(data)),
175
- });
176
- });
177
-
178
- it("should accept FormData directly and return errors", async () => {
179
- const formData = new TestFormData();
180
- formData.set("firstName", "John");
181
- formData.set("lastName", "Doe");
182
- formData.set("address.streetAddress", "123 Main St");
183
- formData.set("address.country", "USA");
184
- formData.set("pets[0].animal", "dog");
185
-
186
- expect(await validator.validate(formData)).toEqual({
187
- data: undefined,
188
- error: {
189
- fieldErrors: {
190
- "address.city": anyString,
191
- "pets[0].name": anyString,
192
- },
193
- subaction: undefined,
194
- },
195
- submittedData: objectFromPathEntries([...formData.entries()]),
196
- });
197
- });
198
-
199
- it("should accept FormData directly and return valid data", async () => {
200
- const formData = new TestFormData();
201
- formData.set("firstName", "John");
202
- formData.set("lastName", "Doe");
203
- formData.set("address.streetAddress", "123 Main St");
204
- formData.set("address.country", "USA");
205
- formData.set("address.city", "Anytown");
206
- formData.set("pets[0].animal", "dog");
207
- formData.set("pets[0].name", "Fido");
208
-
209
- expect(await validator.validate(formData)).toEqual({
210
- data: {
211
- firstName: "John",
212
- lastName: "Doe",
213
- address: {
214
- streetAddress: "123 Main St",
215
- country: "USA",
216
- city: "Anytown",
217
- },
218
- pets: [{ animal: "dog", name: "Fido" }],
219
- },
220
- error: undefined,
221
- subaction: undefined,
222
- submittedData: objectFromPathEntries([...formData.entries()]),
223
- });
224
- });
225
-
226
- it("should return the subaction in the ValidatorError if there is one", async () => {
227
- const person = {
228
- lastName: "Doe",
229
- age: 20,
230
- address: {
231
- streetAddress: "123 Main St",
232
- city: "Anytown",
233
- country: "USA",
234
- },
235
- pets: [{ animal: "dog", name: "Fido" }],
236
- subaction: "updatePerson",
237
- };
238
- expect(await validator.validate(person)).toEqual({
239
- error: {
240
- fieldErrors: {
241
- firstName: anyString,
242
- },
243
- subaction: "updatePerson",
244
- },
245
- data: undefined,
246
- submittedData: person,
247
- });
248
- });
249
- });
250
-
251
- describe("validateField", () => {
252
- it("should not return an error if field is valid", async () => {
253
- const person = {
254
- firstName: "John",
255
- lastName: {}, // invalid, but we should only be validating firstName
256
- };
257
- expect(await validator.validateField(person, "firstName")).toEqual({
258
- error: undefined,
259
- });
260
- });
261
- it("should not return an error if a nested field is valid", async () => {
262
- const person = {
263
- firstName: "John",
264
- lastName: {}, // invalid, but we should only be validating firstName
265
- address: {
266
- streetAddress: "123 Main St",
267
- city: "Anytown",
268
- country: "USA",
269
- },
270
- pets: [{ animal: "dog", name: "Fido" }],
271
- };
272
- expect(
273
- await validator.validateField(person, "address.streetAddress")
274
- ).toEqual({
275
- error: undefined,
276
- });
277
- expect(await validator.validateField(person, "address.city")).toEqual({
278
- error: undefined,
279
- });
280
- expect(
281
- await validator.validateField(person, "address.country")
282
- ).toEqual({
283
- error: undefined,
284
- });
285
- expect(await validator.validateField(person, "pets[0].animal")).toEqual(
286
- {
287
- error: undefined,
288
- }
289
- );
290
- expect(await validator.validateField(person, "pets[0].name")).toEqual({
291
- error: undefined,
292
- });
293
- });
294
-
295
- it("should return an error if field is invalid", async () => {
296
- const person = {
297
- firstName: "John",
298
- lastName: {},
299
- address: {
300
- streetAddress: "123 Main St",
301
- city: 1234,
302
- },
303
- };
304
- expect(await validator.validateField(person, "lastName")).toEqual({
305
- error: anyString,
306
- });
307
- });
308
-
309
- it("should return an error if a nested field is invalid", async () => {
310
- const person = {
311
- firstName: "John",
312
- lastName: {},
313
- address: {
314
- streetAddress: "123 Main St",
315
- city: 1234,
316
- },
317
- pets: [{ animal: "dog" }],
318
- };
319
- expect(
320
- await validator.validateField(person, "address.country")
321
- ).toEqual({
322
- error: anyString,
323
- });
324
- expect(await validator.validateField(person, "pets[0].name")).toEqual({
325
- error: anyString,
326
- });
327
- });
328
- });
329
- });
330
- });