UNPKG

@happyhyep/tree-component

Version:

React Tree Component with Search functionality

774 lines (621 loc) β€’ 20.1 kB
# 🌳 @happyhyep/tree-component [πŸ‡°πŸ‡· ν•œκ΅­μ–΄](#-ν•œκ΅­μ–΄-ver) | [πŸ‡ΊπŸ‡Έ English](#-english) ## πŸ‡ΊπŸ‡Έ English ver > **React Tree Component Library** where **both folders and files are clickable** [![npm version](https://badge.fury.io/js/@happyhyep%2Ftree-component.svg)](https://www.npmjs.com/package/@happyhyep/tree-component) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) ## ✨ Key Differentiators Unlike other Tree components, this **revolutionary Tree component** allows **both folders and files to be clickable** for interaction. | Feature | Typical Tree | **🌳 Tree Component** | | -------------------- | ----------------------------------- | ------------------------------------ | | Folder Click | ❌ Expand/collapse only | βœ… **Click event + expand/collapse** | | File Click | βœ… Clickable | βœ… Clickable | | Search Feature | ❌ Requires separate implementation | βœ… **Built-in search + highlight** | | Default Expand State | ❌ Manual setup | βœ… **Expand all folders at once** | | TypeScript | ⚠️ Limited support | βœ… **Full type safety** | ## 🎬 Demo ### Tree Component Usage Examples ![Tree Component Demo](https://cdn.jsdelivr.net/npm/@happyhyep/tree-component@1.0.2/docs/images/tree-component-example-1.gif) ## πŸ“¦ Installation ```bash # npm npm install @happyhyep/tree-component # yarn yarn add @happyhyep/tree-component # pnpm pnpm add @happyhyep/tree-component ``` ## πŸš€ Quick Start ### 1️⃣ Basic Tree Component ```tsx import React, { useState } from 'react'; import { Tree, TreeItem } from '@happyhyep/tree-component'; interface FileData { name: string; type: 'folder' | 'file'; size?: number; } const data: TreeItem<FileData>[] = [ { id: '1', parentId: null, canOpen: true, data: { name: 'Documents', type: 'folder' }, }, { id: '2', parentId: '1', canOpen: false, data: { name: 'report.pdf', type: 'file', size: 1024 }, }, { id: '3', parentId: '1', canOpen: true, data: { name: 'Projects', type: 'folder' }, }, ]; function MyApp() { const [selectedId, setSelectedId] = useState<string>(); return ( <Tree items={data} selectedId={selectedId} onItemClick={(item) => { setSelectedId(item.id); console.log('Clicked item:', item.data); // πŸ’‘ Both folders and files are clickable! if (item.data.type === 'folder') { console.log('Folder clicked:', item.data.name); } else { console.log('File clicked:', item.data.name); } }} renderLabel={(data) => ( <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}> <span>{data.type === 'folder' ? 'πŸ“' : 'πŸ“„'}</span> <span>{data.name}</span> {data.size && <span>({data.size}KB)</span>} </div> )} /> ); } ``` ### 2️⃣ Tree with Search ```tsx import { TreeWithSearch } from '@happyhyep/tree-component'; function SearchableTree() { const [selectedId, setSelectedId] = useState<string>(); return ( <TreeWithSearch items={data} selectedId={selectedId} onItemClick={(item) => setSelectedId(item.id)} renderLabel={(data, highlight) => ( <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}> <span>{data.type === 'folder' ? 'πŸ“' : 'πŸ“„'}</span> {/* πŸ” Automatic search term highlighting */} <HighlightText text={data.name} highlight={highlight} /> </div> )} searchFn={(data, keyword) => data.name.toLowerCase().includes(keyword.toLowerCase())} > {/* 🎯 Built-in search input */} <TreeWithSearch.Input placeholder="Search files..." /> </TreeWithSearch> ); } // Highlight helper component const HighlightText = ({ text, highlight }) => { if (!highlight) return <span>{text}</span>; const parts = text.split(new RegExp(`(${highlight})`, 'gi')); return ( <span> {parts.map((part, i) => part.toLowerCase() === highlight.toLowerCase() ? <mark key={i}>{part}</mark> : part, )} </span> ); }; ``` ### 3️⃣ Advanced Usage - Default Expand All ```tsx function ExpandedTree() { return ( <Tree items={data} defaultExpandAll={true} // πŸš€ Expand all folders by default renderLabel={(data) => <span>{data.name}</span>} /> ); } ``` ## πŸ“‹ Real-world Use Cases ### File Explorer ```tsx import { Tree } from '@happyhyep/tree-component'; function FileExplorer() { const [selectedFile, setSelectedFile] = useState(null); const handleItemClick = (item) => { if (item.data.type === 'file') { // File click - open file openFile(item.data); } else { // Folder click - select folder (expand/collapse is automatic) setSelectedFolder(item.data); } }; return ( <div style={{ display: 'flex' }}> <Tree items={fileSystemData} onItemClick={handleItemClick} renderLabel={(data) => <FileIcon type={data.type} name={data.name} />} /> {selectedFile && <FilePreview file={selectedFile} />} </div> ); } ``` ### Organization Chart / Hierarchy ```tsx function OrganizationChart() { return ( <Tree items={orgData} defaultExpandAll={true} onItemClick={(item) => { // Both departments and employees are clickable showPersonDetails(item.data); }} renderLabel={(data) => ( <div> <strong>{data.name}</strong> <span>({data.position})</span> </div> )} /> ); } ``` ## πŸ”§ API Documentation ### TreeItem Interface ```tsx interface TreeItem<T = unknown> { id: string; // Unique identifier parentId: string | null; // Parent ID (null for root) data: T; // User data canOpen?: boolean; // Whether it can be expanded hasLeaf?: boolean; // Whether it's a leaf node children?: TreeItem<T>[]; // Child nodes (auto-generated) } ``` ### Tree Props | Props | Type | Required | Default | Description | | ------------------ | ----------------------------- | -------- | ------- | -------------------------------------- | | `items` | `TreeItem<T>[]` | βœ… | - | Tree data | | `renderLabel` | `(data: T) => ReactNode` | βœ… | - | Label rendering function | | `onItemClick` | `(item: TreeItem<T>) => void` | ❌ | - | **Click handler (both folders/files)** | | `selectedId` | `string` | ❌ | - | Selected item ID | | `defaultExpandAll` | `boolean` | ❌ | `false` | **Expand all folders by default** | | `className` | `string` | ❌ | `""` | CSS class | ### TreeWithSearch Props All Tree props + additional: | Props | Type | Required | Description | | ---------- | --------------------------------------- | -------- | ------------------- | | `searchFn` | `(data: T, keyword: string) => boolean` | βœ… | **Search function** | | `children` | `ReactNode` | ❌ | Search input, etc. | ## πŸ’‘ Tips and Tricks ### 1. Conditional Click Handling ```tsx const handleClick = (item) => { if (item.data.type === 'folder') { // Folder click - special logic if (item.data.permissions?.canAccess) { navigateToFolder(item); } else { showPermissionError(); } } else { // File click - open file openFile(item); } }; ``` ### 2. Custom Search ```tsx // Multi-condition search const advancedSearch = (data, keyword) => { return ( data.name.toLowerCase().includes(keyword.toLowerCase()) || data.tags?.some((tag) => tag.includes(keyword)) || data.content?.includes(keyword) ); }; // Extension search const extensionSearch = (data, keyword) => { const extension = data.name.split('.').pop(); return extension?.toLowerCase().includes(keyword.toLowerCase()); }; ``` ### 3. Performance Optimization ```tsx // Memoization for large datasets const MemoizedTree = React.memo(() => ( <Tree items={largeDataSet} renderLabel={React.useCallback( (data) => ( <span>{data.name}</span> ), [], )} /> )); ``` ## 🎨 Styling ### Using CSS Classes ```tsx <Tree className="my-custom-tree" items={data} renderLabel={(data) => <span className={`item-${data.type}`}>{data.name}</span>} /> ``` ```css .my-custom-tree { border: 1px solid #ddd; border-radius: 8px; padding: 16px; } .item-folder { font-weight: bold; color: #4a90e2; } .item-file { color: #666; } ``` ## πŸ› οΈ Development ### Local Development Setup ```bash # Clone repository git clone https://github.com/happyhyep/tree-component.git cd tree-component # Install dependencies pnpm install # Run Storybook pnpm run storybook # Build pnpm run build # Lint pnpm run lint ``` ### Storybook Component documentation and examples are available in Storybook: ```bash pnpm run storybook ``` ## 🀝 Contributing 1. Fork this repository 2. Create a feature branch (`git checkout -b feature/amazing-feature`) 3. Commit your changes (`git commit -m 'Add amazing feature'`) 4. Push to the branch (`git push origin feature/amazing-feature`) 5. Open a Pull Request ## πŸ“„ License This project is licensed under the [MIT License](LICENSE). ## πŸ™‹β€β™‚οΈ Support - πŸ› Bug Reports: [GitHub Issues](https://github.com/happyhyep/tree-component/issues) - πŸ’‘ Feature Requests: [GitHub Discussions](https://github.com/happyhyep/tree-component/discussions) - πŸ“§ Email: happyhyep@example.com --- ## πŸ‡°πŸ‡· ν•œκ΅­μ–΄ ver **폴더와 파일 λͺ¨λ‘ 클릭 κ°€λŠ₯ν•œ** React Tree μ»΄ν¬λ„ŒνŠΈ 라이브러리 μž…λ‹ˆλ‹€. [![npm version](https://badge.fury.io/js/@happyhyep%2Ftree-component.svg)](https://www.npmjs.com/package/@happyhyep/tree-component) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) ## ✨ μ£Όμš” 차별점 λ‹€λ₯Έ Tree μ»΄ν¬λ„ŒνŠΈμ™€ 달리 **폴더와 파일 λͺ¨λ‘ 클릭**ν•˜μ—¬ μƒν˜Έμž‘μš©ν•  수 μžˆλŠ” Tree μ»΄ν¬λ„ŒνŠΈμž…λ‹ˆλ‹€. | κΈ°λŠ₯ | 일반적인 Tree | **🌳 Tree Component** | | -------------- | ----------------- | -------------------------------- | | 폴더 클릭 | ❌ 펼치기/μ ‘κΈ°λ§Œ | βœ… **클릭 이벀트 + 펼치기/μ ‘κΈ°** | | 파일 클릭 | βœ… 클릭 κ°€λŠ₯ | βœ… 클릭 κ°€λŠ₯ | | 검색 κΈ°λŠ₯ | ❌ 별도 κ΅¬ν˜„ ν•„μš” | βœ… **λ‚΄μž₯ 검색 + ν•˜μ΄λΌμ΄νŠΈ** | | κΈ°λ³Έ ν™•μž₯ μƒνƒœ | ❌ μˆ˜λ™ μ„€μ • | βœ… **ν•œ λ²ˆμ— λͺ¨λ“  폴더 ν™•μž₯** | | TypeScript | ⚠️ μ œν•œμ  지원 | βœ… **μ™„μ „ν•œ νƒ€μž… μ•ˆμ „μ„±** | ## 🎬 데λͺ¨ ### Tree μ»΄ν¬λ„ŒνŠΈ μ‚¬μš© μ˜ˆμ‹œ ![Basic Tree Demo](https://cdn.jsdelivr.net/npm/@happyhyep/tree-component@1.0.2/docs/images/tree-component-example-1.gif) ## πŸ“¦ μ„€μΉ˜ ```bash # npm npm install @happyhyep/tree-component # yarn yarn add @happyhyep/tree-component # pnpm pnpm add @happyhyep/tree-component ``` ## πŸš€ λΉ λ₯Έ μ‹œμž‘ ### 1️⃣ κΈ°λ³Έ Tree μ»΄ν¬λ„ŒνŠΈ ```tsx import React, { useState } from 'react'; import { Tree, TreeItem } from '@happyhyep/tree-component'; interface FileData { name: string; type: 'folder' | 'file'; size?: number; } const data: TreeItem<FileData>[] = [ { id: '1', parentId: null, canOpen: true, data: { name: 'Documents', type: 'folder' }, }, { id: '2', parentId: '1', canOpen: false, data: { name: 'report.pdf', type: 'file', size: 1024 }, }, { id: '3', parentId: '1', canOpen: true, data: { name: 'Projects', type: 'folder' }, }, ]; function MyApp() { const [selectedId, setSelectedId] = useState<string>(); return ( <Tree items={data} selectedId={selectedId} onItemClick={(item) => { setSelectedId(item.id); console.log('클릭된 ν•­λͺ©:', item.data); // πŸ’‘ 폴더든 νŒŒμΌμ΄λ“  λͺ¨λ‘ 클릭 κ°€λŠ₯! if (item.data.type === 'folder') { console.log('폴더 클릭:', item.data.name); } else { console.log('파일 클릭:', item.data.name); } }} renderLabel={(data) => ( <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}> <span>{data.type === 'folder' ? 'πŸ“' : 'πŸ“„'}</span> <span>{data.name}</span> {data.size && <span>({data.size}KB)</span>} </div> )} /> ); } ``` ### 2️⃣ 검색 κΈ°λŠ₯이 μžˆλŠ” Tree ```tsx import { TreeWithSearch } from '@happyhyep/tree-component'; function SearchableTree() { const [selectedId, setSelectedId] = useState<string>(); return ( <TreeWithSearch items={data} selectedId={selectedId} onItemClick={(item) => setSelectedId(item.id)} renderLabel={(data, highlight) => ( <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}> <span>{data.type === 'folder' ? 'πŸ“' : 'πŸ“„'}</span> {/* πŸ” 검색어 μžλ™ ν•˜μ΄λΌμ΄νŠΈ */} <HighlightText text={data.name} highlight={highlight} /> </div> )} searchFn={(data, keyword) => data.name.toLowerCase().includes(keyword.toLowerCase())} > {/* 🎯 λ‚΄μž₯ 검색 μž…λ ₯μ°½ */} <TreeWithSearch.Input placeholder="파일λͺ… 검색..." /> </TreeWithSearch> ); } // ν•˜μ΄λΌμ΄νŠΈ 헬퍼 μ»΄ν¬λ„ŒνŠΈ const HighlightText = ({ text, highlight }) => { if (!highlight) return <span>{text}</span>; const parts = text.split(new RegExp(`(${highlight})`, 'gi')); return ( <span> {parts.map((part, i) => part.toLowerCase() === highlight.toLowerCase() ? <mark key={i}>{part}</mark> : part, )} </span> ); }; ``` ### 3️⃣ κ³ κΈ‰ μ‚¬μš©λ²• - λͺ¨λ“  폴더 κΈ°λ³Έ ν™•μž₯ ```tsx function ExpandedTree() { return ( <Tree items={data} defaultExpandAll={true} // πŸš€ λͺ¨λ“  폴더 κΈ°λ³Έ ν™•μž₯ renderLabel={(data) => <span>{data.name}</span>} /> ); } ``` ## πŸ“‹ μ‹€μ œ μ‚¬μš© 사둀 ### 파일 탐색기 ```tsx import { Tree } from '@happyhyep/tree-component'; function FileExplorer() { const [selectedFile, setSelectedFile] = useState(null); const handleItemClick = (item) => { if (item.data.type === 'file') { // 파일 클릭 μ‹œ - 파일 μ—΄κΈ° openFile(item.data); } else { // 폴더 클릭 μ‹œ - 폴더 선택 (펼치기/μ ‘κΈ°λŠ” μžλ™) setSelectedFolder(item.data); } }; return ( <div style={{ display: 'flex' }}> <Tree items={fileSystemData} onItemClick={handleItemClick} renderLabel={(data) => <FileIcon type={data.type} name={data.name} />} /> {selectedFile && <FilePreview file={selectedFile} />} </div> ); } ``` ### 쑰직도 / 계측 ꡬ쑰 ```tsx function OrganizationChart() { return ( <Tree items={orgData} defaultExpandAll={true} onItemClick={(item) => { // λΆ€μ„œλ“  직원이든 클릭 κ°€λŠ₯ showPersonDetails(item.data); }} renderLabel={(data) => ( <div> <strong>{data.name}</strong> <span>({data.position})</span> </div> )} /> ); } ``` ## πŸ”§ API λ¬Έμ„œ ### TreeItem μΈν„°νŽ˜μ΄μŠ€ ```tsx interface TreeItem<T = unknown> { id: string; // 고유 μ‹λ³„μž parentId: string | null; // λΆ€λͺ¨ ID (λ£¨νŠΈλŠ” null) data: T; // μ‚¬μš©μž 데이터 canOpen?: boolean; // 펼칠 수 μžˆλŠ”μ§€ μ—¬λΆ€ hasLeaf?: boolean; // 리프 λ…Έλ“œ μ—¬λΆ€ children?: TreeItem<T>[]; // μžμ‹ λ…Έλ“œ (μžλ™ 생성) } ``` ### Tree Props | Props | νƒ€μž… | ν•„μˆ˜ | κΈ°λ³Έκ°’ | μ„€λͺ… | | ------------------ | ----------------------------- | ---- | ------- | -------------------------------- | | `items` | `TreeItem<T>[]` | βœ… | - | 트리 데이터 | | `renderLabel` | `(data: T) => ReactNode` | βœ… | - | 라벨 λ Œλ”λ§ | | `onItemClick` | `(item: TreeItem<T>) => void` | ❌ | - | **클릭 ν•Έλ“€λŸ¬ (폴더/파일 λͺ¨λ‘)** | | `selectedId` | `string` | ❌ | - | μ„ νƒλœ ν•­λͺ© ID | | `defaultExpandAll` | `boolean` | ❌ | `false` | **λͺ¨λ“  폴더 κΈ°λ³Έ ν™•μž₯** | | `className` | `string` | ❌ | `""` | CSS 클래슀 | ### TreeWithSearch Props Tree의 λͺ¨λ“  props + μΆ”κ°€: | Props | νƒ€μž… | ν•„μˆ˜ | μ„€λͺ… | | ---------- | --------------------------------------- | ---- | -------------- | | `searchFn` | `(data: T, keyword: string) => boolean` | βœ… | **검색 ν•¨μˆ˜** | | `children` | `ReactNode` | ❌ | 검색 μž…λ ₯μ°½ λ“± | ## πŸ’‘ 팁과 트릭 ### 1. 쑰건뢀 클릭 처리 ```tsx const handleClick = (item) => { if (item.data.type === 'folder') { // 폴더 클릭 - νŠΉλ³„ν•œ 둜직 if (item.data.permissions?.canAccess) { navigateToFolder(item); } else { showPermissionError(); } } else { // 파일 클릭 - 파일 μ—΄κΈ° openFile(item); } }; ``` ### 2. μ»€μŠ€ν…€ 검색 ```tsx // 닀쀑 쑰건 검색 const advancedSearch = (data, keyword) => { return ( data.name.toLowerCase().includes(keyword.toLowerCase()) || data.tags?.some((tag) => tag.includes(keyword)) || data.content?.includes(keyword) ); }; // ν™•μž₯자 검색 const extensionSearch = (data, keyword) => { const extension = data.name.split('.').pop(); return extension?.toLowerCase().includes(keyword.toLowerCase()); }; ``` ### 3. μ„±λŠ₯ μ΅œμ ν™” ```tsx // 큰 데이터셋을 μœ„ν•œ λ©”λͺ¨μ΄μ œμ΄μ…˜ const MemoizedTree = React.memo(() => ( <Tree items={largeDataSet} renderLabel={React.useCallback( (data) => ( <span>{data.name}</span> ), [], )} /> )); ``` ## 🎨 μŠ€νƒ€μΌλ§ ### CSS 클래슀 μ‚¬μš© ```tsx <Tree className="my-custom-tree" items={data} renderLabel={(data) => <span className={`item-${data.type}`}>{data.name}</span>} /> ``` ```css .my-custom-tree { border: 1px solid #ddd; border-radius: 8px; padding: 16px; } .item-folder { font-weight: bold; color: #4a90e2; } .item-file { color: #666; } ``` ## πŸ› οΈ 개발 ### 둜컬 개발 ν™˜κ²½ ```bash # μ €μž₯μ†Œ 클둠 git clone https://github.com/happyhyep/tree-component.git cd tree-component # μ˜μ‘΄μ„± μ„€μΉ˜ pnpm install # Storybook μ‹€ν–‰ pnpm run storybook # λΉŒλ“œ pnpm run build # 린트 pnpm run lint ``` ### Storybook μ»΄ν¬λ„ŒνŠΈ λ¬Έμ„œμ™€ μ˜ˆμ œλŠ” Storybookμ—μ„œ 확인할 수 μžˆμŠ΅λ‹ˆλ‹€: ```bash pnpm run storybook ``` ## 🀝 κΈ°μ—¬ν•˜κΈ° 1. 이 μ €μž₯μ†Œλ₯Ό ν¬ν¬ν•˜μ„Έμš” 2. κΈ°λŠ₯ 브랜치λ₯Ό λ§Œλ“œμ„Έμš” (`git checkout -b feature/amazing-feature`) 3. 변경사항을 μ»€λ°‹ν•˜μ„Έμš” (`git commit -m 'Add amazing feature'`) 4. λΈŒλžœμΉ˜μ— ν‘Έμ‹œν•˜μ„Έμš” (`git push origin feature/amazing-feature`) 5. Pull Requestλ₯Ό μ—΄μ–΄μ£Όμ„Έμš” ## πŸ“„ λΌμ΄μ„ΌμŠ€ 이 ν”„λ‘œμ νŠΈλŠ” [MIT λΌμ΄μ„ΌμŠ€](LICENSE) ν•˜μ— λ°°ν¬λ©λ‹ˆλ‹€. ## πŸ™‹β€β™‚οΈ 지원 - πŸ› 버그 리포트: [GitHub Issues](https://github.com/happyhyep/tree-component/issues) - πŸ’‘ κΈ°λŠ₯ μš”μ²­: [GitHub Discussions](https://github.com/happyhyep/tree-component/discussions) - πŸ“§ 이메일: happyhyep@example.com