diff --git a/src/components/react-hook-form/SelectItemFormField/SelectItemFormField.stories.tsx b/src/components/react-hook-form/SelectItemFormField/SelectItemFormField.stories.tsx index 1bf574f2..4349218a 100644 --- a/src/components/react-hook-form/SelectItemFormField/SelectItemFormField.stories.tsx +++ b/src/components/react-hook-form/SelectItemFormField/SelectItemFormField.stories.tsx @@ -19,6 +19,7 @@ import { } from '../../../../.storybook/components' import { ErrorBody } from '../../util' import { Form } from '../Form' +import { Button } from '../../button' import { SearchQueryParameters, PaginatedResponse, @@ -40,9 +41,10 @@ const meta = { dialogTitle: 'Users', dialogLabel: 'Users', noSearchResults: 'No users available.', - extractIdFromItem: (item) => (item as User).id, - renderListItem: (item) => (item as User).name, - renderErrorMessage: (error) => (error as ErrorBody).message, + resolveItem, + extractIdFromItem: (item) => item.id, + renderListItem: (item) => item.name, + renderErrorMessage: (error) => error.message, paginationConfig: { indexType: IndexType.ZERO_BASED }, dialogProps: { overlayClassName: 'z-10' }, }, @@ -56,16 +58,26 @@ const meta = { }) type Person = z.infer const defaultPerson: DefaultValues = { - userId: (context.args.initialItem as User | undefined)?.id, + userId: context.args.initialItem?.id, } const form = useForm({ resolver: zodResolver(personSchema), defaultValues: defaultPerson, }) return ( -
- - + <> +
+ + + + ) }, ], @@ -91,10 +103,10 @@ const meta = { ), }, }, -} satisfies Meta +} satisfies Meta> export default meta -type Story = StoryObj +type Story = StoryObj const useGetDataSuccess = ({ search, @@ -170,6 +182,17 @@ const useGetDataEmpty = ({ } } +function resolveItem(id: number): Promise { + return new Promise((resolve) => + setTimeout(() => { + resolve({ + id, + name: `User ${id.toString()}`, + }) + }, 200), + ) +} + export const Default: Story = { args: { useGetData: useGetDataSuccess }, } diff --git a/src/components/react-hook-form/SelectItemFormField/SelectItemFormField.tsx b/src/components/react-hook-form/SelectItemFormField/SelectItemFormField.tsx index 23abb053..43d784f1 100644 --- a/src/components/react-hook-form/SelectItemFormField/SelectItemFormField.tsx +++ b/src/components/react-hook-form/SelectItemFormField/SelectItemFormField.tsx @@ -26,6 +26,12 @@ export type SelectItemFormFieldProps< SelectItemFormFieldInputProps, 'name' | 'label' | 'placeholder' | 'disabled' > & { + /** + * The function to resolve an item by its id. This will be called when the programmatically set value is not the same as the initial item. + * When the function cannot resolve the item, it has to throw an error. + * Make sure to pass a stable reference to prevent unnecessary calls. + */ + resolveItem?: (id: ItemId) => Item | Promise renderInputItem?: (item: Item) => ReactNode extractIdFromItem: (item: Item) => ItemId /** @@ -69,6 +75,7 @@ export function SelectItemFormField< mode, variant, initialItem, + resolveItem, renderInputItem, label, placeholder, @@ -93,16 +100,66 @@ export function SelectItemFormField< initialItem ?? null, ) + const fieldValueRef = useRef(field.value as ItemId) + useEffect(() => { + fieldValueRef.current = field.value as ItemId + }, [field.value]) + + // This effect is used to set the selected item when the field value changes (i.e. when the value is set programmatically with React Hook Form). useEffect(() => { + if (selectedItem && extractIdFromItem(selectedItem) === field.value) { + return + } + + // If the field value is null or undefined, we set the selected item to null. if (field.value === null || field.value === undefined) { setSelectedItem(null) - } else if ( + return + } + + // If the field value is the same as the initial item's id, we set the selected item to the initial item. + if ( initialItemRef.current && field.value === extractIdFromItem(initialItemRef.current) ) { setSelectedItem(initialItemRef.current) + return } - }, [field.value, extractIdFromItem]) + + // At this point, we don't know the selected item. We have to resolve it with the resolveItem callback. + + if (!resolveItem) { + console.warn( + 'SelectItemFormField: No resolveItem function provided. This is required to resolve the selected item.', + ) + return + } + + const fieldValueToBeResolved = field.value as ItemId + + void (async () => { + try { + const item = await resolveItem(fieldValueToBeResolved) + + // Make sure the field has not been changed in the meantime + if (fieldValueToBeResolved === fieldValueRef.current) { + // Make sure the resolved item is really the item that we want to set + if (extractIdFromItem(item) === fieldValueToBeResolved) { + setSelectedItem(item) + } else { + console.error( + 'SelectItemFormField: The resolved item could not be set as the selected item. It does not have the same id as the field value.', + ) + } + } + } catch (error) { + console.error( + 'SelectItemFormField: Error while resolving the selected item.', + error, + ) + } + })() + }, [field.value, selectedItem, extractIdFromItem, resolveItem]) return ( <>