remix-validated-form 3.2.1 → 3.4.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.
@@ -1,9 +1,9 @@
1
1
  $ npm run build:browser && npm run build:main
2
2
 
3
- > remix-validated-form@3.1.1 build:browser
3
+ > remix-validated-form@3.3.1 build:browser
4
4
  > tsc --module ESNext --outDir ./browser
5
5
 
6
6
 
7
- > remix-validated-form@3.1.1 build:main
7
+ > remix-validated-form@3.3.1 build:main
8
8
  > tsc --module CommonJS --outDir ./build
9
9
 
package/README.md CHANGED
@@ -50,6 +50,8 @@ see the [Validation library support](#validation-library-support) section below.
50
50
  npm install @remix-validated-form/with-zod
51
51
  ```
52
52
 
53
+ If you're using zod, you might also find `zod-form-data` helpful.
54
+
53
55
  ## Create an input component
54
56
 
55
57
  In order to display field errors or do field-by-field validation,
@@ -63,7 +65,7 @@ type MyInputProps = {
63
65
  label: string;
64
66
  };
65
67
 
66
- export const MyInput = ({ name, label }: InputProps) => {
68
+ export const MyInput = ({ name, label }: MyInputProps) => {
67
69
  const { error, getInputProps } = useField(name);
68
70
  return (
69
71
  <div>
@@ -257,4 +259,4 @@ See the [Remix](https://remix.run/docs/en/v1/api/remix#sessionflashkey-value) do
257
259
 
258
260
  ## Why is my cancel button triggering form submission?
259
261
  Problem: the cancel button has an onClick handler to navigate away from the form route but instead it is submitting the form.
260
- A button defaults to `type="submit"` in a form which will submit the form by default. If you want to prevent this you can add `type="reset"` or `type="button"` to the cancel button.
262
+ A button defaults to `type="submit"` in a form which will submit the form by default. If you want to prevent this you can add `type="reset"` or `type="button"` to the cancel button.
@@ -3,7 +3,7 @@ import { Form as RemixForm, useActionData, useFormAction, useTransition, } from
3
3
  import { useEffect, useMemo, useRef, useState, } from "react";
4
4
  import invariant from "tiny-invariant";
5
5
  import { FormContext } from "./internal/formContext";
6
- import { useMultiValueMap, } from "./internal/SingleTypeMultiValueMap";
6
+ import { useMultiValueMap } from "./internal/MultiValueMap";
7
7
  import { useSubmitComplete } from "./internal/submissionCallbacks";
8
8
  import { omit, mergeRefs } from "./internal/util";
9
9
  function useFieldErrorsFromBackend(fetcher, subaction) {
@@ -123,14 +123,24 @@ export function ValidatedForm({ validator, onSubmit, children, fetcher, action,
123
123
  validateField: (fieldName) => {
124
124
  invariant(formRef.current, "Cannot find reference to form");
125
125
  const { error } = validator.validateField(getDataFromForm(formRef.current), fieldName);
126
+ // By checking and returning `prev` here, we can avoid a re-render
127
+ // if the validation state is the same.
126
128
  if (error) {
127
- setFieldErrors((prev) => ({
128
- ...prev,
129
- [fieldName]: error,
130
- }));
129
+ setFieldErrors((prev) => {
130
+ if (prev[fieldName] === error)
131
+ return prev;
132
+ return {
133
+ ...prev,
134
+ [fieldName]: error,
135
+ };
136
+ });
131
137
  }
132
138
  else {
133
- setFieldErrors((prev) => omit(prev, fieldName));
139
+ setFieldErrors((prev) => {
140
+ if (!(fieldName in prev))
141
+ return prev;
142
+ return omit(prev, fieldName);
143
+ });
134
144
  }
135
145
  },
136
146
  registerReceiveFocus: (fieldName, handler) => {
@@ -0,0 +1,9 @@
1
+ export declare class MultiValueMap<Key, Value> {
2
+ private dict;
3
+ add: (key: Key, value: Value) => void;
4
+ remove: (key: Key, value: Value) => void;
5
+ getAll: (key: Key) => Value[];
6
+ entries: () => IterableIterator<[Key, Value[]]>;
7
+ has: (key: Key) => boolean;
8
+ }
9
+ export declare const useMultiValueMap: <Key, Value>() => () => MultiValueMap<Key, Value>;
@@ -1 +1,39 @@
1
- "use strict";
1
+ import { useRef } from "react";
2
+ export class MultiValueMap {
3
+ constructor() {
4
+ this.dict = new Map();
5
+ this.add = (key, value) => {
6
+ if (this.dict.has(key)) {
7
+ this.dict.get(key).push(value);
8
+ }
9
+ else {
10
+ this.dict.set(key, [value]);
11
+ }
12
+ };
13
+ this.remove = (key, value) => {
14
+ if (!this.dict.has(key))
15
+ return;
16
+ const array = this.dict.get(key);
17
+ const index = array.indexOf(value);
18
+ if (index !== -1)
19
+ array.splice(index, 1);
20
+ if (array.length === 0)
21
+ this.dict.delete(key);
22
+ };
23
+ this.getAll = (key) => {
24
+ var _a;
25
+ return (_a = this.dict.get(key)) !== null && _a !== void 0 ? _a : [];
26
+ };
27
+ this.entries = () => this.dict.entries();
28
+ this.has = (key) => this.dict.has(key);
29
+ }
30
+ }
31
+ export const useMultiValueMap = () => {
32
+ const ref = useRef(null);
33
+ return () => {
34
+ if (ref.current)
35
+ return ref.current;
36
+ ref.current = new MultiValueMap();
37
+ return ref.current;
38
+ };
39
+ };
@@ -3,6 +3,7 @@ export declare class MultiValueMap<Key, Value> {
3
3
  add: (key: Key, value: Value) => void;
4
4
  remove: (key: Key, value: Value) => void;
5
5
  getAll: (key: Key) => Value[];
6
+ entries: () => IterableIterator<[Key, Value[]]>;
6
7
  has: (key: Key) => boolean;
7
8
  }
8
9
  export declare const useMultiValueMap: <Key, Value>() => () => MultiValueMap<Key, Value>;
@@ -26,6 +26,7 @@ export class MultiValueMap {
26
26
  var _a;
27
27
  return (_a = this.dict.get(key)) !== null && _a !== void 0 ? _a : [];
28
28
  };
29
+ this.entries = () => this.dict.entries();
29
30
  this.has = (key) => this.dict.has(key);
30
31
  }
31
32
  }
@@ -1,4 +1 @@
1
- import { GenericObject } from "..";
2
1
  export declare const objectFromPathEntries: (entries: [string, any][]) => {};
3
- /** Flatten an object so there are no nested objects or arrays */
4
- export declare function flatten(obj: GenericObject, preserveEmpty?: boolean): GenericObject;
@@ -1,35 +1,7 @@
1
- // `flatten` is taken from https://github.com/richie5um/FlattenJS. Decided to implement them here instead of using that package because this is a core functionality of the library and this will add more flexibility in case we need to change the implementation.
2
- import assign from "lodash/assign";
3
- import isArray from "lodash/isArray";
4
- import isObject from "lodash/isObject";
5
- import keys from "lodash/keys";
6
- import mapKeys from "lodash/mapKeys";
7
1
  import set from "lodash/set";
8
- import transform from "lodash/transform";
9
- export const objectFromPathEntries = (entries) => entries.reduce((acc, [key, value]) => set(acc, key, value), {});
10
- /** Flatten an object so there are no nested objects or arrays */
11
- export function flatten(obj, preserveEmpty = false) {
12
- return transform(obj, function (result, value, key) {
13
- if (isObject(value)) {
14
- let flatMap = mapKeys(flatten(value, preserveEmpty), function (_mvalue, mkey) {
15
- if (isArray(value)) {
16
- let index = mkey.indexOf(".");
17
- if (-1 !== index) {
18
- return `${key}[${mkey.slice(0, index)}]${mkey.slice(index)}`;
19
- }
20
- return `${key}[${mkey}]`;
21
- }
22
- return `${key}.${mkey}`;
23
- });
24
- assign(result, flatMap);
25
- // Preverve Empty arrays and objects
26
- if (preserveEmpty && keys(flatMap).length === 0) {
27
- result[key] = value;
28
- }
29
- }
30
- else {
31
- result[key] = value;
32
- }
33
- return result;
34
- }, {});
35
- }
2
+ import { MultiValueMap } from "./MultiValueMap";
3
+ export const objectFromPathEntries = (entries) => {
4
+ const map = new MultiValueMap();
5
+ entries.forEach(([key, value]) => map.add(key, value));
6
+ return [...map.entries()].reduce((acc, [key, value]) => set(acc, key, value.length === 1 ? value[0] : value), {});
7
+ };
@@ -14,8 +14,16 @@ export declare type CreateGetInputPropsOptions = {
14
14
  validationBehavior?: Partial<ValidationBehaviorOptions>;
15
15
  name: string;
16
16
  };
17
- declare type HandledProps = "name" | "defaultValue";
17
+ declare type HandledProps = "name" | "defaultValue" | "defaultChecked";
18
18
  declare type Callbacks = "onChange" | "onBlur";
19
- export declare type GetInputProps = <T extends Record<string, any>>(props?: Omit<T, HandledProps | Callbacks> & Partial<Pick<T, Callbacks>>) => T;
19
+ declare type MinimalInputProps = {
20
+ onChange?: (...args: any[]) => void;
21
+ onBlur?: (...args: any[]) => void;
22
+ defaultValue?: any;
23
+ defaultChecked?: boolean;
24
+ name?: string;
25
+ type?: string;
26
+ };
27
+ export declare type GetInputProps = <T extends MinimalInputProps>(props?: Omit<T, HandledProps | Callbacks> & Partial<Pick<T, Callbacks>>) => T;
20
28
  export declare const createGetInputProps: ({ clearError, validate, defaultValue, touched, setTouched, hasBeenSubmitted, validationBehavior, name, }: CreateGetInputPropsOptions) => GetInputProps;
21
29
  export {};
@@ -3,18 +3,28 @@ const defaultValidationBehavior = {
3
3
  whenTouched: "onChange",
4
4
  whenSubmitted: "onChange",
5
5
  };
6
+ const getCheckboxDefaultChecked = (value, defaultValue) => {
7
+ if (Array.isArray(defaultValue))
8
+ defaultValue.includes(value);
9
+ if (typeof defaultValue === "boolean")
10
+ return defaultValue;
11
+ if (typeof defaultValue === "string")
12
+ return defaultValue === value;
13
+ return undefined;
14
+ };
6
15
  export const createGetInputProps = ({ clearError, validate, defaultValue, touched, setTouched, hasBeenSubmitted, validationBehavior, name, }) => {
7
16
  const validationBehaviors = {
8
17
  ...defaultValidationBehavior,
9
18
  ...validationBehavior,
10
19
  };
11
20
  return (props = {}) => {
21
+ var _a, _b;
12
22
  const behavior = hasBeenSubmitted
13
23
  ? validationBehaviors.whenSubmitted
14
24
  : touched
15
25
  ? validationBehaviors.whenTouched
16
26
  : validationBehaviors.initial;
17
- const result = {
27
+ const inputProps = {
18
28
  ...props,
19
29
  onChange: (...args) => {
20
30
  var _a;
@@ -31,9 +41,19 @@ export const createGetInputProps = ({ clearError, validate, defaultValue, touche
31
41
  setTouched(true);
32
42
  return (_a = props === null || props === void 0 ? void 0 : props.onBlur) === null || _a === void 0 ? void 0 : _a.call(props, ...args);
33
43
  },
34
- defaultValue,
35
44
  name,
36
45
  };
37
- return result;
46
+ if (inputProps.type === "checkbox") {
47
+ const value = (_a = props.value) !== null && _a !== void 0 ? _a : "on";
48
+ inputProps.defaultChecked = getCheckboxDefaultChecked(value, defaultValue);
49
+ }
50
+ else if (inputProps.type === "radio") {
51
+ const value = (_b = props.value) !== null && _b !== void 0 ? _b : "on";
52
+ inputProps.defaultChecked = defaultValue === value;
53
+ }
54
+ else {
55
+ inputProps.defaultValue = defaultValue;
56
+ }
57
+ return inputProps;
38
58
  };
39
59
  };
@@ -1 +0,0 @@
1
- export {};
@@ -1,10 +1,15 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
2
- import { createGetInputProps } from "./getInputProps";
3
- const CompRequired = (props) => null;
4
- const getProps = createGetInputProps({});
5
- _jsx(CompRequired, { ...getProps({
6
- temp: 21,
7
- bob: "ross",
8
- onBlur: () => { },
9
- // onChange: () => {},
10
- }) }, void 0);
1
+ "use strict";
2
+ z.preprocess((val) => {
3
+ // Somewhat awkward -- this gets processed per item in the form,
4
+ // but as a whole array in the backend
5
+ if (Array.isArray(val)) {
6
+ return val;
7
+ }
8
+ else {
9
+ return [val];
10
+ }
11
+ }, z.array(z.preprocess((val) => {
12
+ return typeof val !== "string" || val === ""
13
+ ? undefined
14
+ : Number.parseInt(val);
15
+ }, z.number())));
@@ -0,0 +1 @@
1
+ export declare const useMultiValueMap: <Key, Value>() => () => any;
@@ -0,0 +1,11 @@
1
+ import { MultiValueMap } from "multi-value-map";
2
+ import { useRef } from "react";
3
+ export const useMultiValueMap = () => {
4
+ const ref = useRef(null);
5
+ return () => {
6
+ if (ref.current)
7
+ return ref.current;
8
+ ref.current = new MultiValueMap();
9
+ return ref.current;
10
+ };
11
+ };
@@ -9,7 +9,7 @@ const react_1 = require("@remix-run/react");
9
9
  const react_2 = require("react");
10
10
  const tiny_invariant_1 = __importDefault(require("tiny-invariant"));
11
11
  const formContext_1 = require("./internal/formContext");
12
- const SingleTypeMultiValueMap_1 = require("./internal/SingleTypeMultiValueMap");
12
+ const MultiValueMap_1 = require("./internal/MultiValueMap");
13
13
  const submissionCallbacks_1 = require("./internal/submissionCallbacks");
14
14
  const util_1 = require("./internal/util");
15
15
  function useFieldErrorsFromBackend(fetcher, subaction) {
@@ -111,7 +111,7 @@ function ValidatedForm({ validator, onSubmit, children, fetcher, action, default
111
111
  (_a = formRef.current) === null || _a === void 0 ? void 0 : _a.reset();
112
112
  }
113
113
  });
114
- const customFocusHandlers = (0, SingleTypeMultiValueMap_1.useMultiValueMap)();
114
+ const customFocusHandlers = (0, MultiValueMap_1.useMultiValueMap)();
115
115
  const contextValue = (0, react_2.useMemo)(() => ({
116
116
  fieldErrors,
117
117
  action,
@@ -129,14 +129,24 @@ function ValidatedForm({ validator, onSubmit, children, fetcher, action, default
129
129
  validateField: (fieldName) => {
130
130
  (0, tiny_invariant_1.default)(formRef.current, "Cannot find reference to form");
131
131
  const { error } = validator.validateField(getDataFromForm(formRef.current), fieldName);
132
+ // By checking and returning `prev` here, we can avoid a re-render
133
+ // if the validation state is the same.
132
134
  if (error) {
133
- setFieldErrors((prev) => ({
134
- ...prev,
135
- [fieldName]: error,
136
- }));
135
+ setFieldErrors((prev) => {
136
+ if (prev[fieldName] === error)
137
+ return prev;
138
+ return {
139
+ ...prev,
140
+ [fieldName]: error,
141
+ };
142
+ });
137
143
  }
138
144
  else {
139
- setFieldErrors((prev) => (0, util_1.omit)(prev, fieldName));
145
+ setFieldErrors((prev) => {
146
+ if (!(fieldName in prev))
147
+ return prev;
148
+ return (0, util_1.omit)(prev, fieldName);
149
+ });
140
150
  }
141
151
  },
142
152
  registerReceiveFocus: (fieldName, handler) => {
@@ -0,0 +1,9 @@
1
+ export declare class MultiValueMap<Key, Value> {
2
+ private dict;
3
+ add: (key: Key, value: Value) => void;
4
+ remove: (key: Key, value: Value) => void;
5
+ getAll: (key: Key) => Value[];
6
+ entries: () => IterableIterator<[Key, Value[]]>;
7
+ has: (key: Key) => boolean;
8
+ }
9
+ export declare const useMultiValueMap: <Key, Value>() => () => MultiValueMap<Key, Value>;
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.useMultiValueMap = exports.MultiValueMap = void 0;
4
+ const react_1 = require("react");
5
+ class MultiValueMap {
6
+ constructor() {
7
+ this.dict = new Map();
8
+ this.add = (key, value) => {
9
+ if (this.dict.has(key)) {
10
+ this.dict.get(key).push(value);
11
+ }
12
+ else {
13
+ this.dict.set(key, [value]);
14
+ }
15
+ };
16
+ this.remove = (key, value) => {
17
+ if (!this.dict.has(key))
18
+ return;
19
+ const array = this.dict.get(key);
20
+ const index = array.indexOf(value);
21
+ if (index !== -1)
22
+ array.splice(index, 1);
23
+ if (array.length === 0)
24
+ this.dict.delete(key);
25
+ };
26
+ this.getAll = (key) => {
27
+ var _a;
28
+ return (_a = this.dict.get(key)) !== null && _a !== void 0 ? _a : [];
29
+ };
30
+ this.entries = () => this.dict.entries();
31
+ this.has = (key) => this.dict.has(key);
32
+ }
33
+ }
34
+ exports.MultiValueMap = MultiValueMap;
35
+ const useMultiValueMap = () => {
36
+ const ref = (0, react_1.useRef)(null);
37
+ return () => {
38
+ if (ref.current)
39
+ return ref.current;
40
+ ref.current = new MultiValueMap();
41
+ return ref.current;
42
+ };
43
+ };
44
+ exports.useMultiValueMap = useMultiValueMap;
@@ -1,4 +1 @@
1
- import { GenericObject } from "..";
2
1
  export declare const objectFromPathEntries: (entries: [string, any][]) => {};
3
- /** Flatten an object so there are no nested objects or arrays */
4
- export declare function flatten(obj: GenericObject, preserveEmpty?: boolean): GenericObject;
@@ -3,41 +3,12 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.flatten = exports.objectFromPathEntries = void 0;
7
- // `flatten` is taken from https://github.com/richie5um/FlattenJS. Decided to implement them here instead of using that package because this is a core functionality of the library and this will add more flexibility in case we need to change the implementation.
8
- const assign_1 = __importDefault(require("lodash/assign"));
9
- const isArray_1 = __importDefault(require("lodash/isArray"));
10
- const isObject_1 = __importDefault(require("lodash/isObject"));
11
- const keys_1 = __importDefault(require("lodash/keys"));
12
- const mapKeys_1 = __importDefault(require("lodash/mapKeys"));
6
+ exports.objectFromPathEntries = void 0;
13
7
  const set_1 = __importDefault(require("lodash/set"));
14
- const transform_1 = __importDefault(require("lodash/transform"));
15
- const objectFromPathEntries = (entries) => entries.reduce((acc, [key, value]) => (0, set_1.default)(acc, key, value), {});
8
+ const MultiValueMap_1 = require("./MultiValueMap");
9
+ const objectFromPathEntries = (entries) => {
10
+ const map = new MultiValueMap_1.MultiValueMap();
11
+ entries.forEach(([key, value]) => map.add(key, value));
12
+ return [...map.entries()].reduce((acc, [key, value]) => (0, set_1.default)(acc, key, value.length === 1 ? value[0] : value), {});
13
+ };
16
14
  exports.objectFromPathEntries = objectFromPathEntries;
17
- /** Flatten an object so there are no nested objects or arrays */
18
- function flatten(obj, preserveEmpty = false) {
19
- return (0, transform_1.default)(obj, function (result, value, key) {
20
- if ((0, isObject_1.default)(value)) {
21
- let flatMap = (0, mapKeys_1.default)(flatten(value, preserveEmpty), function (_mvalue, mkey) {
22
- if ((0, isArray_1.default)(value)) {
23
- let index = mkey.indexOf(".");
24
- if (-1 !== index) {
25
- return `${key}[${mkey.slice(0, index)}]${mkey.slice(index)}`;
26
- }
27
- return `${key}[${mkey}]`;
28
- }
29
- return `${key}.${mkey}`;
30
- });
31
- (0, assign_1.default)(result, flatMap);
32
- // Preverve Empty arrays and objects
33
- if (preserveEmpty && (0, keys_1.default)(flatMap).length === 0) {
34
- result[key] = value;
35
- }
36
- }
37
- else {
38
- result[key] = value;
39
- }
40
- return result;
41
- }, {});
42
- }
43
- exports.flatten = flatten;
@@ -14,8 +14,16 @@ export declare type CreateGetInputPropsOptions = {
14
14
  validationBehavior?: Partial<ValidationBehaviorOptions>;
15
15
  name: string;
16
16
  };
17
- declare type HandledProps = "name" | "defaultValue";
17
+ declare type HandledProps = "name" | "defaultValue" | "defaultChecked";
18
18
  declare type Callbacks = "onChange" | "onBlur";
19
- export declare type GetInputProps = <T extends Record<string, any>>(props?: Omit<T, HandledProps | Callbacks> & Partial<Pick<T, Callbacks>>) => T;
19
+ declare type MinimalInputProps = {
20
+ onChange?: (...args: any[]) => void;
21
+ onBlur?: (...args: any[]) => void;
22
+ defaultValue?: any;
23
+ defaultChecked?: boolean;
24
+ name?: string;
25
+ type?: string;
26
+ };
27
+ export declare type GetInputProps = <T extends MinimalInputProps>(props?: Omit<T, HandledProps | Callbacks> & Partial<Pick<T, Callbacks>>) => T;
20
28
  export declare const createGetInputProps: ({ clearError, validate, defaultValue, touched, setTouched, hasBeenSubmitted, validationBehavior, name, }: CreateGetInputPropsOptions) => GetInputProps;
21
29
  export {};
@@ -6,18 +6,28 @@ const defaultValidationBehavior = {
6
6
  whenTouched: "onChange",
7
7
  whenSubmitted: "onChange",
8
8
  };
9
+ const getCheckboxDefaultChecked = (value, defaultValue) => {
10
+ if (Array.isArray(defaultValue))
11
+ defaultValue.includes(value);
12
+ if (typeof defaultValue === "boolean")
13
+ return defaultValue;
14
+ if (typeof defaultValue === "string")
15
+ return defaultValue === value;
16
+ return undefined;
17
+ };
9
18
  const createGetInputProps = ({ clearError, validate, defaultValue, touched, setTouched, hasBeenSubmitted, validationBehavior, name, }) => {
10
19
  const validationBehaviors = {
11
20
  ...defaultValidationBehavior,
12
21
  ...validationBehavior,
13
22
  };
14
23
  return (props = {}) => {
24
+ var _a, _b;
15
25
  const behavior = hasBeenSubmitted
16
26
  ? validationBehaviors.whenSubmitted
17
27
  : touched
18
28
  ? validationBehaviors.whenTouched
19
29
  : validationBehaviors.initial;
20
- const result = {
30
+ const inputProps = {
21
31
  ...props,
22
32
  onChange: (...args) => {
23
33
  var _a;
@@ -34,10 +44,20 @@ const createGetInputProps = ({ clearError, validate, defaultValue, touched, setT
34
44
  setTouched(true);
35
45
  return (_a = props === null || props === void 0 ? void 0 : props.onBlur) === null || _a === void 0 ? void 0 : _a.call(props, ...args);
36
46
  },
37
- defaultValue,
38
47
  name,
39
48
  };
40
- return result;
49
+ if (inputProps.type === "checkbox") {
50
+ const value = (_a = props.value) !== null && _a !== void 0 ? _a : "on";
51
+ inputProps.defaultChecked = getCheckboxDefaultChecked(value, defaultValue);
52
+ }
53
+ else if (inputProps.type === "radio") {
54
+ const value = (_b = props.value) !== null && _b !== void 0 ? _b : "on";
55
+ inputProps.defaultChecked = defaultValue === value;
56
+ }
57
+ else {
58
+ inputProps.defaultValue = defaultValue;
59
+ }
60
+ return inputProps;
41
61
  };
42
62
  };
43
63
  exports.createGetInputProps = createGetInputProps;
File without changes
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "remix-validated-form",
3
- "version": "3.2.1",
3
+ "version": "3.4.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,10 +14,7 @@ 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
+ import { MultiValueMap, useMultiValueMap } from "./internal/MultiValueMap";
21
18
  import { useSubmitComplete } from "./internal/submissionCallbacks";
22
19
  import { omit, mergeRefs } from "./internal/util";
23
20
  import {
@@ -229,13 +226,22 @@ export function ValidatedForm<DataType>({
229
226
  getDataFromForm(formRef.current),
230
227
  fieldName as any
231
228
  );
229
+
230
+ // By checking and returning `prev` here, we can avoid a re-render
231
+ // if the validation state is the same.
232
232
  if (error) {
233
- setFieldErrors((prev) => ({
234
- ...prev,
235
- [fieldName]: error,
236
- }));
233
+ setFieldErrors((prev) => {
234
+ if (prev[fieldName] === error) return prev;
235
+ return {
236
+ ...prev,
237
+ [fieldName]: error,
238
+ };
239
+ });
237
240
  } else {
238
- setFieldErrors((prev) => omit(prev, fieldName));
241
+ setFieldErrors((prev) => {
242
+ if (!(fieldName in prev)) return prev;
243
+ return omit(prev, fieldName);
244
+ });
239
245
  }
240
246
  },
241
247
  registerReceiveFocus: (fieldName, handler) => {
@@ -4,7 +4,6 @@ export class MultiValueMap<Key, Value> {
4
4
  private dict: Map<Key, Value[]> = new Map();
5
5
 
6
6
  add = (key: Key, value: Value) => {
7
- this.dict.set(key, [...(this.dict.get(key) ?? []), value]);
8
7
  if (this.dict.has(key)) {
9
8
  this.dict.get(key)!.push(value);
10
9
  } else {
@@ -24,6 +23,8 @@ export class MultiValueMap<Key, Value> {
24
23
  return this.dict.get(key) ?? [];
25
24
  };
26
25
 
26
+ entries = (): IterableIterator<[Key, Value[]]> => this.dict.entries();
27
+
27
28
  has = (key: Key): boolean => this.dict.has(key);
28
29
  }
29
30
 
@@ -1,48 +1,11 @@
1
- // `flatten` is taken from https://github.com/richie5um/FlattenJS. Decided to implement them here instead of using that package because this is a core functionality of the library and this will add more flexibility in case we need to change the implementation.
2
- import assign from "lodash/assign";
3
- import isArray from "lodash/isArray";
4
- import isObject from "lodash/isObject";
5
- import keys from "lodash/keys";
6
- import mapKeys from "lodash/mapKeys";
7
1
  import set from "lodash/set";
8
- import transform from "lodash/transform";
9
- import { GenericObject } from "..";
2
+ import { MultiValueMap } from "./MultiValueMap";
10
3
 
11
- export const objectFromPathEntries = (entries: [string, any][]) =>
12
- entries.reduce((acc, [key, value]) => set(acc, key, value), {});
13
-
14
- /** Flatten an object so there are no nested objects or arrays */
15
- export function flatten(obj: GenericObject, preserveEmpty = false) {
16
- return transform(
17
- obj,
18
- function (result: GenericObject, value, key) {
19
- if (isObject(value)) {
20
- let flatMap = mapKeys(
21
- flatten(value, preserveEmpty),
22
- function (_mvalue, mkey) {
23
- if (isArray(value)) {
24
- let index = mkey.indexOf(".");
25
- if (-1 !== index) {
26
- return `${key}[${mkey.slice(0, index)}]${mkey.slice(index)}`;
27
- }
28
- return `${key}[${mkey}]`;
29
- }
30
- return `${key}.${mkey}`;
31
- }
32
- );
33
-
34
- assign(result, flatMap);
35
-
36
- // Preverve Empty arrays and objects
37
- if (preserveEmpty && keys(flatMap).length === 0) {
38
- result[key] = value;
39
- }
40
- } else {
41
- result[key] = value;
42
- }
43
-
44
- return result;
45
- },
4
+ export const objectFromPathEntries = (entries: [string, any][]) => {
5
+ const map = new MultiValueMap<string, any>();
6
+ entries.forEach(([key, value]) => map.add(key, value));
7
+ return [...map.entries()].reduce(
8
+ (acc, [key, value]) => set(acc, key, value.length === 1 ? value[0] : value),
46
9
  {}
47
10
  );
48
- }
11
+ };
@@ -17,10 +17,19 @@ export type CreateGetInputPropsOptions = {
17
17
  name: string;
18
18
  };
19
19
 
20
- type HandledProps = "name" | "defaultValue";
20
+ type HandledProps = "name" | "defaultValue" | "defaultChecked";
21
21
  type Callbacks = "onChange" | "onBlur";
22
22
 
23
- export type GetInputProps = <T extends Record<string, any>>(
23
+ type MinimalInputProps = {
24
+ onChange?: (...args: any[]) => void;
25
+ onBlur?: (...args: any[]) => void;
26
+ defaultValue?: any;
27
+ defaultChecked?: boolean;
28
+ name?: string;
29
+ type?: string;
30
+ };
31
+
32
+ export type GetInputProps = <T extends MinimalInputProps>(
24
33
  props?: Omit<T, HandledProps | Callbacks> & Partial<Pick<T, Callbacks>>
25
34
  ) => T;
26
35
 
@@ -30,6 +39,13 @@ const defaultValidationBehavior: ValidationBehaviorOptions = {
30
39
  whenSubmitted: "onChange",
31
40
  };
32
41
 
42
+ const getCheckboxDefaultChecked = (value: string, defaultValue: any) => {
43
+ if (Array.isArray(defaultValue)) defaultValue.includes(value);
44
+ if (typeof defaultValue === "boolean") return defaultValue;
45
+ if (typeof defaultValue === "string") return defaultValue === value;
46
+ return undefined;
47
+ };
48
+
33
49
  export const createGetInputProps = ({
34
50
  clearError,
35
51
  validate,
@@ -45,14 +61,14 @@ export const createGetInputProps = ({
45
61
  ...validationBehavior,
46
62
  };
47
63
 
48
- return <T extends Record<string, any>>(props = {} as any) => {
64
+ return <T extends MinimalInputProps>(props = {} as any) => {
49
65
  const behavior = hasBeenSubmitted
50
66
  ? validationBehaviors.whenSubmitted
51
67
  : touched
52
68
  ? validationBehaviors.whenTouched
53
69
  : validationBehaviors.initial;
54
70
 
55
- const result: T = {
71
+ const inputProps: T = {
56
72
  ...props,
57
73
  onChange: (...args: unknown[]) => {
58
74
  if (behavior === "onChange") validate();
@@ -64,10 +80,22 @@ export const createGetInputProps = ({
64
80
  setTouched(true);
65
81
  return props?.onBlur?.(...args);
66
82
  },
67
- defaultValue,
68
83
  name,
69
84
  };
70
85
 
71
- return result;
86
+ if (inputProps.type === "checkbox") {
87
+ const value = props.value ?? "on";
88
+ inputProps.defaultChecked = getCheckboxDefaultChecked(
89
+ value,
90
+ defaultValue
91
+ );
92
+ } else if (inputProps.type === "radio") {
93
+ const value = props.value ?? "on";
94
+ inputProps.defaultChecked = defaultValue === value;
95
+ } else {
96
+ inputProps.defaultValue = defaultValue;
97
+ }
98
+
99
+ return inputProps;
72
100
  };
73
101
  };