Examples
Custom Style
Preview
Source
tsx
import { useHeTree, sortFlatData } from "he-tree-react"; import { useState } from 'react'; export default function BasePage() { const keys = { idKey: 'id', parentIdKey: 'parent_id' }; const [data, setdata] = useState(() => sortFlatData([ { id: 1, parent_id: null, name: "Root Category", }, { id: 2, parent_id: 1, name: "Technology", }, { id: 5, parent_id: 2, name: "Hardware", }, { id: 10, parent_id: 5, name: "Computer Components", }, { id: 4, parent_id: 2, name: "Programming", }, { id: 8, parent_id: 4, name: "Python", }, { id: 3, parent_id: 1, name: "Science", }, { id: 7, parent_id: 3, name: "Biology", }, { id: 6, parent_id: 3, name: "Physics", }, ], keys)); const { renderTree, placeholder } = useHeTree({ ...keys, data, dataType: 'flat', onChange: setdata, renderNodeBox: ({ stat, attrs, isPlaceholder }) => ( <div {...attrs} key={attrs.key} className="my-node-box"> {isPlaceholder ? <div className="my-placeholder">DROP HERE</div> : <div className="my-node"> <span className="drag-handler" draggable={stat.draggable}>{dragIcon()}</span> {stat.node.name} </div> } </div> ), }) return <> <h3 style={{ margin: '0 0 0 110px', padding: '20px 0 0px' }}>Draggable Tree</h3> <div> {renderTree({ className: `my-tree ${placeholder ? 'dragging' : 'no-dragging'}` })} </div> <style>{` .my-tree{ width: 300px; border: 1px solid #ccc; border-radius: 5px; margin: 20px; padding: 20px; } .my-placeholder{ height:40px; border: 1px dashed blue; border-radius: 3px; background-color: #f3ffff; display: flex; align-items: center; justify-content: center; font-size: small; } /*.no-dragging .my-node-box:hover{ background-color: #eee; }*/ .my-node-box:not(:last-child){ margin-bottom: 10px; } .my-node{ padding: 5px 10px; padding-left: 30px; border: 1px solid #e2e2e2; border-radius: 3px; background-color: #f0f0f0; display: flex; align-items: center; position: relative; box-shadow: 1px 1px 3px 0px rgb(0 0 0 / 19%); } .no-dragging .my-node:hover{ background-color: #ebfeff; } .drag-handler{ position: absolute; left: 0; top: 0; width: 30px; height: 100%; display: flex; align-items: center; justify-content: center; cursor: grab; } .drag-handler:hover{ background-color: #f0f0f0; } .my-node svg{ width:16px; } `}</style> </> } function dragIcon() { return <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>drag-horizontal-variant</title><path d="M21 11H3V9H21V11M21 13H3V15H21V13Z" /></svg> }
Flat Data
Preview
Source
tsx
import { useHeTree, sortFlatData } from "he-tree-react"; import { useState } from 'react'; export default function BasePage() { const keys = { idKey: 'id', parentIdKey: 'parent_id' }; const [data, setdata] = useState(() => sortFlatData([ { id: 1, parent_id: null, name: "Root Category", }, { id: 2, parent_id: 1, name: "Technology", }, { id: 5, parent_id: 2, name: "Hardware", }, { id: 10, parent_id: 5, name: "Computer Components", }, { id: 4, parent_id: 2, name: "Programming", }, { id: 8, parent_id: 4, name: "Python", }, { id: 3, parent_id: 1, name: "Science", }, { id: 7, parent_id: 3, name: "Biology", }, { id: 6, parent_id: 3, name: "Physics", }, ], keys)); const { renderTree } = useHeTree({ ...keys, data, dataType: 'flat', onChange: setdata, renderNode: ({ id, node, open, checked }) => <div> {node.name} </div>, }) return <div> {renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })} </div> }
Tree-shaped Data
Preview
Source
tsx
import { useHeTree } from "he-tree-react"; import { useState } from 'react'; export default function BasePage() { const [data, setdata] = useState(() => [ { id: 1, name: "Root Category", children: [ { id: 2, name: "Technology", children: [ { id: 5, name: "Hardware", children: [ { id: 10, name: "Computer Components", children: [], }, ], }, { id: 4, name: "Programming", children: [ { id: 8, name: "Python", children: [], }, ], }, ], }, { id: 3, name: "Science", children: [ { id: 7, name: "Biology", children: [], }, { id: 6, name: "Physics", children: [], }, ], }, ], }, ]); const { renderTree } = useHeTree({ data, dataType: 'tree', childrenKey: 'children', onChange: setdata, renderNode: ({ id, node, open, checked }) => <div> {node.name} </div>, }) return <div> {renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })} </div> }
Trigger Element
Preview
Source
tsx
import { useHeTree, sortFlatData } from "he-tree-react"; import { useState } from 'react'; export default function BasePage() { const keys = { idKey: 'id', parentIdKey: 'parent_id' }; // prettier-ignore const [data, setdata] = useState(() => sortFlatData([{ id: 1, parent_id: null, name: "Root Category", }, { id: 2, parent_id: 1, name: "Technology", }, { id: 5, parent_id: 2, name: "Hardware", }, { id: 10, parent_id: 5, name: "Computer Components", }, { id: 4, parent_id: 2, name: "Programming", }, { id: 8, parent_id: 4, name: "Python", }, { id: 3, parent_id: 1, name: "Science", }, { id: 7, parent_id: 3, name: "Biology", }, { id: 6, parent_id: 3, name: "Physics", },], keys)); const { renderTree } = useHeTree({ ...keys, data, dataType: 'flat', onChange: setdata, renderNode: ({ id, node, open, checked, draggable }) => <div> <button draggable={draggable}>Drag</button> {node.name} </div>, }) return <div> {renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })} </div> }
Placeholder
Preview
Source
tsx
import { useHeTree, sortFlatData } from "he-tree-react"; import { useState } from 'react'; export default function BasePage() { const keys = { idKey: 'id', parentIdKey: 'parent_id' }; // prettier-ignore const [data, setdata] = useState(() => sortFlatData([{ id: 1, parent_id: null, name: "Root Category", }, { id: 2, parent_id: 1, name: "Technology", }, { id: 5, parent_id: 2, name: "Hardware", }, { id: 10, parent_id: 5, name: "Computer Components", }, { id: 4, parent_id: 2, name: "Programming", }, { id: 8, parent_id: 4, name: "Python", }, { id: 3, parent_id: 1, name: "Science", }, { id: 7, parent_id: 3, name: "Biology", }, { id: 6, parent_id: 3, name: "Physics", },], keys)); const { renderTree } = useHeTree({ ...keys, data, dataType: 'flat', onChange: setdata, renderNodeBox: ({ stat, attrs, isPlaceholder }) => ( <div {...attrs} key={attrs.key}> {isPlaceholder ? <div className="my-drag-placeholder">drop here</div> : <div className="mynode">{stat.node.name}</div> } </div> ), }) return <div> {renderTree({ className: 'mytree', style: { width: '300px', border: '1px solid #555', padding: '20px' } })} <style>{` .mytree [data-node-box]{ padding: 5px 0; } .mytree [data-node-box]:hover{ background-color: #eee; } .mytree .he-tree-drag-placeholder{ height: 30px; line-height: 30px; text-align: center; border: 1px dashed red; } .mynode{ padding-left:5px; } `}</style> </div> }
Open
Preview
Source
tsx
import { useHeTree, sortFlatData, openParentsInFlatData } from "he-tree-react"; import type { Id } from "he-tree-react"; import { useState } from 'react'; export default function BasePage() { const keys = { idKey: 'id', parentIdKey: 'parent_id' }; // prettier-ignore const [data, setdata] = useState(() => sortFlatData([{ id: 1, parent_id: null, name: "Root Category", }, { id: 2, parent_id: 1, name: "Technology", }, { id: 5, parent_id: 2, name: "Hardware", }, { id: 10, parent_id: 5, name: "Computer Components", }, { id: 4, parent_id: 2, name: "Programming", }, { id: 8, parent_id: 4, name: "Python", }, { id: 3, parent_id: 1, name: "Science", }, { id: 7, parent_id: 3, name: "Biology", }, { id: 6, parent_id: 3, name: "Physics", },], keys)); const [openIds, setopenIds] = useState<Id[] | undefined>([]); const handleOpen = (id: Id, open: boolean) => { if (open) { setopenIds([...(openIds || allIds), id]); } else { setopenIds((openIds || allIds).filter((i) => i !== id)); } } const { renderTree, allIds } = useHeTree({ ...keys, data, dataType: 'flat', onChange: setdata, openIds, renderNode: ({ id, node, open, checked, draggable }) => <div> <button onClick={() => handleOpen(id, !open)}>{open ? '-' : '+'}</button> {node.name} - {id} </div>, }) return <div> <button onClick={() => setopenIds(allIds)}>Open All</button> <button onClick={() => setopenIds([])}>Close All</button> <button onClick={() => setopenIds(openParentsInFlatData(data, openIds || allIds, 8, keys))}>Open 'Python'</button> <button onClick={() => setopenIds(openParentsInFlatData(data, [], 8, keys))}>Only Open 'Python'</button> {renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })} </div> }
Checked
Preview
Source
tsx
import { useHeTree, sortFlatData, updateCheckedInFlatData } from "he-tree-react"; import type { Id } from "he-tree-react"; import { useState } from 'react'; export default function BasePage() { const keys = { idKey: 'id', parentIdKey: 'parent_id' }; // prettier-ignore const [data, setdata] = useState(() => sortFlatData([{ id: 1, parent_id: null, name: "Root Category", }, { id: 2, parent_id: 1, name: "Technology", }, { id: 5, parent_id: 2, name: "Hardware", }, { id: 10, parent_id: 5, name: "Computer Components", }, { id: 4, parent_id: 2, name: "Programming", }, { id: 8, parent_id: 4, name: "Python", }, { id: 3, parent_id: 1, name: "Science", }, { id: 7, parent_id: 3, name: "Biology", }, { id: 6, parent_id: 3, name: "Physics", },], keys)); const [checkedIds, setcheckedIds] = useState<Id[]>([]); const [semiCheckedIds, setsemiCheckedIds] = useState<Id[]>([]); const handleChecked = (id: Id, checked: boolean) => { const r = updateCheckedInFlatData(data, checkedIds, id, checked, keys); setcheckedIds(r[0]); setsemiCheckedIds(r[1]); } const { renderTree } = useHeTree({ ...keys, data, dataType: 'flat', onChange: setdata, checkedIds, renderNode: ({ id, node, open, checked, draggable }) => <div> <input type="checkbox" checked={checked || false} onChange={() => handleChecked(id, !checked)} /> {node.name} - {id} </div>, }) return <div> Checked: {JSON.stringify(checkedIds)} <br /> Semi-Checked: {JSON.stringify(semiCheckedIds)} {renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })} </div> }
Draggable & Droppable
Preview
Source
tsx
import { useHeTree, sortFlatData } from "he-tree-react"; import { useState } from 'react'; export default function BasePage() { const keys = { idKey: 'id', parentIdKey: 'parent_id' }; // prettier-ignore const [data, setdata] = useState(() => sortFlatData([{ id: 2, parent_id: 1, name: "Technology", }, { id: 5, parent_id: 2, name: "Hardware", }, { id: 10, parent_id: 5, name: "Computer Components", }, { id: 4, parent_id: 2, name: "Programming", }, { id: 8, parent_id: 4, name: "Python", }, { id: 3, parent_id: 1, name: "Science", }, { id: 7, parent_id: 3, name: "Biology", }, { id: 6, parent_id: 3, name: "Physics", },], keys)); const { renderTree } = useHeTree({ ...keys, data, dataType: 'flat', onChange: setdata, renderNode: ({ id, node, open, checked, draggable }) => <div> {node.name} - {id} </div>, canDrag: ({ id }) => id === 2 ? true : (id === 3 ? false : undefined), canDrop: ({ id }) => id === 3 ? true : (id === 2 ? false : undefined), canDropToRoot: (index) => false, }) return <div> {renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })} </div> }
Open when drag onto
Preview
Source
tsx
import { useHeTree, sortFlatData } from "he-tree-react"; import type { Id } from "he-tree-react"; import { useState } from 'react'; export default function BasePage() { const keys = { idKey: 'id', parentIdKey: 'parent_id' }; // prettier-ignore const [data, setdata] = useState(() => sortFlatData([{ id: 1, parent_id: null, name: "Root Category", }, { id: 2, parent_id: 1, name: "Technology", }, { id: 5, parent_id: 2, name: "Hardware", }, { id: 10, parent_id: 5, name: "Computer Components", }, { id: 4, parent_id: 2, name: "Programming", }, { id: 8, parent_id: 4, name: "Python", }, { id: 3, parent_id: 1, name: "Science", }, { id: 7, parent_id: 3, name: "Biology", }, { id: 6, parent_id: 3, name: "Physics", },], keys)); const [openIds, setopenIds] = useState<Id[] | undefined>([1, 3]); const handleOpen = (id: Id, open: boolean) => { if (open) { setopenIds([...(openIds || allIds), id]); } else { setopenIds((openIds || allIds).filter((i) => i !== id)); } } const { renderTree, allIds } = useHeTree({ ...keys, data, dataType: 'flat', onChange: setdata, openIds, renderNode: ({ id, node, open, checked, draggable }) => <div> <button onClick={() => handleOpen(id, !open)}>{open ? '-' : '+'}</button> {node.name} - {id} </div>, dragOpen: true, onDragOpen(stat) { handleOpen(stat.id, true) }, }) return <div> {renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })} </div> }
Update Flat Data
Preview
Source
tsx
import { useHeTree, sortFlatData, addToFlatData, removeByIdInFlatData } from "he-tree-react"; import type { Id } from "he-tree-react"; import { useRef, useState } from 'react'; export default function BasePage() { const keys = { idKey: 'id', parentIdKey: 'parent_id' }; // prettier-ignore const [data, setdata] = useState(() => sortFlatData([{ id: 1, parent_id: null, name: "Root Category", }, { id: 2, parent_id: 1, name: "Technology", }, { id: 5, parent_id: 2, name: "Hardware", }, { id: 10, parent_id: 5, name: "Computer Components", }, { id: 4, parent_id: 2, name: "Programming", }, { id: 8, parent_id: 4, name: "Python", }, { id: 3, parent_id: 1, name: "Science", }, { id: 7, parent_id: 3, name: "Biology", }, { id: 6, parent_id: 3, name: "Physics", },], keys)); const add = (pid: Id) => { let id = parseInt(Math.random().toString().substring(2, 5)); let newData = [...data]; addToFlatData(newData, { id, parent_id: pid as number, name: "New" }, 0, keys) setdata(newData); } const remove = (id: Id) => { let newData = [...data]; removeByIdInFlatData(newData, id as number, keys) setdata(newData); } const initialData = useRef<typeof data>(); initialData.current = initialData.current || data; const { renderTree } = useHeTree({ ...keys, data, dataType: 'flat', onChange: setdata, renderNode: ({ id, node, draggable }) => <div> <button draggable={draggable}>👉</button> {node.name} - {id} - <button onClick={() => add(id)}>+</button> <button onClick={() => remove(id)}>-</button> </div>, }) return <div> <button onClick={() => setdata(initialData.current!)}>Restore</button> {renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })} </div> }
Update Flat Data with immer
Preview
Source
tsx
import { useHeTree, sortFlatData, addToFlatData, removeByIdInFlatData } from "he-tree-react"; import type { Id } from "he-tree-react"; import { useRef } from 'react'; import { useImmer } from "use-immer"; export default function BasePage() { const keys = { idKey: 'id', parentIdKey: 'parent_id' }; // prettier-ignore const [data, setdata] = useImmer(() => sortFlatData([{ id: 1, parent_id: null, name: "Root Category", }, { id: 2, parent_id: 1, name: "Technology", }, { id: 5, parent_id: 2, name: "Hardware", }, { id: 10, parent_id: 5, name: "Computer Components", }, { id: 4, parent_id: 2, name: "Programming", }, { id: 8, parent_id: 4, name: "Python", }, { id: 3, parent_id: 1, name: "Science", }, { id: 7, parent_id: 3, name: "Biology", }, { id: 6, parent_id: 3, name: "Physics", },], keys)); const add = (pid: Id) => { let id = parseInt(Math.random().toString().substring(2, 5)); setdata(draft => { addToFlatData(draft, { id, parent_id: pid as number, name: "New" }, 0, keys) }); } const remove = (id: Id) => { setdata(draft => { removeByIdInFlatData(draft, id as number, keys) }) } const edit = (id: Id) => { let newName = prompt("Enter new name") setdata(draft => { if (newName) { draft.find(node => node.id === id)!.name = newName } }) } const initialData = useRef<typeof data>(); initialData.current = initialData.current || data; const { renderTree } = useHeTree({ ...keys, data, dataType: 'flat', onChange: setdata, renderNode: ({ id, node, draggable }) => <div> <button draggable={draggable}>👉</button> {node.name} - {id} - <button onClick={() => add(id)}>+</button> <button onClick={() => remove(id)}>-</button> <button onClick={() => edit(id)}>Edit</button> </div>, }) return <div> <button onClick={() => setdata(initialData.current!)}>Restore</button> {renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })} </div> }
Update Tree Data with immer
Preview
Source
tsx
import { useHeTree, findTreeData } from "he-tree-react"; import type { Id } from "he-tree-react"; import { useRef } from 'react'; import { useImmer } from "use-immer"; export default function BasePage() { const CHILDREN = 'children' const keys = { idKey: 'id', childrenKey: CHILDREN }; // prettier-ignore const [data, setdata] = useImmer(() => [{ id: 1, name: "Root Category", children: [{ id: 2, name: "Technology", children: [{ id: 5, name: "Hardware", children: [{ id: 10, name: "Computer Components", children: [], },], }, { id: 4, name: "Programming", children: [{ id: 8, name: "Python", children: [], },], },], }, { id: 3, name: "Science", children: [{ id: 7, name: "Biology", children: [], }, { id: 6, name: "Physics", children: [], },], },], },]); const add = (pid: Id) => { let id = parseInt(Math.random().toString().substring(2, 5)); setdata(draft => { findTreeData(draft, (node) => node.id === pid, CHILDREN)![CHILDREN].unshift({ id, name: "New", [CHILDREN]: [], }) }) } const remove = (id: Id, pid: Id | null) => { setdata(draft => { const children = findTreeData(draft, (node,) => node.id === pid, CHILDREN)![CHILDREN] children.splice(children.findIndex(t => t.id === id), 1) }) } const edit = (id: Id) => { let newName = prompt("Enter new name") setdata(draft => { if (newName) { findTreeData(draft, (node) => node.id === id, CHILDREN)!.name = newName } }) } const initialData = useRef<typeof data>(); initialData.current = initialData.current || data; const { renderTree } = useHeTree({ ...keys, data, dataType: 'tree', onChange: setdata, renderNode: ({ id, pid, node, draggable }) => <div> <button draggable={draggable}>👉</button> {node.name} - {id} - <button onClick={() => add(id)}>+</button> <button onClick={() => remove(id, pid)}>-</button> <button onClick={() => edit(id)}>Edit</button> </div>, }) return <div> <button onClick={() => setdata(initialData.current!)}>Restore</button> {renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })} </div> }
Drag from External
Preview
Source
tsx
import { useHeTree, sortFlatData, addToFlatData } from "he-tree-react"; import { useImmer } from "use-immer"; export default function BasePage() { const keys = { idKey: 'id', parentIdKey: 'parent_id' }; // prettier-ignore const [data, setdata] = useImmer(() => sortFlatData([{ id: 1, parent_id: null, name: "Root Category", }, { id: 2, parent_id: 1, name: "Technology", }, { id: 5, parent_id: 2, name: "Hardware", }, { id: 10, parent_id: 5, name: "Computer Components", }, { id: 4, parent_id: 2, name: "Programming", }, { id: 8, parent_id: 4, name: "Python", }, { id: 3, parent_id: 1, name: "Science", }, { id: 7, parent_id: 3, name: "Biology", }, { id: 6, parent_id: 3, name: "Physics", },], keys)); const { renderTree, allIds } = useHeTree({ ...keys, data, dataType: 'flat', onChange: setdata, renderNode: ({ id, node, open, checked, draggable }) => <div> {node.name} - {id} </div>, onExternalDragOver: (e) => true, onExternalDrop: (e, parentStat, index) => { setdata(draft => { const newNode = { id: 100 + data.length, parent_id: parentStat?.id ?? null, name: "New Node" } addToFlatData(draft, newNode, index, keys) }) }, }) return <div> <button draggable={true}>Drag me in to the tree</button> {renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })} </div> }
Big Data
Preview
Source
tsx
import { useHeTree, sortFlatData } from "he-tree-react"; import type { Id } from "he-tree-react"; import { useState } from 'react'; export default function BasePage() { const keys = { idKey: 'id', parentIdKey: 'pid' }; // prettier-ignore const [data, setdata] = useState(() => sortFlatData(createData(), keys)); const [openIds, setopenIds] = useState<Id[] | undefined>([]); const handleOpen = (id: Id, open: boolean) => { if (open) { setopenIds([...(openIds || allIds), id]); } else { setopenIds((openIds || allIds).filter((i) => i !== id)); } } const { renderTree, allIds } = useHeTree({ ...keys, data, dataType: 'flat', onChange: setdata, openIds, virtual: true, renderNode: ({ id, node, open, checked, draggable }) => <div> <button onClick={() => handleOpen(id, !open)}>{open ? '-' : '+'}</button> {id} </div>, }) return <div> {renderTree({ style: { width: '300px', height: '300px', border: '1px solid #555', padding: '20px' } })} </div> } // generate 10000 nodes function createData() { const genId = () => result.length const result: { id: number, pid: number | null }[] = []; for (let i = 0; i < 1000; i++) { let id1 = genId() result.push({ id: id1, pid: null }) for (let j = 0; j < 4; j++) { result.push({ id: genId(), pid: id1 }) } let id2 = genId() result.push({ id: id2, pid: null }) for (let j = 0; j < 4; j++) { result.push({ id: genId(), pid: id2 }) } } return result; }
Scroll to Node
Preview
Source
tsx
import { useHeTree, sortFlatData } from "he-tree-react"; import type { Id } from "he-tree-react"; import { useState } from 'react'; export default function BasePage() { const keys = { idKey: 'id', parentIdKey: 'pid' }; // prettier-ignore const [data, setdata] = useState(() => sortFlatData(createData(), keys)); const [openIds, setopenIds] = useState<Id[] | undefined>([]); const handleOpen = (id: Id, open: boolean) => { if (open) { setopenIds([...(openIds || allIds), id]); } else { setopenIds((openIds || allIds).filter((i) => i !== id)); } } const { renderTree, allIds, scrollToNode } = useHeTree({ ...keys, data, dataType: 'flat', onChange: setdata, openIds, virtual: true, renderNode: ({ id, node, open, checked, draggable }) => <div> <button onClick={() => handleOpen(id, !open)}>{open ? '-' : '+'}</button> {id} </div>, }) return <div> <button onClick={() => scrollToNode(910)}>Scroll to 910</button> {renderTree({ style: { width: '300px', height: '300px', border: '1px solid #555', padding: '20px' } })} </div> } // generate 10000 nodes function createData() { const genId = () => result.length const result: { id: number, pid: number | null }[] = []; for (let i = 0; i < 1000; i++) { let id1 = genId() result.push({ id: id1, pid: null }) for (let j = 0; j < 4; j++) { result.push({ id: genId(), pid: id1 }) } let id2 = genId() result.push({ id: id2, pid: null }) for (let j = 0; j < 4; j++) { result.push({ id: genId(), pid: id2 }) } } return result; }