remix-validated-form 5.0.2 → 5.1.1-beta.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.
Files changed (37) hide show
  1. package/.turbo/turbo-build.log +152 -8
  2. package/dist/index.cjs.js +898 -63
  3. package/dist/index.cjs.js.map +1 -1
  4. package/dist/index.d.ts +7 -2
  5. package/dist/index.esm.js +876 -15
  6. package/dist/index.esm.js.map +1 -1
  7. package/package.json +4 -4
  8. package/src/ValidatedForm.tsx +0 -427
  9. package/src/hooks.ts +0 -160
  10. package/src/index.ts +0 -12
  11. package/src/internal/MultiValueMap.ts +0 -44
  12. package/src/internal/constants.ts +0 -4
  13. package/src/internal/flatten.ts +0 -12
  14. package/src/internal/formContext.ts +0 -13
  15. package/src/internal/getInputProps.test.ts +0 -251
  16. package/src/internal/getInputProps.ts +0 -94
  17. package/src/internal/hooks.ts +0 -217
  18. package/src/internal/hydratable.ts +0 -28
  19. package/src/internal/logic/getCheckboxChecked.ts +0 -10
  20. package/src/internal/logic/getRadioChecked.ts +0 -18
  21. package/src/internal/logic/nestedObjectToPathObject.ts +0 -63
  22. package/src/internal/logic/requestSubmit.test.tsx +0 -24
  23. package/src/internal/logic/requestSubmit.ts +0 -103
  24. package/src/internal/state/arrayUtil.ts +0 -451
  25. package/src/internal/state/controlledFields.ts +0 -86
  26. package/src/internal/state/createFormStore.ts +0 -591
  27. package/src/internal/state/fieldArray.tsx +0 -197
  28. package/src/internal/state/storeHooks.ts +0 -9
  29. package/src/internal/state/types.ts +0 -1
  30. package/src/internal/submissionCallbacks.ts +0 -15
  31. package/src/internal/util.ts +0 -39
  32. package/src/server.ts +0 -53
  33. package/src/unreleased/formStateHooks.ts +0 -170
  34. package/src/userFacingFormContext.ts +0 -147
  35. package/src/validation/createValidator.ts +0 -53
  36. package/src/validation/types.ts +0 -72
  37. package/tsconfig.json +0 -8
@@ -1,24 +0,0 @@
1
- import { render } from "@testing-library/react";
2
- import React, { createRef } from "react";
3
- import { describe, expect, it, vi } from "vitest";
4
- import { requestSubmit } from "./requestSubmit";
5
-
6
- describe("requestSubmit polyfill", () => {
7
- it("should polyfill requestSubmit", () => {
8
- const submit = vi.fn();
9
- const ref = createRef<HTMLFormElement>();
10
- render(
11
- <form
12
- onSubmit={(event) => {
13
- event.preventDefault();
14
- submit();
15
- }}
16
- ref={ref}
17
- >
18
- <input name="test" value="testing" />
19
- </form>
20
- );
21
- requestSubmit(ref.current!);
22
- expect(submit).toHaveBeenCalledTimes(1);
23
- });
24
- });
@@ -1,103 +0,0 @@
1
- /**
2
- * Ponyfill of the HTMLFormElement.requestSubmit() method.
3
- * Based on polyfill from: https://github.com/javan/form-request-submit-polyfill/blob/main/form-request-submit-polyfill.js
4
- */
5
- export const requestSubmit = (
6
- element: HTMLFormElement,
7
- submitter?: HTMLElement
8
- ) => {
9
- // In vitest, let's test the polyfill.
10
- // Cypress will test the native implementation by nature of using chrome.
11
- if (
12
- typeof Object.getPrototypeOf(element).requestSubmit === "function" &&
13
- !import.meta.vitest
14
- ) {
15
- element.requestSubmit(submitter);
16
- return;
17
- }
18
-
19
- if (submitter) {
20
- validateSubmitter(element, submitter);
21
- submitter.click();
22
- return;
23
- }
24
-
25
- const dummySubmitter = document.createElement("input");
26
- dummySubmitter.type = "submit";
27
- dummySubmitter.hidden = true;
28
- element.appendChild(dummySubmitter);
29
- dummySubmitter.click();
30
- element.removeChild(dummySubmitter);
31
- };
32
-
33
- function validateSubmitter(element: HTMLFormElement, submitter: HTMLElement) {
34
- // Should be redundant, but here for completeness
35
- const isHtmlElement = submitter instanceof HTMLElement;
36
- if (!isHtmlElement) {
37
- raise(TypeError, "parameter 1 is not of type 'HTMLElement'");
38
- }
39
-
40
- const hasSubmitType =
41
- "type" in submitter && (submitter as HTMLInputElement).type === "submit";
42
- if (!hasSubmitType)
43
- raise(TypeError, "The specified element is not a submit button");
44
-
45
- const isForCorrectForm =
46
- "form" in submitter && (submitter as HTMLInputElement).form === element;
47
- if (!isForCorrectForm)
48
- raise(
49
- DOMException,
50
- "The specified element is not owned by this form element",
51
- "NotFoundError"
52
- );
53
- }
54
-
55
- interface ErrorConstructor {
56
- new (message: string, name?: string): Error;
57
- }
58
-
59
- function raise(
60
- errorConstructor: ErrorConstructor,
61
- message: string,
62
- name?: string
63
- ): never {
64
- throw new errorConstructor(
65
- "Failed to execute 'requestSubmit' on 'HTMLFormElement': " + message + ".",
66
- name
67
- );
68
- }
69
-
70
- if (import.meta.vitest) {
71
- const { it, expect } = import.meta.vitest;
72
- it("should validate the submitter", () => {
73
- const form = document.createElement("form");
74
- document.body.appendChild(form);
75
-
76
- const submitter = document.createElement("input");
77
- expect(() => validateSubmitter(null as any, null as any)).toThrow();
78
- expect(() => validateSubmitter(form, null as any)).toThrow();
79
- expect(() => validateSubmitter(form, submitter)).toThrow();
80
- expect(() =>
81
- validateSubmitter(form, document.createElement("div"))
82
- ).toThrow();
83
-
84
- submitter.type = "submit";
85
- expect(() => validateSubmitter(form, submitter)).toThrow();
86
-
87
- form.appendChild(submitter);
88
- expect(() => validateSubmitter(form, submitter)).not.toThrow();
89
-
90
- form.removeChild(submitter);
91
- expect(() => validateSubmitter(form, submitter)).toThrow();
92
-
93
- document.body.appendChild(submitter);
94
- form.id = "test-form";
95
- submitter.setAttribute("form", "test-form");
96
- expect(() => validateSubmitter(form, submitter)).not.toThrow();
97
-
98
- const button = document.createElement("button");
99
- button.type = "submit";
100
- form.appendChild(button);
101
- expect(() => validateSubmitter(form, button)).not.toThrow();
102
- });
103
- }
@@ -1,451 +0,0 @@
1
- import { getPath, setPath } from "set-get";
2
- import invariant from "tiny-invariant";
3
-
4
- ////
5
- // All of these array helpers are written in a way that mutates the original array.
6
- // This is because we're working with immer.
7
- ////
8
-
9
- export const getArray = (values: any, field: string): unknown[] => {
10
- const value = getPath(values, field);
11
- if (value === undefined || value === null) {
12
- const newValue: unknown[] = [];
13
- setPath(values, field, newValue);
14
- return newValue;
15
- }
16
- invariant(
17
- Array.isArray(value),
18
- `FieldArray: defaultValue value for ${field} must be an array, null, or undefined`
19
- );
20
- return value;
21
- };
22
-
23
- export const sparseCopy = <T>(array: T[]): T[] => array.slice();
24
-
25
- export const swap = (array: unknown[], indexA: number, indexB: number) => {
26
- const itemA = array[indexA];
27
- const itemB = array[indexB];
28
-
29
- const hasItemA = indexA in array;
30
- const hasItemB = indexB in array;
31
-
32
- // If we're dealing with a sparse array (i.e. one of the indeces doesn't exist),
33
- // we should keep it sparse
34
- if (hasItemA) {
35
- array[indexB] = itemA;
36
- } else {
37
- delete array[indexB];
38
- }
39
-
40
- if (hasItemB) {
41
- array[indexA] = itemB;
42
- } else {
43
- delete array[indexA];
44
- }
45
- };
46
-
47
- // A splice that can handle sparse arrays
48
- function sparseSplice(
49
- array: unknown[],
50
- start: number,
51
- deleteCount?: number,
52
- item?: unknown
53
- ) {
54
- // Inserting an item into an array won't behave as we need it to if the array isn't
55
- // at least as long as the start index. We can force the array to be long enough like this.
56
- if (array.length < start && item) {
57
- array.length = start;
58
- }
59
-
60
- // If we just pass item in, it'll be undefined and splice will delete the item.
61
- if (arguments.length === 4) return array.splice(start, deleteCount!, item);
62
- else if (arguments.length === 3) return array.splice(start, deleteCount);
63
- return array.splice(start);
64
- }
65
-
66
- export const move = (array: unknown[], from: number, to: number) => {
67
- const [item] = sparseSplice(array, from, 1);
68
- sparseSplice(array, to, 0, item);
69
- };
70
-
71
- export const insert = (array: unknown[], index: number, value: unknown) => {
72
- sparseSplice(array, index, 0, value);
73
- };
74
-
75
- export const insertEmpty = (array: unknown[], index: number) => {
76
- const tail = sparseSplice(array, index);
77
- tail.forEach((item, i) => {
78
- sparseSplice(array, index + i + 1, 0, item);
79
- });
80
- };
81
-
82
- export const remove = (array: unknown[], index: number) => {
83
- sparseSplice(array, index, 1);
84
- };
85
-
86
- export const replace = (array: unknown[], index: number, value: unknown) => {
87
- sparseSplice(array, index, 1, value);
88
- };
89
-
90
- /**
91
- * The purpose of this helper is to make it easier to update `fieldErrors` and `touchedFields`.
92
- * We key those objects by full paths to the fields.
93
- * When we're doing array mutations, that makes it difficult to update those objects.
94
- */
95
- export const mutateAsArray = (
96
- field: string,
97
- obj: Record<string, any>,
98
- mutate: (arr: any[]) => void
99
- ) => {
100
- const beforeKeys = new Set<string>();
101
- const arr: any[] = [];
102
-
103
- for (const [key, value] of Object.entries(obj)) {
104
- if (key.startsWith(field) && key !== field) {
105
- beforeKeys.add(key);
106
- setPath(arr, key.substring(field.length), value);
107
- }
108
- }
109
-
110
- mutate(arr);
111
- for (const key of beforeKeys) {
112
- delete obj[key];
113
- }
114
-
115
- const newKeys = getDeepArrayPaths(arr);
116
- for (const key of newKeys) {
117
- const val = getPath(arr, key);
118
- if (val !== undefined) {
119
- obj[`${field}${key}`] = val;
120
- }
121
- }
122
- };
123
-
124
- const getDeepArrayPaths = (obj: any, basePath: string = ""): string[] => {
125
- // This only needs to handle arrays and plain objects
126
- // and we can assume the first call is always an array.
127
-
128
- if (Array.isArray(obj)) {
129
- return obj.flatMap((item, index) =>
130
- getDeepArrayPaths(item, `${basePath}[${index}]`)
131
- );
132
- }
133
-
134
- if (typeof obj === "object") {
135
- return Object.keys(obj).flatMap((key) =>
136
- getDeepArrayPaths(obj[key], `${basePath}.${key}`)
137
- );
138
- }
139
-
140
- return [basePath];
141
- };
142
-
143
- if (import.meta.vitest) {
144
- const { describe, expect, it } = import.meta.vitest;
145
-
146
- // Count the actual number of items in the array
147
- // instead of just getting the length.
148
- // This is useful for validating that sparse arrays are handled correctly.
149
- const countArrayItems = (arr: any[]) => {
150
- let count = 0;
151
- arr.forEach(() => count++);
152
- return count;
153
- };
154
-
155
- describe("getArray", () => {
156
- it("shoud get a deeply nested array that can be mutated to update the nested value", () => {
157
- const values = {
158
- d: [
159
- { foo: "bar", baz: [true, false] },
160
- { e: true, f: "hi" },
161
- ],
162
- };
163
- const result = getArray(values, "d[0].baz");
164
- const finalValues = {
165
- d: [
166
- { foo: "bar", baz: [true, false, true] },
167
- { e: true, f: "hi" },
168
- ],
169
- };
170
-
171
- expect(result).toEqual([true, false]);
172
- result.push(true);
173
- expect(values).toEqual(finalValues);
174
- });
175
-
176
- it("should return an empty array that can be mutated if result is null or undefined", () => {
177
- const values = {};
178
- const result = getArray(values, "a.foo[0].bar");
179
- const finalValues = {
180
- a: { foo: [{ bar: ["Bob ross"] }] },
181
- };
182
-
183
- expect(result).toEqual([]);
184
- result.push("Bob ross");
185
- expect(values).toEqual(finalValues);
186
- });
187
-
188
- it("should throw if the value is defined and not an array", () => {
189
- const values = { foo: "foo" };
190
- expect(() => getArray(values, "foo")).toThrow();
191
- });
192
- });
193
-
194
- describe("swap", () => {
195
- it("should swap two items", () => {
196
- const array = [1, 2, 3];
197
- swap(array, 0, 1);
198
- expect(array).toEqual([2, 1, 3]);
199
- });
200
-
201
- it("should work for sparse arrays", () => {
202
- // A bit of a sanity check for native array behavior
203
- const arr = [] as any[];
204
- arr[0] = true;
205
- swap(arr, 0, 2);
206
-
207
- expect(countArrayItems(arr)).toEqual(1);
208
- expect(0 in arr).toBe(false);
209
- expect(2 in arr).toBe(true);
210
- expect(arr[2]).toEqual(true);
211
- });
212
- });
213
-
214
- describe("move", () => {
215
- it("should move an item to a new index", () => {
216
- const array = [1, 2, 3];
217
- move(array, 0, 1);
218
- expect(array).toEqual([2, 1, 3]);
219
- });
220
-
221
- it("should work with sparse arrays", () => {
222
- const array = [1];
223
- move(array, 0, 2);
224
-
225
- expect(countArrayItems(array)).toEqual(1);
226
- expect(array).toEqual([undefined, undefined, 1]);
227
- });
228
- });
229
-
230
- describe("insert", () => {
231
- it("should insert an item at a new index", () => {
232
- const array = [1, 2, 3];
233
- insert(array, 1, 4);
234
- expect(array).toEqual([1, 4, 2, 3]);
235
- });
236
-
237
- it("should be able to insert falsey values", () => {
238
- const array = [1, 2, 3];
239
- insert(array, 1, null);
240
- expect(array).toEqual([1, null, 2, 3]);
241
- });
242
-
243
- it("should handle sparse arrays", () => {
244
- const array: any[] = [];
245
- array[2] = true;
246
- insert(array, 0, true);
247
-
248
- expect(countArrayItems(array)).toEqual(2);
249
- expect(array).toEqual([true, undefined, undefined, true]);
250
- });
251
- });
252
-
253
- describe("insertEmpty", () => {
254
- it("should insert an empty item at a given index", () => {
255
- const array = [1, 2, 3];
256
- insertEmpty(array, 1);
257
- // eslint-disable-next-line no-sparse-arrays
258
- expect(array).toStrictEqual([1, , 2, 3]);
259
- expect(array).not.toStrictEqual([1, undefined, 2, 3]);
260
- });
261
-
262
- it("should work with already sparse arrays", () => {
263
- // eslint-disable-next-line no-sparse-arrays
264
- const array = [, , 1, , 2, , 3];
265
- insertEmpty(array, 3);
266
- // eslint-disable-next-line no-sparse-arrays
267
- expect(array).toStrictEqual([, , 1, , , 2, , 3]);
268
- expect(array).not.toStrictEqual([
269
- undefined,
270
- undefined,
271
- 1,
272
- undefined,
273
- undefined,
274
- 2,
275
- undefined,
276
- 3,
277
- ]);
278
- });
279
- });
280
-
281
- describe("remove", () => {
282
- it("should remove an item at a given index", () => {
283
- const array = [1, 2, 3];
284
- remove(array, 1);
285
- expect(array).toEqual([1, 3]);
286
- });
287
-
288
- it("should handle sparse arrays", () => {
289
- const array: any[] = [];
290
- array[2] = true;
291
- remove(array, 0);
292
-
293
- expect(countArrayItems(array)).toEqual(1);
294
- expect(array).toEqual([undefined, true]);
295
- });
296
- });
297
-
298
- describe("replace", () => {
299
- it("should replace an item at a given index", () => {
300
- const array = [1, 2, 3];
301
- replace(array, 1, 4);
302
- expect(array).toEqual([1, 4, 3]);
303
- });
304
-
305
- it("should handle sparse arrays", () => {
306
- const array: any[] = [];
307
- array[2] = true;
308
- replace(array, 0, true);
309
- expect(countArrayItems(array)).toEqual(2);
310
- expect(array).toEqual([true, undefined, true]);
311
- });
312
- });
313
-
314
- describe("mutateAsArray", () => {
315
- it("should handle swap", () => {
316
- const values = {
317
- myField: "something",
318
- "myField[0]": "foo",
319
- "myField[2]": "bar",
320
- otherField: "baz",
321
- "otherField[0]": "something else",
322
- };
323
- mutateAsArray("myField", values, (arr) => {
324
- swap(arr, 0, 2);
325
- });
326
- expect(values).toEqual({
327
- myField: "something",
328
- "myField[0]": "bar",
329
- "myField[2]": "foo",
330
- otherField: "baz",
331
- "otherField[0]": "something else",
332
- });
333
- });
334
-
335
- it("should swap sparse arrays", () => {
336
- const values = {
337
- myField: "something",
338
- "myField[0]": "foo",
339
- otherField: "baz",
340
- "otherField[0]": "something else",
341
- };
342
- mutateAsArray("myField", values, (arr) => {
343
- swap(arr, 0, 2);
344
- });
345
- expect(values).toEqual({
346
- myField: "something",
347
- "myField[2]": "foo",
348
- otherField: "baz",
349
- "otherField[0]": "something else",
350
- });
351
- });
352
-
353
- it("should handle arrays with nested values", () => {
354
- const values = {
355
- myField: "something",
356
- "myField[0].title": "foo",
357
- "myField[0].note": "bar",
358
- "myField[2].title": "other",
359
- "myField[2].note": "other",
360
- otherField: "baz",
361
- "otherField[0]": "something else",
362
- };
363
- mutateAsArray("myField", values, (arr) => {
364
- swap(arr, 0, 2);
365
- });
366
- expect(values).toEqual({
367
- myField: "something",
368
- "myField[0].title": "other",
369
- "myField[0].note": "other",
370
- "myField[2].title": "foo",
371
- "myField[2].note": "bar",
372
- otherField: "baz",
373
- "otherField[0]": "something else",
374
- });
375
- });
376
-
377
- it("should handle move", () => {
378
- const values = {
379
- myField: "something",
380
- "myField[0]": "foo",
381
- "myField[1]": "bar",
382
- "myField[2]": "baz",
383
- "otherField[0]": "something else",
384
- };
385
- mutateAsArray("myField", values, (arr) => {
386
- move(arr, 0, 2);
387
- });
388
- expect(values).toEqual({
389
- myField: "something",
390
- "myField[0]": "bar",
391
- "myField[1]": "baz",
392
- "myField[2]": "foo",
393
- "otherField[0]": "something else",
394
- });
395
- });
396
-
397
- it("should not create keys for `undefined`", () => {
398
- const values = {
399
- "myField[0]": "foo",
400
- };
401
- mutateAsArray("myField", values, (arr) => {
402
- arr.unshift(undefined);
403
- });
404
- expect(Object.keys(values)).toHaveLength(1);
405
- expect(values).toEqual({
406
- "myField[1]": "foo",
407
- });
408
- });
409
-
410
- it("should handle remove", () => {
411
- const values = {
412
- myField: "something",
413
- "myField[0]": "foo",
414
- "myField[1]": "bar",
415
- "myField[2]": "baz",
416
- "otherField[0]": "something else",
417
- };
418
- mutateAsArray("myField", values, (arr) => {
419
- remove(arr, 1);
420
- });
421
- expect(values).toEqual({
422
- myField: "something",
423
- "myField[0]": "foo",
424
- "myField[1]": "baz",
425
- "otherField[0]": "something else",
426
- });
427
- expect("myField[2]" in values).toBe(false);
428
- });
429
- });
430
-
431
- describe("getDeepArrayPaths", () => {
432
- it("should return all paths recursively", () => {
433
- const obj = [
434
- true,
435
- true,
436
- [true, true],
437
- { foo: true, bar: { baz: true, test: [true] } },
438
- ];
439
-
440
- expect(getDeepArrayPaths(obj, "myField")).toEqual([
441
- "myField[0]",
442
- "myField[1]",
443
- "myField[2][0]",
444
- "myField[2][1]",
445
- "myField[3].foo",
446
- "myField[3].bar.baz",
447
- "myField[3].bar.test[0]",
448
- ]);
449
- });
450
- });
451
- }
@@ -1,86 +0,0 @@
1
- import { useCallback, useEffect } from "react";
2
- import { InternalFormContextValue } from "../formContext";
3
- import { useFieldDefaultValue } from "../hooks";
4
- import { useFormStore } from "./storeHooks";
5
- import { InternalFormId } from "./types";
6
-
7
- export const useControlledFieldValue = (
8
- context: InternalFormContextValue,
9
- field: string
10
- ) => {
11
- const value = useFormStore(context.formId, (state) =>
12
- state.controlledFields.getValue(field)
13
- );
14
- const isFormHydrated = useFormStore(
15
- context.formId,
16
- (state) => state.isHydrated
17
- );
18
- const defaultValue = useFieldDefaultValue(field, context);
19
-
20
- return isFormHydrated ? value : defaultValue;
21
- };
22
-
23
- export const useRegisterControlledField = (
24
- context: InternalFormContextValue,
25
- field: string
26
- ) => {
27
- const resolveUpdate = useFormStore(
28
- context.formId,
29
- (state) => state.controlledFields.valueUpdateResolvers[field]
30
- );
31
- useEffect(() => {
32
- resolveUpdate?.();
33
- }, [resolveUpdate]);
34
-
35
- const register = useFormStore(
36
- context.formId,
37
- (state) => state.controlledFields.register
38
- );
39
- const unregister = useFormStore(
40
- context.formId,
41
- (state) => state.controlledFields.unregister
42
- );
43
- useEffect(() => {
44
- register(field);
45
- return () => unregister(field);
46
- }, [context.formId, field, register, unregister]);
47
- };
48
-
49
- export const useControllableValue = (
50
- context: InternalFormContextValue,
51
- field: string
52
- ) => {
53
- useRegisterControlledField(context, field);
54
-
55
- const setControlledFieldValue = useFormStore(
56
- context.formId,
57
- (state) => state.controlledFields.setValue
58
- );
59
- const setValue = useCallback(
60
- (value: unknown) => setControlledFieldValue(field, value),
61
- [field, setControlledFieldValue]
62
- );
63
-
64
- const value = useControlledFieldValue(context, field);
65
-
66
- return [value, setValue] as const;
67
- };
68
-
69
- export const useUpdateControllableValue = (formId: InternalFormId) => {
70
- const setValue = useFormStore(
71
- formId,
72
- (state) => state.controlledFields.setValue
73
- );
74
- return useCallback(
75
- (field: string, value: unknown) => setValue(field, value),
76
- [setValue]
77
- );
78
- };
79
-
80
- export const useAwaitValue = (formId: InternalFormId) => {
81
- const awaitValue = useFormStore(
82
- formId,
83
- (state) => state.controlledFields.awaitValueUpdate
84
- );
85
- return useCallback((field: string) => awaitValue(field), [awaitValue]);
86
- };