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
InvoiceId
value 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
order
property 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'suseForm
hook.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,T
would be of typeInvoiceDetail
.newItemFactory: (...args: any[]) => Partial<T>
A factory function to create a new child entity.args
will be passed from the returnedappend
method to this factory.
The useOrderdFieldArray
hooks would return the following methods:
append: (...args: any[]) => void;
Method to append new child,args
will be passed tonewItemFactory
input 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 thefields
returned by React Hook Form'suseFieldArray
hook, it is to be used to render form controlssetFields: Dispatch<SetStateAction<T[]>>;
fields
setter form the caller to setfields
if appropriate.updateFieldsFromContext: () => void;
Method to copy data fromformContext
intofields
. 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
Fields
to render child forms. - wire up helper methods to buttons.
I can confirm that the above served me well so far.
Top comments (0)