remix-validated-form 3.2.0 → 3.3.1

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.0 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.0 build:main
8
8
  > tsc --module CommonJS --outDir ./build
9
9
 
package/README.md CHANGED
@@ -63,7 +63,7 @@ type MyInputProps = {
63
63
  label: string;
64
64
  };
65
65
 
66
- export const MyInput = ({ name, label }: InputProps) => {
66
+ export const MyInput = ({ name, label }: MyInputProps) => {
67
67
  const { error, getInputProps } = useField(name);
68
68
  return (
69
69
  <div>
@@ -257,4 +257,4 @@ See the [Remix](https://remix.run/docs/en/v1/api/remix#sessionflashkey-value) do
257
257
 
258
258
  ## Why is my cancel button triggering form submission?
259
259
  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.
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.
@@ -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
  }
@@ -6,7 +6,12 @@ import keys from "lodash/keys";
6
6
  import mapKeys from "lodash/mapKeys";
7
7
  import set from "lodash/set";
8
8
  import transform from "lodash/transform";
9
- export const objectFromPathEntries = (entries) => entries.reduce((acc, [key, value]) => set(acc, key, value), {});
9
+ import { MultiValueMap } from "./MultiValueMap";
10
+ export const objectFromPathEntries = (entries) => {
11
+ const map = new MultiValueMap();
12
+ entries.forEach(([key, value]) => map.add(key, value));
13
+ return [...map.entries()].reduce((acc, [key, value]) => set(acc, key, value.length === 1 ? value[0] : value), {});
14
+ };
10
15
  /** Flatten an object so there are no nested objects or arrays */
11
16
  export function flatten(obj, preserveEmpty = false) {
12
17
  return transform(obj, function (result, value, key) {
@@ -14,15 +14,10 @@ export declare type CreateGetInputPropsOptions = {
14
14
  validationBehavior?: Partial<ValidationBehaviorOptions>;
15
15
  name: string;
16
16
  };
17
- export declare type MinimalInputProps = {
18
- onChange?: (...args: any[]) => void;
19
- onBlur?: (...args: any[]) => void;
17
+ declare type OmitHandledProps = {
18
+ name?: never;
19
+ defaultValue?: never;
20
20
  };
21
- export declare type MinimalResult = {
22
- name: string;
23
- onChange: (...args: any[]) => void;
24
- onBlur: (...args: any[]) => void;
25
- defaultValue?: any;
26
- };
27
- export declare type GetInputProps = <T extends {}>(props?: T & MinimalInputProps) => T & MinimalResult;
21
+ export declare type GetInputProps = <T extends Record<string, any>>(props?: T & OmitHandledProps) => T;
28
22
  export declare const createGetInputProps: ({ clearError, validate, defaultValue, touched, setTouched, hasBeenSubmitted, validationBehavior, name, }: CreateGetInputPropsOptions) => GetInputProps;
23
+ export {};
@@ -14,7 +14,7 @@ export const createGetInputProps = ({ clearError, validate, defaultValue, touche
14
14
  : touched
15
15
  ? validationBehaviors.whenTouched
16
16
  : validationBehaviors.initial;
17
- return {
17
+ const result = {
18
18
  ...props,
19
19
  onChange: (...args) => {
20
20
  var _a;
@@ -34,5 +34,6 @@ export const createGetInputProps = ({ clearError, validate, defaultValue, touche
34
34
  defaultValue,
35
35
  name,
36
36
  };
37
+ return result;
37
38
  };
38
39
  };
@@ -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;
@@ -12,7 +12,12 @@ const keys_1 = __importDefault(require("lodash/keys"));
12
12
  const mapKeys_1 = __importDefault(require("lodash/mapKeys"));
13
13
  const set_1 = __importDefault(require("lodash/set"));
14
14
  const transform_1 = __importDefault(require("lodash/transform"));
15
- const objectFromPathEntries = (entries) => entries.reduce((acc, [key, value]) => (0, set_1.default)(acc, key, value), {});
15
+ const MultiValueMap_1 = require("./MultiValueMap");
16
+ const objectFromPathEntries = (entries) => {
17
+ const map = new MultiValueMap_1.MultiValueMap();
18
+ entries.forEach(([key, value]) => map.add(key, value));
19
+ return [...map.entries()].reduce((acc, [key, value]) => (0, set_1.default)(acc, key, value.length === 1 ? value[0] : value), {});
20
+ };
16
21
  exports.objectFromPathEntries = objectFromPathEntries;
17
22
  /** Flatten an object so there are no nested objects or arrays */
18
23
  function flatten(obj, preserveEmpty = false) {
@@ -14,15 +14,10 @@ export declare type CreateGetInputPropsOptions = {
14
14
  validationBehavior?: Partial<ValidationBehaviorOptions>;
15
15
  name: string;
16
16
  };
17
- export declare type MinimalInputProps = {
18
- onChange?: (...args: any[]) => void;
19
- onBlur?: (...args: any[]) => void;
17
+ declare type OmitHandledProps = {
18
+ name?: never;
19
+ defaultValue?: never;
20
20
  };
21
- export declare type MinimalResult = {
22
- name: string;
23
- onChange: (...args: any[]) => void;
24
- onBlur: (...args: any[]) => void;
25
- defaultValue?: any;
26
- };
27
- export declare type GetInputProps = <T extends {}>(props?: T & MinimalInputProps) => T & MinimalResult;
21
+ export declare type GetInputProps = <T extends Record<string, any>>(props?: T & OmitHandledProps) => T;
28
22
  export declare const createGetInputProps: ({ clearError, validate, defaultValue, touched, setTouched, hasBeenSubmitted, validationBehavior, name, }: CreateGetInputPropsOptions) => GetInputProps;
23
+ export {};
@@ -17,7 +17,7 @@ const createGetInputProps = ({ clearError, validate, defaultValue, touched, setT
17
17
  : touched
18
18
  ? validationBehaviors.whenTouched
19
19
  : validationBehaviors.initial;
20
- return {
20
+ const result = {
21
21
  ...props,
22
22
  onChange: (...args) => {
23
23
  var _a;
@@ -37,6 +37,7 @@ const createGetInputProps = ({ clearError, validate, defaultValue, touched, setT
37
37
  defaultValue,
38
38
  name,
39
39
  };
40
+ return result;
40
41
  };
41
42
  };
42
43
  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.0",
3
+ "version": "3.3.1",
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
 
@@ -7,9 +7,16 @@ import mapKeys from "lodash/mapKeys";
7
7
  import set from "lodash/set";
8
8
  import transform from "lodash/transform";
9
9
  import { GenericObject } from "..";
10
+ import { MultiValueMap } from "./MultiValueMap";
10
11
 
11
- export const objectFromPathEntries = (entries: [string, any][]) =>
12
- entries.reduce((acc, [key, value]) => set(acc, key, value), {});
12
+ export const objectFromPathEntries = (entries: [string, any][]) => {
13
+ const map = new MultiValueMap<string, any>();
14
+ entries.forEach(([key, value]) => map.add(key, value));
15
+ return [...map.entries()].reduce(
16
+ (acc, [key, value]) => set(acc, key, value.length === 1 ? value[0] : value),
17
+ {}
18
+ );
19
+ };
13
20
 
14
21
  /** Flatten an object so there are no nested objects or arrays */
15
22
  export function flatten(obj: GenericObject, preserveEmpty = false) {
@@ -17,21 +17,13 @@ export type CreateGetInputPropsOptions = {
17
17
  name: string;
18
18
  };
19
19
 
20
- export type MinimalInputProps = {
21
- onChange?: (...args: any[]) => void;
22
- onBlur?: (...args: any[]) => void;
23
- };
20
+ // Using Omit<T, HandledProps> breaks type inference sometimes for some reason.
21
+ // Doing T & OmitHandledProps gives us the same behavior without breaking type inference.
22
+ type OmitHandledProps = { name?: never; defaultValue?: never };
24
23
 
25
- export type MinimalResult = {
26
- name: string;
27
- onChange: (...args: any[]) => void;
28
- onBlur: (...args: any[]) => void;
29
- defaultValue?: any;
30
- };
31
-
32
- export type GetInputProps = <T extends {}>(
33
- props?: T & MinimalInputProps
34
- ) => T & MinimalResult;
24
+ export type GetInputProps = <T extends Record<string, any>>(
25
+ props?: T & OmitHandledProps
26
+ ) => T;
35
27
 
36
28
  const defaultValidationBehavior: ValidationBehaviorOptions = {
37
29
  initial: "onBlur",
@@ -54,20 +46,21 @@ export const createGetInputProps = ({
54
46
  ...validationBehavior,
55
47
  };
56
48
 
57
- return (props = {} as any) => {
49
+ return <T extends Record<string, any>>(props = {} as any) => {
58
50
  const behavior = hasBeenSubmitted
59
51
  ? validationBehaviors.whenSubmitted
60
52
  : touched
61
53
  ? validationBehaviors.whenTouched
62
54
  : validationBehaviors.initial;
63
- return {
55
+
56
+ const result: T = {
64
57
  ...props,
65
- onChange: (...args) => {
58
+ onChange: (...args: unknown[]) => {
66
59
  if (behavior === "onChange") validate();
67
60
  else clearError();
68
61
  return props?.onChange?.(...args);
69
62
  },
70
- onBlur: (...args) => {
63
+ onBlur: (...args: unknown[]) => {
71
64
  if (behavior === "onBlur") validate();
72
65
  setTouched(true);
73
66
  return props?.onBlur?.(...args);
@@ -75,5 +68,7 @@ export const createGetInputProps = ({
75
68
  defaultValue,
76
69
  name,
77
70
  };
71
+
72
+ return result;
78
73
  };
79
74
  };