Skip to content

Commit

Permalink
fix(#49): use splitAtom instead of immutable object path (#50)
Browse files Browse the repository at this point in the history
  • Loading branch information
MiroslavPetrik authored Oct 20, 2023
1 parent 23ad6cf commit 7fb372b
Show file tree
Hide file tree
Showing 4 changed files with 115 additions and 69 deletions.
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,6 @@
"zod": "3.21.4"
},
"dependencies": {
"object-path-immutable": "^4.1.2",
"react-render-prop-type": "0.1.0"
},
"release": {
Expand Down
61 changes: 25 additions & 36 deletions src/components/list-field/ListField.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,5 @@
import {
FieldAtom,
FormAtom,
FormFieldValues,
FormFields,
useForm,
} from "form-atoms";
import React, { Fragment, useMemo } from "react";
import { FieldAtom, FormAtom, FormFieldValues, FormFields } from "form-atoms";
import React, { Fragment, useCallback } from "react";
import { RenderProp } from "react-render-prop-type";

import { useListFieldActions } from "./useListFieldActions";
Expand Down Expand Up @@ -46,11 +40,11 @@ export type ListItemRenderProps<Fields> = RenderProp<
index: number;
fields: Fields;
add: () => void;
remove: (index: number) => void;
remove: (field: FieldAtom<any> | FormFields) => void;
} & RenderProp<unknown, "RemoveItemButton">
>;

type ListFields = FieldAtom<any>[] | FormFields[];
export type ListFields = FieldAtom<any>[] | FormFields[];

type RecurrFormFields = FormFields | ListFields;

Expand Down Expand Up @@ -117,42 +111,37 @@ export function ListField<
),
EmptyMessage,
}: ListFieldProps<Fields, Path>) {
const { fieldAtoms } = useForm(form);

const { add, remove } = useListFieldActions(form, builder, path);

const array: ListFields = useMemo(() => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
return path.reduce(
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
(fields, key) => fields[key],
fieldAtoms
) as ListFields;
}, [path, fieldAtoms]);
const keyExtractor = useCallback(
(fields: FieldAtom<any> | FormFields) => {
if (typeof keyFrom === "string" && keyFrom in fields) {
// @ts-ignore
return `${fields[keyFrom]}`;
} else {
return `${fields}`;
}
},
[keyFrom]
);

const keyFn = (fields: FieldAtom<any> | FormFields) => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
return typeof keyFrom === "string" ? `${fields[keyFrom]}` : `${fields}`;
};
const { add, isEmpty, items } = useListFieldActions(
form,
builder,
path,
keyExtractor
);

return (
<>
{array.length === 0 && EmptyMessage ? <EmptyMessage /> : undefined}
{array.map((fields, index) => (
<Fragment key={keyFn(fields)}>
{isEmpty && EmptyMessage ? <EmptyMessage /> : undefined}
{items.map(({ remove, fields, key }, index) => (
<Fragment key={key}>
{children({
add,
remove,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
fields,
index,
RemoveItemButton: () => (
<RemoveItemButton remove={() => remove(index)} />
),
RemoveItemButton: () => <RemoveItemButton remove={remove} />,
})}
</Fragment>
))}
Expand Down
104 changes: 90 additions & 14 deletions src/components/list-field/useListFieldActions.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,107 @@
import { FieldAtom, FormAtom, FormFields, useFormActions } from "form-atoms";
import { del, push } from "object-path-immutable";
import { useCallback } from "react";
import {
FieldAtom,
FormAtom,
FormFields,
useForm,
useFormActions,
} from "form-atoms";
import { useCallback, useMemo } from "react";
import { splitAtom } from "jotai/utils";
import { ListFields } from "./ListField";
import { PrimitiveAtom, atom, useAtom, useAtomValue, useSetAtom } from "jotai";
import { atomEffect } from "jotai-effect";

const getAt = (obj: Record<any, unknown>, path: (string | number)[]) =>
// @ts-expect-error TODO recursive typing
path.reduce((fields, key) => fields[key], obj);

// Possible extension:
// TODO value atom - walk fields
// TODO initialValue/dirty atom
// TODO error/validate atoms
const listFieldAtom = (listFields: ListFields) => {
const valueAtom = atom(listFields);
// @ts-ignore
const splitListAtom = splitAtom(valueAtom);
const emptyAtom = atom((get) => get(valueAtom).length === 0);

const list = {
value: valueAtom,
splitList: splitListAtom,
empty: emptyAtom,
};

return atom(list);
};

export const useListFieldActions = <
Fields extends FormFields,
Item extends FieldAtom<any> | FormFields
>(
form: FormAtom<Fields>,
builder: () => Item,
path: (string | number)[]
path: (string | number)[],
keyExtractor: (item: Item) => string
) => {
const { fieldAtoms } = useForm(form);
const { updateFields } = useFormActions(form);

// could be defined statically, will require changes in the core form-atoms api
const listAtom = useMemo(
() => listFieldAtom(getAt(fieldAtoms, path) as unknown as ListFields),
[]
);

const list = useAtomValue(listAtom);
const [splitItems, dispatch] = useAtom(list.splitList);
const isEmpty = useAtomValue(list.empty);
const value = useAtomValue(list.value);

const syncListEffect = useMemo(
() =>
atomEffect((get) => {
const arr = get(list.value);

updateFields((fields) => {
// @ts-ignore
path.reduce((fields, key, index) => {
if (index === path.length - 1) {
// when a path key is the last, update the list reference
// @ts-ignore
fields[key] = arr;
} else {
// otherwise walk the path towards the list
return fields[key];
}
}, fields);

return fields;
});
}),
[]
);

useAtom(syncListEffect);

const remove = useCallback(
(index: number) => {
return updateFields((current) => {
return del(current, [...path, index]);
});
(atom: PrimitiveAtom<FieldAtom<any>> | PrimitiveAtom<FormFields>) => {
// @ts-ignore TODO | FormFields?
dispatch({ type: "remove", atom });
},
[form]
[]
);

const add = useCallback(() => {
updateFields((current) => {
return push(current, path, builder());
});
}, [form]);
// @ts-ignore
dispatch({ type: "insert", value: builder() });
}, []);

const items = splitItems.map((item, index) => ({
// @ts-ignore
key: keyExtractor(value[index]!),
fields: value[index]!,
remove: () => remove(item),
}));

return { remove, add };
return { remove, add, isEmpty, items };
};
18 changes: 0 additions & 18 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3364,7 +3364,6 @@ __metadata:
jotai-effect: 0.1.0
jsdom: ^21.1.0
lodash.shuffle: ^4.2.0
object-path-immutable: ^4.1.2
prettier: 2.8.4
react: ^18.2.0
react-dom: ^18.2.0
Expand Down Expand Up @@ -14157,23 +14156,6 @@ __metadata:
languageName: node
linkType: hard

"object-path-immutable@npm:^4.1.2":
version: 4.1.2
resolution: "object-path-immutable@npm:4.1.2"
dependencies:
is-plain-object: ^5.0.0
object-path: ^0.11.8
checksum: e5d730cc7bd5a5048fd6810a95624b173f044a906db5689e725d6c8bce3614465429d41ef71677f388b6e719099d3c7d41e5c1740173940c52a3cdd9f9d367fa
languageName: node
linkType: hard

"object-path@npm:^0.11.8":
version: 0.11.8
resolution: "object-path@npm:0.11.8"
checksum: 684ccf0fb6b82f067dc81e2763481606692b8485bec03eb2a64e086a44dbea122b2b9ef44423a08e09041348fe4b4b67bd59985598f1652f67df95f0618f5968
languageName: node
linkType: hard

"object.assign@npm:^4.1.4":
version: 4.1.4
resolution: "object.assign@npm:4.1.4"
Expand Down

0 comments on commit 7fb372b

Please sign in to comment.