I have a list consisting of custom components (MaterialBar) containing a couple of fields:
I do a bulk update/save, so when I hit the "Save materials"-button, all the new materials should be created (if the required fields have a value) and the current ones should be updated, if they have changed. I am just unsure how I get the state of the nested MaterialBars (isValid for the new items and isDirty for the existing ones). How I do it right now:
Parent/useFieldArray:
export interface ProjectMaterialFormValues {
materialBars: IMaterialBar[];
}
function ProjectMaterials({
project,
attachMaterial,
detachMaterial,
}: ProjectMaterialsProps) {
const { control, handleSubmit, formState } =
useForm<ProjectMaterialFormValues>({
defaultValues: { materialBars: projectMaterialsForProject(project) },
mode: "onChange",
});
const { fields, remove, append } = useFieldArray({
control,
name: "materialBars",
});
return (
<>
{fields.map(({ id }, index) => (
<MaterialBar
key={id}
index={index}
onDelete={(projectMaterialId) =>
setMaterialToDelete(projectMaterialId)
}
control={control}
/>
))}
<ButtonsContainer>
<Button variant="contained" onClick={() => navigate(routes.INDEX)}>
{texts.HOME_RETURN_HOME}
</Button>
<Button variant="contained" onClick={addMaterial}>
{texts.BUTTON_ADD_NEW_MATERIALS}
</Button>
<Button
variant="contained"
disabled={isSaving}
onClick={handleSubmit(saveAllMaterials)}
>
{texts.BUTTON_SAVE_MATERIALS}
</Button>
</ButtonsContainer>
</>
);
}
Each MaterialBar is then registered though a Controller:
MaterialBar:
interface MaterialBarProps {
index: number;
onDelete: (id: number) => void;
control: Control<ProjectMaterialFormValues>;
}
function MaterialBar({ index, onDelete, control }: MaterialBarProps) {
const { materials } = useAllMaterials();
return (
<Container>
<FormField>
<Controller
control={control}
name={`materialBars.${index}.materialId`}
rules={{ required: true }}
render={({ field: { onChange, value } }) => (
<Dropdown
label="Material"
items={materials}
selectedItem={value}
onSelect={onChange}
/>
)}
/>
</FormField>
// the other fields
<FormField>
<Controller
control={control}
name={`materialBars.${index}.amount`}
rules={{ required: true }}
render={({ field: { onChange, value } }) => (
<TextField
label="Amount"
type="number"
value={value}
onChange={onChange}
/>
)}
/>
</FormField>
<Button onClick={handleDelete}>
<img src={remove} alt="delete" />
</Button>
</Container>
);
}
Doing it like this, I cannot get the state of each component which means that I do all the vaidation manually right now:
const saveAllMaterials: SubmitHandler<ProjectMaterialFormValues> = ({
materialBars,
}) => {
materialBars.forEach((newMaterial, index) => {
if (newMaterial.projectMaterialId) {
if (formState.dirtyFields.materialBars) {
const dirtyIndexes = Object.keys(
dirtyFields.materialBars as unknown as string
);
if (dirtyIndexes.includes(index.toString())) {
// The API does not support update. Instead the material must
// be deleted and a new one is created with the updated values
detachMaterial(fields[index].materialId!);
attachMaterial(newMaterial);
}
}
} else if (
// Would be nice if I can do something like "newMaterial.isValid"
!newMaterial.projectMaterialId &&
newMaterial.materialId &&
newMaterial.typeId &&
newMaterial.amount
) {
attachMaterial(newMaterial);
}
});
};
This seems really unnecessary. Is there a way where I can access the state of of the child when I iterate through all the children in the fieldArray, so I don't have to do the verbose calculations?
I was thinking that perhaps I need to have another useForm in the MaterialBar, but how do I pass the state to the parent upon saving?
