remix-validated-form 4.6.0-beta.0 → 4.6.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.
@@ -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.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,6 +38,7 @@
38
38
  },
39
39
  "devDependencies": {
40
40
  "@remix-run/react": "^1.6.5",
41
+ "@testing-library/react": "^13.3.0",
41
42
  "@types/lodash": "^4.14.178",
42
43
  "@types/react": "^18.0.9",
43
44
  "fetch-blob": "^3.1.3",
@@ -1,4 +1,9 @@
1
- import { Form as RemixForm, useFetcher, useSubmit } from "@remix-run/react";
1
+ import {
2
+ Form as RemixForm,
3
+ FormMethod,
4
+ useFetcher,
5
+ useSubmit,
6
+ } from "@remix-run/react";
2
7
  import uniq from "lodash/uniq";
3
8
  import React, {
4
9
  ComponentProps,
@@ -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
 
@@ -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
+ }
@@ -10,7 +10,9 @@ import {
10
10
  ValidationResult,
11
11
  Validator,
12
12
  } from "../../validation/types";
13
+ import { requestSubmit } from "../logic/requestSubmit";
13
14
  import * as arrayUtil from "./arrayUtil";
15
+ import { useControlledFieldStore } from "./controlledFieldStore";
14
16
  import { InternalFormId } from "./types";
15
17
 
16
18
  export type SyncedFormProps = {
@@ -264,7 +266,7 @@ const createFormState = (
264
266
  "Cannot find reference to form. This is probably a bug in remix-validated-form."
265
267
  );
266
268
 
267
- formElement.requestSubmit();
269
+ requestSubmit(formElement);
268
270
  },
269
271
 
270
272
  getValues: () => new FormData(get().formElement ?? undefined),
@@ -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,3 +1,4 @@
1
+ import omit from "lodash/omit";
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
+ 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 omit from "lodash/omit";
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: omit(person, 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({