remix-validated-form 4.6.0-beta.0 → 4.6.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 (32) hide show
  1. package/.turbo/turbo-build.log +10 -10
  2. package/browser/ValidatedForm.js +13 -5
  3. package/browser/internal/hooks.d.ts +1 -3
  4. package/browser/internal/logic/requestSubmit.d.ts +5 -0
  5. package/browser/internal/logic/requestSubmit.js +66 -0
  6. package/browser/internal/state/createFormStore.js +2 -1
  7. package/browser/internal/state/fieldArray.d.ts +1 -1
  8. package/browser/internal/state/fieldArray.js +2 -1
  9. package/browser/validation/createValidator.js +3 -1
  10. package/dist/remix-validated-form.cjs.js +4 -4
  11. package/dist/remix-validated-form.cjs.js.map +1 -1
  12. package/dist/remix-validated-form.es.js +796 -1734
  13. package/dist/remix-validated-form.es.js.map +1 -1
  14. package/dist/remix-validated-form.umd.js +4 -4
  15. package/dist/remix-validated-form.umd.js.map +1 -1
  16. package/dist/types/internal/flatten.d.ts +1 -1
  17. package/dist/types/internal/logic/requestSubmit.d.ts +5 -0
  18. package/dist/types/internal/state/fieldArray.d.ts +1 -1
  19. package/package.json +6 -4
  20. package/src/ValidatedForm.tsx +22 -8
  21. package/src/internal/flatten.ts +4 -3
  22. package/src/internal/getInputProps.ts +2 -2
  23. package/src/internal/hooks.ts +3 -3
  24. package/src/internal/logic/requestSubmit.test.tsx +24 -0
  25. package/src/internal/logic/requestSubmit.ts +103 -0
  26. package/src/internal/state/arrayUtil.ts +5 -6
  27. package/src/internal/state/createFormStore.ts +9 -10
  28. package/src/internal/state/fieldArray.tsx +1 -1
  29. package/src/internal/util.ts +2 -2
  30. package/src/validation/createValidator.ts +5 -1
  31. package/src/validation/validation.test.ts +26 -0
  32. package/stats.html +4044 -0
@@ -1 +1 @@
1
- export declare const objectFromPathEntries: (entries: [string, any][]) => {};
1
+ export declare const objectFromPathEntries: (entries: [string, any][]) => Record<string, any>;
@@ -0,0 +1,5 @@
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 declare const requestSubmit: (element: HTMLFormElement, submitter?: HTMLElement | undefined) => void;
@@ -25,4 +25,4 @@ export declare type FieldArrayProps = {
25
25
  formId?: string;
26
26
  validationBehavior?: FieldArrayValidationBehaviorOptions;
27
27
  };
28
- export declare const FieldArray: ({ name, children, formId, validationBehavior, }: FieldArrayProps) => React.ReactNode;
28
+ export declare const FieldArray: ({ name, children, formId, validationBehavior, }: FieldArrayProps) => JSX.Element;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "remix-validated-form",
3
- "version": "4.6.0-beta.0",
3
+ "version": "4.6.1-beta.0",
4
4
  "description": "Form component and utils for easy form validation in remix",
5
5
  "browser": "./dist/remix-validated-form.cjs.js",
6
6
  "main": "./dist/remix-validated-form.umd.js",
@@ -38,17 +38,19 @@
38
38
  },
39
39
  "devDependencies": {
40
40
  "@remix-run/react": "^1.6.5",
41
- "@types/lodash": "^4.14.178",
41
+ "@testing-library/react": "^13.3.0",
42
42
  "@types/react": "^18.0.9",
43
43
  "fetch-blob": "^3.1.3",
44
44
  "react": "^18.1.0",
45
+ "ts-toolbelt": "^9.6.0",
45
46
  "tsconfig": "*",
46
- "typescript": "^4.5.3",
47
+ "typescript": "^4.8.4",
47
48
  "vite-config": "*"
48
49
  },
49
50
  "dependencies": {
50
51
  "immer": "^9.0.12",
51
- "lodash": "^4.17.21",
52
+ "remeda": "^1.2.0",
53
+ "setGet": "*",
52
54
  "tiny-invariant": "^1.2.0",
53
55
  "zustand": "^4.0.0-rc.1"
54
56
  }
@@ -1,5 +1,9 @@
1
- import { Form as RemixForm, useFetcher, useSubmit } from "@remix-run/react";
2
- import uniq from "lodash/uniq";
1
+ import {
2
+ Form as RemixForm,
3
+ FormMethod,
4
+ useFetcher,
5
+ useSubmit,
6
+ } from "@remix-run/react";
3
7
  import React, {
4
8
  ComponentProps,
5
9
  FormEvent,
@@ -10,6 +14,7 @@ import React, {
10
14
  useRef,
11
15
  useState,
12
16
  } from "react";
17
+ import * as R from "remeda";
13
18
  import { useIsSubmitting, useIsValid } from "./hooks";
14
19
  import { FORM_ID_FIELD } from "./internal/constants";
15
20
  import {
@@ -103,7 +108,7 @@ const focusFirstInvalidInput = (
103
108
  })
104
109
  .filter(nonNull)
105
110
  .filter((name) => name in fieldErrors);
106
- const uniqueNamesInOrder = uniq(namesInOrder);
111
+ const uniqueNamesInOrder = R.uniq(namesInOrder);
107
112
 
108
113
  for (const fieldName of uniqueNamesInOrder) {
109
114
  if (customFocusHandlers.has(fieldName)) {
@@ -301,10 +306,16 @@ export function ValidatedForm<DataType>({
301
306
  nativeEvent: HTMLSubmitEvent["nativeEvent"]
302
307
  ) => {
303
308
  startSubmit();
304
- const result = await validator.validate(getDataFromForm(e.currentTarget));
309
+ const submitter = nativeEvent.submitter as HTMLFormSubmitter | null;
310
+ const formDataToValidate = getDataFromForm(e.currentTarget);
311
+ if (submitter?.name) {
312
+ formDataToValidate.append(submitter.name, submitter.value);
313
+ }
314
+
315
+ const result = await validator.validate(formDataToValidate);
305
316
  if (result.error) {
306
- endSubmit();
307
317
  setFieldErrors(result.error.fieldErrors);
318
+ endSubmit();
308
319
  if (!disableFocusOnError) {
309
320
  focusFirstInvalidInput(
310
321
  result.error.fieldErrors,
@@ -313,6 +324,7 @@ export function ValidatedForm<DataType>({
313
324
  );
314
325
  }
315
326
  } else {
327
+ setFieldErrors({});
316
328
  const eventProxy = formEventProxy(e);
317
329
  await onSubmit?.(result.data, eventProxy);
318
330
  if (eventProxy.defaultPrevented) {
@@ -320,15 +332,17 @@ export function ValidatedForm<DataType>({
320
332
  return;
321
333
  }
322
334
 
323
- const submitter = nativeEvent.submitter as HTMLFormSubmitter | null;
324
-
325
335
  // We deviate from the remix code here a bit because of our async submit.
326
336
  // In remix's `FormImpl`, they use `event.currentTarget` to get the form,
327
337
  // but we already have the form in `formRef.current` so we can just use that.
328
338
  // If we use `event.currentTarget` here, it will break because `currentTarget`
329
339
  // will have changed since the start of the submission.
330
340
  if (fetcher) fetcher.submit(submitter || e.currentTarget);
331
- else submit(submitter || target, { replace });
341
+ else
342
+ submit(submitter || target, {
343
+ replace,
344
+ method: (submitter?.formMethod as FormMethod) || method,
345
+ });
332
346
  }
333
347
  };
334
348
 
@@ -1,11 +1,12 @@
1
- import set from "lodash/set";
1
+ import * as R from "remeda";
2
2
  import { MultiValueMap } from "./MultiValueMap";
3
3
 
4
4
  export const objectFromPathEntries = (entries: [string, any][]) => {
5
5
  const map = new MultiValueMap<string, any>();
6
6
  entries.forEach(([key, value]) => map.add(key, value));
7
7
  return [...map.entries()].reduce(
8
- (acc, [key, value]) => set(acc, key, value.length === 1 ? value[0] : value),
9
- {}
8
+ (acc, [key, value]) =>
9
+ R.set(acc, key, value.length === 1 ? value[0] : value),
10
+ {} as Record<string, any>
10
11
  );
11
12
  };
@@ -1,4 +1,4 @@
1
- import omitBy from "lodash/omitBy";
1
+ import * as R from "remeda";
2
2
  import { getCheckboxChecked } from "./logic/getCheckboxChecked";
3
3
  import { getRadioChecked } from "./logic/getRadioChecked";
4
4
 
@@ -89,6 +89,6 @@ export const createGetInputProps = ({
89
89
  inputProps.defaultValue = defaultValue;
90
90
  }
91
91
 
92
- return omitBy(inputProps, (value) => value === undefined) as T;
92
+ return R.omitBy(inputProps, (value) => value === undefined) as T;
93
93
  };
94
94
  };
@@ -1,6 +1,6 @@
1
1
  import { useActionData, useMatches, useTransition } from "@remix-run/react";
2
- import lodashGet from "lodash/get";
3
2
  import { useCallback, useContext } from "react";
3
+ import { getPath } from "setGet";
4
4
  import invariant from "tiny-invariant";
5
5
  import { FieldErrors, ValidationErrorResponseData } from "..";
6
6
  import { formDefaultValuesKey } from "./constants";
@@ -145,7 +145,7 @@ export const useCurrentDefaultValueForField = (
145
145
  formId: InternalFormId,
146
146
  field: string
147
147
  ) =>
148
- useFormStore(formId, (state) => lodashGet(state.currentDefaultValues, field));
148
+ useFormStore(formId, (state) => getPath(state.currentDefaultValues, field));
149
149
 
150
150
  export const useFieldDefaultValue = (
151
151
  name: string,
@@ -154,7 +154,7 @@ export const useFieldDefaultValue = (
154
154
  const defaultValues = useDefaultValuesForForm(context);
155
155
  const state = useCurrentDefaultValueForField(context.formId, name);
156
156
 
157
- return defaultValues.map((val) => lodashGet(val, name)).hydrateTo(state);
157
+ return defaultValues.map((val) => getPath(val, name)).hydrateTo(state);
158
158
  };
159
159
 
160
160
  export const useInternalIsSubmitting = (formId: InternalFormId) =>
@@ -0,0 +1,24 @@
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
+ });
@@ -0,0 +1,103 @@
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,5 +1,4 @@
1
- import lodashGet from "lodash/get";
2
- import lodashSet from "lodash/set";
1
+ import { getPath, setPath } from "setGet";
3
2
  import invariant from "tiny-invariant";
4
3
 
5
4
  ////
@@ -8,10 +7,10 @@ import invariant from "tiny-invariant";
8
7
  ////
9
8
 
10
9
  export const getArray = (values: any, field: string): unknown[] => {
11
- const value = lodashGet(values, field);
10
+ const value = getPath(values, field);
12
11
  if (value === undefined || value === null) {
13
12
  const newValue: unknown[] = [];
14
- lodashSet(values, field, newValue);
13
+ setPath(values, field, newValue);
15
14
  return newValue;
16
15
  }
17
16
  invariant(
@@ -94,8 +93,8 @@ export const mutateAsArray = (
94
93
  for (const [key, value] of Object.entries(obj)) {
95
94
  if (key.startsWith(field) && key !== field) {
96
95
  beforeKeys.add(key);
96
+ setPath(arr, key.substring(field.length), value);
97
97
  }
98
- lodashSet(arr, key.substring(field.length), value);
99
98
  }
100
99
 
101
100
  mutate(arr);
@@ -105,7 +104,7 @@ export const mutateAsArray = (
105
104
 
106
105
  const newKeys = getDeepArrayPaths(arr);
107
106
  for (const key of newKeys) {
108
- const val = lodashGet(arr, key);
107
+ const val = getPath(arr, key);
109
108
  obj[`${field}${key}`] = val;
110
109
  }
111
110
  };
@@ -1,6 +1,5 @@
1
1
  import { WritableDraft } from "immer/dist/internal";
2
- import lodashGet from "lodash/get";
3
- import lodashSet from "lodash/set";
2
+ import { getPath, setPath } from "setGet";
4
3
  import invariant from "tiny-invariant";
5
4
  import create, { GetState } from "zustand";
6
5
  import { immer } from "zustand/middleware/immer";
@@ -10,6 +9,7 @@ import {
10
9
  ValidationResult,
11
10
  Validator,
12
11
  } from "../../validation/types";
12
+ import { requestSubmit } from "../logic/requestSubmit";
13
13
  import * as arrayUtil from "./arrayUtil";
14
14
  import { InternalFormId } from "./types";
15
15
 
@@ -264,7 +264,7 @@ const createFormState = (
264
264
  "Cannot find reference to form. This is probably a bug in remix-validated-form."
265
265
  );
266
266
 
267
- formElement.requestSubmit();
267
+ requestSubmit(formElement);
268
268
  },
269
269
 
270
270
  getValues: () => new FormData(get().formElement ?? undefined),
@@ -300,26 +300,25 @@ const createFormState = (
300
300
 
301
301
  // When nested within a field array, we should leave resetting up to the field array
302
302
  if (!isNested) {
303
- lodashSet(
303
+ setPath(
304
304
  state.controlledFields.values,
305
305
  fieldName,
306
- lodashGet(state.formProps?.defaultValues, fieldName)
306
+ getPath(state.formProps?.defaultValues, fieldName)
307
307
  );
308
- lodashSet(
308
+ setPath(
309
309
  state.currentDefaultValues,
310
310
  fieldName,
311
- lodashGet(state.formProps?.defaultValues, fieldName)
311
+ getPath(state.formProps?.defaultValues, fieldName)
312
312
  );
313
313
  }
314
314
 
315
315
  delete state.controlledFields.refCounts[fieldName];
316
316
  });
317
317
  },
318
- getValue: (fieldName) =>
319
- lodashGet(get().controlledFields.values, fieldName),
318
+ getValue: (fieldName) => getPath(get().controlledFields.values, fieldName),
320
319
  setValue: (fieldName, value) => {
321
320
  set((state) => {
322
- lodashSet(state.controlledFields.values, fieldName, value);
321
+ setPath(state.controlledFields.values, fieldName, value);
323
322
  });
324
323
  get().controlledFields.kickoffValueUpdate(fieldName);
325
324
  },
@@ -151,5 +151,5 @@ export const FieldArray = ({
151
151
  name,
152
152
  validationBehavior
153
153
  );
154
- return children(value, helpers, error);
154
+ return <>{children(value, helpers, error)}</>;
155
155
  };
@@ -1,6 +1,6 @@
1
- import isEqual from "lodash/isEqual";
2
1
  import type React from "react";
3
2
  import { useEffect, useLayoutEffect, useRef } from "react";
3
+ import * as R from "remeda";
4
4
 
5
5
  export const omit = (obj: any, ...keys: string[]) => {
6
6
  const result = { ...obj };
@@ -29,7 +29,7 @@ export const useIsomorphicLayoutEffect =
29
29
 
30
30
  export const useDeepEqualsMemo = <T>(item: T): T => {
31
31
  const ref = useRef<T>(item);
32
- const areEqual = ref.current === item || isEqual(ref.current, item);
32
+ const areEqual = ref.current === item || R.equals(ref.current, item);
33
33
  useEffect(() => {
34
34
  if (!areEqual) {
35
35
  ref.current = item;
@@ -1,3 +1,4 @@
1
+ import * as R from "remeda";
1
2
  import { CreateValidatorArg, GenericObject, Validator } from "..";
2
3
  import { FORM_ID_FIELD } from "../internal/constants";
3
4
  import { objectFromPathEntries } from "../internal/flatten";
@@ -10,6 +11,9 @@ const preprocessFormData = (data: GenericObject | FormData): GenericObject => {
10
11
  return objectFromPathEntries(Object.entries(data));
11
12
  };
12
13
 
14
+ const omitInternalFields = (data: GenericObject): GenericObject =>
15
+ R.omit(data, [FORM_ID_FIELD]);
16
+
13
17
  /**
14
18
  * Used to create a validator for a form.
15
19
  * It provides built-in handling for unflattening nested objects and
@@ -21,7 +25,7 @@ export function createValidator<T>(
21
25
  return {
22
26
  validate: async (value) => {
23
27
  const data = preprocessFormData(value);
24
- const result = await validator.validate(data);
28
+ const result = await validator.validate(omitInternalFields(data));
25
29
 
26
30
  if (result.error) {
27
31
  return {
@@ -1,11 +1,13 @@
1
1
  import { anyString, TestFormData } from "@remix-validated-form/test-utils";
2
2
  import { withYup } from "@remix-validated-form/with-yup/src";
3
3
  import { withZod } from "@remix-validated-form/with-zod";
4
+ import * as R from "remeda";
4
5
  import { Validator } from "remix-validated-form/src";
5
6
  import { objectFromPathEntries } from "remix-validated-form/src/internal/flatten";
6
7
  import { describe, it, expect } from "vitest";
7
8
  import * as yup from "yup";
8
9
  import { z } from "zod";
10
+ import { FORM_ID_FIELD } from "../internal/constants";
9
11
 
10
12
  // If adding an adapter, write a validator that validates this shape
11
13
  type Person = {
@@ -101,6 +103,30 @@ describe("Validation", () => {
101
103
  });
102
104
  });
103
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
+
104
130
  it("should return field errors when invalid", async () => {
105
131
  const obj = { age: "hi!", pets: [{ animal: "dog" }] };
106
132
  expect(await validator.validate(obj)).toEqual({