remix-validated-form 4.5.2 → 4.6.0-beta.0
Sign up to get free protection for your applications and to get access to all the features.
- package/.turbo/turbo-build.log +8 -9
- package/browser/ValidatedForm.js +0 -3
- package/browser/index.d.ts +1 -0
- package/browser/index.js +1 -0
- package/browser/internal/hooks.d.ts +1 -0
- package/browser/internal/hooks.js +3 -4
- package/browser/internal/state/controlledFields.d.ts +1 -0
- package/browser/internal/state/controlledFields.js +17 -29
- package/browser/internal/state/createFormStore.d.ts +31 -1
- package/browser/internal/state/createFormStore.js +177 -14
- package/browser/server.d.ts +2 -2
- package/browser/server.js +1 -1
- package/dist/remix-validated-form.cjs.js +12 -3
- package/dist/remix-validated-form.cjs.js.map +1 -1
- package/dist/remix-validated-form.es.js +361 -131
- package/dist/remix-validated-form.es.js.map +1 -1
- package/dist/remix-validated-form.umd.js +12 -3
- package/dist/remix-validated-form.umd.js.map +1 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/internal/hooks.d.ts +1 -0
- package/dist/types/internal/state/arrayUtil.d.ts +12 -0
- package/dist/types/internal/state/controlledFields.d.ts +1 -0
- package/dist/types/internal/state/createFormStore.d.ts +31 -1
- package/dist/types/internal/state/fieldArray.d.ts +28 -0
- package/dist/types/server.d.ts +2 -2
- package/package.json +1 -3
- package/src/ValidatedForm.tsx +0 -3
- package/src/index.ts +6 -0
- package/src/internal/hooks.ts +9 -4
- package/src/internal/logic/nestedObjectToPathObject.ts +63 -0
- package/src/internal/state/arrayUtil.ts +399 -0
- package/src/internal/state/controlledFields.ts +39 -43
- package/src/internal/state/createFormStore.ts +288 -20
- package/src/internal/state/fieldArray.tsx +155 -0
- package/src/server.ts +1 -1
- package/vite.config.ts +1 -1
- package/dist/types/internal/state/controlledFieldStore.d.ts +0 -26
- package/src/internal/state/controlledFieldStore.ts +0 -112
package/dist/types/index.d.ts
CHANGED
@@ -4,3 +4,4 @@ export * from "./ValidatedForm";
|
|
4
4
|
export * from "./validation/types";
|
5
5
|
export * from "./validation/createValidator";
|
6
6
|
export * from "./userFacingFormContext";
|
7
|
+
export { FieldArray, useFieldArray, type FieldArrayProps, type FieldArrayHelpers, } from "./internal/state/fieldArray";
|
@@ -13,6 +13,7 @@ export declare const useHasActiveFormSubmit: ({ fetcher, }: InternalFormContextV
|
|
13
13
|
export declare const useFieldTouched: (field: string, { formId }: InternalFormContextValue) => readonly [boolean, (touched: boolean) => void];
|
14
14
|
export declare const useFieldError: (name: string, context: InternalFormContextValue) => string | undefined;
|
15
15
|
export declare const useClearError: (context: InternalFormContextValue) => (field: string) => void;
|
16
|
+
export declare const useCurrentDefaultValueForField: (formId: InternalFormId, field: string) => any;
|
16
17
|
export declare const useFieldDefaultValue: (name: string, context: InternalFormContextValue) => any;
|
17
18
|
export declare const useInternalIsSubmitting: (formId: InternalFormId) => boolean;
|
18
19
|
export declare const useInternalIsValid: (formId: InternalFormId) => boolean;
|
@@ -0,0 +1,12 @@
|
|
1
|
+
export declare const getArray: (values: any, field: string) => unknown[];
|
2
|
+
export declare const swap: (array: unknown[], indexA: number, indexB: number) => void;
|
3
|
+
export declare const move: (array: unknown[], from: number, to: number) => void;
|
4
|
+
export declare const insert: (array: unknown[], index: number, value: unknown) => void;
|
5
|
+
export declare const remove: (array: unknown[], index: number) => void;
|
6
|
+
export declare const replace: (array: unknown[], index: number, value: unknown) => void;
|
7
|
+
/**
|
8
|
+
* The purpose of this helper is to make it easier to update `fieldErrors` and `touchedFields`.
|
9
|
+
* We key those objects by full paths to the fields.
|
10
|
+
* When we're doing array mutations, that makes it difficult to update those objects.
|
11
|
+
*/
|
12
|
+
export declare const mutateAsArray: (field: string, obj: Record<string, any>, mutate: (arr: any[]) => void) => void;
|
@@ -1,6 +1,7 @@
|
|
1
1
|
import { InternalFormContextValue } from "../formContext";
|
2
2
|
import { InternalFormId } from "./types";
|
3
3
|
export declare const useControlledFieldValue: (context: InternalFormContextValue, field: string) => any;
|
4
|
+
export declare const useRegisterControlledField: (context: InternalFormContextValue, field: string) => void;
|
4
5
|
export declare const useControllableValue: (context: InternalFormContextValue, field: string) => readonly [any, (value: unknown) => void];
|
5
6
|
export declare const useUpdateControllableValue: (formId: InternalFormId) => (field: string, value: unknown) => void;
|
6
7
|
export declare const useAwaitValue: (formId: InternalFormId) => (field: string) => Promise<void>;
|
@@ -27,6 +27,7 @@ export declare type FormState = {
|
|
27
27
|
touchedFields: TouchedFields;
|
28
28
|
formProps?: SyncedFormProps;
|
29
29
|
formElement: HTMLFormElement | null;
|
30
|
+
currentDefaultValues: Record<string, any>;
|
30
31
|
isValid: () => boolean;
|
31
32
|
startSubmit: () => void;
|
32
33
|
endSubmit: () => void;
|
@@ -36,13 +37,42 @@ export declare type FormState = {
|
|
36
37
|
clearFieldError: (field: string) => void;
|
37
38
|
reset: () => void;
|
38
39
|
syncFormProps: (props: SyncedFormProps) => void;
|
39
|
-
setHydrated: () => void;
|
40
40
|
setFormElement: (formElement: HTMLFormElement | null) => void;
|
41
41
|
validateField: (fieldName: string) => Promise<string | null>;
|
42
42
|
validate: () => Promise<ValidationResult<unknown>>;
|
43
43
|
resetFormElement: () => void;
|
44
44
|
submit: () => void;
|
45
45
|
getValues: () => FormData;
|
46
|
+
controlledFields: {
|
47
|
+
values: {
|
48
|
+
[fieldName: string]: any;
|
49
|
+
};
|
50
|
+
refCounts: {
|
51
|
+
[fieldName: string]: number;
|
52
|
+
};
|
53
|
+
valueUpdatePromises: {
|
54
|
+
[fieldName: string]: Promise<void>;
|
55
|
+
};
|
56
|
+
valueUpdateResolvers: {
|
57
|
+
[fieldName: string]: () => void;
|
58
|
+
};
|
59
|
+
register: (fieldName: string) => void;
|
60
|
+
unregister: (fieldName: string) => void;
|
61
|
+
setValue: (fieldName: string, value: unknown) => void;
|
62
|
+
kickoffValueUpdate: (fieldName: string) => void;
|
63
|
+
getValue: (fieldName: string) => unknown;
|
64
|
+
awaitValueUpdate: (fieldName: string) => Promise<void>;
|
65
|
+
array: {
|
66
|
+
push: (fieldName: string, value: unknown) => void;
|
67
|
+
swap: (fieldName: string, indexA: number, indexB: number) => void;
|
68
|
+
move: (fieldName: string, fromIndex: number, toIndex: number) => void;
|
69
|
+
insert: (fieldName: string, index: number, value: unknown) => void;
|
70
|
+
unshift: (fieldName: string, value: unknown) => void;
|
71
|
+
remove: (fieldName: string, index: number) => void;
|
72
|
+
pop: (fieldName: string) => void;
|
73
|
+
replace: (fieldName: string, index: number, value: unknown) => void;
|
74
|
+
};
|
75
|
+
};
|
46
76
|
};
|
47
77
|
export declare const useRootFormStore: import("zustand").UseBoundStore<Omit<import("zustand").StoreApi<FormStoreState>, "setState"> & {
|
48
78
|
setState(nextStateOrUpdater: FormStoreState | Partial<FormStoreState> | ((state: WritableDraft<FormStoreState>) => void), shouldReplace?: boolean | undefined): void;
|
@@ -0,0 +1,28 @@
|
|
1
|
+
import React from "react";
|
2
|
+
export declare type FieldArrayValidationBehavior = "onChange" | "onSubmit";
|
3
|
+
export declare type FieldArrayValidationBehaviorOptions = {
|
4
|
+
initial: FieldArrayValidationBehavior;
|
5
|
+
whenSubmitted: FieldArrayValidationBehavior;
|
6
|
+
};
|
7
|
+
export declare type FieldArrayHelpers<Item = any> = {
|
8
|
+
push: (item: Item) => void;
|
9
|
+
swap: (indexA: number, indexB: number) => void;
|
10
|
+
move: (from: number, to: number) => void;
|
11
|
+
insert: (index: number, value: Item) => void;
|
12
|
+
unshift: (value: Item) => void;
|
13
|
+
remove: (index: number) => void;
|
14
|
+
pop: () => void;
|
15
|
+
replace: (index: number, value: Item) => void;
|
16
|
+
};
|
17
|
+
export declare type UseFieldArrayOptions = {
|
18
|
+
formId?: string;
|
19
|
+
validationBehavior?: Partial<FieldArrayValidationBehaviorOptions>;
|
20
|
+
};
|
21
|
+
export declare function useFieldArray<Item = any>(name: string, { formId, validationBehavior }?: UseFieldArrayOptions): [itemDefaults: Item[], helpers: FieldArrayHelpers<any>, error: string | undefined];
|
22
|
+
export declare type FieldArrayProps = {
|
23
|
+
name: string;
|
24
|
+
children: (itemDefaults: any[], helpers: FieldArrayHelpers, error: string | undefined) => React.ReactNode;
|
25
|
+
formId?: string;
|
26
|
+
validationBehavior?: FieldArrayValidationBehaviorOptions;
|
27
|
+
};
|
28
|
+
export declare const FieldArray: ({ name, children, formId, validationBehavior, }: FieldArrayProps) => React.ReactNode;
|
package/dist/types/server.d.ts
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
import { FORM_DEFAULTS_FIELD } from "./internal/constants";
|
2
|
-
import { ValidatorError } from "./validation/types";
|
2
|
+
import { ValidatorError, ValidationErrorResponseData } from "./validation/types";
|
3
3
|
/**
|
4
4
|
* Takes the errors from a `Validator` and returns a `Response`.
|
5
5
|
* When you return this from your action, `ValidatedForm` on the frontend will automatically
|
@@ -14,7 +14,7 @@ import { ValidatorError } from "./validation/types";
|
|
14
14
|
* if (result.error) return validationError(result.error, result.submittedData);
|
15
15
|
* ```
|
16
16
|
*/
|
17
|
-
export declare function validationError(error: ValidatorError, repopulateFields?: unknown, init?: ResponseInit):
|
17
|
+
export declare function validationError(error: ValidatorError, repopulateFields?: unknown, init?: ResponseInit): import("@remix-run/server-runtime").TypedResponse<ValidationErrorResponseData>;
|
18
18
|
export declare type FormDefaults = {
|
19
19
|
[formDefaultsKey: `${typeof FORM_DEFAULTS_FIELD}_${string}`]: any;
|
20
20
|
};
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "remix-validated-form",
|
3
|
-
"version": "4.
|
3
|
+
"version": "4.6.0-beta.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",
|
@@ -34,12 +34,10 @@
|
|
34
34
|
],
|
35
35
|
"peerDependencies": {
|
36
36
|
"@remix-run/react": "1.x",
|
37
|
-
"@remix-run/server-runtime": "1.x",
|
38
37
|
"react": "^17.0.2 || ^18.0.0"
|
39
38
|
},
|
40
39
|
"devDependencies": {
|
41
40
|
"@remix-run/react": "^1.6.5",
|
42
|
-
"@remix-run/server-runtime": "^1.6.5",
|
43
41
|
"@types/lodash": "^4.14.178",
|
44
42
|
"@types/react": "^18.0.9",
|
45
43
|
"fetch-blob": "^3.1.3",
|
package/src/ValidatedForm.tsx
CHANGED
@@ -23,7 +23,6 @@ import {
|
|
23
23
|
useSetFieldErrors,
|
24
24
|
} from "./internal/hooks";
|
25
25
|
import { MultiValueMap, useMultiValueMap } from "./internal/MultiValueMap";
|
26
|
-
import { useControlledFieldStore } from "./internal/state/controlledFieldStore";
|
27
26
|
import {
|
28
27
|
SyncedFormProps,
|
29
28
|
useRootFormStore,
|
@@ -237,7 +236,6 @@ export function ValidatedForm<DataType>({
|
|
237
236
|
const setFieldErrors = useSetFieldErrors(formId);
|
238
237
|
const setFieldError = useFormStore(formId, (state) => state.setFieldError);
|
239
238
|
const reset = useFormStore(formId, (state) => state.reset);
|
240
|
-
const resetControlledFields = useControlledFieldStore((state) => state.reset);
|
241
239
|
const startSubmit = useFormStore(formId, (state) => state.startSubmit);
|
242
240
|
const endSubmit = useFormStore(formId, (state) => state.endSubmit);
|
243
241
|
const syncFormProps = useFormStore(formId, (state) => state.syncFormProps);
|
@@ -354,7 +352,6 @@ export function ValidatedForm<DataType>({
|
|
354
352
|
onReset?.(event);
|
355
353
|
if (event.defaultPrevented) return;
|
356
354
|
reset();
|
357
|
-
resetControlledFields(formId);
|
358
355
|
}}
|
359
356
|
>
|
360
357
|
<InternalFormContext.Provider value={contextValue}>
|
package/src/index.ts
CHANGED
@@ -4,3 +4,9 @@ export * from "./ValidatedForm";
|
|
4
4
|
export * from "./validation/types";
|
5
5
|
export * from "./validation/createValidator";
|
6
6
|
export * from "./userFacingFormContext";
|
7
|
+
export {
|
8
|
+
FieldArray,
|
9
|
+
useFieldArray,
|
10
|
+
type FieldArrayProps,
|
11
|
+
type FieldArrayHelpers,
|
12
|
+
} from "./internal/state/fieldArray";
|
package/src/internal/hooks.ts
CHANGED
@@ -141,15 +141,20 @@ export const useClearError = (context: InternalFormContextValue) => {
|
|
141
141
|
return useFormStore(formId, (state) => state.clearFieldError);
|
142
142
|
};
|
143
143
|
|
144
|
+
export const useCurrentDefaultValueForField = (
|
145
|
+
formId: InternalFormId,
|
146
|
+
field: string
|
147
|
+
) =>
|
148
|
+
useFormStore(formId, (state) => lodashGet(state.currentDefaultValues, field));
|
149
|
+
|
144
150
|
export const useFieldDefaultValue = (
|
145
151
|
name: string,
|
146
152
|
context: InternalFormContextValue
|
147
153
|
) => {
|
148
154
|
const defaultValues = useDefaultValuesForForm(context);
|
149
|
-
const state =
|
150
|
-
|
151
|
-
|
152
|
-
.hydrateTo(lodashGet(state, name));
|
155
|
+
const state = useCurrentDefaultValueForField(context.formId, name);
|
156
|
+
|
157
|
+
return defaultValues.map((val) => lodashGet(val, name)).hydrateTo(state);
|
153
158
|
};
|
154
159
|
|
155
160
|
export const useInternalIsSubmitting = (formId: InternalFormId) =>
|
@@ -0,0 +1,63 @@
|
|
1
|
+
export const nestedObjectToPathObject = (
|
2
|
+
val: any,
|
3
|
+
acc: Record<string, any>,
|
4
|
+
path: string
|
5
|
+
): any => {
|
6
|
+
if (Array.isArray(val)) {
|
7
|
+
val.forEach((v, index) =>
|
8
|
+
nestedObjectToPathObject(v, acc, `${path}[${index}]`)
|
9
|
+
);
|
10
|
+
return acc;
|
11
|
+
}
|
12
|
+
|
13
|
+
if (typeof val === "object") {
|
14
|
+
Object.entries(val).forEach(([key, value]) => {
|
15
|
+
const nextPath = path ? `${path}.${key}` : key;
|
16
|
+
nestedObjectToPathObject(value, acc, nextPath);
|
17
|
+
});
|
18
|
+
return acc;
|
19
|
+
}
|
20
|
+
|
21
|
+
if (val !== undefined) {
|
22
|
+
acc[path] = val;
|
23
|
+
}
|
24
|
+
|
25
|
+
return acc;
|
26
|
+
};
|
27
|
+
|
28
|
+
if (import.meta.vitest) {
|
29
|
+
const { describe, expect, it } = import.meta.vitest;
|
30
|
+
|
31
|
+
describe("nestedObjectToPathObject", () => {
|
32
|
+
it("should return an object with the correct path", () => {
|
33
|
+
const result = nestedObjectToPathObject(
|
34
|
+
{
|
35
|
+
a: 1,
|
36
|
+
b: 2,
|
37
|
+
c: { foo: "bar", baz: [true, false] },
|
38
|
+
d: [
|
39
|
+
{ foo: "bar", baz: [true, false] },
|
40
|
+
{ e: true, f: "hi" },
|
41
|
+
],
|
42
|
+
g: undefined,
|
43
|
+
},
|
44
|
+
{},
|
45
|
+
""
|
46
|
+
);
|
47
|
+
|
48
|
+
expect(result).toEqual({
|
49
|
+
a: 1,
|
50
|
+
b: 2,
|
51
|
+
"c.foo": "bar",
|
52
|
+
"c.baz[0]": true,
|
53
|
+
"c.baz[1]": false,
|
54
|
+
"d[0].foo": "bar",
|
55
|
+
"d[0].baz[0]": true,
|
56
|
+
"d[0].baz[1]": false,
|
57
|
+
"d[1].e": true,
|
58
|
+
"d[1].f": "hi",
|
59
|
+
});
|
60
|
+
expect(Object.keys(result)).toHaveLength(10);
|
61
|
+
});
|
62
|
+
});
|
63
|
+
}
|
@@ -0,0 +1,399 @@
|
|
1
|
+
import lodashGet from "lodash/get";
|
2
|
+
import lodashSet from "lodash/set";
|
3
|
+
import invariant from "tiny-invariant";
|
4
|
+
|
5
|
+
////
|
6
|
+
// All of these array helpers are written in a way that mutates the original array.
|
7
|
+
// This is because we're working with immer.
|
8
|
+
////
|
9
|
+
|
10
|
+
export const getArray = (values: any, field: string): unknown[] => {
|
11
|
+
const value = lodashGet(values, field);
|
12
|
+
if (value === undefined || value === null) {
|
13
|
+
const newValue: unknown[] = [];
|
14
|
+
lodashSet(values, field, newValue);
|
15
|
+
return newValue;
|
16
|
+
}
|
17
|
+
invariant(
|
18
|
+
Array.isArray(value),
|
19
|
+
`FieldArray: defaultValue value for ${field} must be an array, null, or undefined`
|
20
|
+
);
|
21
|
+
return value;
|
22
|
+
};
|
23
|
+
|
24
|
+
export const swap = (array: unknown[], indexA: number, indexB: number) => {
|
25
|
+
const itemA = array[indexA];
|
26
|
+
const itemB = array[indexB];
|
27
|
+
|
28
|
+
const hasItemA = indexA in array;
|
29
|
+
const hasItemB = indexB in array;
|
30
|
+
|
31
|
+
// If we're dealing with a sparse array (i.e. one of the indeces doesn't exist),
|
32
|
+
// we should keep it sparse
|
33
|
+
if (hasItemA) {
|
34
|
+
array[indexB] = itemA;
|
35
|
+
} else {
|
36
|
+
delete array[indexB];
|
37
|
+
}
|
38
|
+
|
39
|
+
if (hasItemB) {
|
40
|
+
array[indexA] = itemB;
|
41
|
+
} else {
|
42
|
+
delete array[indexA];
|
43
|
+
}
|
44
|
+
};
|
45
|
+
|
46
|
+
// A splice that can handle sparse arrays
|
47
|
+
function sparseSplice(
|
48
|
+
array: unknown[],
|
49
|
+
start: number,
|
50
|
+
deleteCount: number,
|
51
|
+
item?: unknown
|
52
|
+
) {
|
53
|
+
// Inserting an item into an array won't behave as we need it to if the array isn't
|
54
|
+
// at least as long as the start index. We can force the array to be long enough like this.
|
55
|
+
if (array.length < start && item) {
|
56
|
+
array.length = start;
|
57
|
+
}
|
58
|
+
|
59
|
+
// If we just pass item in, it'll be undefined and splice will delete the item.
|
60
|
+
if (arguments.length === 4) return array.splice(start, deleteCount, item);
|
61
|
+
return array.splice(start, deleteCount);
|
62
|
+
}
|
63
|
+
|
64
|
+
export const move = (array: unknown[], from: number, to: number) => {
|
65
|
+
const [item] = sparseSplice(array, from, 1);
|
66
|
+
sparseSplice(array, to, 0, item);
|
67
|
+
};
|
68
|
+
|
69
|
+
export const insert = (array: unknown[], index: number, value: unknown) => {
|
70
|
+
sparseSplice(array, index, 0, value);
|
71
|
+
};
|
72
|
+
|
73
|
+
export const remove = (array: unknown[], index: number) => {
|
74
|
+
sparseSplice(array, index, 1);
|
75
|
+
};
|
76
|
+
|
77
|
+
export const replace = (array: unknown[], index: number, value: unknown) => {
|
78
|
+
sparseSplice(array, index, 1, value);
|
79
|
+
};
|
80
|
+
|
81
|
+
/**
|
82
|
+
* The purpose of this helper is to make it easier to update `fieldErrors` and `touchedFields`.
|
83
|
+
* We key those objects by full paths to the fields.
|
84
|
+
* When we're doing array mutations, that makes it difficult to update those objects.
|
85
|
+
*/
|
86
|
+
export const mutateAsArray = (
|
87
|
+
field: string,
|
88
|
+
obj: Record<string, any>,
|
89
|
+
mutate: (arr: any[]) => void
|
90
|
+
) => {
|
91
|
+
const beforeKeys = new Set<string>();
|
92
|
+
const arr: any[] = [];
|
93
|
+
|
94
|
+
for (const [key, value] of Object.entries(obj)) {
|
95
|
+
if (key.startsWith(field) && key !== field) {
|
96
|
+
beforeKeys.add(key);
|
97
|
+
}
|
98
|
+
lodashSet(arr, key.substring(field.length), value);
|
99
|
+
}
|
100
|
+
|
101
|
+
mutate(arr);
|
102
|
+
for (const key of beforeKeys) {
|
103
|
+
delete obj[key];
|
104
|
+
}
|
105
|
+
|
106
|
+
const newKeys = getDeepArrayPaths(arr);
|
107
|
+
for (const key of newKeys) {
|
108
|
+
const val = lodashGet(arr, key);
|
109
|
+
obj[`${field}${key}`] = val;
|
110
|
+
}
|
111
|
+
};
|
112
|
+
|
113
|
+
const getDeepArrayPaths = (obj: any, basePath: string = ""): string[] => {
|
114
|
+
// This only needs to handle arrays and plain objects
|
115
|
+
// and we can assume the first call is always an array.
|
116
|
+
|
117
|
+
if (Array.isArray(obj)) {
|
118
|
+
return obj.flatMap((item, index) =>
|
119
|
+
getDeepArrayPaths(item, `${basePath}[${index}]`)
|
120
|
+
);
|
121
|
+
}
|
122
|
+
|
123
|
+
if (typeof obj === "object") {
|
124
|
+
return Object.keys(obj).flatMap((key) =>
|
125
|
+
getDeepArrayPaths(obj[key], `${basePath}.${key}`)
|
126
|
+
);
|
127
|
+
}
|
128
|
+
|
129
|
+
return [basePath];
|
130
|
+
};
|
131
|
+
|
132
|
+
if (import.meta.vitest) {
|
133
|
+
const { describe, expect, it } = import.meta.vitest;
|
134
|
+
|
135
|
+
// Count the actual number of items in the array
|
136
|
+
// instead of just getting the length.
|
137
|
+
// This is useful for validating that sparse arrays are handled correctly.
|
138
|
+
const countArrayItems = (arr: any[]) => {
|
139
|
+
let count = 0;
|
140
|
+
arr.forEach(() => count++);
|
141
|
+
return count;
|
142
|
+
};
|
143
|
+
|
144
|
+
describe("getArray", () => {
|
145
|
+
it("shoud get a deeply nested array that can be mutated to update the nested value", () => {
|
146
|
+
const values = {
|
147
|
+
d: [
|
148
|
+
{ foo: "bar", baz: [true, false] },
|
149
|
+
{ e: true, f: "hi" },
|
150
|
+
],
|
151
|
+
};
|
152
|
+
const result = getArray(values, "d[0].baz");
|
153
|
+
const finalValues = {
|
154
|
+
d: [
|
155
|
+
{ foo: "bar", baz: [true, false, true] },
|
156
|
+
{ e: true, f: "hi" },
|
157
|
+
],
|
158
|
+
};
|
159
|
+
|
160
|
+
expect(result).toEqual([true, false]);
|
161
|
+
result.push(true);
|
162
|
+
expect(values).toEqual(finalValues);
|
163
|
+
});
|
164
|
+
|
165
|
+
it("should return an empty array that can be mutated if result is null or undefined", () => {
|
166
|
+
const values = {};
|
167
|
+
const result = getArray(values, "a.foo[0].bar");
|
168
|
+
const finalValues = {
|
169
|
+
a: { foo: [{ bar: ["Bob ross"] }] },
|
170
|
+
};
|
171
|
+
|
172
|
+
expect(result).toEqual([]);
|
173
|
+
result.push("Bob ross");
|
174
|
+
expect(values).toEqual(finalValues);
|
175
|
+
});
|
176
|
+
|
177
|
+
it("should throw if the value is defined and not an array", () => {
|
178
|
+
const values = { foo: "foo" };
|
179
|
+
expect(() => getArray(values, "foo")).toThrow();
|
180
|
+
});
|
181
|
+
});
|
182
|
+
|
183
|
+
describe("swap", () => {
|
184
|
+
it("should swap two items", () => {
|
185
|
+
const array = [1, 2, 3];
|
186
|
+
swap(array, 0, 1);
|
187
|
+
expect(array).toEqual([2, 1, 3]);
|
188
|
+
});
|
189
|
+
|
190
|
+
it("should work for sparse arrays", () => {
|
191
|
+
// A bit of a sanity check for native array behavior
|
192
|
+
const arr = [] as any[];
|
193
|
+
arr[0] = true;
|
194
|
+
swap(arr, 0, 2);
|
195
|
+
|
196
|
+
expect(countArrayItems(arr)).toEqual(1);
|
197
|
+
expect(0 in arr).toBe(false);
|
198
|
+
expect(2 in arr).toBe(true);
|
199
|
+
expect(arr[2]).toEqual(true);
|
200
|
+
});
|
201
|
+
});
|
202
|
+
|
203
|
+
describe("move", () => {
|
204
|
+
it("should move an item to a new index", () => {
|
205
|
+
const array = [1, 2, 3];
|
206
|
+
move(array, 0, 1);
|
207
|
+
expect(array).toEqual([2, 1, 3]);
|
208
|
+
});
|
209
|
+
|
210
|
+
it("should work with sparse arrays", () => {
|
211
|
+
const array = [1];
|
212
|
+
move(array, 0, 2);
|
213
|
+
|
214
|
+
expect(countArrayItems(array)).toEqual(1);
|
215
|
+
expect(array).toEqual([undefined, undefined, 1]);
|
216
|
+
});
|
217
|
+
});
|
218
|
+
|
219
|
+
describe("insert", () => {
|
220
|
+
it("should insert an item at a new index", () => {
|
221
|
+
const array = [1, 2, 3];
|
222
|
+
insert(array, 1, 4);
|
223
|
+
expect(array).toEqual([1, 4, 2, 3]);
|
224
|
+
});
|
225
|
+
|
226
|
+
it("should be able to insert falsey values", () => {
|
227
|
+
const array = [1, 2, 3];
|
228
|
+
insert(array, 1, null);
|
229
|
+
expect(array).toEqual([1, null, 2, 3]);
|
230
|
+
});
|
231
|
+
|
232
|
+
it("should handle sparse arrays", () => {
|
233
|
+
const array: any[] = [];
|
234
|
+
array[2] = true;
|
235
|
+
insert(array, 0, true);
|
236
|
+
|
237
|
+
expect(countArrayItems(array)).toEqual(2);
|
238
|
+
expect(array).toEqual([true, undefined, undefined, true]);
|
239
|
+
});
|
240
|
+
});
|
241
|
+
|
242
|
+
describe("remove", () => {
|
243
|
+
it("should remove an item at a given index", () => {
|
244
|
+
const array = [1, 2, 3];
|
245
|
+
remove(array, 1);
|
246
|
+
expect(array).toEqual([1, 3]);
|
247
|
+
});
|
248
|
+
|
249
|
+
it("should handle sparse arrays", () => {
|
250
|
+
const array: any[] = [];
|
251
|
+
array[2] = true;
|
252
|
+
remove(array, 0);
|
253
|
+
|
254
|
+
expect(countArrayItems(array)).toEqual(1);
|
255
|
+
expect(array).toEqual([undefined, true]);
|
256
|
+
});
|
257
|
+
});
|
258
|
+
|
259
|
+
describe("replace", () => {
|
260
|
+
it("should replace an item at a given index", () => {
|
261
|
+
const array = [1, 2, 3];
|
262
|
+
replace(array, 1, 4);
|
263
|
+
expect(array).toEqual([1, 4, 3]);
|
264
|
+
});
|
265
|
+
|
266
|
+
it("should handle sparse arrays", () => {
|
267
|
+
const array: any[] = [];
|
268
|
+
array[2] = true;
|
269
|
+
replace(array, 0, true);
|
270
|
+
expect(countArrayItems(array)).toEqual(2);
|
271
|
+
expect(array).toEqual([true, undefined, true]);
|
272
|
+
});
|
273
|
+
});
|
274
|
+
|
275
|
+
describe("mutateAsArray", () => {
|
276
|
+
it("should handle swap", () => {
|
277
|
+
const values = {
|
278
|
+
myField: "something",
|
279
|
+
"myField[0]": "foo",
|
280
|
+
"myField[2]": "bar",
|
281
|
+
otherField: "baz",
|
282
|
+
"otherField[0]": "something else",
|
283
|
+
};
|
284
|
+
mutateAsArray("myField", values, (arr) => {
|
285
|
+
swap(arr, 0, 2);
|
286
|
+
});
|
287
|
+
expect(values).toEqual({
|
288
|
+
myField: "something",
|
289
|
+
"myField[0]": "bar",
|
290
|
+
"myField[2]": "foo",
|
291
|
+
otherField: "baz",
|
292
|
+
"otherField[0]": "something else",
|
293
|
+
});
|
294
|
+
});
|
295
|
+
|
296
|
+
it("should swap sparse arrays", () => {
|
297
|
+
const values = {
|
298
|
+
myField: "something",
|
299
|
+
"myField[0]": "foo",
|
300
|
+
otherField: "baz",
|
301
|
+
"otherField[0]": "something else",
|
302
|
+
};
|
303
|
+
mutateAsArray("myField", values, (arr) => {
|
304
|
+
swap(arr, 0, 2);
|
305
|
+
});
|
306
|
+
expect(values).toEqual({
|
307
|
+
myField: "something",
|
308
|
+
"myField[2]": "foo",
|
309
|
+
otherField: "baz",
|
310
|
+
"otherField[0]": "something else",
|
311
|
+
});
|
312
|
+
});
|
313
|
+
|
314
|
+
it("should handle arrays with nested values", () => {
|
315
|
+
const values = {
|
316
|
+
myField: "something",
|
317
|
+
"myField[0].title": "foo",
|
318
|
+
"myField[0].note": "bar",
|
319
|
+
"myField[2].title": "other",
|
320
|
+
"myField[2].note": "other",
|
321
|
+
otherField: "baz",
|
322
|
+
"otherField[0]": "something else",
|
323
|
+
};
|
324
|
+
mutateAsArray("myField", values, (arr) => {
|
325
|
+
swap(arr, 0, 2);
|
326
|
+
});
|
327
|
+
expect(values).toEqual({
|
328
|
+
myField: "something",
|
329
|
+
"myField[0].title": "other",
|
330
|
+
"myField[0].note": "other",
|
331
|
+
"myField[2].title": "foo",
|
332
|
+
"myField[2].note": "bar",
|
333
|
+
otherField: "baz",
|
334
|
+
"otherField[0]": "something else",
|
335
|
+
});
|
336
|
+
});
|
337
|
+
|
338
|
+
it("should handle move", () => {
|
339
|
+
const values = {
|
340
|
+
myField: "something",
|
341
|
+
"myField[0]": "foo",
|
342
|
+
"myField[1]": "bar",
|
343
|
+
"myField[2]": "baz",
|
344
|
+
"otherField[0]": "something else",
|
345
|
+
};
|
346
|
+
mutateAsArray("myField", values, (arr) => {
|
347
|
+
move(arr, 0, 2);
|
348
|
+
});
|
349
|
+
expect(values).toEqual({
|
350
|
+
myField: "something",
|
351
|
+
"myField[0]": "bar",
|
352
|
+
"myField[1]": "baz",
|
353
|
+
"myField[2]": "foo",
|
354
|
+
"otherField[0]": "something else",
|
355
|
+
});
|
356
|
+
});
|
357
|
+
|
358
|
+
it("should handle remove", () => {
|
359
|
+
const values = {
|
360
|
+
myField: "something",
|
361
|
+
"myField[0]": "foo",
|
362
|
+
"myField[1]": "bar",
|
363
|
+
"myField[2]": "baz",
|
364
|
+
"otherField[0]": "something else",
|
365
|
+
};
|
366
|
+
mutateAsArray("myField", values, (arr) => {
|
367
|
+
remove(arr, 1);
|
368
|
+
});
|
369
|
+
expect(values).toEqual({
|
370
|
+
myField: "something",
|
371
|
+
"myField[0]": "foo",
|
372
|
+
"myField[1]": "baz",
|
373
|
+
"otherField[0]": "something else",
|
374
|
+
});
|
375
|
+
expect("myField[2]" in values).toBe(false);
|
376
|
+
});
|
377
|
+
});
|
378
|
+
|
379
|
+
describe("getDeepArrayPaths", () => {
|
380
|
+
it("should return all paths recursively", () => {
|
381
|
+
const obj = [
|
382
|
+
true,
|
383
|
+
true,
|
384
|
+
[true, true],
|
385
|
+
{ foo: true, bar: { baz: true, test: [true] } },
|
386
|
+
];
|
387
|
+
|
388
|
+
expect(getDeepArrayPaths(obj, "myField")).toEqual([
|
389
|
+
"myField[0]",
|
390
|
+
"myField[1]",
|
391
|
+
"myField[2][0]",
|
392
|
+
"myField[2][1]",
|
393
|
+
"myField[3].foo",
|
394
|
+
"myField[3].bar.baz",
|
395
|
+
"myField[3].bar.test[0]",
|
396
|
+
]);
|
397
|
+
});
|
398
|
+
});
|
399
|
+
}
|