Skip to content

Instantly share code, notes, and snippets.

@Divuzki
Last active July 11, 2023 09:36
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Divuzki/07fdfa130e5260e6f72d9d4f07b4eace to your computer and use it in GitHub Desktop.
Save Divuzki/07fdfa130e5260e6f72d9d4f07b4eace to your computer and use it in GitHub Desktop.
Versatile Dropdown Search Component for React
import React, { memo, useEffect, useId, useState } from "react";
import { MdChevronLeft } from "react-icons/md";
import { motion } from "framer-motion";
import InfiniteScroll from "react-infinite-scroller";
import ClickOutSideComponent from "./ClickOutSideComponent";
const SingleSelectComponent = memo(
({
name,
handleChange,
lists,
list2,
type,
isRadio,
selectedValue,
setSelectedValue,
performRegx,
isArrObect,
}) => {
let nameRegx = `${name}`;
if (performRegx && !isArrObect)
nameRegx =
name &&
name
.replaceAll(" ", "")
.replaceAll("-", "")
.replaceAll(/ *\([^)]*\) */g, "")
.replaceAll(/ *\([^]]*\) */g, "")
.replaceAll(/ *\([^}]*\) */g, "")
.replaceAll(
/[\!\@\#\$\%\^\&\*\)\(\+\=\.\<\>\{\}\[\]\:\;\'\"\|\~\`\_\-\,\/\\]/g,
""
);
const [isChecked, setIsChecked] = useState(
type && type === "object"
? lists && lists.includes(isArrObect ? name["id"] : nameRegx)
: lists && lists[isArrObect ? name["id"] : nameRegx]
);
useEffect(() => {
if (isRadio) {
setIsChecked(
isArrObect ? selectedValue === name["id"] : selectedValue === nameRegx
);
} else {
if (type && type === "object") {
setIsChecked(
lists && lists.includes(isArrObect ? name["id"] : nameRegx)
);
} else {
setIsChecked(lists && lists[isArrObect ? name["id"] : nameRegx]);
}
}
}, [lists, list2, selectedValue]);
return (
<div key={name} className="flex cursor-pointer items-center gap-2">
<input
type="checkbox"
checked={isChecked}
name={isArrObect ? name["id"] : nameRegx}
onChange={(e) => {
handleChange(e);
setIsChecked(e.target.checked);
setSelectedValue(
e.target.checked && isArrObect ? name["id"] : nameRegx
);
}}
className="w-4 h-4 cursor-pointer text-blue-600 bg-gray-100 rounded border-gray-300
focus:ring-blue-500 dark:focus:ring-blue-600
dark:ring-offset-gray-700 focus:ring-2 dark:bg-gray-600 dark:border-gray-500"
id={`${isArrObect ? name["id"] : nameRegx}-checkbox`}
/>
<label
className="text-sm capitalize cursor-pointer w-full h-full text-gray-700"
htmlFor={`${isArrObect ? name["id"] : nameRegx}-checkbox`}
>
{isArrObect ? name["name"] : name}
</label>
</div>
);
}
);
const DropDownSearchComponent = ({
list,
list_name,
label,
setFormData,
formData,
className,
type,
c_name,
loading,
setState,
setTrigger,
text_color,
isRadio,
hide_overlay,
performRegx,
isArrObect,
emptyText
}) => {
const [showSearch, setShowSearch] = useState(false);
const [lists, setLists] = useState(list);
const [firstLoad, setFirstLoad] = useState(true);
const itemsPerPage = 10;
const [hasMore, setHasMore] = useState(true);
const [records, setRecords] = useState(itemsPerPage);
const [selectedValue, setSelectedValue] = useState("");
const input_id = useId();
useEffect(() => {
setLists(list || []);
}, [loading]);
useEffect(() => {
if (firstLoad === true) {
setLists(list);
setFirstLoad(false);
}
}, [list]);
const handleChange = (e) => {
const { name, checked } = e.target;
// now i am checking if the `list_name` have space
let split_word = list_name.split(" ");
if (split_word && split_word.length > 0) {
// if the `list_name` have space
split_word[0] = split_word[0].toLowerCase(); // then i am making the first word to lower case
split_word = split_word.join(""); // then i am joining the array to make it a string
list_name = split_word; // then i am assigning the new string to the `list_name`
} else list_name = list_name.toLowerCase().replaceAll(" ", ""); // if the `list_name` don't have space then i am making the `list_name` to lower case and removing the space
let data = { ...formData[list_name] } || {};
if (isRadio) {
setFormData({ ...formData, [list_name]: checked === true ? name : "" });
if (setState) setState(checked === true ? name : "");
} else {
if (type && type === "object") {
data = formData[list_name] || [];
if (formData[list_name].includes(name)) {
data = data.filter((n) => n !== name);
} else {
data = [...formData[list_name], name];
data = data.filter((n, index) => data.indexOf(n) === index) || [];
}
} else {
data = { ...data, [name]: checked === true ? true : false };
}
setFormData({ ...formData, [list_name]: data });
if (setState) setState(data);
}
if (setTrigger) setTrigger(true);
setRecords(list.length <= 20 ? 20 : records);
};
// console.log(formData);
const handleSearch = (e) => {
if (list && list.length > 0) {
let query = e.target.value.toUpperCase();
let suggestions = list || [];
if (query.length > 0) {
const regex = new RegExp(`^${query}`, "gi");
suggestions = list
.sort()
.filter(
(name) =>
regex.test(name) || name.toUpperCase().indexOf(query) !== -1
);
}
setLists(suggestions);
setRecords(
suggestions.length <= records ? suggestions.length : itemsPerPage
);
setHasMore(suggestions.length <= records ? false : true);
}
};
const loadMore = () => {
if (lists && lists.length > 0 && records >= lists.length) {
setHasMore(false);
} else {
setTimeout(() => {
setRecords(records + itemsPerPage);
}, 1000);
}
};
const inputRef = React.useRef(null);
// Automatic Focus On Input
useEffect(() => {
if (showSearch && document.getElementById(input_id))
document.getElementById(input_id).focus();
return () => {
if (showSearch && document.getElementById(input_id))
document.getElementById(input_id).blur();
};
}, [showSearch]);
return (
<ClickOutSideComponent
setState={setShowSearch}
visible={hide_overlay ? !hide_overlay : showSearch}
>
<div
className={`${showSearch === true ? "z-30" : "z-[1]"} ${
className ||
`bg-white rounded-lg shadow-lg hover:shadow-md transition-all
select-none flex flex-col gap-2 p-2 w-full lg:w-[480px]`
}`}
>
<div className="flex flex-col">
<div
className={`flex transition-all ${
text_color || "text-[#221F60]"
} cursor-pointer ${
showSearch
? `h-auto pt-2 pb-1 ${
className ? "justify-between" : "justify-center"
}`
: `${
className
? "text-sm py-1 justify-between opacity-75"
: "mt-2 text-lg"
}`
}`}
onClick={() =>
setShowSearch(list && list.length > 0 ? !showSearch : false)
}
>
{loading ? (
<div className="flex items-center justify-center w-full py-2">
<div className="animate-spin rounded-full h-3 w-3 border-b-2 border-gray-900"></div>
</div>
) : (
<>
{list && list.length > 0 ? (
<>
<span className="capitalize text-center font-semibold">
{label || list_name}
</span>
<span
style={{
transform: "rotate(270deg)",
}}
className="transition-all duration-300"
>
<MdChevronLeft />
</span>
</>
) : (
<>
<span className="w-full capitalize text-center font-semibold">
{emptyText && typeof emptyText === 'string' ? emptyText : `Select ${c_name || list_name}`}
</span>
</>
)}
</>
)}
</div>
{/* Search */}
<div
className={`transition-all flex px-1 flex-row gap-1 justify-center items-center delay-75 w-full bg-[#f0ffff7d] rounded-md shadow-md ${
showSearch ? "opacity-100" : "opacity-0 z-0 scale-0 h-0"
}`}
>
<span className="h-full">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="w-4 h-4 text-black items-center justify-center "
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607z"
/>
</svg>
</span>
<input
type="text"
id={input_id}
className="w-full h-full bg-transparent py-2 font-semibold outline-none"
placeholder={`Search ${label || list_name}`}
onChange={handleSearch}
onBlur={handleSearch}
onKeyDown={handleSearch}
onKeyUp={handleSearch}
/>
</div>
</div>
<motion.div
initial={{ height: 0, opacity: 0, zIndex: 0 }}
animate={{
opacity: showSearch ? 1 : 0,
zIndex: showSearch ? 1 : 0,
height: showSearch ? (lists && lists.length > 8 ? 200 : "auto") : 0,
}}
exit={{ height: 0, opacity: 0, zIndex: 0 }}
transition={{ duration: 0.5 }}
className={`${
className && showSearch === false ? "hidden" : "flex"
} flex-col px-2 gap-2 ${
lists && lists.length > 8
? "overflow-y-auto overflow-x-hidden"
: "overflow-hidden"
}`}
>
{list && list.length > 0 ? (
<InfiniteScroll
pageStart={0}
loadMore={loadMore}
hasMore={hasMore}
loader={
<div className="flex gap-2 py-2 w-full justify-center items-center">
<div className="h-2 w-full animate-pulse rounded-full bg-gray-400 opacity-75"></div>
<div className="h-2 w-8 animate-pulse rounded-full bg-gray-400 opacity-75"></div>
<div className="h-2 w-full animate-pulse rounded-full bg-gray-400 opacity-75"></div>
</div>
}
useWindow={false}
>
{lists &&
lists.length > 0 &&
lists
.slice(0, records)
.map((n, idx) => (
<SingleSelectComponent
key={idx}
performRegx={performRegx}
isArrObect={isArrObect}
idx={idx}
name={n}
lists={formData[list_name.toLowerCase()]}
list2={lists}
handleChange={handleChange}
type={type}
isRadio={isRadio}
selectedValue={selectedValue}
setSelectedValue={setSelectedValue}
/>
))}
</InfiniteScroll>
) : (
<div className="text-center text-gray-500">
No {label || list_name} found
</div>
)}
</motion.div>
</div>
</ClickOutSideComponent>
);
};
export default DropDownSearchComponent;
@Divuzki
Copy link
Author

Divuzki commented Jul 11, 2023

DropDownSearchComponent - A versatile and reusable dropdown search component for React.

This code showcases a customisable dropdown search component with search functionality, infinite scrolling, and support for single and multiple selections. It efficiently handles data and integrates with popular libraries like React Icons and Framer Motion.

Key Features:

Search functionality for easy option filtering
Infinite scrolling for efficient handling of large option lists
Versatile data management supporting single and multiple selections
Clean and readable code with proper organisation and comments
Integration with React Icons and Framer Motion for enhanced functionality and visuals
This component provides a user-friendly and flexible solution for implementing dropdown search functionality in your React applications. It is modular, customisable, and easy to integrate into existing projects.

Feel free to use and modify this code according to your specific requirements. Happy coding!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment