Подскажите, пожалуйста, возможно ли сделать мой собственный компонент Select таким образом, чтобы его можно было зарегистрировать в useForm так же, как обычные элементы HTML, например ?
Вот пример кода в
codesanbox
import "./styles.css";
import { Controller, useForm } from "react-hook-form";
import Select, { ISelectOption } from "./Select";
const people: ISelectOption[] = [
{ id: 1, value: "1", label: "Durward Reynolds" },
{ id: 2, value: "2", label: "Kenton Towne" },
{ id: 3, value: "3", label: "Therese Wunsch" },
{ id: 4, value: "4", label: "Benedict Kessler" },
{ id: 5, value: "5", label: "Benedict Kessler" },
{ id: 6, value: "6", label: "Benedict Kessler" },
{ id: 7, value: "7", label: "Benedict Kessler" },
{ id: 8, value: "8", label: "Katelyn Rohan" }
];
export default function App() {
const { control, handleSubmit } = useForm();
const onSubmit = (data) => {
console.log(data.Select);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Select
options={people}
name="Select"
defaultText="Select"
required
placeholder="Select placeholder"
control={control}
Controller={Controller}
defaultValue={people[2].value}
/>
<button type="submit">Submit</button>
</form>
);
}
import { ChangeEvent, FC, useEffect, useMemo, useRef, useState } from "react";
import { Listbox } from "@headlessui/react";
import { Control, ControllerProps, FieldValues } from "react-hook-form";
export interface ISelectOption {
id: number | string;
value: string | number;
label: string;
}
interface ISelect {
name: string;
classes?: string;
disabled?: boolean;
required?: boolean;
defaultText: string;
placeholder?: string;
options: ISelectOption[];
control: Control<FieldValues, any>;
defaultValue?: ISelectOption["value"];
Controller: FC<ControllerProps<FieldValues, string>>;
}
const Select: FC<ISelect> = ({
control,
Controller,
required,
name,
defaultText,
disabled,
classes,
options,
placeholder,
defaultValue
}) => {
const selectRef = useRef<HTMLDivElement>(null);
const defaultOption = useMemo(
() =>
defaultValue
? options.find((option) => option.value === defaultValue)
: options[0],
[defaultValue, options]
);
const [isOpen, setIsOpen] = useState<boolean>(false);
const [selectedOptionValue, setSelectedOptionValue] = useState<
ISelectOption["value"] | null
>(defaultValue ?? "");
const [selectedOption, setSelectedOption] = useState<ISelectOption | null>(
defaultOption
);
const [searchValue, setSearchValue] = useState("");
const filteredOptions = useMemo(
() =>
options.filter(({ label }) =>
label.toLowerCase().includes(searchValue.toLowerCase())
),
[options, searchValue]
);
const shouldShowSearch = options.length > 5;
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (
selectRef.current &&
!selectRef.current.contains(event.target as Node)
) {
setIsOpen(false);
}
};
document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}, [selectRef]);
const onSelectOption = (option: ISelectOption) => {
setIsOpen(false);
setSelectedOptionValue(option.value);
setSelectedOption(option);
};
return (
<Controller
name={name}
control={control}
rules={{ required: !defaultValue && required }}
defaultValue={defaultValue ? defaultOption : null}
render={({
field: { onChange, value },
fieldState: { error },
formState
}) => (
<div
className={`select form-label ${isOpen ? "--open" : ""} ${
classes ?? ""
} ${disabled ? "--disabled" : ""} ${error ? "--error" : ""}`}
ref={selectRef}
>
{placeholder && (
<span className="form-label__placeholder">{placeholder}:</span>
)}
<Listbox value={value} onChange={(value) => onChange(value)}>
<Listbox.Button onClick={() => setIsOpen(!isOpen)}>
<p>{selectedOptionValue ? selectedOption?.label : defaultText}</p>
<i className="icon icon-arrow"></i>
</Listbox.Button>
<Listbox.Options>
{shouldShowSearch && (
<label className="select__search">
<input
value={searchValue}
placeholder="Search..."
onChange={(event: ChangeEvent<HTMLInputElement>) =>
setSearchValue(event.target.value)
}
/>
<i className="icon icon-search" />
</label>
)}
{filteredOptions.length > 0 ? (
filteredOptions.map(({ id, value, label }: ISelectOption) => (
<Listbox.Option
key={id}
value={{ id, value, label }}
onClick={() => onSelectOption({ id, value, label })}
className={
selectedOptionValue === value ? "--selected" : ""
}
>
{label}
</Listbox.Option>
))
) : (
<li className="select__not-results">No results found</li>
)}
</Listbox.Options>
</Listbox>
</div>
)}
/>
);
};
export default Select;