Last active
July 11, 2023 09:36
-
-
Save Divuzki/07fdfa130e5260e6f72d9d4f07b4eace to your computer and use it in GitHub Desktop.
Versatile Dropdown Search Component for React
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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!