From 95da83af00506ef4eb40af5f36b1d60bc2088041 Mon Sep 17 00:00:00 2001 From: Miroslav Petrik Date: Fri, 20 Oct 2023 11:13:21 +0200 Subject: [PATCH] fix(#49): use splitAtom instead of immutable object path --- package.json | 1 - src/components/list-field/ListField.tsx | 61 +++++----- .../list-field/useListFieldActions.ts | 105 +++++++++++++++--- yarn.lock | 18 --- 4 files changed, 116 insertions(+), 69 deletions(-) diff --git a/package.json b/package.json index f7bf2d0..6fe1842 100644 --- a/package.json +++ b/package.json @@ -100,7 +100,6 @@ "zod": "3.21.4" }, "dependencies": { - "object-path-immutable": "^4.1.2", "react-render-prop-type": "0.1.0" }, "release": { diff --git a/src/components/list-field/ListField.tsx b/src/components/list-field/ListField.tsx index 8455de8..59535dc 100644 --- a/src/components/list-field/ListField.tsx +++ b/src/components/list-field/ListField.tsx @@ -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"; @@ -46,11 +40,11 @@ export type ListItemRenderProps = RenderProp< index: number; fields: Fields; add: () => void; - remove: (index: number) => void; + remove: (field: FieldAtom | FormFields) => void; } & RenderProp >; -type ListFields = FieldAtom[] | FormFields[]; +export type ListFields = FieldAtom[] | FormFields[]; type RecurrFormFields = FormFields | ListFields; @@ -117,42 +111,37 @@ export function ListField< ), EmptyMessage, }: ListFieldProps) { - 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 | FormFields) => { + if (typeof keyFrom === "string" && keyFrom in fields) { + // @ts-ignore + return `${fields[keyFrom]}`; + } else { + return `${fields}`; + } + }, + [keyFrom] + ); - const keyFn = (fields: FieldAtom | 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 ? : undefined} - {array.map((fields, index) => ( - + {isEmpty && EmptyMessage ? : undefined} + {items.map(({ remove, fields, key }, index) => ( + {children({ add, remove, - // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore fields, index, - RemoveItemButton: () => ( - remove(index)} /> - ), + RemoveItemButton: () => , })} ))} diff --git a/src/components/list-field/useListFieldActions.ts b/src/components/list-field/useListFieldActions.ts index 8941fa2..f5e49a4 100644 --- a/src/components/list-field/useListFieldActions.ts +++ b/src/components/list-field/useListFieldActions.ts @@ -1,6 +1,38 @@ -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, 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, @@ -8,24 +40,69 @@ export const useListFieldActions = < >( form: FormAtom, 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> | PrimitiveAtom) => { + console.log("remove", atom); + // @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 }; }; diff --git a/yarn.lock b/yarn.lock index 9d6c728..ba36a8f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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 @@ -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"