diff --git a/.changeset/smart-toes-marry.md b/.changeset/smart-toes-marry.md new file mode 100644 index 0000000..90b1ec0 --- /dev/null +++ b/.changeset/smart-toes-marry.md @@ -0,0 +1,5 @@ +--- +'@warkypublic/oranguru': patch +--- + +feat(Former): enhance state management with additional callbacks and state retrieval diff --git a/src/Former/Former.store.tsx b/src/Former/Former.store.tsx index 77f50cd..4fcfedc 100644 --- a/src/Former/Former.store.tsx +++ b/src/Former/Former.store.tsx @@ -2,13 +2,16 @@ import { newUUID } from '@warkypublic/artemis-kit'; import { createSyncStore } from '@warkypublic/zustandsyncstore'; import { produce } from 'immer'; -import type { FormerProps, FormerState } from './Former.types'; +import type { FormerProps, FormerState, FormStateAndProps } from './Former.types'; const { Provider: FormerProvider, useStore: useFormerStore } = createSyncStore< FormerState & Partial>, FormerProps >( (set, get) => ({ + getAllState: () => { + return get() as FormStateAndProps; + }, getState: (key) => { const current = get(); return current?.[key]; @@ -26,17 +29,19 @@ const { Provider: FormerProvider, useStore: useFormerStore } = createSyncStore< keyValue ); if (get().afterGet) { - data = await get().afterGet!({ ...data }); + data = await get().afterGet!({ ...data }, get()); } set({ loading: false, values: data }); - get().onChange?.(data); + get().onChange?.(data, get()); } if (reset && get().getFormMethods) { const formMethods = get().getFormMethods!(); formMethods.reset(); } } catch (e) { - set({ error: (e as Error)?.message ?? e, loading: false }); + const errorMessage = (e as Error)?.message ?? e; + set({ error: errorMessage, loading: false }); + get().onError?.(errorMessage, get()); } set({ loading: false }); }, @@ -66,7 +71,7 @@ const { Provider: FormerProvider, useStore: useFormerStore } = createSyncStore< let data = formMethods.getValues(); if (get().beforeSave) { - const newData = await get().beforeSave!(data); + const newData = await get().beforeSave!(data, get()); data = newData; } @@ -76,7 +81,9 @@ const { Provider: FormerProvider, useStore: useFormerStore } = createSyncStore< data = newdata; }, (errors) => { - set({ error: errors.root?.message || 'Validation errors', loading: false }); + const errorMessage = errors.root?.message || 'Validation errors'; + set({ error: errorMessage, loading: false }); + get().onError?.(errorMessage, get()); exit = true; } ); @@ -107,21 +114,22 @@ const { Provider: FormerProvider, useStore: useFormerStore } = createSyncStore< data, keyValue ); + const newData = { ...data, ...savedData }; //Merge what we had. In case the API doesn't return all fields, we don't want to lose them if (get().afterSave) { - await get().afterSave!(savedData); + await get().afterSave!(newData, get()); } - set({ loading: false, values: savedData }); - get().onChange?.(savedData); - formMethods.reset(savedData); //reset with saved data to clear dirty state + set({ loading: false, values: newData }); + get().onChange?.(newData, get()); + formMethods.reset(newData); //reset with saved data to clear dirty state if (!keepOpen) { - get().onClose?.(savedData); + get().onClose?.(newData); } - return savedData; + return newData; } set({ loading: false, values: data }); formMethods.reset(data); //reset with saved data to clear dirty state - get().onChange?.(data); + get().onChange?.(data, get()); if (!keepOpen) { get().onClose?.(data); } @@ -129,7 +137,9 @@ const { Provider: FormerProvider, useStore: useFormerStore } = createSyncStore< return data; } } catch (e) { - set({ error: (e as Error)?.message ?? e, loading: false }); + const errorMessage = (e as Error)?.message ?? e; + set({ error: errorMessage, loading: false }); + get().onError?.(errorMessage, get()); } return undefined; @@ -181,20 +191,20 @@ const { Provider: FormerProvider, useStore: useFormerStore } = createSyncStore< return { id: !id ? newUUID() : id, - onClose: () => { + onClose: (data?: any) => { const dirty = useStoreApi.getState().dirty; const setState = useStoreApi.getState().setState; if (dirty) { if (confirm('You have unsaved changes. Are you sure you want to close?')) { if (onClose) { - onClose(); + onClose(data); } else { setState('opened', false); } } } else { if (onClose) { - onClose(); + onClose(data); } else { setState('opened', false); } diff --git a/src/Former/Former.tsx b/src/Former/Former.tsx index 189bf36..a1f6824 100644 --- a/src/Former/Former.tsx +++ b/src/Former/Former.tsx @@ -12,6 +12,7 @@ const FormerInner = forwardRef, Partial> & Props ref: any ) { const { + getAllState, getState, onChange, onClose, @@ -26,6 +27,7 @@ const FormerInner = forwardRef, Partial> & Props values, wrapper, } = useFormerStore((state) => ({ + getAllState: state.getAllState, getState: state.getState, onChange: state.onChange, onClose: state.onClose, @@ -54,7 +56,7 @@ const FormerInner = forwardRef, Partial> & Props () => ({ close: async () => { //console.log('close called'); - onClose?.(); + onClose?.(getState('values')); setState('opened', false); }, getValue: () => { @@ -67,7 +69,7 @@ const FormerInner = forwardRef, Partial> & Props return await save(); }, setValue: (value: T) => { - onChange?.(value); + onChange?.(value, getAllState()); }, show: async () => { //console.log('show called'); @@ -78,7 +80,7 @@ const FormerInner = forwardRef, Partial> & Props return await validate(); }, }), - [getState, onChange, validate, save, reset, setState, onClose, onOpen] + [getState, getAllState, onChange, validate, save, reset, setState, onClose, onOpen] ); useEffect(() => { @@ -97,7 +99,19 @@ const FormerInner = forwardRef, Partial> & Props return ( {typeof wrapper === 'function' ? ( - wrapper({props.children}, opened ??false, onClose ?? (() => {setState('opened', false)}), onOpen ?? (() => {setState('opened', true)}), getState) + wrapper( + {props.children}, + opened ?? false, + onClose ?? + (() => { + setState('opened', false); + }), + onOpen ?? + (() => { + setState('opened', true); + }), + getState + ) ) : ( {props.children || null} )} diff --git a/src/Former/Former.types.ts b/src/Former/Former.types.ts index f895f97..99729c7 100644 --- a/src/Former/Former.types.ts +++ b/src/Former/Former.types.ts @@ -15,15 +15,15 @@ export type FormerAPICallType = ( ) => Promise; export interface FormerProps { - afterGet?: (data: T) => Promise | void; - afterSave?: (data: T) => Promise | void; - beforeSave?: (data: T) => Promise | T; + afterGet?: (data: T, state: Partial>) => Promise | void; + afterSave?: (data: T, state: Partial>) => Promise | void; + beforeSave?: (data: T, state: Partial>) => Promise | T; dirty?: boolean; disableHTMlForm?: boolean; id?: string; keepOpen?: boolean; layout?: { - buttonArea?: "bottom" | "none" | "top"; + buttonArea?: 'bottom' | 'none' | 'top'; buttonAreaGroupProps?: GroupProps; closeButtonProps?: ButtonProps; closeButtonTitle?: React.ReactNode; @@ -35,10 +35,10 @@ export interface FormerProps { }; onAPICall?: FormerAPICallType; onCancel?: () => void; - onChange?: (value: T) => void; - onClose?: (data?: T) => void; + onChange?: (value: T, state: Partial>) => void; + onClose?: (data: T | undefined) => void; onConfirmDelete?: (values?: T) => Promise; - + onError?: (error: Error | string, state: Partial>) => void; onOpen?: (data?: T) => void; opened?: boolean; primeData?: T; @@ -62,15 +62,16 @@ export interface FormerRef { export type FormerSectionRender = ( children: React.ReactNode, - opened: boolean , - onClose: ((data?: T) => void), - onOpen: ((data?: T) => void) , + opened: boolean, + onClose: (data?: T) => void, + onOpen: (data?: T) => void, getState: FormerState['getState'] ) => React.ReactNode; export interface FormerState { deleteConfirmed?: boolean; error?: string; + getAllState: () => FormStateAndProps; getFormMethods?: () => UseFormReturn; getState: >(key: K) => FormStateAndProps[K]; load: (reset?: boolean) => Promise;