feat(form): Add controlled mode to Select component

Refactors the `Select` component to support being used as a controlled component by accepting a `value` prop. This allows its state to be managed externally, for example, by React Hook Form.

This change was necessary to properly implement the `clearable` functionality within forms. Previously, clearing the select would only update its internal state, not the form's state.

- Added `value` and `useEffect` to sync with external state.
- Updated the `onClear` handler to propagate the change via `onChange`.
- Modified `AdminListFilters` to use the `Select` component in a controlled manner, passing `watch` and `setValue` from React Hook Form.
- Adjusted the filter layout to be more responsive.
This commit is contained in:
AmirReza Jamali
2025-10-13 15:17:18 +03:30
parent 19a3f72325
commit 0084b131f8
4 changed files with 47 additions and 12 deletions
+20 -3
View File
@@ -3,7 +3,7 @@
import { ChevronUpIcon } from "@/assets/icons";
import { cn } from "@/lib/utils";
import { ILabelValue } from "@/types/shared";
import { useId, useState, type ChangeEvent } from "react";
import { useEffect, useId, useState, type ChangeEvent } from "react";
import {
type FieldErrors,
type FieldValues,
@@ -24,8 +24,9 @@ type PropsType<T extends FieldValues> = {
required?: boolean;
clearable?: boolean;
onClear?: () => void;
value?: string;
} & (
| { placeholder?: string; defaultValue: string }
| { placeholder?: string; defaultValue?: string }
| { placeholder: string; defaultValue?: string }
) &
Partial<UseFormRegisterReturn>;
@@ -46,6 +47,7 @@ export function Select<T extends FieldValues>({
ref,
clearable = false,
onClear,
value: controlledValue,
...props
}: PropsType<T>) {
const id = useId();
@@ -54,10 +56,23 @@ export function Select<T extends FieldValues>({
const [value, setValue] = useState(defaultValue || "");
const error = errors && errors[name];
useEffect(() => {
if (controlledValue !== undefined) {
setValue(controlledValue);
setIsOptionSelected(!!controlledValue);
}
}, [controlledValue]);
const handleClear = () => {
setValue("");
setIsOptionSelected(false);
onClear?.();
if (onChange) {
const event = {
target: { value: "" },
} as ChangeEvent<HTMLSelectElement>;
onChange(event);
}
};
return (
@@ -83,7 +98,9 @@ export function Select<T extends FieldValues>({
ref={ref}
value={value}
onChange={(e) => {
setValue(e.target.value);
if (controlledValue === undefined) {
setValue(e.target.value);
}
setIsOptionSelected(!!e.target.value);
onChange?.(e);
}}
@@ -1,11 +1,13 @@
import InputGroup from "@/components/FormElements/InputGroup";
import { Select } from "@/components/FormElements/select";
import { AdminRoles, AdminStatus } from "@/constants/enums";
import { AdminStatus } from "@/constants/enums";
import { getEnumAsArray } from "@/utils/shared";
import {
FieldValues,
UseFormHandleSubmit,
UseFormRegister,
UseFormSetValue,
UseFormWatch,
} from "react-hook-form";
interface AdminListFiltersProps {
@@ -14,6 +16,8 @@ interface AdminListFiltersProps {
isMutating: number;
handleFilterSubmit: UseFormHandleSubmit<FieldValues>;
search: (data: any) => void;
watch: UseFormWatch<FieldValues>;
setValue: UseFormSetValue<any>;
}
const AdminListFilters = ({
@@ -22,10 +26,12 @@ const AdminListFilters = ({
isMutating,
handleFilterSubmit,
search,
watch,
setValue,
}: AdminListFiltersProps) => {
return (
<form
className="grid !min-w-full grid-cols-3 gap-4"
className="grid !min-w-full grid-cols-1 gap-4 xl:grid-cols-2"
onSubmit={handleFilterSubmit(search)}
>
<Select
@@ -35,6 +41,8 @@ const AdminListFilters = ({
name="status"
placeholder="Status"
clearable
value={watch("status")}
onClear={() => setValue("status", undefined)}
/>
<InputGroup
{...filterRegister("search")}
@@ -50,13 +58,16 @@ const AdminListFilters = ({
placeholder="email"
type="email"
/> */}
<Select
{/* <Select
{...filterRegister("role")}
name="role"
items={getEnumAsArray(AdminRoles)}
label="Role"
placeholder="Role"
/>
clearable
value={watch("role")}
onClear={() => setValue("role", undefined)}
/> */}
{/* <InputGroup
{...filterRegister("username")}
label="username"
@@ -71,6 +82,7 @@ const AdminListFilters = ({
placeholder="full name"
type="text"
/> */}
<div className="flex gap-4">
<button
type="submit"
+6 -4
View File
@@ -39,10 +39,12 @@ const AdminsTable = () => {
currentUser,
setStatus,
status,
setFilterValue,
handleFilterSubmit,
search,
filterRegister,
isMutating,
watch,
} = useAdminsPresenter();
return (
@@ -85,9 +87,7 @@ const AdminsTable = () => {
<TableCell className="text-start" colSpan={100}>
{item?.email}
</TableCell>
<TableCell className="text-start" colSpan={100}>
{item?.role}
</TableCell>
<TableCell className="text-start" colSpan={100}>
{item?.username}
</TableCell>
@@ -162,15 +162,17 @@ const AdminsTable = () => {
<Modal
isOpen={isFilterModalOpen}
onClose={() => setIsFilterModalOpen(false)}
classNames="w-3/4"
classNames="w-full xl:w-1/2"
showButtons={false}
>
<AdminListFilters
setValue={setFilterValue}
filterRegister={filterRegister}
handleFilterSubmit={handleFilterSubmit}
isMutating={isMutating as number}
search={search}
setIsFilterModalOpen={setIsFilterModalOpen}
watch={watch}
/>
</Modal>
{isModalOpen && (
@@ -29,6 +29,8 @@ export const useAdminsPresenter = () => {
register: filterRegister,
handleSubmit: handleFilterSubmit,
reset: filterFormReset,
watch,
setValue: setFilterValue,
} = useForm();
const {
@@ -86,13 +88,14 @@ export const useAdminsPresenter = () => {
"row",
"full name",
"email",
"role",
"username",
"status",
"actions",
];
return {
allUsers,
setFilterValue,
metadata,
columns,
isPending,
@@ -118,5 +121,6 @@ export const useAdminsPresenter = () => {
filterRegister,
handleFilterSubmit,
search,
watch,
};
};