remix-validated-form 5.0.2 → 5.1.1-beta.0

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