I have the following requirements for my invoice entity:
The Invoice entity has a collection of InvoiceDetail entity.
User should be able to append, remove, move up and down InvoiceDetails
InvoiceDetail's order needs to be consistent because they are listed in the printout of the invoice
Other documents such as contract and purchase order would have similar requirements.
The above translate to the below technical requirements:
On appending, set InvoiceDetail's foreign key
InvoiceIdvalue to its parent Invoice's id on appending.On appending, set InvoiceDetail's id. I use UUID for all my domain entities, and my backend expects the front end to generate UUID, and it doesn't generate UUID automatically.
On appending, moving up and down, set and maintain the
orderproperty of InvoiceDetails automaticallyOn removing, maintain the order of the rest of InvoiceDetails.
React Hook Form has its own useFeildArray API for handling child entity collections in one-many relationships. However, for the above requirements, I decided that I would reinvent the wheels and implement my own useOrderedFieldArray hook, both as a challenge to myself and more controls potentially If I succeed.
The useOrderdFieldArray hooks would take four inputs:
formContext: UseFormReturn<any>
The form context we get back from React Hook form'suseFormhook.name: string
The name of the child collection, for example, the Invoice entity has a property 'invoiceDetails' for its Invoice Details. The name would be this 'invoiceDetails'items: T[]
The child collection data for initialisation aka InvoiceDetails, in the Invoice case,Twould be of typeInvoiceDetail.newItemFactory: (...args: any[]) => Partial<T>
A factory function to create a new child entity.argswill be passed from the returnedappendmethod to this factory.
The useOrderdFieldArray hooks would return the following methods:
append: (...args: any[]) => void;
Method to append new child,argswill be passed tonewItemFactoryinput methodmoveDown: (index: number) => void;
Method to move a child one step down takes the child's index in the collection arraymoveUp: (index: number) => void;
Method to move a child one step up.remove: (item: T) => void;
Remove a child from the child collection.fields: T[];
Similar to thefieldsreturned by React Hook Form'suseFieldArrayhook, it is to be used to render form controlssetFields: Dispatch<SetStateAction<T[]>>;
fieldssetter form the caller to setfieldsif appropriate.updateFieldsFromContext: () => void;
Method to copy data fromformContextintofields. When the user copy data from a selected proforma invoice to create a new commercial invoice, this method is required to sync the child forms.
Below is the code for the hook:
import { useCallback, useEffect, useMemo, useState, Dispatch, SetStateAction } from 'react';
import { UseFormReturn } from 'react-hook-form/dist/types';
import { OrderedFieldArrayMethods } from './orderedFieldArrayMethods';
interface OrderedFieldArrayMethods<T> {
append: (...args: any[]) => void;
moveDown: (index: number) => void;
moveUp: (index: number) => void;
remove: (item: T) => void;
updateFieldsFromContext: () => void;
fields: T[];
setFields: Dispatch<SetStateAction<T[]>>;
}
export function useOrderedFieldArray<T extends { id: string; order: number }>({
name,
items,
formContext,
newItemFactory,
}: {
name: string;
items: T[];
formContext: UseFormReturn<any>;
newItemFactory: (...args: any[]) => Partial<T>;
}): OrderedFieldArrayMethods<T> {
const { unregister, setValue } = formContext;
const [fields, setFields] = useState<T[]>(() => items.sort((a, b) => a.order - b.order));
const append = useCallback(
(...args: any[]) => {
setFields((fields) => [...fields, { ...newItemFactory(...args), order: fields.length } as T]);
},
[newItemFactory]
);
const moveUp = useCallback(
(index: number) => {
const newFields = [...fields];
[newFields[index], newFields[index - 1]] = [newFields[index - 1], newFields[index]];
setFields(newFields);
},
[fields]
);
const moveDown = useCallback(
(index: number) => {
const newFields = [...fields];
[newFields[index], newFields[index + 1]] = [newFields[index + 1], newFields[index]];
setFields(newFields);
},
[fields]
);
const remove = useCallback(
(detail: { id: string }) => {
unregister(name);
setFields((fields) => [...fields.filter((x) => x.id !== detail.id)]);
},
[name, unregister]
);
const updateFieldsFromContext = useCallback(() => {
setFields(formContext.getValues(name));
}, [formContext, name]);
useEffect(() => {
return () => unregister(name);
}, [name, unregister]);
useEffect(() => {
for (let i = 0; i < fields.length; i++) {
setValue(`${name}[${i}].order` as any, i);
}
}, [fields, name, setValue]);
return useMemo(
() => ({
fields,
setFields,
append,
moveDown,
moveUp,
remove,
updateFieldsFromContext,
}),
[append, fields, moveDown, moveUp, remove, updateFieldsFromContext]
);
}
Usage:
const { getValues } = formContext;
const newItemFactory = useCallback(
() => ({ id: v4(), inoviceId: getValues('id') }),
[getValues]
);
const { fields, moveUp, moveDown, remove, append, updateFieldsFromContext } = useOrderedFieldArray({
items,
formContext,
newItemFactory,
name: 'invoiceDetails',
});
- Use
Fieldsto render child forms. - wire up helper methods to buttons.
I can confirm that the above served me well so far.
Top comments (0)