Skip to content

Latest commit

ย 

History

32 Commits

Folders and files

NameName
Last commit message
Last commit date
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 

Repository files navigation

๐ŸŒณ @happyhyep/tree-component

๐Ÿ‡ฐ๐Ÿ‡ท ํ•œ๊ตญ์–ด | ๐Ÿ‡บ๐Ÿ‡ธ English

๐Ÿ‡บ๐Ÿ‡ธ English ver

React Tree Component Library where both folders and files are clickable

npm version License: 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

๐Ÿ“ฆ Installation

# npm
npm install @happyhyep/tree-component

# yarn
yarn add @happyhyep/tree-component

# pnpm
pnpm add @happyhyep/tree-component

๐Ÿš€ Quick Start

1๏ธโƒฃ Basic Tree Component

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

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

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

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

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

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

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

// 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

// Memoization for large datasets
const MemoizedTree = React.memo(() => (
  <Tree
    items={largeDataSet}
    renderLabel={React.useCallback(
      (data) => (
        <span>{data.name}</span>
      ),
      [],
    )}
  />
));

๐ŸŽจ Styling

Using CSS Classes

<Tree
  className="my-custom-tree"
  items={data}
  renderLabel={(data) => <span className={`item-${data.type}`}>{data.name}</span>}
/>
.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

# 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:

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.

๐Ÿ™‹โ€โ™‚๏ธ Support


๐Ÿ‡ฐ๐Ÿ‡ท ํ•œ๊ตญ์–ด ver

ํด๋”์™€ ํŒŒ์ผ ๋ชจ๋‘ ํด๋ฆญ ๊ฐ€๋Šฅํ•œ React Tree ์ปดํฌ๋„ŒํŠธ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์ž…๋‹ˆ๋‹ค.

npm version License: MIT

โœจ ์ฃผ์š” ์ฐจ๋ณ„์ 

๋‹ค๋ฅธ Tree ์ปดํฌ๋„ŒํŠธ์™€ ๋‹ฌ๋ฆฌ ํด๋”์™€ ํŒŒ์ผ ๋ชจ๋‘ ํด๋ฆญํ•˜์—ฌ ์ƒํ˜ธ์ž‘์šฉํ•  ์ˆ˜ ์žˆ๋Š” Tree ์ปดํฌ๋„ŒํŠธ์ž…๋‹ˆ๋‹ค.

๊ธฐ๋Šฅ ์ผ๋ฐ˜์ ์ธ Tree ๐ŸŒณ Tree Component
ํด๋” ํด๋ฆญ โŒ ํŽผ์น˜๊ธฐ/์ ‘๊ธฐ๋งŒ โœ… ํด๋ฆญ ์ด๋ฒคํŠธ + ํŽผ์น˜๊ธฐ/์ ‘๊ธฐ
ํŒŒ์ผ ํด๋ฆญ โœ… ํด๋ฆญ ๊ฐ€๋Šฅ โœ… ํด๋ฆญ ๊ฐ€๋Šฅ
๊ฒ€์ƒ‰ ๊ธฐ๋Šฅ โŒ ๋ณ„๋„ ๊ตฌํ˜„ ํ•„์š” โœ… ๋‚ด์žฅ ๊ฒ€์ƒ‰ + ํ•˜์ด๋ผ์ดํŠธ
๊ธฐ๋ณธ ํ™•์žฅ ์ƒํƒœ โŒ ์ˆ˜๋™ ์„ค์ • โœ… ํ•œ ๋ฒˆ์— ๋ชจ๋“  ํด๋” ํ™•์žฅ
TypeScript โš ๏ธ ์ œํ•œ์  ์ง€์› โœ… ์™„์ „ํ•œ ํƒ€์ž… ์•ˆ์ „์„ฑ

๐ŸŽฌ ๋ฐ๋ชจ

Tree ์ปดํฌ๋„ŒํŠธ ์‚ฌ์šฉ ์˜ˆ์‹œ

Basic Tree Demo

๐Ÿ“ฆ ์„ค์น˜

# npm
npm install @happyhyep/tree-component

# yarn
yarn add @happyhyep/tree-component

# pnpm
pnpm add @happyhyep/tree-component

๐Ÿš€ ๋น ๋ฅธ ์‹œ์ž‘

1๏ธโƒฃ ๊ธฐ๋ณธ Tree ์ปดํฌ๋„ŒํŠธ

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

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๏ธโƒฃ ๊ณ ๊ธ‰ ์‚ฌ์šฉ๋ฒ• - ๋ชจ๋“  ํด๋” ๊ธฐ๋ณธ ํ™•์žฅ

function ExpandedTree() {
  return (
    <Tree
      items={data}
      defaultExpandAll={true} // ๐Ÿš€ ๋ชจ๋“  ํด๋” ๊ธฐ๋ณธ ํ™•์žฅ
      renderLabel={(data) => <span>{data.name}</span>}
    />
  );
}

๐Ÿ“‹ ์‹ค์ œ ์‚ฌ์šฉ ์‚ฌ๋ก€

ํŒŒ์ผ ํƒ์ƒ‰๊ธฐ

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>
  );
}

์กฐ์ง๋„ / ๊ณ„์ธต ๊ตฌ์กฐ

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 ์ธํ„ฐํŽ˜์ด์Šค

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. ์กฐ๊ฑด๋ถ€ ํด๋ฆญ ์ฒ˜๋ฆฌ

const handleClick = (item) => {
  if (item.data.type === 'folder') {
    // ํด๋” ํด๋ฆญ - ํŠน๋ณ„ํ•œ ๋กœ์ง
    if (item.data.permissions?.canAccess) {
      navigateToFolder(item);
    } else {
      showPermissionError();
    }
  } else {
    // ํŒŒ์ผ ํด๋ฆญ - ํŒŒ์ผ ์—ด๊ธฐ
    openFile(item);
  }
};

2. ์ปค์Šคํ…€ ๊ฒ€์ƒ‰

// ๋‹ค์ค‘ ์กฐ๊ฑด ๊ฒ€์ƒ‰
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. ์„ฑ๋Šฅ ์ตœ์ ํ™”

// ํฐ ๋ฐ์ดํ„ฐ์…‹์„ ์œ„ํ•œ ๋ฉ”๋ชจ์ด์ œ์ด์…˜
const MemoizedTree = React.memo(() => (
  <Tree
    items={largeDataSet}
    renderLabel={React.useCallback(
      (data) => (
        <span>{data.name}</span>
      ),
      [],
    )}
  />
));

๐ŸŽจ ์Šคํƒ€์ผ๋ง

CSS ํด๋ž˜์Šค ์‚ฌ์šฉ

<Tree
  className="my-custom-tree"
  items={data}
  renderLabel={(data) => <span className={`item-${data.type}`}>{data.name}</span>}
/>
.my-custom-tree {
  border: 1px solid #ddd;
  border-radius: 8px;
  padding: 16px;
}

.item-folder {
  font-weight: bold;
  color: #4a90e2;
}

.item-file {
  color: #666;
}

๐Ÿ› ๏ธ ๊ฐœ๋ฐœ

๋กœ์ปฌ ๊ฐœ๋ฐœ ํ™˜๊ฒฝ

# ์ €์žฅ์†Œ ํด๋ก 
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์—์„œ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

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 ๋ผ์ด์„ผ์Šค ํ•˜์— ๋ฐฐํฌ๋ฉ๋‹ˆ๋‹ค.

๐Ÿ™‹โ€โ™‚๏ธ ์ง€์›

About

React Tree Component Library where both folders and files are clickable

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages