DEV Community

Cover image for Creating a Custom Select Component with Tree View in React
Ayesha Munir
Ayesha Munir

Posted on

Creating a Custom Select Component with Tree View in React

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
Enter fullscreen mode Exit fullscreen mode

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.

Custom Select Component


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
Enter fullscreen mode Exit fullscreen mode

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.

Tree view Component

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)

Collapse
 
efpage profile image
Eckehard

Can you please provide an example how to populate the tree view from any given data?

Collapse
 
ashmunir profile image
Ayesha Munir • Edited

Yeah sure!

Let me share here :)

const { data } = useFolder(Id)
Enter fullscreen mode Exit fullscreen mode

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.

<CustomSelect options={data} selectedOption={formik.value} setSelectedOption={formik.onChange} />
Enter fullscreen mode Exit fullscreen mode

Let me know if you need further help. Thanks for your query and reading blog :)