In this blog post, we'll walk through the process of creating a custom select component in React that includes a tree view for displaying options and sub-options. 🚀
Custom Select Component 🎛️
The custom select component is a dropdown menu that allows users to select an option from a list. Here's the basic structure of our custom select component:
import PropTypes from 'prop-types'
import TreeView from './treeView'
import React, { useState, useEffect, useRef } from 'react'
import { iconExtraSmall } from '@/lib/utils/helperUtils/constants'
import { IconChevronDown, IconChevronUp } from '@tabler/icons-react'
const CustomSelect = ({ options, selectedOption, setSelectedOption }) => {
const dropdownRef = useRef(null)
const [expanded, setExpanded] = useState({})
const [isOpen, setIsOpen] = useState(false)
const [selectedOptionObject, setSelectedOptionObject] = useState(null)
useEffect(() => {
const handleClickOutside = (event) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
setIsOpen(false)
}
}
document.addEventListener('mousedown', handleClickOutside)
return () => {
document.removeEventListener('mousedown', handleClickOutside)
}
}, [])
const handleExpand = (id) => {
setExpanded((prev) => ({ ...prev, [id]: !prev[id] }))
}
const handleSelect = (option) => {
setSelectedOption(option.id)
setSelectedOptionObject(option)
}
return (
<div className="relative" ref={dropdownRef}>
<div className="w-full text-xs font-normal h-[40px] text-fieldsText rounded-md outline-none border-input bg-fieldsBg z-50 max-h-96 min-w-[8rem] overflow-hidden">
<button
className="flex h-10 w-full items-center whitespace-nowrap rounded-md bg-transparent px-3 py-2 text-xs shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1"
onClick={() => setIsOpen(!isOpen)}
disabled={!options?.length}
type="button"
>
{selectedOption && selectedOptionObject?.name}
<div className="ml-auto">
{isOpen ? (
<IconChevronUp size={iconExtraSmall} className={'cursor-pointer'} />
) : (
<IconChevronDown size={iconExtraSmall} className={'cursor-pointer'} />
)}
</div>
</button>
</div>
{isOpen && (
<div className="absolute top-full w-full mt-1 bg-white border border-gray-200 rounded-md shadow-lg z-50 p-1 overflow-auto max-h-36">
{options?.map((option) => (
<TreeView
key={option.id}
option={option}
handleExpand={handleExpand}
handleSelect={handleSelect}
expanded={expanded}
selectedOption={selectedOption}
/>
))}
</div>
)}
</div>
)
}
CustomSelect.propTypes = {
options: PropTypes.array,
selectedOption: PropTypes.string,
setSelectedOption: PropTypes.func,
}
export default CustomSelect
Here you can see Select field with Chevron down icon on right. By clicking on field it will expand the dropdown and you will be able to see main options in one go.
Tree View Component
The tree view component is a visual representation of hierarchical data. It displays data as a tree-like structure with collapsible nodes. This is particularly useful when dealing with nested data, like a directory of folders and files or a nested menu structure.
import PropTypes from 'prop-types'
const TreeView = ({ option, handleExpand, handleSelect, expanded, selectedOption, level = 0 }) => {
const getIcon = (option) => {
if (option.subFolders.length > 0) {
return (
<div>
<button onClick={() => handleExpand(option.id)} className="flex cursor-default items-center justify-center">
<div className="flex items-center justify-center w-3 h-3 border border-fieldsText text-fieldsText text-xs">
{expanded[option.id] ? '-' : '+'}
</div>
</button>
</div>
)
} else {
return (
<div className="flex items-center justify-center w-3 h-3 border border-fieldsText text-fieldsText text-[0.6rem] ">
x
</div>
)
}
}
return (
<div key={option.id} className="w-full">
<button
type="button"
onClick={() => handleSelect(option)}
style={{ marginLeft: `${level * 1.25}rem`, width: `calc(100% - ${level * 1.25}rem)` }}
className={`relative flex py-2 px-1.5 cursor-default text-fieldsText select-none items-center rounded-sm text-xs outline-none font-normal ${option === selectedOption ? 'bg-accent text-accent-foreground' : ''} hover:bg-accent focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 whitespace-nowrap`}
>
<div className="flex items-center">
<div className="pr-1.5">{getIcon(option)}</div>
{option.name}
</div>
</button>
{expanded[option.id] &&
option.subFolders.map((subOption) => (
<TreeView
key={subOption.id}
option={subOption}
handleExpand={handleExpand}
handleSelect={handleSelect}
expanded={expanded}
selectedOption={selectedOption}
level={level + 1}
/>
))}
</div>
)
}
TreeView.propTypes = {
option: PropTypes.object,
handleExpand: PropTypes.func,
handleSelect: PropTypes.func,
expanded: PropTypes.object,
selectedOption: PropTypes.string,
level: PropTypes.number,
}
export default TreeView
In the tree view component, It allows users to interact with hierarchical data. It uses '+' and '-' icons to represent expandable and collapsible options respectively. Options without sub-options are represented by an 'x' icon.
This is Complete view of Select custom component and Tree view for showing and selecting from main options or sub options.
In summary, the tree view component in React effectively displays hierarchical data, offering a user-friendly interface for navigation and selection, and enhancing interactivity with its expand/collapse features. 🎉
If you have any thoughts or queries, don't hesitate to share them in the comments section. Your input is greatly appreciated! 😊
If you found this post helpful, please consider giving it a thumbs up or a share to help others discover it.
Keep coding and stay curious! 🎈
🌐 Get in touch: Ayesha Munir
👥 Connect: Linkedin | Facebook| Instagram
Top comments (2)
Can you please provide an example how to populate the tree view from any given data?
Yeah sure!
Let me share here :)
I am getting data from swr Hook for fetching data from API you can just have your data into main where you want to CustomSelect component and then pass data into options.
Let me know if you need further help. Thanks for your query and reading blog :)