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
+19 -2
View File
@@ -3,7 +3,7 @@
import { ChevronUpIcon } from "@/assets/icons"; import { ChevronUpIcon } from "@/assets/icons";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { ILabelValue } from "@/types/shared"; import { ILabelValue } from "@/types/shared";
import { useId, useState, type ChangeEvent } from "react"; import { useEffect, useId, useState, type ChangeEvent } from "react";
import { import {
type FieldErrors, type FieldErrors,
type FieldValues, type FieldValues,
@@ -24,8 +24,9 @@ type PropsType<T extends FieldValues> = {
required?: boolean; required?: boolean;
clearable?: boolean; clearable?: boolean;
onClear?: () => void; onClear?: () => void;
value?: string;
} & ( } & (
| { placeholder?: string; defaultValue: string } | { placeholder?: string; defaultValue?: string }
| { placeholder: string; defaultValue?: string } | { placeholder: string; defaultValue?: string }
) & ) &
Partial<UseFormRegisterReturn>; Partial<UseFormRegisterReturn>;
@@ -46,6 +47,7 @@ export function Select<T extends FieldValues>({
ref, ref,
clearable = false, clearable = false,
onClear, onClear,
value: controlledValue,
...props ...props
}: PropsType<T>) { }: PropsType<T>) {
const id = useId(); const id = useId();
@@ -54,10 +56,23 @@ export function Select<T extends FieldValues>({
const [value, setValue] = useState(defaultValue || ""); const [value, setValue] = useState(defaultValue || "");
const error = errors && errors[name]; const error = errors && errors[name];
useEffect(() => {
if (controlledValue !== undefined) {
setValue(controlledValue);
setIsOptionSelected(!!controlledValue);
}
}, [controlledValue]);
const handleClear = () => { const handleClear = () => {
setValue(""); setValue("");
setIsOptionSelected(false); setIsOptionSelected(false);
onClear?.(); onClear?.();
if (onChange) {
const event = {
target: { value: "" },
} as ChangeEvent<HTMLSelectElement>;
onChange(event);
}
}; };
return ( return (
@@ -83,7 +98,9 @@ export function Select<T extends FieldValues>({
ref={ref} ref={ref}
value={value} value={value}
onChange={(e) => { onChange={(e) => {
if (controlledValue === undefined) {
setValue(e.target.value); setValue(e.target.value);
}
setIsOptionSelected(!!e.target.value); setIsOptionSelected(!!e.target.value);
onChange?.(e); onChange?.(e);
}} }}
@@ -1,11 +1,13 @@
import InputGroup from "@/components/FormElements/InputGroup"; import InputGroup from "@/components/FormElements/InputGroup";
import { Select } from "@/components/FormElements/select"; import { Select } from "@/components/FormElements/select";
import { AdminRoles, AdminStatus } from "@/constants/enums"; import { AdminStatus } from "@/constants/enums";
import { getEnumAsArray } from "@/utils/shared"; import { getEnumAsArray } from "@/utils/shared";
import { import {
FieldValues, FieldValues,
UseFormHandleSubmit, UseFormHandleSubmit,
UseFormRegister, UseFormRegister,
UseFormSetValue,
UseFormWatch,
} from "react-hook-form"; } from "react-hook-form";
interface AdminListFiltersProps { interface AdminListFiltersProps {
@@ -14,6 +16,8 @@ interface AdminListFiltersProps {
isMutating: number; isMutating: number;
handleFilterSubmit: UseFormHandleSubmit<FieldValues>; handleFilterSubmit: UseFormHandleSubmit<FieldValues>;
search: (data: any) => void; search: (data: any) => void;
watch: UseFormWatch<FieldValues>;
setValue: UseFormSetValue<any>;
} }
const AdminListFilters = ({ const AdminListFilters = ({
@@ -22,10 +26,12 @@ const AdminListFilters = ({
isMutating, isMutating,
handleFilterSubmit, handleFilterSubmit,
search, search,
watch,
setValue,
}: AdminListFiltersProps) => { }: AdminListFiltersProps) => {
return ( return (
<form <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)} onSubmit={handleFilterSubmit(search)}
> >
<Select <Select
@@ -35,6 +41,8 @@ const AdminListFilters = ({
name="status" name="status"
placeholder="Status" placeholder="Status"
clearable clearable
value={watch("status")}
onClear={() => setValue("status", undefined)}
/> />
<InputGroup <InputGroup
{...filterRegister("search")} {...filterRegister("search")}
@@ -50,13 +58,16 @@ const AdminListFilters = ({
placeholder="email" placeholder="email"
type="email" type="email"
/> */} /> */}
<Select {/* <Select
{...filterRegister("role")} {...filterRegister("role")}
name="role" name="role"
items={getEnumAsArray(AdminRoles)} items={getEnumAsArray(AdminRoles)}
label="Role" label="Role"
placeholder="Role" placeholder="Role"
/> clearable
value={watch("role")}
onClear={() => setValue("role", undefined)}
/> */}
{/* <InputGroup {/* <InputGroup
{...filterRegister("username")} {...filterRegister("username")}
label="username" label="username"
@@ -71,6 +82,7 @@ const AdminListFilters = ({
placeholder="full name" placeholder="full name"
type="text" type="text"
/> */} /> */}
<div className="flex gap-4"> <div className="flex gap-4">
<button <button
type="submit" type="submit"
+6 -4
View File
@@ -39,10 +39,12 @@ const AdminsTable = () => {
currentUser, currentUser,
setStatus, setStatus,
status, status,
setFilterValue,
handleFilterSubmit, handleFilterSubmit,
search, search,
filterRegister, filterRegister,
isMutating, isMutating,
watch,
} = useAdminsPresenter(); } = useAdminsPresenter();
return ( return (
@@ -85,9 +87,7 @@ const AdminsTable = () => {
<TableCell className="text-start" colSpan={100}> <TableCell className="text-start" colSpan={100}>
{item?.email} {item?.email}
</TableCell> </TableCell>
<TableCell className="text-start" colSpan={100}>
{item?.role}
</TableCell>
<TableCell className="text-start" colSpan={100}> <TableCell className="text-start" colSpan={100}>
{item?.username} {item?.username}
</TableCell> </TableCell>
@@ -162,15 +162,17 @@ const AdminsTable = () => {
<Modal <Modal
isOpen={isFilterModalOpen} isOpen={isFilterModalOpen}
onClose={() => setIsFilterModalOpen(false)} onClose={() => setIsFilterModalOpen(false)}
classNames="w-3/4" classNames="w-full xl:w-1/2"
showButtons={false} showButtons={false}
> >
<AdminListFilters <AdminListFilters
setValue={setFilterValue}
filterRegister={filterRegister} filterRegister={filterRegister}
handleFilterSubmit={handleFilterSubmit} handleFilterSubmit={handleFilterSubmit}
isMutating={isMutating as number} isMutating={isMutating as number}
search={search} search={search}
setIsFilterModalOpen={setIsFilterModalOpen} setIsFilterModalOpen={setIsFilterModalOpen}
watch={watch}
/> />
</Modal> </Modal>
{isModalOpen && ( {isModalOpen && (
@@ -29,6 +29,8 @@ export const useAdminsPresenter = () => {
register: filterRegister, register: filterRegister,
handleSubmit: handleFilterSubmit, handleSubmit: handleFilterSubmit,
reset: filterFormReset, reset: filterFormReset,
watch,
setValue: setFilterValue,
} = useForm(); } = useForm();
const { const {
@@ -86,13 +88,14 @@ export const useAdminsPresenter = () => {
"row", "row",
"full name", "full name",
"email", "email",
"role",
"username", "username",
"status", "status",
"actions", "actions",
]; ];
return { return {
allUsers, allUsers,
setFilterValue,
metadata, metadata,
columns, columns,
isPending, isPending,
@@ -118,5 +121,6 @@ export const useAdminsPresenter = () => {
filterRegister, filterRegister,
handleFilterSubmit, handleFilterSubmit,
search, search,
watch,
}; };
}; };