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.
- package/.turbo/turbo-build.log +10 -10
- package/browser/ValidatedForm.js +13 -5
- package/browser/internal/hooks.d.ts +1 -3
- package/browser/internal/logic/requestSubmit.d.ts +5 -0
- package/browser/internal/logic/requestSubmit.js +66 -0
- package/browser/internal/state/createFormStore.js +2 -1
- package/browser/internal/state/fieldArray.d.ts +1 -1
- package/browser/internal/state/fieldArray.js +2 -1
- package/browser/validation/createValidator.js +3 -1
- package/dist/remix-validated-form.cjs.js +4 -4
- package/dist/remix-validated-form.cjs.js.map +1 -1
- package/dist/remix-validated-form.es.js +1282 -584
- package/dist/remix-validated-form.es.js.map +1 -1
- package/dist/remix-validated-form.umd.js +4 -4
- package/dist/remix-validated-form.umd.js.map +1 -1
- package/dist/types/internal/logic/requestSubmit.d.ts +5 -0
- package/dist/types/internal/state/fieldArray.d.ts +1 -1
- package/package.json +2 -1
- package/src/ValidatedForm.tsx +20 -6
- package/src/internal/logic/requestSubmit.test.tsx +24 -0
- package/src/internal/logic/requestSubmit.ts +103 -0
- package/src/internal/state/createFormStore.ts +3 -1
- package/src/internal/state/fieldArray.tsx +1 -1
- package/src/validation/createValidator.ts +5 -1
- package/src/validation/validation.test.ts +26 -0
@@ -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) =>
|
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
|
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",
|
package/src/ValidatedForm.tsx
CHANGED
@@ -1,4 +1,9 @@
|
|
1
|
-
import {
|
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
|
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
|
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
|
-
|
269
|
+
requestSubmit(formElement);
|
268
270
|
},
|
269
271
|
|
270
272
|
getValues: () => new FormData(get().formElement ?? undefined),
|
@@ -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({
|