remix-validated-form 2.1.0 → 3.1.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 +2 -2
- package/.turbo/turbo-test.log +10 -35
- package/README.md +20 -8
- package/browser/ValidatedForm.d.ts +6 -1
- package/browser/ValidatedForm.js +37 -2
- package/browser/hooks.d.ts +8 -1
- package/browser/hooks.js +8 -3
- package/browser/index.d.ts +0 -2
- package/browser/index.js +0 -2
- package/browser/internal/MultiValueMap.d.ts +0 -0
- package/browser/internal/MultiValueMap.js +1 -0
- package/browser/internal/SingleTypeMultiValueMap.d.ts +8 -0
- package/browser/internal/SingleTypeMultiValueMap.js +40 -0
- package/browser/internal/formContext.d.ts +10 -0
- package/browser/internal/formContext.js +2 -0
- package/browser/server.d.ts +1 -1
- package/browser/server.js +11 -1
- package/browser/validation/types.d.ts +1 -0
- package/build/ValidatedForm.d.ts +6 -1
- package/build/ValidatedForm.js +37 -2
- package/build/hooks.d.ts +8 -1
- package/build/hooks.js +7 -2
- package/build/index.d.ts +0 -2
- package/build/index.js +0 -2
- package/build/internal/SingleTypeMultiValueMap.d.ts +8 -0
- package/build/internal/SingleTypeMultiValueMap.js +45 -0
- package/build/internal/formContext.d.ts +10 -0
- package/build/internal/formContext.js +2 -0
- package/build/server.d.ts +1 -1
- package/build/server.js +11 -1
- package/build/validation/types.d.ts +1 -0
- package/package.json +3 -8
- package/src/ValidatedForm.tsx +59 -1
- package/src/hooks.ts +26 -4
- package/src/index.ts +0 -2
- package/src/internal/SingleTypeMultiValueMap.ts +37 -0
- package/src/internal/formContext.ts +12 -0
- package/src/server.ts +18 -2
- package/src/validation/types.ts +6 -0
- package/build/test-data/testFormData.d.ts +0 -15
- package/build/test-data/testFormData.js +0 -50
- package/build/validation/validation.test.d.ts +0 -1
- package/build/validation/validation.test.js +0 -295
- package/build/validation/withYup.d.ts +0 -6
- package/build/validation/withYup.js +0 -44
- package/build/validation/withZod.d.ts +0 -6
- package/build/validation/withZod.js +0 -57
- package/jest.config.js +0 -10
- package/src/test-data/testFormData.ts +0 -55
- package/src/validation/validation.test.ts +0 -322
- package/src/validation/withYup.ts +0 -43
- package/src/validation/withZod.ts +0 -51
package/build/server.js
CHANGED
@@ -7,5 +7,15 @@ const server_runtime_1 = require("@remix-run/server-runtime");
|
|
7
7
|
* The `ValidatedForm` on the frontend will automatically display the errors
|
8
8
|
* if this is returned from the action.
|
9
9
|
*/
|
10
|
-
const validationError = (errors
|
10
|
+
const validationError = (errors, submittedData) => {
|
11
|
+
if (submittedData) {
|
12
|
+
return (0, server_runtime_1.json)({
|
13
|
+
fieldErrors: {
|
14
|
+
...errors,
|
15
|
+
_submittedData: submittedData,
|
16
|
+
},
|
17
|
+
}, { status: 422 });
|
18
|
+
}
|
19
|
+
return (0, server_runtime_1.json)({ fieldErrors: errors }, { status: 422 });
|
20
|
+
};
|
11
21
|
exports.validationError = validationError;
|
@@ -28,3 +28,4 @@ export declare type Validator<DataType> = {
|
|
28
28
|
validate: (unvalidatedData: GenericObject) => ValidationResult<DataType>;
|
29
29
|
validateField: (unvalidatedData: GenericObject, field: string) => ValidateFieldResult;
|
30
30
|
};
|
31
|
+
export declare type ValidatorData<T extends Validator<any>> = T extends Validator<infer U> ? U : never;
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "remix-validated-form",
|
3
|
-
"version": "
|
3
|
+
"version": "3.1.0",
|
4
4
|
"description": "Form component and utils for easy form validation in remix",
|
5
5
|
"browser": "./browser/index.js",
|
6
6
|
"main": "./build/index.js",
|
@@ -14,8 +14,6 @@
|
|
14
14
|
"build": "npm run build:browser && npm run build:main",
|
15
15
|
"build:browser": "tsc --module ESNext --outDir ./browser",
|
16
16
|
"build:main": "tsc --module CommonJS --outDir ./build",
|
17
|
-
"test": "jest src",
|
18
|
-
"test:watch": "jest src --watch",
|
19
17
|
"prepublishOnly": "cp ../../README.md ./README.md && npm run build",
|
20
18
|
"postpublish": "rm ./README.md"
|
21
19
|
},
|
@@ -31,6 +29,7 @@
|
|
31
29
|
"react",
|
32
30
|
"form",
|
33
31
|
"yup",
|
32
|
+
"zod",
|
34
33
|
"validation"
|
35
34
|
],
|
36
35
|
"peerDependencies": {
|
@@ -41,16 +40,12 @@
|
|
41
40
|
"devDependencies": {
|
42
41
|
"@remix-run/react": "^1.0.6",
|
43
42
|
"@remix-run/server-runtime": "^1.0.6",
|
44
|
-
"@types/jest": "^27.0.3",
|
45
43
|
"@types/lodash": "^4.14.178",
|
46
44
|
"@types/react": "^17.0.37",
|
47
45
|
"fetch-blob": "^3.1.3",
|
48
46
|
"react": "^17.0.2",
|
49
|
-
"ts-jest": "^27.1.1",
|
50
47
|
"tsconfig": "*",
|
51
|
-
"typescript": "^4.5.3"
|
52
|
-
"yup": "^0.32.11",
|
53
|
-
"zod": "^3.11.6"
|
48
|
+
"typescript": "^4.5.3"
|
54
49
|
},
|
55
50
|
"dependencies": {
|
56
51
|
"lodash": "^4.17.21",
|
package/src/ValidatedForm.tsx
CHANGED
@@ -14,6 +14,10 @@ import React, {
|
|
14
14
|
} from "react";
|
15
15
|
import invariant from "tiny-invariant";
|
16
16
|
import { FormContext, FormContextValue } from "./internal/formContext";
|
17
|
+
import {
|
18
|
+
MultiValueMap,
|
19
|
+
useMultiValueMap,
|
20
|
+
} from "./internal/SingleTypeMultiValueMap";
|
17
21
|
import { useSubmitComplete } from "./internal/submissionCallbacks";
|
18
22
|
import { omit, mergeRefs } from "./internal/util";
|
19
23
|
import {
|
@@ -59,6 +63,11 @@ export type FormProps<DataType> = {
|
|
59
63
|
* and don't redirect in-between submissions.
|
60
64
|
*/
|
61
65
|
resetAfterSubmit?: boolean;
|
66
|
+
/**
|
67
|
+
* Normally, the first invalid input will be focused when the validation fails on form submit.
|
68
|
+
* Set this to `false` to disable this behavior.
|
69
|
+
*/
|
70
|
+
disableFocusOnError?: boolean;
|
62
71
|
} & Omit<ComponentProps<typeof RemixForm>, "onSubmit">;
|
63
72
|
|
64
73
|
function useFieldErrorsFromBackend(
|
@@ -136,6 +145,36 @@ function useDefaultValues<DataType>(
|
|
136
145
|
return defaultsFromValidationError ?? defaultValues;
|
137
146
|
}
|
138
147
|
|
148
|
+
const focusFirstInvalidInput = (
|
149
|
+
fieldErrors: FieldErrors,
|
150
|
+
customFocusHandlers: MultiValueMap<string, () => void>,
|
151
|
+
formElement: HTMLFormElement
|
152
|
+
) => {
|
153
|
+
const invalidInputSelector = Object.keys(fieldErrors)
|
154
|
+
.map((fieldName) => `input[name="${fieldName}"]`)
|
155
|
+
.join(",");
|
156
|
+
const invalidInputs = formElement.querySelectorAll(invalidInputSelector);
|
157
|
+
for (const element of invalidInputs) {
|
158
|
+
const input = element as HTMLInputElement;
|
159
|
+
|
160
|
+
if (customFocusHandlers.has(input.name)) {
|
161
|
+
customFocusHandlers.getAll(input.name).forEach((handler) => {
|
162
|
+
handler();
|
163
|
+
});
|
164
|
+
break;
|
165
|
+
}
|
166
|
+
|
167
|
+
// We don't filter these out ahead of time because
|
168
|
+
// they could have a custom focus handler
|
169
|
+
if (input.type === "hidden") {
|
170
|
+
continue;
|
171
|
+
}
|
172
|
+
|
173
|
+
input.focus();
|
174
|
+
break;
|
175
|
+
}
|
176
|
+
};
|
177
|
+
|
139
178
|
/**
|
140
179
|
* The primary form component of `remix-validated-form`.
|
141
180
|
*/
|
@@ -150,6 +189,7 @@ export function ValidatedForm<DataType>({
|
|
150
189
|
onReset,
|
151
190
|
subaction,
|
152
191
|
resetAfterSubmit,
|
192
|
+
disableFocusOnError,
|
153
193
|
...rest
|
154
194
|
}: FormProps<DataType>) {
|
155
195
|
const fieldErrorsFromBackend = useFieldErrorsFromBackend(fetcher, subaction);
|
@@ -162,6 +202,7 @@ export function ValidatedForm<DataType>({
|
|
162
202
|
formRef.current?.reset();
|
163
203
|
}
|
164
204
|
});
|
205
|
+
const customFocusHandlers = useMultiValueMap<string, () => void>();
|
165
206
|
|
166
207
|
const contextValue = useMemo<FormContextValue>(
|
167
208
|
() => ({
|
@@ -169,6 +210,7 @@ export function ValidatedForm<DataType>({
|
|
169
210
|
action,
|
170
211
|
defaultValues: defaultsToUse,
|
171
212
|
isSubmitting: isSubmitting ?? false,
|
213
|
+
isValid: Object.keys(fieldErrors).length === 0,
|
172
214
|
clearError: (fieldName) => {
|
173
215
|
setFieldErrors((prev) => omit(prev, fieldName));
|
174
216
|
},
|
@@ -185,6 +227,12 @@ export function ValidatedForm<DataType>({
|
|
185
227
|
}));
|
186
228
|
}
|
187
229
|
},
|
230
|
+
registerReceiveFocus: (fieldName, handler) => {
|
231
|
+
customFocusHandlers().add(fieldName, handler);
|
232
|
+
return () => {
|
233
|
+
customFocusHandlers().remove(fieldName, handler);
|
234
|
+
};
|
235
|
+
},
|
188
236
|
}),
|
189
237
|
[
|
190
238
|
fieldErrors,
|
@@ -193,6 +241,7 @@ export function ValidatedForm<DataType>({
|
|
193
241
|
isSubmitting,
|
194
242
|
setFieldErrors,
|
195
243
|
validator,
|
244
|
+
customFocusHandlers,
|
196
245
|
]
|
197
246
|
);
|
198
247
|
|
@@ -208,6 +257,13 @@ export function ValidatedForm<DataType>({
|
|
208
257
|
if (result.error) {
|
209
258
|
event.preventDefault();
|
210
259
|
setFieldErrors(result.error);
|
260
|
+
if (!disableFocusOnError) {
|
261
|
+
focusFirstInvalidInput(
|
262
|
+
result.error,
|
263
|
+
customFocusHandlers(),
|
264
|
+
formRef.current!
|
265
|
+
);
|
266
|
+
}
|
211
267
|
} else {
|
212
268
|
onSubmit?.(result.data, event);
|
213
269
|
}
|
@@ -219,7 +275,9 @@ export function ValidatedForm<DataType>({
|
|
219
275
|
}}
|
220
276
|
>
|
221
277
|
<FormContext.Provider value={contextValue}>
|
222
|
-
|
278
|
+
{subaction && (
|
279
|
+
<input type="hidden" value={subaction} name="subaction" />
|
280
|
+
)}
|
223
281
|
{children}
|
224
282
|
</FormContext.Provider>
|
225
283
|
</Form>
|
package/src/hooks.ts
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
import get from "lodash/get";
|
2
2
|
import toPath from "lodash/toPath";
|
3
|
-
import { useContext, useMemo } from "react";
|
3
|
+
import { useContext, useEffect, useMemo } from "react";
|
4
4
|
import { FormContext } from "./internal/formContext";
|
5
5
|
|
6
6
|
export type FieldProps = {
|
@@ -25,9 +25,31 @@ export type FieldProps = {
|
|
25
25
|
/**
|
26
26
|
* Provides the data and helpers necessary to set up a field.
|
27
27
|
*/
|
28
|
-
export const useField = (
|
29
|
-
|
30
|
-
|
28
|
+
export const useField = (
|
29
|
+
name: string,
|
30
|
+
options?: {
|
31
|
+
/**
|
32
|
+
* Allows you to configure a custom function that will be called
|
33
|
+
* when the input needs to receive focus due to a validation error.
|
34
|
+
* This is useful for custom components that use a hidden input.
|
35
|
+
*/
|
36
|
+
handleReceiveFocus?: () => void;
|
37
|
+
}
|
38
|
+
): FieldProps => {
|
39
|
+
const {
|
40
|
+
fieldErrors,
|
41
|
+
clearError,
|
42
|
+
validateField,
|
43
|
+
defaultValues,
|
44
|
+
registerReceiveFocus,
|
45
|
+
} = useContext(FormContext);
|
46
|
+
|
47
|
+
const { handleReceiveFocus } = options ?? {};
|
48
|
+
|
49
|
+
useEffect(() => {
|
50
|
+
if (handleReceiveFocus)
|
51
|
+
return registerReceiveFocus(name, handleReceiveFocus);
|
52
|
+
}, [handleReceiveFocus, name, registerReceiveFocus]);
|
31
53
|
|
32
54
|
const field = useMemo<FieldProps>(
|
33
55
|
() => ({
|
package/src/index.ts
CHANGED
@@ -2,7 +2,5 @@ export * from "./hooks";
|
|
2
2
|
export * from "./server";
|
3
3
|
export * from "./ValidatedForm";
|
4
4
|
export * from "./validation/types";
|
5
|
-
export * from "./validation/withYup";
|
6
|
-
export * from "./validation/withZod";
|
7
5
|
export * from "./validation/createValidator";
|
8
6
|
export type { FormContextValue } from "./internal/formContext";
|
@@ -0,0 +1,37 @@
|
|
1
|
+
import { useRef } from "react";
|
2
|
+
|
3
|
+
export class MultiValueMap<Key, Value> {
|
4
|
+
private dict: Map<Key, Value[]> = new Map();
|
5
|
+
|
6
|
+
add = (key: Key, value: Value) => {
|
7
|
+
this.dict.set(key, [...(this.dict.get(key) ?? []), value]);
|
8
|
+
if (this.dict.has(key)) {
|
9
|
+
this.dict.get(key)!.push(value);
|
10
|
+
} else {
|
11
|
+
this.dict.set(key, [value]);
|
12
|
+
}
|
13
|
+
};
|
14
|
+
|
15
|
+
remove = (key: Key, value: Value) => {
|
16
|
+
if (!this.dict.has(key)) return;
|
17
|
+
const array = this.dict.get(key)!;
|
18
|
+
const index = array.indexOf(value);
|
19
|
+
if (index !== -1) array.splice(index, 1);
|
20
|
+
if (array.length === 0) this.dict.delete(key);
|
21
|
+
};
|
22
|
+
|
23
|
+
getAll = (key: Key): Value[] => {
|
24
|
+
return this.dict.get(key) ?? [];
|
25
|
+
};
|
26
|
+
|
27
|
+
has = (key: Key): boolean => this.dict.has(key);
|
28
|
+
}
|
29
|
+
|
30
|
+
export const useMultiValueMap = <Key, Value>() => {
|
31
|
+
const ref = useRef<MultiValueMap<Key, Value> | null>(null);
|
32
|
+
return () => {
|
33
|
+
if (ref.current) return ref.current;
|
34
|
+
ref.current = new MultiValueMap();
|
35
|
+
return ref.current;
|
36
|
+
};
|
37
|
+
};
|
@@ -22,10 +22,20 @@ export type FormContextValue = {
|
|
22
22
|
* Whether or not the form is submitting.
|
23
23
|
*/
|
24
24
|
isSubmitting: boolean;
|
25
|
+
/**
|
26
|
+
* Whether or not the form is valid.
|
27
|
+
* This is a shortcut for `Object.keys(fieldErrors).length === 0`.
|
28
|
+
*/
|
29
|
+
isValid: boolean;
|
25
30
|
/**
|
26
31
|
* The default values of the form.
|
27
32
|
*/
|
28
33
|
defaultValues?: { [fieldName: string]: any };
|
34
|
+
/**
|
35
|
+
* Register a custom focus handler to be used when
|
36
|
+
* the field needs to receive focus due to a validation error.
|
37
|
+
*/
|
38
|
+
registerReceiveFocus: (fieldName: string, handler: () => void) => () => void;
|
29
39
|
};
|
30
40
|
|
31
41
|
export const FormContext = createContext<FormContextValue>({
|
@@ -33,4 +43,6 @@ export const FormContext = createContext<FormContextValue>({
|
|
33
43
|
clearError: () => {},
|
34
44
|
validateField: () => {},
|
35
45
|
isSubmitting: false,
|
46
|
+
isValid: true,
|
47
|
+
registerReceiveFocus: () => () => {},
|
36
48
|
});
|
package/src/server.ts
CHANGED
@@ -6,5 +6,21 @@ import { FieldErrors } from "./validation/types";
|
|
6
6
|
* The `ValidatedForm` on the frontend will automatically display the errors
|
7
7
|
* if this is returned from the action.
|
8
8
|
*/
|
9
|
-
export const validationError = (
|
10
|
-
|
9
|
+
export const validationError = (
|
10
|
+
errors: FieldErrors,
|
11
|
+
submittedData?: unknown
|
12
|
+
) => {
|
13
|
+
if (submittedData) {
|
14
|
+
return json(
|
15
|
+
{
|
16
|
+
fieldErrors: {
|
17
|
+
...errors,
|
18
|
+
_submittedData: submittedData,
|
19
|
+
},
|
20
|
+
},
|
21
|
+
{ status: 422 }
|
22
|
+
);
|
23
|
+
}
|
24
|
+
|
25
|
+
return json({ fieldErrors: errors }, { status: 422 });
|
26
|
+
};
|
package/src/validation/types.ts
CHANGED
@@ -1,15 +0,0 @@
|
|
1
|
-
export declare class TestFormData implements FormData {
|
2
|
-
private _params;
|
3
|
-
constructor(body?: string);
|
4
|
-
append(name: string, value: string | Blob, fileName?: string): void;
|
5
|
-
delete(name: string): void;
|
6
|
-
get(name: string): FormDataEntryValue | null;
|
7
|
-
getAll(name: string): FormDataEntryValue[];
|
8
|
-
has(name: string): boolean;
|
9
|
-
set(name: string, value: string | Blob, fileName?: string): void;
|
10
|
-
forEach(callbackfn: (value: FormDataEntryValue, key: string, parent: FormData) => void, thisArg?: any): void;
|
11
|
-
entries(): IterableIterator<[string, FormDataEntryValue]>;
|
12
|
-
keys(): IterableIterator<string>;
|
13
|
-
values(): IterableIterator<FormDataEntryValue>;
|
14
|
-
[Symbol.iterator](): IterableIterator<[string, FormDataEntryValue]>;
|
15
|
-
}
|
@@ -1,50 +0,0 @@
|
|
1
|
-
"use strict";
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
-
exports.TestFormData = void 0;
|
4
|
-
// Copied from remix to use in tests
|
5
|
-
// https://github.com/remix-run/remix/blob/a69a631cb5add72d5fb24211ab2a0be367b6f2fd/packages/remix-node/form-data.ts
|
6
|
-
class TestFormData {
|
7
|
-
constructor(body) {
|
8
|
-
this._params = new URLSearchParams(body);
|
9
|
-
}
|
10
|
-
append(name, value, fileName) {
|
11
|
-
if (typeof value !== "string") {
|
12
|
-
throw new Error("formData.append can only accept a string");
|
13
|
-
}
|
14
|
-
this._params.append(name, value);
|
15
|
-
}
|
16
|
-
delete(name) {
|
17
|
-
this._params.delete(name);
|
18
|
-
}
|
19
|
-
get(name) {
|
20
|
-
return this._params.get(name);
|
21
|
-
}
|
22
|
-
getAll(name) {
|
23
|
-
return this._params.getAll(name);
|
24
|
-
}
|
25
|
-
has(name) {
|
26
|
-
return this._params.has(name);
|
27
|
-
}
|
28
|
-
set(name, value, fileName) {
|
29
|
-
if (typeof value !== "string") {
|
30
|
-
throw new Error("formData.set can only accept a string");
|
31
|
-
}
|
32
|
-
this._params.set(name, value);
|
33
|
-
}
|
34
|
-
forEach(callbackfn, thisArg) {
|
35
|
-
this._params.forEach(callbackfn, thisArg);
|
36
|
-
}
|
37
|
-
entries() {
|
38
|
-
return this._params.entries();
|
39
|
-
}
|
40
|
-
keys() {
|
41
|
-
return this._params.keys();
|
42
|
-
}
|
43
|
-
values() {
|
44
|
-
return this._params.values();
|
45
|
-
}
|
46
|
-
*[Symbol.iterator]() {
|
47
|
-
yield* this._params;
|
48
|
-
}
|
49
|
-
}
|
50
|
-
exports.TestFormData = TestFormData;
|
@@ -1 +0,0 @@
|
|
1
|
-
export {};
|