TreeTable
TreeTable renders hierarchical data with the same feature set as DataTable, sort, filter, pagination, selection, scroll, frozen, resize, reorder, editing, export.
| Name | Size | Type |
|---|---|---|
Documents | 75kb | Folder |
Work | 55kb | Folder |
Home | 20kb | Folder |
Pictures | 150kb | Folder |
Movies | 1500kb | Folder |
Usage#
import { DataTable } from '@primereact/ui/datatable';TreeTable is DataTable with the treeMode prop set on DataTable.Root. Every feature shown in DataTable continues to work over the flattened tree, the only structural difference is that the first column wraps its cell content with DataTable.RowToggle to render the expand/collapse chevron.
<DataTable.Root data={treeNodes} dataKey="key" treeMode expandedKeys={expandedKeys} onExpandedChange={(e) => setExpandedKeys(e.value)}>
<DataTable.TableContainer>
<DataTable.Table>
<DataTable.THead>
<DataTable.THeadRow>
<DataTable.THeadCell>Name</DataTable.THeadCell>
</DataTable.THeadRow>
</DataTable.THead>
<DataTable.TBody>
{({ item }) => (
<DataTable.Row>
<DataTable.Cell>
<DataTable.RowToggle />
{item.name}
</DataTable.Cell>
</DataTable.Row>
)}
</DataTable.TBody>
</DataTable.Table>
</DataTable.TableContainer>
</DataTable.Root>Data shape#
Each node carries its own key, arbitrary data, and an optional children array:
type TreeNode = {
key: string;
data: Record<string, unknown>;
children?: TreeNode[];
};Set dataKey="key" and resolve column values against item.<field>, the component flattens data onto the row so columns can read directly.
Rows are augmented with derived fields the component consumes internally: _treeLevel, _treeHasChildren, _treePosInSet, _treeSetSize. These drive the ARIA attributes (aria-level, aria-expanded, aria-posinset, aria-setsize) automatically.
Examples#
Basic#
Displays hierarchical nodes with expand/collapse chevrons on the first column.
'use client';
import { ChevronDown } from '@primeicons/react/chevron-down';
import { ChevronRight } from '@primeicons/react/chevron-right';
import type { DataTableExpansionEvent } from '@primereact/ui/datatable';
import { DataTable } from '@primereact/ui/datatable';
import * as React from 'react';
interface TreeNode {
key: string;
data: { name: string; size: string; type: string };
children?: TreeNode[];
}
const nodes: TreeNode[] = [
{
key: '0',
data: { name: 'Documents', size: '75kb', type: 'Folder' },
children: [
{
key: '0-0',
data: { name: 'Work', size: '55kb', type: 'Folder' },
children: [{ key: '0-0-0', data: { name: 'Expenses.doc', size: '30kb', type: 'Document' } }]
},
{
key: '0-1',
data: { name: 'Home', size: '20kb', type: 'Folder' },
children: [{ key: '0-1-0', data: { name: 'Invoices.txt', size: '20kb', type: 'Text' } }]
}
]
},
{
key: '1',
data: { name: 'Pictures', size: '150kb', type: 'Folder' },
children: [
{ key: '1-0', data: { name: 'barcelona.jpg', size: '90kb', type: 'Picture' } },
{ key: '1-1', data: { name: 'primeui.png', size: '30kb', type: 'Picture' } }
]
}
];
export default function BasicDemo() {
const [expandedKeys, setExpandedKeys] = React.useState<Record<string, boolean>>({ '0': true });
return (
<div className="w-full">
<DataTable.Root
data={nodes}
dataKey="key"
treeMode
expandedKeys={expandedKeys}
onExpandedChange={(e: DataTableExpansionEvent) => setExpandedKeys(e.value)}
>
<DataTable.TableContainer>
<DataTable.Table style={{ minWidth: '30rem', tableLayout: 'fixed' }}>
<DataTable.THead>
<DataTable.THeadRow>
<DataTable.THeadCell style={{ width: '60%' }}>
<DataTable.THeadTitle>Name</DataTable.THeadTitle>
</DataTable.THeadCell>
<DataTable.THeadCell style={{ width: '20%' }}>
<DataTable.THeadTitle>Size</DataTable.THeadTitle>
</DataTable.THeadCell>
<DataTable.THeadCell style={{ width: '20%' }}>
<DataTable.THeadTitle>Type</DataTable.THeadTitle>
</DataTable.THeadCell>
</DataTable.THeadRow>
</DataTable.THead>
<DataTable.TBody>
{({ item }: { item: any }) => (
<DataTable.Row key={String(item.key)}>
<DataTable.Cell>
<div className="flex items-center gap-2">
<DataTable.RowToggle>
<DataTable.RowToggleIndicator match="expanded">
<ChevronDown />
</DataTable.RowToggleIndicator>
<DataTable.RowToggleIndicator match="collapsed">
<ChevronRight />
</DataTable.RowToggleIndicator>
</DataTable.RowToggle>
<span>{String(item.name)}</span>
</div>
</DataTable.Cell>
<DataTable.Cell>{String(item.size)}</DataTable.Cell>
<DataTable.Cell>{String(item.type)}</DataTable.Cell>
</DataTable.Row>
)}
</DataTable.TBody>
</DataTable.Table>
</DataTable.TableContainer>
</DataTable.Root>
</div>
);
}
Size#
Use the size prop with small or large to adjust cell padding.
'use client';
import { ChevronDown } from '@primeicons/react/chevron-down';
import { Database } from '@primeicons/react/database';
import { ChevronRight } from '@primeicons/react/chevron-right';
import { File } from '@primeicons/react/file';
import { Folder } from '@primeicons/react/folder';
import { Image as ImageIcon } from '@primeicons/react/image';
import { Video } from '@primeicons/react/video';
import type { DataTableExpansionEvent } from '@primereact/ui/datatable';
import { DataTable } from '@primereact/ui/datatable';
import { Tag } from '@primereact/ui/tag';
import { ToggleButton } from '@primereact/ui/togglebutton';
import { ToggleButtonGroup } from '@primereact/ui/togglebuttongroup';
import * as React from 'react';
const typeSeverity: Record<string, 'info' | 'warn' | 'success' | 'secondary'> = {
Folder: 'warn',
Document: 'info',
Text: 'secondary',
Picture: 'success',
Video: 'success'
};
function TypeIcon({ type }: { type: string }) {
if (type === 'Picture') return <ImageIcon size={16} />;
if (type === 'Video') return <Video size={16} />;
if (type === 'Folder') return <Folder size={16} />;
return <File size={16} />;
}
interface TreeNode {
key: string;
data: {
name: string;
size: string;
type: string;
};
children?: TreeNode[];
}
const nodes: TreeNode[] = [
{
key: '0',
data: { name: 'Documents', size: '75kb', type: 'Folder' },
children: [
{
key: '0-0',
data: { name: 'Work', size: '55kb', type: 'Folder' },
children: [
{ key: '0-0-0', data: { name: 'Expenses.doc', size: '30kb', type: 'Document' } },
{ key: '0-0-1', data: { name: 'Resume.doc', size: '25kb', type: 'Document' } }
]
},
{
key: '0-1',
data: { name: 'Home', size: '20kb', type: 'Folder' },
children: [{ key: '0-1-0', data: { name: 'Invoices.txt', size: '20kb', type: 'Text' } }]
}
]
},
{
key: '1',
data: { name: 'Pictures', size: '150kb', type: 'Folder' },
children: [
{ key: '1-0', data: { name: 'barcelona.jpg', size: '90kb', type: 'Picture' } },
{ key: '1-1', data: { name: 'primeui.png', size: '30kb', type: 'Picture' } },
{ key: '1-2', data: { name: 'optimus.jpg', size: '30kb', type: 'Picture' } }
]
},
{
key: '2',
data: { name: 'Movies', size: '1500kb', type: 'Folder' },
children: [
{ key: '2-0', data: { name: 'Scarface', size: '500kb', type: 'Video' } },
{ key: '2-1', data: { name: 'Goodfellas', size: '250kb', type: 'Video' } }
]
}
];
type Size = 'small' | 'normal' | 'large';
const sizeOptions: { label: string; value: Size }[] = [
{ label: 'Small', value: 'small' },
{ label: 'Normal', value: 'normal' },
{ label: 'Large', value: 'large' }
];
export default function TreeTableSizeDemo() {
const [expandedKeys, setExpandedKeys] = React.useState<Record<string, boolean>>({ '0': true });
const [size, setSize] = React.useState<Size>('normal');
return (
<div className="w-full">
<div style={{ marginBottom: '0.75rem' }}>
<ToggleButtonGroup
allowEmpty={false}
multiple={false}
value={size}
onValueChange={(e: { value: unknown }) => setSize(e.value as Size)}
>
{sizeOptions.map((opt) => (
<ToggleButton.Root key={opt.value} value={opt.value}>
<ToggleButton.Indicator>{opt.label}</ToggleButton.Indicator>
</ToggleButton.Root>
))}
</ToggleButtonGroup>
</div>
<DataTable.Root
data={nodes}
dataKey="key"
treeMode
expandedKeys={expandedKeys}
onExpandedChange={(e: DataTableExpansionEvent) => setExpandedKeys(e.value)}
size={size === 'normal' ? undefined : size}
>
<DataTable.TableContainer>
<DataTable.Table style={{ minWidth: '50rem' }}>
<DataTable.THead>
<DataTable.THeadRow>
<DataTable.THeadCell style={{ width: '50%' }}>
<DataTable.THeadTitle>Name</DataTable.THeadTitle>
</DataTable.THeadCell>
<DataTable.THeadCell style={{ width: '20%' }}>
<DataTable.THeadTitle>Size</DataTable.THeadTitle>
</DataTable.THeadCell>
<DataTable.THeadCell style={{ width: '30%' }}>
<DataTable.THeadTitle>Type</DataTable.THeadTitle>
</DataTable.THeadCell>
</DataTable.THeadRow>
</DataTable.THead>
<DataTable.TBody>
{({ item }: { item: any }) => (
<DataTable.Row key={String(item.key)}>
<DataTable.Cell>
<div className="flex items-center gap-2">
<DataTable.RowToggle>
<DataTable.RowToggleIndicator match="expanded">
<ChevronDown />
</DataTable.RowToggleIndicator>
<DataTable.RowToggleIndicator match="collapsed">
<ChevronRight />
</DataTable.RowToggleIndicator>
</DataTable.RowToggle>
<TypeIcon type={String(item.type)} />
<span className={item.type === 'Folder' ? 'font-medium' : ''}>{String(item.name)}</span>
</div>
</DataTable.Cell>
<DataTable.Cell>
<span className="text-sm text-surface-500 dark:text-surface-400">{String(item.size)}</span>
</DataTable.Cell>
<DataTable.Cell>
<Tag severity={typeSeverity[String(item.type)] ?? 'secondary'}>{String(item.type)}</Tag>
</DataTable.Cell>
</DataTable.Row>
)}
</DataTable.TBody>
<DataTable.EmptyTBody>
<DataTable.Row>
<DataTable.Cell colSpan={3}>
<div className="flex flex-col items-center justify-center gap-3 py-24 text-center">
<div className="relative">
<div className="w-16 h-16 rounded-full bg-surface-100 dark:bg-surface-800 flex items-center justify-center">
<Database className="w-8 h-8 text-surface-400 dark:text-surface-500" />
</div>
<span className="absolute -bottom-0.5 -right-0.5 w-4 h-4 rounded-full border-2 border-surface-0 dark:border-surface-900 bg-primary animate-pulse" />
</div>
<div>
<p className="m-0 font-semibold text-surface-900 dark:text-surface-0">Loading tree</p>
<p className="mt-1 text-sm text-surface-500 dark:text-surface-400">Hang tight, the data is on its way.</p>
</div>
</div>
</DataTable.Cell>
</DataTable.Row>
</DataTable.EmptyTBody>
</DataTable.Table>
</DataTable.TableContainer>
</DataTable.Root>
</div>
);
}
Gridlines#
Enable the showGridlines prop to render borders between cells.
'use client';
import { ChevronDown } from '@primeicons/react/chevron-down';
import { Database } from '@primeicons/react/database';
import { ChevronRight } from '@primeicons/react/chevron-right';
import { File } from '@primeicons/react/file';
import { Folder } from '@primeicons/react/folder';
import { Image as ImageIcon } from '@primeicons/react/image';
import { Video } from '@primeicons/react/video';
import type { DataTableExpansionEvent } from '@primereact/ui/datatable';
import { DataTable } from '@primereact/ui/datatable';
import { Tag } from '@primereact/ui/tag';
import * as React from 'react';
const typeSeverity: Record<string, 'info' | 'warn' | 'success' | 'secondary'> = {
Folder: 'warn',
Document: 'info',
Text: 'secondary',
Picture: 'success',
Video: 'success'
};
function TypeIcon({ type }: { type: string }) {
if (type === 'Picture') return <ImageIcon size={16} />;
if (type === 'Video') return <Video size={16} />;
if (type === 'Folder') return <Folder size={16} />;
return <File size={16} />;
}
interface TreeNode {
key: string;
data: {
name: string;
size: string;
type: string;
};
children?: TreeNode[];
}
const nodes: TreeNode[] = [
{
key: '0',
data: { name: 'Documents', size: '75kb', type: 'Folder' },
children: [
{
key: '0-0',
data: { name: 'Work', size: '55kb', type: 'Folder' },
children: [
{ key: '0-0-0', data: { name: 'Expenses.doc', size: '30kb', type: 'Document' } },
{ key: '0-0-1', data: { name: 'Resume.doc', size: '25kb', type: 'Document' } }
]
},
{
key: '0-1',
data: { name: 'Home', size: '20kb', type: 'Folder' },
children: [{ key: '0-1-0', data: { name: 'Invoices.txt', size: '20kb', type: 'Text' } }]
}
]
},
{
key: '1',
data: { name: 'Pictures', size: '150kb', type: 'Folder' },
children: [
{ key: '1-0', data: { name: 'barcelona.jpg', size: '90kb', type: 'Picture' } },
{ key: '1-1', data: { name: 'primeui.png', size: '30kb', type: 'Picture' } },
{ key: '1-2', data: { name: 'optimus.jpg', size: '30kb', type: 'Picture' } }
]
},
{
key: '2',
data: { name: 'Movies', size: '1500kb', type: 'Folder' },
children: [
{ key: '2-0', data: { name: 'Scarface', size: '500kb', type: 'Video' } },
{ key: '2-1', data: { name: 'Goodfellas', size: '250kb', type: 'Video' } }
]
}
];
export default function TreeTableGridlinesDemo() {
const [expandedKeys, setExpandedKeys] = React.useState<Record<string, boolean>>({ '0': true });
return (
<div className="w-full">
<DataTable.Root
data={nodes}
dataKey="key"
treeMode
expandedKeys={expandedKeys}
onExpandedChange={(e: DataTableExpansionEvent) => setExpandedKeys(e.value)}
showGridlines
>
<DataTable.TableContainer>
<DataTable.Table style={{ minWidth: '50rem' }}>
<DataTable.THead>
<DataTable.THeadRow>
<DataTable.THeadCell style={{ width: '50%' }}>
<DataTable.THeadTitle>Name</DataTable.THeadTitle>
</DataTable.THeadCell>
<DataTable.THeadCell style={{ width: '20%' }}>
<DataTable.THeadTitle>Size</DataTable.THeadTitle>
</DataTable.THeadCell>
<DataTable.THeadCell style={{ width: '30%' }}>
<DataTable.THeadTitle>Type</DataTable.THeadTitle>
</DataTable.THeadCell>
</DataTable.THeadRow>
</DataTable.THead>
<DataTable.TBody>
{({ item }: { item: any }) => (
<DataTable.Row key={String(item.key)}>
<DataTable.Cell>
<div className="flex items-center gap-2">
<DataTable.RowToggle>
<DataTable.RowToggleIndicator match="expanded">
<ChevronDown />
</DataTable.RowToggleIndicator>
<DataTable.RowToggleIndicator match="collapsed">
<ChevronRight />
</DataTable.RowToggleIndicator>
</DataTable.RowToggle>
<TypeIcon type={String(item.type)} />
<span className={item.type === 'Folder' ? 'font-medium' : ''}>{String(item.name)}</span>
</div>
</DataTable.Cell>
<DataTable.Cell>
<span className="text-sm text-surface-500 dark:text-surface-400">{String(item.size)}</span>
</DataTable.Cell>
<DataTable.Cell>
<Tag severity={typeSeverity[String(item.type)] ?? 'secondary'}>{String(item.type)}</Tag>
</DataTable.Cell>
</DataTable.Row>
)}
</DataTable.TBody>
<DataTable.EmptyTBody>
<DataTable.Row>
<DataTable.Cell colSpan={3}>
<div className="flex flex-col items-center justify-center gap-3 py-24 text-center">
<div className="relative">
<div className="w-16 h-16 rounded-full bg-surface-100 dark:bg-surface-800 flex items-center justify-center">
<Database className="w-8 h-8 text-surface-400 dark:text-surface-500" />
</div>
<span className="absolute -bottom-0.5 -right-0.5 w-4 h-4 rounded-full border-2 border-surface-0 dark:border-surface-900 bg-primary animate-pulse" />
</div>
<div>
<p className="m-0 font-semibold text-surface-900 dark:text-surface-0">Loading tree</p>
<p className="mt-1 text-sm text-surface-500 dark:text-surface-400">Hang tight, the data is on its way.</p>
</div>
</div>
</DataTable.Cell>
</DataTable.Row>
</DataTable.EmptyTBody>
</DataTable.Table>
</DataTable.TableContainer>
</DataTable.Root>
</div>
);
}
Striped Rows#
The stripedRows prop renders rows with alternating background colors across the flattened tree.
'use client';
import { ChevronDown } from '@primeicons/react/chevron-down';
import { Database } from '@primeicons/react/database';
import { ChevronRight } from '@primeicons/react/chevron-right';
import { File } from '@primeicons/react/file';
import { Folder } from '@primeicons/react/folder';
import { Image as ImageIcon } from '@primeicons/react/image';
import { Video } from '@primeicons/react/video';
import type { DataTableExpansionEvent } from '@primereact/ui/datatable';
import { DataTable } from '@primereact/ui/datatable';
import { Tag } from '@primereact/ui/tag';
import * as React from 'react';
const typeSeverity: Record<string, 'info' | 'warn' | 'success' | 'secondary'> = {
Folder: 'warn',
Document: 'info',
Text: 'secondary',
Picture: 'success',
Video: 'success'
};
function TypeIcon({ type }: { type: string }) {
if (type === 'Picture') return <ImageIcon size={16} />;
if (type === 'Video') return <Video size={16} />;
if (type === 'Folder') return <Folder size={16} />;
return <File size={16} />;
}
interface TreeNode {
key: string;
data: {
name: string;
size: string;
type: string;
};
children?: TreeNode[];
}
const nodes: TreeNode[] = [
{
key: '0',
data: { name: 'Documents', size: '75kb', type: 'Folder' },
children: [
{
key: '0-0',
data: { name: 'Work', size: '55kb', type: 'Folder' },
children: [
{ key: '0-0-0', data: { name: 'Expenses.doc', size: '30kb', type: 'Document' } },
{ key: '0-0-1', data: { name: 'Resume.doc', size: '25kb', type: 'Document' } }
]
},
{
key: '0-1',
data: { name: 'Home', size: '20kb', type: 'Folder' },
children: [{ key: '0-1-0', data: { name: 'Invoices.txt', size: '20kb', type: 'Text' } }]
}
]
},
{
key: '1',
data: { name: 'Pictures', size: '150kb', type: 'Folder' },
children: [
{ key: '1-0', data: { name: 'barcelona.jpg', size: '90kb', type: 'Picture' } },
{ key: '1-1', data: { name: 'primeui.png', size: '30kb', type: 'Picture' } },
{ key: '1-2', data: { name: 'optimus.jpg', size: '30kb', type: 'Picture' } }
]
},
{
key: '2',
data: { name: 'Movies', size: '1500kb', type: 'Folder' },
children: [
{ key: '2-0', data: { name: 'Scarface', size: '500kb', type: 'Video' } },
{ key: '2-1', data: { name: 'Goodfellas', size: '250kb', type: 'Video' } }
]
}
];
export default function TreeTableStripedRowsDemo() {
const [expandedKeys, setExpandedKeys] = React.useState<Record<string, boolean>>({ '0': true, '1': true });
return (
<div className="w-full">
<DataTable.Root
data={nodes}
dataKey="key"
treeMode
expandedKeys={expandedKeys}
onExpandedChange={(e: DataTableExpansionEvent) => setExpandedKeys(e.value)}
stripedRows
>
<DataTable.TableContainer>
<DataTable.Table style={{ minWidth: '50rem' }}>
<DataTable.THead>
<DataTable.THeadRow>
<DataTable.THeadCell style={{ width: '50%' }}>
<DataTable.THeadTitle>Name</DataTable.THeadTitle>
</DataTable.THeadCell>
<DataTable.THeadCell style={{ width: '20%' }}>
<DataTable.THeadTitle>Size</DataTable.THeadTitle>
</DataTable.THeadCell>
<DataTable.THeadCell style={{ width: '30%' }}>
<DataTable.THeadTitle>Type</DataTable.THeadTitle>
</DataTable.THeadCell>
</DataTable.THeadRow>
</DataTable.THead>
<DataTable.TBody>
{({ item }: { item: any }) => (
<DataTable.Row key={String(item.key)}>
<DataTable.Cell>
<div className="flex items-center gap-2">
<DataTable.RowToggle>
<DataTable.RowToggleIndicator match="expanded">
<ChevronDown />
</DataTable.RowToggleIndicator>
<DataTable.RowToggleIndicator match="collapsed">
<ChevronRight />
</DataTable.RowToggleIndicator>
</DataTable.RowToggle>
<TypeIcon type={String(item.type)} />
<span className={item.type === 'Folder' ? 'font-medium' : ''}>{String(item.name)}</span>
</div>
</DataTable.Cell>
<DataTable.Cell>
<span className="text-sm text-surface-500 dark:text-surface-400">{String(item.size)}</span>
</DataTable.Cell>
<DataTable.Cell>
<Tag severity={typeSeverity[String(item.type)] ?? 'secondary'}>{String(item.type)}</Tag>
</DataTable.Cell>
</DataTable.Row>
)}
</DataTable.TBody>
<DataTable.EmptyTBody>
<DataTable.Row>
<DataTable.Cell colSpan={3}>
<div className="flex flex-col items-center justify-center gap-3 py-24 text-center">
<div className="relative">
<div className="w-16 h-16 rounded-full bg-surface-100 dark:bg-surface-800 flex items-center justify-center">
<Database className="w-8 h-8 text-surface-400 dark:text-surface-500" />
</div>
<span className="absolute -bottom-0.5 -right-0.5 w-4 h-4 rounded-full border-2 border-surface-0 dark:border-surface-900 bg-primary animate-pulse" />
</div>
<div>
<p className="m-0 font-semibold text-surface-900 dark:text-surface-0">Loading tree</p>
<p className="mt-1 text-sm text-surface-500 dark:text-surface-400">Hang tight, the data is on its way.</p>
</div>
</div>
</DataTable.Cell>
</DataTable.Row>
</DataTable.EmptyTBody>
</DataTable.Table>
</DataTable.TableContainer>
</DataTable.Root>
</div>
);
}
Selection#
Selection works exactly like DataTable, selectionMode controls the mode, and the DataTable.Selection render prop exposes helpers for row-level and header-level toggles. Checkbox selection propagates across the hierarchy with tri-state semantics.
Single#
'use client';
import { ChevronDown } from '@primeicons/react/chevron-down';
import { Database } from '@primeicons/react/database';
import { ChevronRight } from '@primeicons/react/chevron-right';
import { NodeService, type TreeTableNode } from '@/shared/services/node.service';
import type { DataTableExpansionEvent, DataTableSelectionEvent } from '@primereact/ui/datatable';
import { DataTable } from '@primereact/ui/datatable';
import { Tag } from '@primereact/ui/tag';
import * as React from 'react';
import { File } from '@primeicons/react/file';
import { Folder } from '@primeicons/react/folder';
import { Image as ImageIcon } from '@primeicons/react/image';
import { Video } from '@primeicons/react/video';
const typeSeverity: Record<string, 'info' | 'warn' | 'success' | 'secondary'> = {
Folder: 'warn',
Document: 'info',
Resume: 'info',
Text: 'secondary',
Application: 'info',
Picture: 'success',
Video: 'success',
Zip: 'secondary',
PDF: 'info',
Link: 'secondary'
};
function TypeIcon({ type }: { type: string }) {
if (type === 'Picture') return <ImageIcon size={16} />;
if (type === 'Video') return <Video size={16} />;
if (type === 'Folder') return <Folder size={16} />;
return <File size={16} />;
}
export default function SingleSelectionDemo() {
const [nodes, setNodes] = React.useState<TreeTableNode[]>([]);
const [selectionKeys, setSelectionKeys] = React.useState<Record<string, boolean>>({});
const [expandedKeys, setExpandedKeys] = React.useState<Record<string, boolean>>({ '0': true });
React.useEffect(() => {
setNodes(NodeService.getTreeTableNodesData().slice(0, 5));
}, []);
return (
<div className="w-full">
<DataTable.Root
data={nodes}
dataKey="key"
treeMode
expandedKeys={expandedKeys}
onExpandedChange={(e: DataTableExpansionEvent) => setExpandedKeys(e.value)}
selectionMode="single"
selectionKeys={selectionKeys}
onSelectionChange={(e: DataTableSelectionEvent) => setSelectionKeys(e.value)}
>
<DataTable.TableContainer>
<DataTable.Table style={{ minWidth: '40rem' }}>
<DataTable.THead>
<DataTable.THeadRow>
<DataTable.THeadCell style={{ width: '50%' }}>
<DataTable.THeadTitle>Name</DataTable.THeadTitle>
</DataTable.THeadCell>
<DataTable.THeadCell style={{ width: '20%' }}>
<DataTable.THeadTitle>Size</DataTable.THeadTitle>
</DataTable.THeadCell>
<DataTable.THeadCell style={{ width: '30%' }}>
<DataTable.THeadTitle>Type</DataTable.THeadTitle>
</DataTable.THeadCell>
</DataTable.THeadRow>
</DataTable.THead>
<DataTable.TBody>
{({ item }: { item: any }) => (
<DataTable.Row key={String(item.key)}>
<DataTable.Cell>
<div className="flex items-center gap-2">
<DataTable.RowToggle>
<DataTable.RowToggleIndicator match="expanded">
<ChevronDown />
</DataTable.RowToggleIndicator>
<DataTable.RowToggleIndicator match="collapsed">
<ChevronRight />
</DataTable.RowToggleIndicator>
</DataTable.RowToggle>
<TypeIcon type={String(item.type)} />
<span className={item.type === 'Folder' ? 'font-medium' : ''}>{String(item.name)}</span>
</div>
</DataTable.Cell>
<DataTable.Cell>
<span className="text-sm text-surface-500 dark:text-surface-400">{String(item.size)}</span>
</DataTable.Cell>
<DataTable.Cell>
<Tag severity={typeSeverity[String(item.type)] ?? 'secondary'}>{String(item.type)}</Tag>
</DataTable.Cell>
</DataTable.Row>
)}
</DataTable.TBody>
<DataTable.EmptyTBody>
<DataTable.Row>
<DataTable.Cell colSpan={3}>
<div className="flex flex-col items-center justify-center gap-3 py-24 text-center">
<div className="relative">
<div className="w-16 h-16 rounded-full bg-surface-100 dark:bg-surface-800 flex items-center justify-center">
<Database className="w-8 h-8 text-surface-400 dark:text-surface-500" />
</div>
<span className="absolute -bottom-0.5 -right-0.5 w-4 h-4 rounded-full border-2 border-surface-0 dark:border-surface-900 bg-primary animate-pulse" />
</div>
<div>
<p className="m-0 font-semibold text-surface-900 dark:text-surface-0">Loading tree</p>
<p className="mt-1 text-sm text-surface-500 dark:text-surface-400">Hang tight, the data is on its way.</p>
</div>
</div>
</DataTable.Cell>
</DataTable.Row>
</DataTable.EmptyTBody>
</DataTable.Table>
</DataTable.TableContainer>
</DataTable.Root>
</div>
);
}
Multiple#
Pair selectionMode="multiple" with metaKeySelection so Ctrl/Cmd + Click toggles rows and Shift + Click selects a range.
'use client';
import { ChevronDown } from '@primeicons/react/chevron-down';
import { Database } from '@primeicons/react/database';
import { ChevronRight } from '@primeicons/react/chevron-right';
import { NodeService, type TreeTableNode } from '@/shared/services/node.service';
import type { DataTableExpansionEvent, DataTableSelectionEvent } from '@primereact/ui/datatable';
import { DataTable } from '@primereact/ui/datatable';
import { Tag } from '@primereact/ui/tag';
import * as React from 'react';
import { File } from '@primeicons/react/file';
import { Folder } from '@primeicons/react/folder';
import { Image as ImageIcon } from '@primeicons/react/image';
import { Video } from '@primeicons/react/video';
const typeSeverity: Record<string, 'info' | 'warn' | 'success' | 'secondary'> = {
Folder: 'warn',
Document: 'info',
Resume: 'info',
Text: 'secondary',
Application: 'info',
Picture: 'success',
Video: 'success',
Zip: 'secondary',
PDF: 'info',
Link: 'secondary'
};
function TypeIcon({ type }: { type: string }) {
if (type === 'Picture') return <ImageIcon size={16} />;
if (type === 'Video') return <Video size={16} />;
if (type === 'Folder') return <Folder size={16} />;
return <File size={16} />;
}
export default function MultipleSelectionDemo() {
const [nodes, setNodes] = React.useState<TreeTableNode[]>([]);
const [selectionKeys, setSelectionKeys] = React.useState<Record<string, boolean>>({});
const [expandedKeys, setExpandedKeys] = React.useState<Record<string, boolean>>({ '0': true });
React.useEffect(() => {
setNodes(NodeService.getTreeTableNodesData().slice(0, 5));
}, []);
return (
<div className="w-full">
<DataTable.Root
data={nodes}
dataKey="key"
treeMode
expandedKeys={expandedKeys}
onExpandedChange={(e: DataTableExpansionEvent) => setExpandedKeys(e.value)}
selectionMode="multiple"
metaKeySelection
selectionKeys={selectionKeys}
onSelectionChange={(e: DataTableSelectionEvent) => setSelectionKeys(e.value)}
>
<DataTable.TableContainer>
<DataTable.Table style={{ minWidth: '40rem' }}>
<DataTable.THead>
<DataTable.THeadRow>
<DataTable.THeadCell style={{ width: '50%' }}>
<DataTable.THeadTitle>Name</DataTable.THeadTitle>
</DataTable.THeadCell>
<DataTable.THeadCell style={{ width: '20%' }}>
<DataTable.THeadTitle>Size</DataTable.THeadTitle>
</DataTable.THeadCell>
<DataTable.THeadCell style={{ width: '30%' }}>
<DataTable.THeadTitle>Type</DataTable.THeadTitle>
</DataTable.THeadCell>
</DataTable.THeadRow>
</DataTable.THead>
<DataTable.TBody>
{({ item }: { item: any }) => (
<DataTable.Row key={String(item.key)}>
<DataTable.Cell>
<div className="flex items-center gap-2">
<DataTable.RowToggle>
<DataTable.RowToggleIndicator match="expanded">
<ChevronDown />
</DataTable.RowToggleIndicator>
<DataTable.RowToggleIndicator match="collapsed">
<ChevronRight />
</DataTable.RowToggleIndicator>
</DataTable.RowToggle>
<TypeIcon type={String(item.type)} />
<span className={item.type === 'Folder' ? 'font-medium' : ''}>{String(item.name)}</span>
</div>
</DataTable.Cell>
<DataTable.Cell>
<span className="text-sm text-surface-500 dark:text-surface-400">{String(item.size)}</span>
</DataTable.Cell>
<DataTable.Cell>
<Tag severity={typeSeverity[String(item.type)] ?? 'secondary'}>{String(item.type)}</Tag>
</DataTable.Cell>
</DataTable.Row>
)}
</DataTable.TBody>
<DataTable.EmptyTBody>
<DataTable.Row>
<DataTable.Cell colSpan={3}>
<div className="flex flex-col items-center justify-center gap-3 py-24 text-center">
<div className="relative">
<div className="w-16 h-16 rounded-full bg-surface-100 dark:bg-surface-800 flex items-center justify-center">
<Database className="w-8 h-8 text-surface-400 dark:text-surface-500" />
</div>
<span className="absolute -bottom-0.5 -right-0.5 w-4 h-4 rounded-full border-2 border-surface-0 dark:border-surface-900 bg-primary animate-pulse" />
</div>
<div>
<p className="m-0 font-semibold text-surface-900 dark:text-surface-0">Loading tree</p>
<p className="mt-1 text-sm text-surface-500 dark:text-surface-400">Hang tight, the data is on its way.</p>
</div>
</div>
</DataTable.Cell>
</DataTable.Row>
</DataTable.EmptyTBody>
</DataTable.Table>
</DataTable.TableContainer>
</DataTable.Root>
</div>
);
}
Checkbox#
Checkbox column with propagating selection, toggling a parent selects all descendants, partial state renders as indeterminate.
'use client';
import { Check } from '@primeicons/react/check';
import { Database } from '@primeicons/react/database';
import { ChevronDown } from '@primeicons/react/chevron-down';
import { ChevronRight } from '@primeicons/react/chevron-right';
import { File } from '@primeicons/react/file';
import { Folder } from '@primeicons/react/folder';
import { Image as ImageIcon } from '@primeicons/react/image';
import { Minus } from '@primeicons/react/minus';
import { Video } from '@primeicons/react/video';
import type { DataTableExpansionEvent, DataTableSelectionEvent } from '@primereact/ui/datatable';
import type { CheckboxRootChangeEvent } from '@primereact/ui/checkbox';
import type { DataTableSelectionInstance } from '@primereact/ui/datatable';
import { Checkbox } from '@primereact/ui/checkbox';
import { DataTable } from '@primereact/ui/datatable';
import { Tag } from '@primereact/ui/tag';
import * as React from 'react';
const typeSeverity: Record<string, 'info' | 'warn' | 'success' | 'secondary'> = {
Folder: 'warn',
Document: 'info',
Text: 'secondary',
Picture: 'success',
Video: 'success'
};
function TypeIcon({ type }: { type: string }) {
if (type === 'Picture') return <ImageIcon size={16} />;
if (type === 'Video') return <Video size={16} />;
if (type === 'Folder') return <Folder size={16} />;
return <File size={16} />;
}
interface TreeNode {
key: string;
data: {
name: string;
size: string;
type: string;
};
children?: TreeNode[];
}
const nodes: TreeNode[] = [
{
key: '0',
data: { name: 'Documents', size: '75kb', type: 'Folder' },
children: [
{
key: '0-0',
data: { name: 'Work', size: '55kb', type: 'Folder' },
children: [
{ key: '0-0-0', data: { name: 'Expenses.doc', size: '30kb', type: 'Document' } },
{ key: '0-0-1', data: { name: 'Resume.doc', size: '25kb', type: 'Document' } }
]
},
{
key: '0-1',
data: { name: 'Home', size: '20kb', type: 'Folder' },
children: [{ key: '0-1-0', data: { name: 'Invoices.txt', size: '20kb', type: 'Text' } }]
}
]
},
{
key: '1',
data: { name: 'Pictures', size: '150kb', type: 'Folder' },
children: [
{ key: '1-0', data: { name: 'barcelona.jpg', size: '90kb', type: 'Picture' } },
{ key: '1-1', data: { name: 'primeui.png', size: '30kb', type: 'Picture' } },
{ key: '1-2', data: { name: 'optimus.jpg', size: '30kb', type: 'Picture' } }
]
},
{
key: '2',
data: { name: 'Movies', size: '1500kb', type: 'Folder' },
children: [
{
key: '2-0',
data: { name: 'Al Pacino', size: '1000kb', type: 'Folder' },
children: [
{ key: '2-0-0', data: { name: 'Scarface', size: '500kb', type: 'Video' } },
{ key: '2-0-1', data: { name: 'Serpico', size: '500kb', type: 'Video' } }
]
},
{
key: '2-1',
data: { name: 'Robert De Niro', size: '500kb', type: 'Folder' },
children: [
{ key: '2-1-0', data: { name: 'Goodfellas', size: '250kb', type: 'Video' } },
{ key: '2-1-1', data: { name: 'Untouchables', size: '250kb', type: 'Video' } }
]
}
]
}
];
export default function TreeTableSelectionDemo() {
const [expandedKeys, setExpandedKeys] = React.useState<Record<string, boolean>>({ '0': true, '0-0': true });
const [selectionKeys, setSelectionKeys] = React.useState<Record<string, boolean>>({});
return (
<div className="w-full">
<div style={{ marginBottom: '0.5rem' }}>
<strong>Selected:</strong>{' '}
{Object.keys(selectionKeys)
.filter((k) => selectionKeys[k])
.join(', ') || 'None'}
</div>
<DataTable.Root
data={nodes}
dataKey="key"
treeMode
selectionMode="multiple"
expandedKeys={expandedKeys}
onExpandedChange={(e: DataTableExpansionEvent) => setExpandedKeys(e.value)}
selectionKeys={selectionKeys}
onSelectionChange={(e: DataTableSelectionEvent) => setSelectionKeys(e.value)}
>
<DataTable.TableContainer>
<DataTable.Table style={{ minWidth: '50rem' }}>
<DataTable.THead>
<DataTable.THeadRow>
<DataTable.THeadCell style={{ width: '50%' }}>
<div className="flex items-center gap-2">
<DataTable.Selection>
{({ isAllSelected, isSomeSelected, toggleAll }: DataTableSelectionInstance) => (
<Checkbox.Root
checked={isAllSelected}
indeterminate={isSomeSelected && !isAllSelected}
onCheckedChange={(e: CheckboxRootChangeEvent) => toggleAll(e.originalEvent)}
>
<Checkbox.Box>
<Checkbox.Indicator match="checked">
<Check />
</Checkbox.Indicator>
<Checkbox.Indicator match="indeterminate">
<Minus />
</Checkbox.Indicator>
</Checkbox.Box>
</Checkbox.Root>
)}
</DataTable.Selection>
<DataTable.THeadTitle>Name</DataTable.THeadTitle>
</div>
</DataTable.THeadCell>
<DataTable.THeadCell style={{ width: '20%' }}>
<DataTable.THeadTitle>Size</DataTable.THeadTitle>
</DataTable.THeadCell>
<DataTable.THeadCell style={{ width: '30%' }}>
<DataTable.THeadTitle>Type</DataTable.THeadTitle>
</DataTable.THeadCell>
</DataTable.THeadRow>
</DataTable.THead>
<DataTable.TBody key={JSON.stringify(selectionKeys)}>
{({ item }: { item: any }) => (
<DataTable.Row key={String(item.key)}>
<DataTable.Cell>
<div className="flex items-center gap-2">
<DataTable.RowToggle>
<DataTable.RowToggleIndicator match="expanded">
<ChevronDown />
</DataTable.RowToggleIndicator>
<DataTable.RowToggleIndicator match="collapsed">
<ChevronRight />
</DataTable.RowToggleIndicator>
</DataTable.RowToggle>
<DataTable.Selection>
{({ isSelected, isPartiallySelected, toggle }: DataTableSelectionInstance) => (
<Checkbox.Root
checked={isSelected}
indeterminate={isPartiallySelected}
onCheckedChange={(e: CheckboxRootChangeEvent) => toggle(e.originalEvent)}
>
<Checkbox.Box>
<Checkbox.Indicator match="checked">
<Check />
</Checkbox.Indicator>
<Checkbox.Indicator match="indeterminate">
<Minus />
</Checkbox.Indicator>
</Checkbox.Box>
</Checkbox.Root>
)}
</DataTable.Selection>
<TypeIcon type={String(item.type)} />
<span className={item.type === 'Folder' ? 'font-medium' : ''}>{String(item.name)}</span>
</div>
</DataTable.Cell>
<DataTable.Cell>
<span className="text-sm text-surface-500 dark:text-surface-400">{String(item.size)}</span>
</DataTable.Cell>
<DataTable.Cell>
<Tag severity={typeSeverity[String(item.type)] ?? 'secondary'}>{String(item.type)}</Tag>
</DataTable.Cell>
</DataTable.Row>
)}
</DataTable.TBody>
<DataTable.EmptyTBody>
<DataTable.Row>
<DataTable.Cell colSpan={3}>
<div className="flex flex-col items-center justify-center gap-3 py-24 text-center">
<div className="relative">
<div className="w-16 h-16 rounded-full bg-surface-100 dark:bg-surface-800 flex items-center justify-center">
<Database className="w-8 h-8 text-surface-400 dark:text-surface-500" />
</div>
<span className="absolute -bottom-0.5 -right-0.5 w-4 h-4 rounded-full border-2 border-surface-0 dark:border-surface-900 bg-primary animate-pulse" />
</div>
<div>
<p className="m-0 font-semibold text-surface-900 dark:text-surface-0">Loading tree</p>
<p className="mt-1 text-sm text-surface-500 dark:text-surface-400">Hang tight, the data is on its way.</p>
</div>
</div>
</DataTable.Cell>
</DataTable.Row>
</DataTable.EmptyTBody>
</DataTable.Table>
</DataTable.TableContainer>
</DataTable.Root>
</div>
);
}
Radio#
'use client';
import { ChevronDown } from '@primeicons/react/chevron-down';
import { Database } from '@primeicons/react/database';
import { ChevronRight } from '@primeicons/react/chevron-right';
import { NodeService, type TreeTableNode } from '@/shared/services/node.service';
import type { DataTableExpansionEvent, DataTableSelectionEvent } from '@primereact/ui/datatable';
import type { DataTableSelectionInstance } from '@primereact/ui/datatable';
import type { RadioButtonRootChangeEvent } from '@primereact/ui/radiobutton';
import { DataTable } from '@primereact/ui/datatable';
import { RadioButton } from '@primereact/ui/radiobutton';
import { Tag } from '@primereact/ui/tag';
import * as React from 'react';
import { File } from '@primeicons/react/file';
import { Folder } from '@primeicons/react/folder';
import { Image as ImageIcon } from '@primeicons/react/image';
import { Video } from '@primeicons/react/video';
const typeSeverity: Record<string, 'info' | 'warn' | 'success' | 'secondary'> = {
Folder: 'warn',
Document: 'info',
Resume: 'info',
Text: 'secondary',
Application: 'info',
Picture: 'success',
Video: 'success',
Zip: 'secondary',
PDF: 'info',
Link: 'secondary'
};
function TypeIcon({ type }: { type: string }) {
if (type === 'Picture') return <ImageIcon size={16} />;
if (type === 'Video') return <Video size={16} />;
if (type === 'Folder') return <Folder size={16} />;
return <File size={16} />;
}
export default function RadioSelectionDemo() {
const [nodes, setNodes] = React.useState<TreeTableNode[]>([]);
const [selectionKeys, setSelectionKeys] = React.useState<Record<string, boolean>>({});
const [expandedKeys, setExpandedKeys] = React.useState<Record<string, boolean>>({ '0': true });
React.useEffect(() => {
setNodes(NodeService.getTreeTableNodesData().slice(0, 5));
}, []);
return (
<div className="w-full">
<DataTable.Root
data={nodes}
dataKey="key"
treeMode
expandedKeys={expandedKeys}
onExpandedChange={(e: DataTableExpansionEvent) => setExpandedKeys(e.value)}
selectionMode="single"
selectionKeys={selectionKeys}
onSelectionChange={(e: DataTableSelectionEvent) => setSelectionKeys(e.value)}
>
<DataTable.TableContainer>
<DataTable.Table style={{ minWidth: '40rem' }}>
<DataTable.THead>
<DataTable.THeadRow>
<DataTable.THeadCell style={{ width: '3rem' }} />
<DataTable.THeadCell style={{ width: '50%' }}>
<DataTable.THeadTitle>Name</DataTable.THeadTitle>
</DataTable.THeadCell>
<DataTable.THeadCell style={{ width: '20%' }}>
<DataTable.THeadTitle>Size</DataTable.THeadTitle>
</DataTable.THeadCell>
<DataTable.THeadCell style={{ width: '30%' }}>
<DataTable.THeadTitle>Type</DataTable.THeadTitle>
</DataTable.THeadCell>
</DataTable.THeadRow>
</DataTable.THead>
<DataTable.TBody>
{({ item }: { item: any }) => (
<DataTable.Row key={String(item.key)}>
<DataTable.Cell>
<DataTable.Selection mode="radio">
{({ isSelected, toggle }: DataTableSelectionInstance) => (
<RadioButton.Root
checked={isSelected}
onCheckedChange={(e: RadioButtonRootChangeEvent) => toggle(e.originalEvent)}
>
<RadioButton.Box>
<RadioButton.Indicator match="checked" />
</RadioButton.Box>
</RadioButton.Root>
)}
</DataTable.Selection>
</DataTable.Cell>
<DataTable.Cell>
<div className="flex items-center gap-2">
<DataTable.RowToggle>
<DataTable.RowToggleIndicator match="expanded">
<ChevronDown />
</DataTable.RowToggleIndicator>
<DataTable.RowToggleIndicator match="collapsed">
<ChevronRight />
</DataTable.RowToggleIndicator>
</DataTable.RowToggle>
<TypeIcon type={String(item.type)} />
<span className={item.type === 'Folder' ? 'font-medium' : ''}>{String(item.name)}</span>
</div>
</DataTable.Cell>
<DataTable.Cell>
<span className="text-sm text-surface-500 dark:text-surface-400">{String(item.size)}</span>
</DataTable.Cell>
<DataTable.Cell>
<Tag severity={typeSeverity[String(item.type)] ?? 'secondary'}>{String(item.type)}</Tag>
</DataTable.Cell>
</DataTable.Row>
)}
</DataTable.TBody>
<DataTable.EmptyTBody>
<DataTable.Row>
<DataTable.Cell colSpan={4}>
<div className="flex flex-col items-center justify-center gap-3 py-24 text-center">
<div className="relative">
<div className="w-16 h-16 rounded-full bg-surface-100 dark:bg-surface-800 flex items-center justify-center">
<Database className="w-8 h-8 text-surface-400 dark:text-surface-500" />
</div>
<span className="absolute -bottom-0.5 -right-0.5 w-4 h-4 rounded-full border-2 border-surface-0 dark:border-surface-900 bg-primary animate-pulse" />
</div>
<div>
<p className="m-0 font-semibold text-surface-900 dark:text-surface-0">Loading tree</p>
<p className="mt-1 text-sm text-surface-500 dark:text-surface-400">Hang tight, the data is on its way.</p>
</div>
</div>
</DataTable.Cell>
</DataTable.Row>
</DataTable.EmptyTBody>
</DataTable.Table>
</DataTable.TableContainer>
</DataTable.Root>
</div>
);
}
Keyboard#
Arrow Up/Down moves focus between rows, Arrow Right/Left expands/collapses, Space or Enter toggles the focused row, and Shift + Arrow extends a range.
'use client';
import { ChevronDown } from '@primeicons/react/chevron-down';
import { Database } from '@primeicons/react/database';
import { ChevronRight } from '@primeicons/react/chevron-right';
import { NodeService, type TreeTableNode } from '@/shared/services/node.service';
import type { DataTableExpansionEvent, DataTableSelectionEvent } from '@primereact/ui/datatable';
import { Badge } from '@primereact/ui/badge';
import { DataTable } from '@primereact/ui/datatable';
import { Tag } from '@primereact/ui/tag';
import * as React from 'react';
import { File } from '@primeicons/react/file';
import { Folder } from '@primeicons/react/folder';
import { Image as ImageIcon } from '@primeicons/react/image';
import { Video } from '@primeicons/react/video';
const typeSeverity: Record<string, 'info' | 'warn' | 'success' | 'secondary'> = {
Folder: 'warn',
Document: 'info',
Resume: 'info',
Text: 'secondary',
Application: 'info',
Picture: 'success',
Video: 'success',
Zip: 'secondary',
PDF: 'info',
Link: 'secondary'
};
function TypeIcon({ type }: { type: string }) {
if (type === 'Picture') return <ImageIcon size={16} />;
if (type === 'Video') return <Video size={16} />;
if (type === 'Folder') return <Folder size={16} />;
return <File size={16} />;
}
export default function KeyboardDemo() {
const [nodes, setNodes] = React.useState<TreeTableNode[]>([]);
const [selectionKeys, setSelectionKeys] = React.useState<Record<string, boolean>>({});
const [expandedKeys, setExpandedKeys] = React.useState<Record<string, boolean>>({ '0': true });
React.useEffect(() => {
setNodes(NodeService.getTreeTableNodesData().slice(0, 5));
}, []);
const selectedCount = Object.values(selectionKeys).filter(Boolean).length;
return (
<div className="w-full">
<div className="flex items-center justify-between gap-3 mb-3 p-3 rounded-md border border-surface-200 dark:border-surface-700 bg-surface-50 dark:bg-surface-900">
<span className="text-sm text-surface-500 dark:text-surface-400">
<kbd className="px-1.5 py-0.5 text-xs rounded bg-surface-200 dark:bg-surface-700">↑</kbd>{' '}
<kbd className="px-1.5 py-0.5 text-xs rounded bg-surface-200 dark:bg-surface-700">↓</kbd> navigate,{' '}
<kbd className="px-1.5 py-0.5 text-xs rounded bg-surface-200 dark:bg-surface-700">→</kbd> expand,{' '}
<kbd className="px-1.5 py-0.5 text-xs rounded bg-surface-200 dark:bg-surface-700">←</kbd> collapse,{' '}
<kbd className="px-1.5 py-0.5 text-xs rounded bg-surface-200 dark:bg-surface-700">Space</kbd> select
</span>
<div className="flex items-center gap-2">
<span className="text-sm font-medium">Selected</span>
<Badge severity={selectedCount ? 'info' : 'secondary'}>{selectedCount}</Badge>
</div>
</div>
<DataTable.Root
data={nodes}
dataKey="key"
treeMode
expandedKeys={expandedKeys}
onExpandedChange={(e: DataTableExpansionEvent) => setExpandedKeys(e.value)}
selectionMode="multiple"
metaKeySelection
selectionKeys={selectionKeys}
onSelectionChange={(e: DataTableSelectionEvent) => setSelectionKeys(e.value)}
>
<DataTable.TableContainer>
<DataTable.Table style={{ minWidth: '40rem' }}>
<DataTable.THead>
<DataTable.THeadRow>
<DataTable.THeadCell style={{ width: '50%' }}>
<DataTable.THeadTitle>Name</DataTable.THeadTitle>
</DataTable.THeadCell>
<DataTable.THeadCell style={{ width: '20%' }}>
<DataTable.THeadTitle>Size</DataTable.THeadTitle>
</DataTable.THeadCell>
<DataTable.THeadCell style={{ width: '30%' }}>
<DataTable.THeadTitle>Type</DataTable.THeadTitle>
</DataTable.THeadCell>
</DataTable.THeadRow>
</DataTable.THead>
<DataTable.TBody>
{({ item }: { item: any }) => (
<DataTable.Row key={String(item.key)}>
<DataTable.Cell>
<div className="flex items-center gap-2">
<DataTable.RowToggle>
<DataTable.RowToggleIndicator match="expanded">
<ChevronDown />
</DataTable.RowToggleIndicator>
<DataTable.RowToggleIndicator match="collapsed">
<ChevronRight />
</DataTable.RowToggleIndicator>
</DataTable.RowToggle>
<TypeIcon type={String(item.type)} />
<span className={item.type === 'Folder' ? 'font-medium' : ''}>{String(item.name)}</span>
</div>
</DataTable.Cell>
<DataTable.Cell>
<span className="text-sm text-surface-500 dark:text-surface-400">{String(item.size)}</span>
</DataTable.Cell>
<DataTable.Cell>
<Tag severity={typeSeverity[String(item.type)] ?? 'secondary'}>{String(item.type)}</Tag>
</DataTable.Cell>
</DataTable.Row>
)}
</DataTable.TBody>
<DataTable.EmptyTBody>
<DataTable.Row>
<DataTable.Cell colSpan={3}>
<div className="flex flex-col items-center justify-center gap-3 py-24 text-center">
<div className="relative">
<div className="w-16 h-16 rounded-full bg-surface-100 dark:bg-surface-800 flex items-center justify-center">
<Database className="w-8 h-8 text-surface-400 dark:text-surface-500" />
</div>
<span className="absolute -bottom-0.5 -right-0.5 w-4 h-4 rounded-full border-2 border-surface-0 dark:border-surface-900 bg-primary animate-pulse" />
</div>
<div>
<p className="m-0 font-semibold text-surface-900 dark:text-surface-0">Loading tree</p>
<p className="mt-1 text-sm text-surface-500 dark:text-surface-400">Hang tight, the data is on its way.</p>
</div>
</div>
</DataTable.Cell>
</DataTable.Row>
</DataTable.EmptyTBody>
</DataTable.Table>
</DataTable.TableContainer>
</DataTable.Root>
</div>
);
}
Sort#
Sorting is evaluated per tree level, siblings are sorted against each other while the hierarchy stays intact.
Single#
'use client';
import { ChevronDown } from '@primeicons/react/chevron-down';
import { Database } from '@primeicons/react/database';
import { ChevronRight } from '@primeicons/react/chevron-right';
import { SortAlt } from '@primeicons/react/sort-alt';
import { SortAmountDown } from '@primeicons/react/sort-amount-down';
import { SortAmountUpAlt } from '@primeicons/react/sort-amount-up-alt';
import { NodeService, type TreeTableNode } from '@/shared/services/node.service';
import type { DataTableExpansionEvent } from '@primereact/ui/datatable';
import type { DataTableSortOrderInstance } from '@primereact/ui/datatable';
import { Badge } from '@primereact/ui/badge';
import { DataTable } from '@primereact/ui/datatable';
import { OverlayBadge } from '@primereact/ui/overlaybadge';
import { Tag } from '@primereact/ui/tag';
import * as React from 'react';
import { File } from '@primeicons/react/file';
import { Folder } from '@primeicons/react/folder';
import { Image as ImageIcon } from '@primeicons/react/image';
import { Video } from '@primeicons/react/video';
const typeSeverity: Record<string, 'info' | 'warn' | 'success' | 'secondary'> = {
Folder: 'warn',
Document: 'info',
Resume: 'info',
Text: 'secondary',
Application: 'info',
Picture: 'success',
Video: 'success',
Zip: 'secondary',
PDF: 'info',
Link: 'secondary'
};
function TypeIcon({ type }: { type: string }) {
if (type === 'Picture') return <ImageIcon size={16} />;
if (type === 'Video') return <Video size={16} />;
if (type === 'Folder') return <Folder size={16} />;
return <File size={16} />;
}
function SortableHeader({ field, children }: { field: string; children: React.ReactNode }) {
return (
<DataTable.Sort field={field} className="inline-flex items-center gap-2">
{children}
<OverlayBadge className="px-1">
<DataTable.SortIndicator match="unsorted">
<SortAlt />
</DataTable.SortIndicator>
<DataTable.SortIndicator match="asc">
<SortAmountUpAlt />
</DataTable.SortIndicator>
<DataTable.SortIndicator match="desc">
<SortAmountDown />
</DataTable.SortIndicator>
<DataTable.SortOrder asChild>
{({ index }: DataTableSortOrderInstance) => (
<Badge shape="circle" size="small" severity="info" className="mt-1">
{index}
</Badge>
)}
</DataTable.SortOrder>
</OverlayBadge>
</DataTable.Sort>
);
}
export default function SortDemo() {
const [nodes, setNodes] = React.useState<TreeTableNode[]>([]);
const [expandedKeys, setExpandedKeys] = React.useState<Record<string, boolean>>({ '0': true });
React.useEffect(() => {
setNodes(NodeService.getTreeTableNodesData().slice(0, 5));
}, []);
return (
<div className="w-full">
<DataTable.Root
data={nodes}
dataKey="key"
treeMode
expandedKeys={expandedKeys}
onExpandedChange={(e: DataTableExpansionEvent) => setExpandedKeys(e.value)}
removableSort
>
<DataTable.TableContainer>
<DataTable.Table style={{ minWidth: '40rem' }}>
<DataTable.THead>
<DataTable.THeadRow>
<DataTable.THeadCell style={{ width: '50%' }}>
<SortableHeader field="name">
<DataTable.THeadTitle>Name</DataTable.THeadTitle>
</SortableHeader>
</DataTable.THeadCell>
<DataTable.THeadCell style={{ width: '20%' }}>
<SortableHeader field="size">
<DataTable.THeadTitle>Size</DataTable.THeadTitle>
</SortableHeader>
</DataTable.THeadCell>
<DataTable.THeadCell style={{ width: '30%' }}>
<SortableHeader field="type">
<DataTable.THeadTitle>Type</DataTable.THeadTitle>
</SortableHeader>
</DataTable.THeadCell>
</DataTable.THeadRow>
</DataTable.THead>
<DataTable.TBody>
{({ item }: { item: any }) => (
<DataTable.Row key={String(item.key)}>
<DataTable.Cell>
<div className="flex items-center gap-2">
<DataTable.RowToggle>
<DataTable.RowToggleIndicator match="expanded">
<ChevronDown />
</DataTable.RowToggleIndicator>
<DataTable.RowToggleIndicator match="collapsed">
<ChevronRight />
</DataTable.RowToggleIndicator>
</DataTable.RowToggle>
<TypeIcon type={String(item.type)} />
<span className={item.type === 'Folder' ? 'font-medium' : ''}>{String(item.name)}</span>
</div>
</DataTable.Cell>
<DataTable.Cell>
<span className="text-sm text-surface-500 dark:text-surface-400">{String(item.size)}</span>
</DataTable.Cell>
<DataTable.Cell>
<Tag severity={typeSeverity[String(item.type)] ?? 'secondary'}>{String(item.type)}</Tag>
</DataTable.Cell>
</DataTable.Row>
)}
</DataTable.TBody>
<DataTable.EmptyTBody>
<DataTable.Row>
<DataTable.Cell colSpan={3}>
<div className="flex flex-col items-center justify-center gap-3 py-24 text-center">
<div className="relative">
<div className="w-16 h-16 rounded-full bg-surface-100 dark:bg-surface-800 flex items-center justify-center">
<Database className="w-8 h-8 text-surface-400 dark:text-surface-500" />
</div>
<span className="absolute -bottom-0.5 -right-0.5 w-4 h-4 rounded-full border-2 border-surface-0 dark:border-surface-900 bg-primary animate-pulse" />
</div>
<div>
<p className="m-0 font-semibold text-surface-900 dark:text-surface-0">Loading tree</p>
<p className="mt-1 text-sm text-surface-500 dark:text-surface-400">Hang tight, the data is on its way.</p>
</div>
</div>
</DataTable.Cell>
</DataTable.Row>
</DataTable.EmptyTBody>
</DataTable.Table>
</DataTable.TableContainer>
</DataTable.Root>
</div>
);
}
Multiple#
'use client';
import { ChevronDown } from '@primeicons/react/chevron-down';
import { Database } from '@primeicons/react/database';
import { ChevronRight } from '@primeicons/react/chevron-right';
import { SortAlt } from '@primeicons/react/sort-alt';
import { SortAmountDown } from '@primeicons/react/sort-amount-down';
import { SortAmountUpAlt } from '@primeicons/react/sort-amount-up-alt';
import { NodeService, type TreeTableNode } from '@/shared/services/node.service';
import type { DataTableExpansionEvent } from '@primereact/ui/datatable';
import type { DataTableSortOrderInstance } from '@primereact/ui/datatable';
import { Badge } from '@primereact/ui/badge';
import { DataTable } from '@primereact/ui/datatable';
import { OverlayBadge } from '@primereact/ui/overlaybadge';
import { Tag } from '@primereact/ui/tag';
import * as React from 'react';
import { File } from '@primeicons/react/file';
import { Folder } from '@primeicons/react/folder';
import { Image as ImageIcon } from '@primeicons/react/image';
import { Video } from '@primeicons/react/video';
const typeSeverity: Record<string, 'info' | 'warn' | 'success' | 'secondary'> = {
Folder: 'warn',
Document: 'info',
Resume: 'info',
Text: 'secondary',
Application: 'info',
Picture: 'success',
Video: 'success',
Zip: 'secondary',
PDF: 'info',
Link: 'secondary'
};
function TypeIcon({ type }: { type: string }) {
if (type === 'Picture') return <ImageIcon size={16} />;
if (type === 'Video') return <Video size={16} />;
if (type === 'Folder') return <Folder size={16} />;
return <File size={16} />;
}
function SortableHeader({ field, children }: { field: string; children: React.ReactNode }) {
return (
<DataTable.Sort field={field} className="inline-flex items-center gap-2">
{children}
<OverlayBadge className="px-1">
<DataTable.SortIndicator match="unsorted">
<SortAlt />
</DataTable.SortIndicator>
<DataTable.SortIndicator match="asc">
<SortAmountUpAlt />
</DataTable.SortIndicator>
<DataTable.SortIndicator match="desc">
<SortAmountDown />
</DataTable.SortIndicator>
<DataTable.SortOrder asChild>
{({ index }: DataTableSortOrderInstance) => (
<Badge shape="circle" size="small" severity="info" className="mt-1">
{index}
</Badge>
)}
</DataTable.SortOrder>
</OverlayBadge>
</DataTable.Sort>
);
}
export default function MultiSortDemo() {
const [nodes, setNodes] = React.useState<TreeTableNode[]>([]);
const [expandedKeys, setExpandedKeys] = React.useState<Record<string, boolean>>({ '0': true });
React.useEffect(() => {
setNodes(NodeService.getTreeTableNodesData().slice(0, 5));
}, []);
return (
<div className="w-full">
<DataTable.Root
data={nodes}
dataKey="key"
treeMode
expandedKeys={expandedKeys}
onExpandedChange={(e: DataTableExpansionEvent) => setExpandedKeys(e.value)}
removableSort
>
<DataTable.TableContainer>
<DataTable.Table style={{ minWidth: '40rem' }}>
<DataTable.THead>
<DataTable.THeadRow>
<DataTable.THeadCell style={{ width: '50%' }}>
<SortableHeader field="name">
<DataTable.THeadTitle>Name</DataTable.THeadTitle>
</SortableHeader>
</DataTable.THeadCell>
<DataTable.THeadCell style={{ width: '20%' }}>
<SortableHeader field="size">
<DataTable.THeadTitle>Size</DataTable.THeadTitle>
</SortableHeader>
</DataTable.THeadCell>
<DataTable.THeadCell style={{ width: '30%' }}>
<SortableHeader field="type">
<DataTable.THeadTitle>Type</DataTable.THeadTitle>
</SortableHeader>
</DataTable.THeadCell>
</DataTable.THeadRow>
</DataTable.THead>
<DataTable.TBody>
{({ item }: { item: any }) => (
<DataTable.Row key={String(item.key)}>
<DataTable.Cell>
<div className="flex items-center gap-2">
<DataTable.RowToggle>
<DataTable.RowToggleIndicator match="expanded">
<ChevronDown />
</DataTable.RowToggleIndicator>
<DataTable.RowToggleIndicator match="collapsed">
<ChevronRight />
</DataTable.RowToggleIndicator>
</DataTable.RowToggle>
<TypeIcon type={String(item.type)} />
<span className={item.type === 'Folder' ? 'font-medium' : ''}>{String(item.name)}</span>
</div>
</DataTable.Cell>
<DataTable.Cell>
<span className="text-sm text-surface-500 dark:text-surface-400">{String(item.size)}</span>
</DataTable.Cell>
<DataTable.Cell>
<Tag severity={typeSeverity[String(item.type)] ?? 'secondary'}>{String(item.type)}</Tag>
</DataTable.Cell>
</DataTable.Row>
)}
</DataTable.TBody>
<DataTable.EmptyTBody>
<DataTable.Row>
<DataTable.Cell colSpan={3}>
<div className="flex flex-col items-center justify-center gap-3 py-24 text-center">
<div className="relative">
<div className="w-16 h-16 rounded-full bg-surface-100 dark:bg-surface-800 flex items-center justify-center">
<Database className="w-8 h-8 text-surface-400 dark:text-surface-500" />
</div>
<span className="absolute -bottom-0.5 -right-0.5 w-4 h-4 rounded-full border-2 border-surface-0 dark:border-surface-900 bg-primary animate-pulse" />
</div>
<div>
<p className="m-0 font-semibold text-surface-900 dark:text-surface-0">Loading tree</p>
<p className="mt-1 text-sm text-surface-500 dark:text-surface-400">Hang tight, the data is on its way.</p>
</div>
</div>
</DataTable.Cell>
</DataTable.Row>
</DataTable.EmptyTBody>
</DataTable.Table>
</DataTable.TableContainer>
</DataTable.Root>
</div>
);
}
Presort#
'use client';
import { ChevronDown } from '@primeicons/react/chevron-down';
import { Database } from '@primeicons/react/database';
import { ChevronRight } from '@primeicons/react/chevron-right';
import { SortAlt } from '@primeicons/react/sort-alt';
import { SortAmountDown } from '@primeicons/react/sort-amount-down';
import { SortAmountUpAlt } from '@primeicons/react/sort-amount-up-alt';
import { NodeService, type TreeTableNode } from '@/shared/services/node.service';
import type { DataTableExpansionEvent } from '@primereact/ui/datatable';
import { DataTable } from '@primereact/ui/datatable';
import { Tag } from '@primereact/ui/tag';
import * as React from 'react';
import { File } from '@primeicons/react/file';
import { Folder } from '@primeicons/react/folder';
import { Image as ImageIcon } from '@primeicons/react/image';
import { Video } from '@primeicons/react/video';
const typeSeverity: Record<string, 'info' | 'warn' | 'success' | 'secondary'> = {
Folder: 'warn',
Document: 'info',
Resume: 'info',
Text: 'secondary',
Application: 'info',
Picture: 'success',
Video: 'success',
Zip: 'secondary',
PDF: 'info',
Link: 'secondary'
};
function TypeIcon({ type }: { type: string }) {
if (type === 'Picture') return <ImageIcon size={16} />;
if (type === 'Video') return <Video size={16} />;
if (type === 'Folder') return <Folder size={16} />;
return <File size={16} />;
}
export default function PresortDemo() {
const [nodes, setNodes] = React.useState<TreeTableNode[]>([]);
const [expandedKeys, setExpandedKeys] = React.useState<Record<string, boolean>>({ '0': true });
React.useEffect(() => {
setNodes(NodeService.getTreeTableNodesData().slice(0, 5));
}, []);
return (
<div className="w-full">
<DataTable.Root
data={nodes}
dataKey="key"
treeMode
expandedKeys={expandedKeys}
onExpandedChange={(e: DataTableExpansionEvent) => setExpandedKeys(e.value)}
defaultSortField="size"
defaultSortOrder={-1}
removableSort
>
<DataTable.TableContainer>
<DataTable.Table style={{ minWidth: '40rem' }}>
<DataTable.THead>
<DataTable.THeadRow>
<DataTable.THeadCell style={{ width: '50%' }}>
<DataTable.Sort field="name" className="inline-flex items-center gap-2">
<DataTable.THeadTitle>Name</DataTable.THeadTitle>
<DataTable.SortIndicator match="unsorted">
<SortAlt />
</DataTable.SortIndicator>
<DataTable.SortIndicator match="asc">
<SortAmountUpAlt />
</DataTable.SortIndicator>
<DataTable.SortIndicator match="desc">
<SortAmountDown />
</DataTable.SortIndicator>
</DataTable.Sort>
</DataTable.THeadCell>
<DataTable.THeadCell style={{ width: '20%' }}>
<DataTable.Sort field="size" className="inline-flex items-center gap-2">
<DataTable.THeadTitle>Size</DataTable.THeadTitle>
<DataTable.SortIndicator match="unsorted">
<SortAlt />
</DataTable.SortIndicator>
<DataTable.SortIndicator match="asc">
<SortAmountUpAlt />
</DataTable.SortIndicator>
<DataTable.SortIndicator match="desc">
<SortAmountDown />
</DataTable.SortIndicator>
</DataTable.Sort>
</DataTable.THeadCell>
<DataTable.THeadCell style={{ width: '30%' }}>
<DataTable.Sort field="type" className="inline-flex items-center gap-2">
<DataTable.THeadTitle>Type</DataTable.THeadTitle>
<DataTable.SortIndicator match="unsorted">
<SortAlt />
</DataTable.SortIndicator>
<DataTable.SortIndicator match="asc">
<SortAmountUpAlt />
</DataTable.SortIndicator>
<DataTable.SortIndicator match="desc">
<SortAmountDown />
</DataTable.SortIndicator>
</DataTable.Sort>
</DataTable.THeadCell>
</DataTable.THeadRow>
</DataTable.THead>
<DataTable.TBody>
{({ item }: { item: any }) => (
<DataTable.Row key={String(item.key)}>
<DataTable.Cell>
<div className="flex items-center gap-2">
<DataTable.RowToggle>
<DataTable.RowToggleIndicator match="expanded">
<ChevronDown />
</DataTable.RowToggleIndicator>
<DataTable.RowToggleIndicator match="collapsed">
<ChevronRight />
</DataTable.RowToggleIndicator>
</DataTable.RowToggle>
<TypeIcon type={String(item.type)} />
<span className={item.type === 'Folder' ? 'font-medium' : ''}>{String(item.name)}</span>
</div>
</DataTable.Cell>
<DataTable.Cell>
<span className="text-sm text-surface-500 dark:text-surface-400">{String(item.size)}</span>
</DataTable.Cell>
<DataTable.Cell>
<Tag severity={typeSeverity[String(item.type)] ?? 'secondary'}>{String(item.type)}</Tag>
</DataTable.Cell>
</DataTable.Row>
)}
</DataTable.TBody>
<DataTable.EmptyTBody>
<DataTable.Row>
<DataTable.Cell colSpan={3}>
<div className="flex flex-col items-center justify-center gap-3 py-24 text-center">
<div className="relative">
<div className="w-16 h-16 rounded-full bg-surface-100 dark:bg-surface-800 flex items-center justify-center">
<Database className="w-8 h-8 text-surface-400 dark:text-surface-500" />
</div>
<span className="absolute -bottom-0.5 -right-0.5 w-4 h-4 rounded-full border-2 border-surface-0 dark:border-surface-900 bg-primary animate-pulse" />
</div>
<div>
<p className="m-0 font-semibold text-surface-900 dark:text-surface-0">Loading tree</p>
<p className="mt-1 text-sm text-surface-500 dark:text-surface-400">Hang tight, the data is on its way.</p>
</div>
</div>
</DataTable.Cell>
</DataTable.Row>
</DataTable.EmptyTBody>
</DataTable.Table>
</DataTable.TableContainer>
</DataTable.Root>
</div>
);
}
'use client';
import { ChevronDown } from '@primeicons/react/chevron-down';
import { Database } from '@primeicons/react/database';
import { ChevronRight } from '@primeicons/react/chevron-right';
import { SortAlt } from '@primeicons/react/sort-alt';
import { SortAmountDown } from '@primeicons/react/sort-amount-down';
import { SortAmountUpAlt } from '@primeicons/react/sort-amount-up-alt';
import { NodeService, type TreeTableNode } from '@/shared/services/node.service';
import type { DataTableExpansionEvent } from '@primereact/ui/datatable';
import type { DataTableSortOrderInstance } from '@primereact/ui/datatable';
import { Badge } from '@primereact/ui/badge';
import { DataTable } from '@primereact/ui/datatable';
import { OverlayBadge } from '@primereact/ui/overlaybadge';
import { Tag } from '@primereact/ui/tag';
import * as React from 'react';
import { File } from '@primeicons/react/file';
import { Folder } from '@primeicons/react/folder';
import { Image as ImageIcon } from '@primeicons/react/image';
import { Video } from '@primeicons/react/video';
const typeSeverity: Record<string, 'info' | 'warn' | 'success' | 'secondary'> = {
Folder: 'warn',
Document: 'info',
Resume: 'info',
Text: 'secondary',
Application: 'info',
Picture: 'success',
Video: 'success',
Zip: 'secondary',
PDF: 'info',
Link: 'secondary'
};
function TypeIcon({ type }: { type: string }) {
if (type === 'Picture') return <ImageIcon size={16} />;
if (type === 'Video') return <Video size={16} />;
if (type === 'Folder') return <Folder size={16} />;
return <File size={16} />;
}
export default function PresortMultiDemo() {
const [nodes, setNodes] = React.useState<TreeTableNode[]>([]);
const [expandedKeys, setExpandedKeys] = React.useState<Record<string, boolean>>({ '0': true });
React.useEffect(() => {
setNodes(NodeService.getTreeTableNodesData().slice(0, 5));
}, []);
return (
<div className="w-full">
<DataTable.Root
data={nodes}
dataKey="key"
treeMode
expandedKeys={expandedKeys}
onExpandedChange={(e: DataTableExpansionEvent) => setExpandedKeys(e.value)}
defaultMultiSortMeta={[
{ field: 'type', order: 1 },
{ field: 'name', order: 1 }
]}
removableSort
>
<DataTable.TableContainer>
<DataTable.Table style={{ minWidth: '40rem' }}>
<DataTable.THead>
<DataTable.THeadRow>
<DataTable.THeadCell style={{ width: '50%' }}>
<DataTable.Sort field="name" className="inline-flex items-center gap-2">
<DataTable.THeadTitle>Name</DataTable.THeadTitle>
<OverlayBadge className="px-1">
<DataTable.SortIndicator match="unsorted">
<SortAlt />
</DataTable.SortIndicator>
<DataTable.SortIndicator match="asc">
<SortAmountUpAlt />
</DataTable.SortIndicator>
<DataTable.SortIndicator match="desc">
<SortAmountDown />
</DataTable.SortIndicator>
<DataTable.SortOrder asChild>
{({ index }: DataTableSortOrderInstance) => (
<Badge shape="circle" size="small" severity="info" className="mt-1">
{index}
</Badge>
)}
</DataTable.SortOrder>
</OverlayBadge>
</DataTable.Sort>
</DataTable.THeadCell>
<DataTable.THeadCell style={{ width: '20%' }}>
<DataTable.Sort field="size" className="inline-flex items-center gap-2">
<DataTable.THeadTitle>Size</DataTable.THeadTitle>
<OverlayBadge className="px-1">
<DataTable.SortIndicator match="unsorted">
<SortAlt />
</DataTable.SortIndicator>
<DataTable.SortIndicator match="asc">
<SortAmountUpAlt />
</DataTable.SortIndicator>
<DataTable.SortIndicator match="desc">
<SortAmountDown />
</DataTable.SortIndicator>
<DataTable.SortOrder asChild>
{({ index }: DataTableSortOrderInstance) => (
<Badge shape="circle" size="small" severity="info" className="mt-1">
{index}
</Badge>
)}
</DataTable.SortOrder>
</OverlayBadge>
</DataTable.Sort>
</DataTable.THeadCell>
<DataTable.THeadCell style={{ width: '30%' }}>
<DataTable.Sort field="type" className="inline-flex items-center gap-2">
<DataTable.THeadTitle>Type</DataTable.THeadTitle>
<OverlayBadge className="px-1">
<DataTable.SortIndicator match="unsorted">
<SortAlt />
</DataTable.SortIndicator>
<DataTable.SortIndicator match="asc">
<SortAmountUpAlt />
</DataTable.SortIndicator>
<DataTable.SortIndicator match="desc">
<SortAmountDown />
</DataTable.SortIndicator>
<DataTable.SortOrder asChild>
{({ index }: DataTableSortOrderInstance) => (
<Badge shape="circle" size="small" severity="info" className="mt-1">
{index}
</Badge>
)}
</DataTable.SortOrder>
</OverlayBadge>
</DataTable.Sort>
</DataTable.THeadCell>
</DataTable.THeadRow>
</DataTable.THead>
<DataTable.TBody>
{({ item }: { item: any }) => (
<DataTable.Row key={String(item.key)}>
<DataTable.Cell>
<div className="flex items-center gap-2">
<DataTable.RowToggle>
<DataTable.RowToggleIndicator match="expanded">
<ChevronDown />
</DataTable.RowToggleIndicator>
<DataTable.RowToggleIndicator match="collapsed">
<ChevronRight />
</DataTable.RowToggleIndicator>
</DataTable.RowToggle>
<TypeIcon type={String(item.type)} />
<span className={item.type === 'Folder' ? 'font-medium' : ''}>{String(item.name)}</span>
</div>
</DataTable.Cell>
<DataTable.Cell>
<span className="text-sm text-surface-500 dark:text-surface-400">{String(item.size)}</span>
</DataTable.Cell>
<DataTable.Cell>
<Tag severity={typeSeverity[String(item.type)] ?? 'secondary'}>{String(item.type)}</Tag>
</DataTable.Cell>
</DataTable.Row>
)}
</DataTable.TBody>
<DataTable.EmptyTBody>
<DataTable.Row>
<DataTable.Cell colSpan={3}>
<div className="flex flex-col items-center justify-center gap-3 py-24 text-center">
<div className="relative">
<div className="w-16 h-16 rounded-full bg-surface-100 dark:bg-surface-800 flex items-center justify-center">
<Database className="w-8 h-8 text-surface-400 dark:text-surface-500" />
</div>
<span className="absolute -bottom-0.5 -right-0.5 w-4 h-4 rounded-full border-2 border-surface-0 dark:border-surface-900 bg-primary animate-pulse" />
</div>
<div>
<p className="m-0 font-semibold text-surface-900 dark:text-surface-0">Loading tree</p>
<p className="mt-1 text-sm text-surface-500 dark:text-surface-400">Hang tight, the data is on its way.</p>
</div>
</div>
</DataTable.Cell>
</DataTable.Row>
</DataTable.EmptyTBody>
</DataTable.Table>
</DataTable.TableContainer>
</DataTable.Root>
</div>
);
}
Pagination#
Pagination operates on root-level nodes, expanded children never get paginated away.
'use client';
import { ChevronDown } from '@primeicons/react/chevron-down';
import { Database } from '@primeicons/react/database';
import { ChevronRight } from '@primeicons/react/chevron-right';
import { EllipsisH } from '@primeicons/react';
import { AngleDoubleLeft } from '@primeicons/react/angle-double-left';
import { AngleDoubleRight } from '@primeicons/react/angle-double-right';
import { AngleLeft } from '@primeicons/react/angle-left';
import { AngleRight } from '@primeicons/react/angle-right';
import { NodeService, type TreeTableNode } from '@/shared/services/node.service';
import type { DataTableExpansionEvent } from '@primereact/ui/datatable';
import type { PaginatorRootChangeEvent } from '@primereact/ui/paginator';
import type { DataTablePaginationInstance } from '@primereact/ui/datatable';
import { DataTable } from '@primereact/ui/datatable';
import { Paginator } from '@primereact/ui/paginator';
import { Tag } from '@primereact/ui/tag';
import type { PaginatorPagesInstance } from '@primereact/ui/paginator';
import * as React from 'react';
import { File } from '@primeicons/react/file';
import { Folder } from '@primeicons/react/folder';
import { Image as ImageIcon } from '@primeicons/react/image';
import { Video } from '@primeicons/react/video';
const typeSeverity: Record<string, 'info' | 'warn' | 'success' | 'secondary'> = {
Folder: 'warn',
Document: 'info',
Resume: 'info',
Text: 'secondary',
Application: 'info',
Picture: 'success',
Video: 'success',
Zip: 'secondary',
PDF: 'info',
Link: 'secondary'
};
function TypeIcon({ type }: { type: string }) {
if (type === 'Picture') return <ImageIcon size={16} />;
if (type === 'Video') return <Video size={16} />;
if (type === 'Folder') return <Folder size={16} />;
return <File size={16} />;
}
export default function PaginationDemo() {
const [nodes, setNodes] = React.useState<TreeTableNode[]>([]);
const [expandedKeys, setExpandedKeys] = React.useState<Record<string, boolean>>({});
React.useEffect(() => {
setNodes(NodeService.getTreeTableNodesData());
}, []);
return (
<div className="w-full">
<DataTable.Root
data={nodes}
dataKey="key"
treeMode
expandedKeys={expandedKeys}
onExpandedChange={(e: DataTableExpansionEvent) => setExpandedKeys(e.value)}
paginator
defaultRows={3}
>
<DataTable.TableContainer>
<DataTable.Table style={{ minWidth: '40rem' }}>
<DataTable.THead>
<DataTable.THeadRow>
<DataTable.THeadCell style={{ width: '60%' }}>
<DataTable.THeadTitle>Name</DataTable.THeadTitle>
</DataTable.THeadCell>
<DataTable.THeadCell style={{ width: '20%' }}>
<DataTable.THeadTitle>Size</DataTable.THeadTitle>
</DataTable.THeadCell>
<DataTable.THeadCell style={{ width: '20%' }}>
<DataTable.THeadTitle>Type</DataTable.THeadTitle>
</DataTable.THeadCell>
</DataTable.THeadRow>
</DataTable.THead>
<DataTable.TBody>
{({ item }: { item: any }) => (
<DataTable.Row key={String(item.key)}>
<DataTable.Cell>
<div className="flex items-center gap-2">
<DataTable.RowToggle>
<DataTable.RowToggleIndicator match="expanded">
<ChevronDown />
</DataTable.RowToggleIndicator>
<DataTable.RowToggleIndicator match="collapsed">
<ChevronRight />
</DataTable.RowToggleIndicator>
</DataTable.RowToggle>
<TypeIcon type={String(item.type)} />
<span className={item.type === 'Folder' ? 'font-medium' : ''}>{String(item.name)}</span>
</div>
</DataTable.Cell>
<DataTable.Cell>
<span className="text-sm text-surface-500 dark:text-surface-400">{String(item.size)}</span>
</DataTable.Cell>
<DataTable.Cell>
<Tag severity={typeSeverity[String(item.type)] ?? 'secondary'}>{String(item.type)}</Tag>
</DataTable.Cell>
</DataTable.Row>
)}
</DataTable.TBody>
<DataTable.EmptyTBody>
<DataTable.Row>
<DataTable.Cell colSpan={3}>
<div className="flex flex-col items-center justify-center gap-3 py-24 text-center">
<div className="relative">
<div className="w-16 h-16 rounded-full bg-surface-100 dark:bg-surface-800 flex items-center justify-center">
<Database className="w-8 h-8 text-surface-400 dark:text-surface-500" />
</div>
<span className="absolute -bottom-0.5 -right-0.5 w-4 h-4 rounded-full border-2 border-surface-0 dark:border-surface-900 bg-primary animate-pulse" />
</div>
<div>
<p className="m-0 font-semibold text-surface-900 dark:text-surface-0">Loading tree</p>
<p className="mt-1 text-sm text-surface-500 dark:text-surface-400">Hang tight, the data is on its way.</p>
</div>
</div>
</DataTable.Cell>
</DataTable.Row>
</DataTable.EmptyTBody>
</DataTable.Table>
</DataTable.TableContainer>
<DataTable.Pagination>
{({ page, rows, totalRecords, onPageChange }: DataTablePaginationInstance) => (
<Paginator.Root
className="py-3 px-3.5 border-t border-surface-200 dark:border-surface-700"
page={page + 1}
total={totalRecords}
itemsPerPage={rows}
onPageChange={(e: PaginatorRootChangeEvent) => {
if (e.originalEvent) onPageChange(e.originalEvent, e.value - 1);
}}
>
<Paginator.Content>
<Paginator.First>
<AngleDoubleLeft />
</Paginator.First>
<Paginator.Prev>
<AngleLeft />
</Paginator.Prev>
<Paginator.Pages>
{({ paginator }: PaginatorPagesInstance) =>
paginator?.pages.map((p, index) =>
p.type === 'page' ? (
<Paginator.Page key={index} value={p.value} />
) : (
<Paginator.Ellipsis key={index}>
<EllipsisH />
</Paginator.Ellipsis>
)
)
}
</Paginator.Pages>
<Paginator.Next>
<AngleRight />
</Paginator.Next>
<Paginator.Last>
<AngleDoubleRight />
</Paginator.Last>
</Paginator.Content>
</Paginator.Root>
)}
</DataTable.Pagination>
</DataTable.Root>
</div>
);
}
Scroll#
scrollable + scrollHeight works the same way as in DataTable; the flattened rows scroll while the sticky header stays pinned.
Vertical#
'use client';
import { ChevronDown } from '@primeicons/react/chevron-down';
import { Database } from '@primeicons/react/database';
import { ChevronRight } from '@primeicons/react/chevron-right';
import { File } from '@primeicons/react/file';
import { Folder } from '@primeicons/react/folder';
import { Image as ImageIcon } from '@primeicons/react/image';
import { Video } from '@primeicons/react/video';
import type { DataTableExpansionEvent } from '@primereact/ui/datatable';
import { DataTable } from '@primereact/ui/datatable';
import { Tag } from '@primereact/ui/tag';
import * as React from 'react';
interface TreeNode {
key: string;
data: {
name: string;
size: string;
type: string;
};
children?: TreeNode[];
}
const typeSeverity: Record<string, 'info' | 'warn' | 'success' | 'secondary'> = {
Folder: 'warn',
Document: 'info',
Text: 'secondary',
Picture: 'success',
Video: 'success'
};
function TypeIcon({ type }: { type: string }) {
if (type === 'Picture') return <ImageIcon size={16} />;
if (type === 'Video') return <Video size={16} />;
if (type === 'Folder') return <Folder size={16} />;
return <File size={16} />;
}
const types = ['Document', 'Text', 'Picture', 'Video'];
const nodes: TreeNode[] = Array.from({ length: 20 }, (_, i) => ({
key: String(i),
data: { name: `Folder ${i + 1}`, size: '—', type: 'Folder' },
children: Array.from({ length: 6 }, (_, j) => {
const type = types[j % types.length];
const size = ((i * 37 + j * 113) % 1950) + 50;
return {
key: `${i}-${j}`,
data: {
name: `${type} ${i + 1}.${j + 1}`,
size: `${size}kb`,
type
}
};
})
}));
export default function TreeTableScrollDemo() {
const [expandedKeys, setExpandedKeys] = React.useState<Record<string, boolean>>({ '0': true, '1': true });
return (
<div className="w-full">
<DataTable.Root
data={nodes}
dataKey="key"
treeMode
scrollable
scrollHeight="400px"
expandedKeys={expandedKeys}
onExpandedChange={(e: DataTableExpansionEvent) => setExpandedKeys(e.value)}
>
<DataTable.TableContainer>
<DataTable.Table style={{ minWidth: '50rem' }}>
<DataTable.THead>
<DataTable.THeadRow>
<DataTable.THeadCell style={{ width: '50%' }}>
<DataTable.THeadTitle>Name</DataTable.THeadTitle>
</DataTable.THeadCell>
<DataTable.THeadCell style={{ width: '25%' }}>
<DataTable.THeadTitle>Size</DataTable.THeadTitle>
</DataTable.THeadCell>
<DataTable.THeadCell style={{ width: '25%' }}>
<DataTable.THeadTitle>Type</DataTable.THeadTitle>
</DataTable.THeadCell>
</DataTable.THeadRow>
</DataTable.THead>
<DataTable.TBody>
{({ item }: { item: any }) => (
<DataTable.Row key={String(item.key)}>
<DataTable.Cell>
<div className="flex items-center gap-2">
<DataTable.RowToggle>
<DataTable.RowToggleIndicator match="expanded">
<ChevronDown />
</DataTable.RowToggleIndicator>
<DataTable.RowToggleIndicator match="collapsed">
<ChevronRight />
</DataTable.RowToggleIndicator>
</DataTable.RowToggle>
<TypeIcon type={String(item.type)} />
<span className={item.type === 'Folder' ? 'font-medium' : ''}>{String(item.name)}</span>
</div>
</DataTable.Cell>
<DataTable.Cell>
<span className="text-sm text-surface-500 dark:text-surface-400">{String(item.size)}</span>
</DataTable.Cell>
<DataTable.Cell>
<Tag severity={typeSeverity[String(item.type)] ?? 'secondary'}>{String(item.type)}</Tag>
</DataTable.Cell>
</DataTable.Row>
)}
</DataTable.TBody>
<DataTable.EmptyTBody>
<DataTable.Row>
<DataTable.Cell colSpan={3}>
<div className="flex flex-col items-center justify-center gap-3 py-24 text-center">
<div className="relative">
<div className="w-16 h-16 rounded-full bg-surface-100 dark:bg-surface-800 flex items-center justify-center">
<Database className="w-8 h-8 text-surface-400 dark:text-surface-500" />
</div>
<span className="absolute -bottom-0.5 -right-0.5 w-4 h-4 rounded-full border-2 border-surface-0 dark:border-surface-900 bg-primary animate-pulse" />
</div>
<div>
<p className="m-0 font-semibold text-surface-900 dark:text-surface-0">Loading tree</p>
<p className="mt-1 text-sm text-surface-500 dark:text-surface-400">Hang tight, the data is on its way.</p>
</div>
</div>
</DataTable.Cell>
</DataTable.Row>
</DataTable.EmptyTBody>
</DataTable.Table>
</DataTable.TableContainer>
</DataTable.Root>
</div>
);
}
Horizontal#
'use client';
import { ChevronDown } from '@primeicons/react/chevron-down';
import { Database } from '@primeicons/react/database';
import { ChevronRight } from '@primeicons/react/chevron-right';
import { NodeService, type TreeTableNode } from '@/shared/services/node.service';
import type { DataTableExpansionEvent } from '@primereact/ui/datatable';
import { DataTable } from '@primereact/ui/datatable';
import { Tag } from '@primereact/ui/tag';
import * as React from 'react';
import { File } from '@primeicons/react/file';
import { Folder } from '@primeicons/react/folder';
import { Image as ImageIcon } from '@primeicons/react/image';
import { Video } from '@primeicons/react/video';
const typeSeverity: Record<string, 'info' | 'warn' | 'success' | 'secondary'> = {
Folder: 'warn',
Document: 'info',
Resume: 'info',
Text: 'secondary',
Application: 'info',
Picture: 'success',
Video: 'success',
Zip: 'secondary',
PDF: 'info',
Link: 'secondary'
};
function TypeIcon({ type }: { type: string }) {
if (type === 'Picture') return <ImageIcon size={16} />;
if (type === 'Video') return <Video size={16} />;
if (type === 'Folder') return <Folder size={16} />;
return <File size={16} />;
}
export default function ScrollHorizontalDemo() {
const [nodes, setNodes] = React.useState<TreeTableNode[]>([]);
const [expandedKeys, setExpandedKeys] = React.useState<Record<string, boolean>>({ '0': true });
React.useEffect(() => {
setNodes(NodeService.getTreeTableNodesData());
}, []);
return (
<div className="w-full">
<DataTable.Root
data={nodes}
dataKey="key"
treeMode
expandedKeys={expandedKeys}
onExpandedChange={(e: DataTableExpansionEvent) => setExpandedKeys(e.value)}
scrollable
scrollHeight="400px"
>
<DataTable.TableContainer>
<DataTable.Table>
<DataTable.THead>
<DataTable.THeadRow>
<DataTable.THeadCell style={{ minWidth: '20rem' }}>
<DataTable.THeadTitle>Name</DataTable.THeadTitle>
</DataTable.THeadCell>
<DataTable.THeadCell style={{ minWidth: '10rem' }}>
<DataTable.THeadTitle>Size</DataTable.THeadTitle>
</DataTable.THeadCell>
<DataTable.THeadCell style={{ minWidth: '12rem' }}>
<DataTable.THeadTitle>Type</DataTable.THeadTitle>
</DataTable.THeadCell>
<DataTable.THeadCell style={{ minWidth: '14rem' }}>
<DataTable.THeadTitle>Key</DataTable.THeadTitle>
</DataTable.THeadCell>
<DataTable.THeadCell style={{ minWidth: '12rem' }}>
<DataTable.THeadTitle>Level</DataTable.THeadTitle>
</DataTable.THeadCell>
<DataTable.THeadCell style={{ minWidth: '12rem' }}>
<DataTable.THeadTitle>Has Children</DataTable.THeadTitle>
</DataTable.THeadCell>
</DataTable.THeadRow>
</DataTable.THead>
<DataTable.TBody>
{({ item }: { item: any }) => (
<DataTable.Row key={String(item.key)}>
<DataTable.Cell>
<div className="flex items-center gap-2">
<DataTable.RowToggle>
<DataTable.RowToggleIndicator match="expanded">
<ChevronDown />
</DataTable.RowToggleIndicator>
<DataTable.RowToggleIndicator match="collapsed">
<ChevronRight />
</DataTable.RowToggleIndicator>
</DataTable.RowToggle>
<TypeIcon type={String(item.type)} />
<span className={item.type === 'Folder' ? 'font-medium' : ''}>{String(item.name)}</span>
</div>
</DataTable.Cell>
<DataTable.Cell>
<span className="text-sm text-surface-500 dark:text-surface-400">{String(item.size)}</span>
</DataTable.Cell>
<DataTable.Cell>
<Tag severity={typeSeverity[String(item.type)] ?? 'secondary'}>{String(item.type)}</Tag>
</DataTable.Cell>
<DataTable.Cell>
<code className="text-xs text-surface-500 dark:text-surface-400">{String(item.key)}</code>
</DataTable.Cell>
<DataTable.Cell>{Number(item._treeLevel ?? 0)}</DataTable.Cell>
<DataTable.Cell>{item._treeHasChildren ? 'Yes' : 'No'}</DataTable.Cell>
</DataTable.Row>
)}
</DataTable.TBody>
<DataTable.EmptyTBody>
<DataTable.Row>
<DataTable.Cell colSpan={6}>
<div className="flex flex-col items-center justify-center gap-3 py-24 text-center">
<div className="relative">
<div className="w-16 h-16 rounded-full bg-surface-100 dark:bg-surface-800 flex items-center justify-center">
<Database className="w-8 h-8 text-surface-400 dark:text-surface-500" />
</div>
<span className="absolute -bottom-0.5 -right-0.5 w-4 h-4 rounded-full border-2 border-surface-0 dark:border-surface-900 bg-primary animate-pulse" />
</div>
<div>
<p className="m-0 font-semibold text-surface-900 dark:text-surface-0">Loading tree</p>
<p className="mt-1 text-sm text-surface-500 dark:text-surface-400">Hang tight, the data is on its way.</p>
</div>
</div>
</DataTable.Cell>
</DataTable.Row>
</DataTable.EmptyTBody>
</DataTable.Table>
</DataTable.TableContainer>
</DataTable.Root>
</div>
);
}
Flex#
'use client';
import { ChevronDown } from '@primeicons/react/chevron-down';
import { Database } from '@primeicons/react/database';
import { ChevronRight } from '@primeicons/react/chevron-right';
import { NodeService, type TreeTableNode } from '@/shared/services/node.service';
import type { DataTableExpansionEvent } from '@primereact/ui/datatable';
import { DataTable } from '@primereact/ui/datatable';
import { Tag } from '@primereact/ui/tag';
import * as React from 'react';
import { File } from '@primeicons/react/file';
import { Folder } from '@primeicons/react/folder';
import { Image as ImageIcon } from '@primeicons/react/image';
import { Video } from '@primeicons/react/video';
const typeSeverity: Record<string, 'info' | 'warn' | 'success' | 'secondary'> = {
Folder: 'warn',
Document: 'info',
Resume: 'info',
Text: 'secondary',
Application: 'info',
Picture: 'success',
Video: 'success',
Zip: 'secondary',
PDF: 'info',
Link: 'secondary'
};
function TypeIcon({ type }: { type: string }) {
if (type === 'Picture') return <ImageIcon size={16} />;
if (type === 'Video') return <Video size={16} />;
if (type === 'Folder') return <Folder size={16} />;
return <File size={16} />;
}
export default function ScrollFlexDemo() {
const [nodes, setNodes] = React.useState<TreeTableNode[]>([]);
const [expandedKeys, setExpandedKeys] = React.useState<Record<string, boolean>>({ '0': true });
React.useEffect(() => {
setNodes(NodeService.getTreeTableNodesData());
}, []);
return (
<div className="card flex flex-col h-[24rem]">
<DataTable.Root
data={nodes}
dataKey="key"
treeMode
expandedKeys={expandedKeys}
onExpandedChange={(e: DataTableExpansionEvent) => setExpandedKeys(e.value)}
scrollable
scrollHeight="flex"
>
<DataTable.TableContainer>
<DataTable.Table style={{ minWidth: '40rem' }}>
<DataTable.THead>
<DataTable.THeadRow>
<DataTable.THeadCell style={{ width: '50%' }}>
<DataTable.THeadTitle>Name</DataTable.THeadTitle>
</DataTable.THeadCell>
<DataTable.THeadCell style={{ width: '20%' }}>
<DataTable.THeadTitle>Size</DataTable.THeadTitle>
</DataTable.THeadCell>
<DataTable.THeadCell style={{ width: '30%' }}>
<DataTable.THeadTitle>Type</DataTable.THeadTitle>
</DataTable.THeadCell>
</DataTable.THeadRow>
</DataTable.THead>
<DataTable.TBody>
{({ item }: { item: any }) => (
<DataTable.Row key={String(item.key)}>
<DataTable.Cell>
<div className="flex items-center gap-2">
<DataTable.RowToggle>
<DataTable.RowToggleIndicator match="expanded">
<ChevronDown />
</DataTable.RowToggleIndicator>
<DataTable.RowToggleIndicator match="collapsed">
<ChevronRight />
</DataTable.RowToggleIndicator>
</DataTable.RowToggle>
<TypeIcon type={String(item.type)} />
<span className={item.type === 'Folder' ? 'font-medium' : ''}>{String(item.name)}</span>
</div>
</DataTable.Cell>
<DataTable.Cell>
<span className="text-sm text-surface-500 dark:text-surface-400">{String(item.size)}</span>
</DataTable.Cell>
<DataTable.Cell>
<Tag severity={typeSeverity[String(item.type)] ?? 'secondary'}>{String(item.type)}</Tag>
</DataTable.Cell>
</DataTable.Row>
)}
</DataTable.TBody>
<DataTable.EmptyTBody>
<DataTable.Row>
<DataTable.Cell colSpan={3}>
<div className="flex flex-col items-center justify-center gap-3 py-24 text-center">
<div className="relative">
<div className="w-16 h-16 rounded-full bg-surface-100 dark:bg-surface-800 flex items-center justify-center">
<Database className="w-8 h-8 text-surface-400 dark:text-surface-500" />
</div>
<span className="absolute -bottom-0.5 -right-0.5 w-4 h-4 rounded-full border-2 border-surface-0 dark:border-surface-900 bg-primary animate-pulse" />
</div>
<div>
<p className="m-0 font-semibold text-surface-900 dark:text-surface-0">Loading tree</p>
<p className="mt-1 text-sm text-surface-500 dark:text-surface-400">Hang tight, the data is on its way.</p>
</div>
</div>
</DataTable.Cell>
</DataTable.Row>
</DataTable.EmptyTBody>
</DataTable.Table>
</DataTable.TableContainer>
</DataTable.Root>
</div>
);
}
Frozen Columns#
Pin the tree column (or any column) during horizontal scroll with the frozen prop on DataTable.Column.
'use client';
import { ChevronDown } from '@primeicons/react/chevron-down';
import { ChevronRight } from '@primeicons/react/chevron-right';
import { Database } from '@primeicons/react/database';
import { File } from '@primeicons/react/file';
import { Folder } from '@primeicons/react/folder';
import { Image as ImageIcon } from '@primeicons/react/image';
import { Video } from '@primeicons/react/video';
import type { DataTableExpansionEvent } from '@primereact/ui/datatable';
import { DataTable } from '@primereact/ui/datatable';
import { Tag } from '@primereact/ui/tag';
import * as React from 'react';
interface TreeNode {
key: string;
data: { name: string; size: string; type: string; owner: string; modified: string };
children?: TreeNode[];
}
const typeSeverity: Record<string, 'info' | 'warn' | 'success' | 'secondary'> = {
Folder: 'warn',
Document: 'info',
Text: 'secondary',
Picture: 'success',
Video: 'success'
};
function TypeIcon({ type }: { type: string }) {
if (type === 'Picture') return <ImageIcon size={16} />;
if (type === 'Video') return <Video size={16} />;
if (type === 'Folder') return <Folder size={16} />;
return <File size={16} />;
}
const owners = ['Amy Elsner', 'Anna Fali', 'Bernardo Dominic', 'Ioni Bowcher', 'Stephen Shaw'];
const types = ['Document', 'Text', 'Picture', 'Video'];
const nodes: TreeNode[] = Array.from({ length: 10 }, (_, i) => ({
key: String(i),
data: {
name: `Folder ${i + 1}`,
size: '—',
type: 'Folder',
owner: owners[i % owners.length],
modified: `2026-0${(i % 9) + 1}-15`
},
children: Array.from({ length: 5 }, (_, j) => {
const type = types[j % types.length];
const size = ((i * 37 + j * 113) % 1950) + 50;
return {
key: `${i}-${j}`,
data: {
name: `${type} ${i + 1}.${j + 1}`,
size: `${size}kb`,
type,
owner: owners[(i + j) % owners.length],
modified: `2026-0${((i + j) % 9) + 1}-20`
}
};
})
}));
export default function TreeTableFrozenColumnsDemo() {
const [expandedKeys, setExpandedKeys] = React.useState<Record<string, boolean>>({ '0': true, '1': true });
return (
<div className="w-full">
<DataTable.Root
data={nodes}
dataKey="key"
treeMode
scrollable
scrollHeight="400px"
expandedKeys={expandedKeys}
onExpandedChange={(e: DataTableExpansionEvent) => setExpandedKeys(e.value)}
>
<DataTable.TableContainer>
<DataTable.Table>
<DataTable.THead>
<DataTable.THeadRow>
<DataTable.THeadCell frozen style={{ minWidth: '300px' }}>
<DataTable.THeadTitle>Name</DataTable.THeadTitle>
</DataTable.THeadCell>
<DataTable.THeadCell style={{ minWidth: '160px' }}>
<DataTable.THeadTitle>Size</DataTable.THeadTitle>
</DataTable.THeadCell>
<DataTable.THeadCell style={{ minWidth: '160px' }}>
<DataTable.THeadTitle>Type</DataTable.THeadTitle>
</DataTable.THeadCell>
<DataTable.THeadCell style={{ minWidth: '200px' }}>
<DataTable.THeadTitle>Owner</DataTable.THeadTitle>
</DataTable.THeadCell>
<DataTable.THeadCell style={{ minWidth: '160px' }}>
<DataTable.THeadTitle>Modified</DataTable.THeadTitle>
</DataTable.THeadCell>
<DataTable.THeadCell style={{ minWidth: '140px' }}>
<DataTable.THeadTitle>Actions</DataTable.THeadTitle>
</DataTable.THeadCell>
</DataTable.THeadRow>
</DataTable.THead>
<DataTable.TBody>
{({ item }: { item: any }) => (
<DataTable.Row key={String(item.key)}>
<DataTable.Cell frozen>
<div className="flex items-center gap-2">
<DataTable.RowToggle>
<DataTable.RowToggleIndicator match="expanded">
<ChevronDown />
</DataTable.RowToggleIndicator>
<DataTable.RowToggleIndicator match="collapsed">
<ChevronRight />
</DataTable.RowToggleIndicator>
</DataTable.RowToggle>
<TypeIcon type={String(item.type)} />
<span className={item.type === 'Folder' ? 'font-semibold' : ''}>{String(item.name)}</span>
</div>
</DataTable.Cell>
<DataTable.Cell>
<span className="text-sm text-surface-500 dark:text-surface-400">{String(item.size)}</span>
</DataTable.Cell>
<DataTable.Cell>
<Tag severity={typeSeverity[String(item.type)] ?? 'secondary'}>{String(item.type)}</Tag>
</DataTable.Cell>
<DataTable.Cell>{String(item.owner)}</DataTable.Cell>
<DataTable.Cell>{String(item.modified)}</DataTable.Cell>
<DataTable.Cell>
<span className="text-xs text-surface-500 dark:text-surface-400">Open · Share</span>
</DataTable.Cell>
</DataTable.Row>
)}
</DataTable.TBody>
<DataTable.EmptyTBody>
<DataTable.Row>
<DataTable.Cell colSpan={6}>
<div className="flex flex-col items-center justify-center gap-3 py-24 text-center">
<div className="relative">
<div className="w-16 h-16 rounded-full bg-surface-100 dark:bg-surface-800 flex items-center justify-center">
<Database className="w-8 h-8 text-surface-400 dark:text-surface-500" />
</div>
<span className="absolute -bottom-0.5 -right-0.5 w-4 h-4 rounded-full border-2 border-surface-0 dark:border-surface-900 bg-primary animate-pulse" />
</div>
<div>
<p className="m-0 font-semibold text-surface-900 dark:text-surface-0">Loading tree</p>
<p className="mt-1 text-sm text-surface-500 dark:text-surface-400">Hang tight, the data is on its way.</p>
</div>
</div>
</DataTable.Cell>
</DataTable.Row>
</DataTable.EmptyTBody>
</DataTable.Table>
</DataTable.TableContainer>
</DataTable.Root>
</div>
);
}
Multiple columns can be frozen on both sides, pass alignFrozen="right" to pin to the right edge.
'use client';
import { ChevronDown } from '@primeicons/react/chevron-down';
import { Database } from '@primeicons/react/database';
import { ChevronRight } from '@primeicons/react/chevron-right';
import { NodeService, type TreeTableNode } from '@/shared/services/node.service';
import type { DataTableExpansionEvent } from '@primereact/ui/datatable';
import { DataTable } from '@primereact/ui/datatable';
import { Tag } from '@primereact/ui/tag';
import * as React from 'react';
import { File } from '@primeicons/react/file';
import { Folder } from '@primeicons/react/folder';
import { Image as ImageIcon } from '@primeicons/react/image';
import { Video } from '@primeicons/react/video';
const typeSeverity: Record<string, 'info' | 'warn' | 'success' | 'secondary'> = {
Folder: 'warn',
Document: 'info',
Resume: 'info',
Text: 'secondary',
Application: 'info',
Picture: 'success',
Video: 'success',
Zip: 'secondary',
PDF: 'info',
Link: 'secondary'
};
function TypeIcon({ type }: { type: string }) {
if (type === 'Picture') return <ImageIcon size={16} />;
if (type === 'Video') return <Video size={16} />;
if (type === 'Folder') return <Folder size={16} />;
return <File size={16} />;
}
export default function ScrollFrozenColumnsMultiDemo() {
const [nodes, setNodes] = React.useState<TreeTableNode[]>([]);
const [expandedKeys, setExpandedKeys] = React.useState<Record<string, boolean>>({ '0': true });
React.useEffect(() => {
setNodes(NodeService.getTreeTableNodesData());
}, []);
return (
<div className="w-full">
<DataTable.Root
data={nodes}
dataKey="key"
treeMode
expandedKeys={expandedKeys}
onExpandedChange={(e: DataTableExpansionEvent) => setExpandedKeys(e.value)}
scrollable
scrollHeight="400px"
>
<DataTable.TableContainer>
<DataTable.Table style={{ minWidth: '1200px' }}>
<DataTable.THead>
<DataTable.THeadRow>
<DataTable.THeadCell frozen style={{ minWidth: '20rem' }}>
<DataTable.THeadTitle>Name</DataTable.THeadTitle>
</DataTable.THeadCell>
<DataTable.THeadCell style={{ minWidth: '10rem' }}>
<DataTable.THeadTitle>Size</DataTable.THeadTitle>
</DataTable.THeadCell>
<DataTable.THeadCell style={{ minWidth: '14rem' }}>
<DataTable.THeadTitle>Key</DataTable.THeadTitle>
</DataTable.THeadCell>
<DataTable.THeadCell style={{ minWidth: '10rem' }}>
<DataTable.THeadTitle>Level</DataTable.THeadTitle>
</DataTable.THeadCell>
<DataTable.THeadCell style={{ minWidth: '12rem' }}>
<DataTable.THeadTitle>Has Children</DataTable.THeadTitle>
</DataTable.THeadCell>
<DataTable.THeadCell frozen alignFrozen="right" style={{ minWidth: '10rem' }}>
<DataTable.THeadTitle>Type</DataTable.THeadTitle>
</DataTable.THeadCell>
</DataTable.THeadRow>
</DataTable.THead>
<DataTable.TBody>
{({ item }: { item: any }) => (
<DataTable.Row key={String(item.key)}>
<DataTable.Cell frozen>
<div className="flex items-center gap-2">
<DataTable.RowToggle>
<DataTable.RowToggleIndicator match="expanded">
<ChevronDown />
</DataTable.RowToggleIndicator>
<DataTable.RowToggleIndicator match="collapsed">
<ChevronRight />
</DataTable.RowToggleIndicator>
</DataTable.RowToggle>
<TypeIcon type={String(item.type)} />
<span className={item.type === 'Folder' ? 'font-medium' : ''}>{String(item.name)}</span>
</div>
</DataTable.Cell>
<DataTable.Cell>
<span className="text-sm text-surface-500 dark:text-surface-400">{String(item.size)}</span>
</DataTable.Cell>
<DataTable.Cell>
<code className="text-xs text-surface-500 dark:text-surface-400">{String(item.key)}</code>
</DataTable.Cell>
<DataTable.Cell>{Number(item._treeLevel ?? 0)}</DataTable.Cell>
<DataTable.Cell>{item._treeHasChildren ? 'Yes' : 'No'}</DataTable.Cell>
<DataTable.Cell frozen alignFrozen="right">
<Tag severity={typeSeverity[String(item.type)] ?? 'secondary'}>{String(item.type)}</Tag>
</DataTable.Cell>
</DataTable.Row>
)}
</DataTable.TBody>
<DataTable.EmptyTBody>
<DataTable.Row>
<DataTable.Cell colSpan={6}>
<div className="flex flex-col items-center justify-center gap-3 py-24 text-center">
<div className="relative">
<div className="w-16 h-16 rounded-full bg-surface-100 dark:bg-surface-800 flex items-center justify-center">
<Database className="w-8 h-8 text-surface-400 dark:text-surface-500" />
</div>
<span className="absolute -bottom-0.5 -right-0.5 w-4 h-4 rounded-full border-2 border-surface-0 dark:border-surface-900 bg-primary animate-pulse" />
</div>
<div>
<p className="m-0 font-semibold text-surface-900 dark:text-surface-0">Loading tree</p>
<p className="mt-1 text-sm text-surface-500 dark:text-surface-400">Hang tight, the data is on its way.</p>
</div>
</div>
</DataTable.Cell>
</DataTable.Row>
</DataTable.EmptyTBody>
</DataTable.Table>
</DataTable.TableContainer>
</DataTable.Root>
</div>
);
}
Frozen Rows#
Keep specific nodes pinned above the scrollable body by rendering them inside DataTable.FrozenTBody.
'use client';
import { File } from '@primeicons/react/file';
import { Database } from '@primeicons/react/database';
import { Folder } from '@primeicons/react/folder';
import { Image as ImageIcon } from '@primeicons/react/image';
import { Lock } from '@primeicons/react/lock';
import { LockOpen } from '@primeicons/react/lock-open';
import { Video } from '@primeicons/react/video';
import { Button } from '@primereact/ui/button';
import { DataTable } from '@primereact/ui/datatable';
import { Tag } from '@primereact/ui/tag';
import * as React from 'react';
interface TreeNode {
key: string;
data: { name: string; size: string; type: string };
children?: TreeNode[];
}
const typeSeverity: Record<string, 'info' | 'warn' | 'success' | 'secondary'> = {
Folder: 'warn',
Document: 'info',
Text: 'secondary',
Picture: 'success',
Video: 'success'
};
function TypeIcon({ type }: { type: string }) {
if (type === 'Picture') return <ImageIcon size={16} />;
if (type === 'Video') return <Video size={16} />;
if (type === 'Folder') return <Folder size={16} />;
return <File size={16} />;
}
const types = ['Document', 'Text', 'Picture', 'Video'];
const rootFolders: TreeNode[] = Array.from({ length: 10 }, (_, i) => ({
key: String(i),
data: { name: `Folder ${i + 1}`, size: '—', type: 'Folder' },
children: Array.from({ length: 4 }, (_, j) => {
const type = types[j % types.length];
const size = ((i * 37 + j * 113) % 1950) + 50;
return {
key: `${i}-${j}`,
data: { name: `${type} ${i + 1}.${j + 1}`, size: `${size}kb`, type }
};
})
}));
function flattenBranch(node: TreeNode, level = 0): Array<TreeNode & { level: number }> {
const out: Array<TreeNode & { level: number }> = [{ ...node, level }];
if (node.children) for (const child of node.children) out.push(...flattenBranch(child, level + 1));
return out;
}
export default function TreeTableFrozenRowsDemo() {
const [lockedKeys, setLockedKeys] = React.useState<string[]>(['0']);
const toggleLock = (key: string) => {
setLockedKeys((prev) => (prev.includes(key) ? prev.filter((k) => k !== key) : prev.length >= 1 ? [key] : [...prev, key]));
};
const lockedFolders = rootFolders.filter((f) => lockedKeys.includes(f.key));
const unlockedFolders = rootFolders.filter((f) => !lockedKeys.includes(f.key));
const renderFrozenRow = (node: TreeNode & { level: number }) => (
<DataTable.Row key={node.key}>
<DataTable.Cell>
<div className="flex items-center gap-2" style={{ paddingInlineStart: `${node.level * 1.25}rem` }}>
<TypeIcon type={node.data.type} />
<span className={node.data.type === 'Folder' ? 'font-semibold' : ''}>{node.data.name}</span>
</div>
</DataTable.Cell>
<DataTable.Cell>
<span className="text-sm text-surface-500 dark:text-surface-400">{node.data.size}</span>
</DataTable.Cell>
<DataTable.Cell>
<Tag severity={typeSeverity[node.data.type] ?? 'secondary'}>{node.data.type}</Tag>
</DataTable.Cell>
<DataTable.Cell style={{ width: '4rem', textAlign: 'center' }}>
{node.level === 0 && (
<Button variant="text" severity="secondary" size="small" iconOnly onClick={() => toggleLock(node.key)} aria-label="Unlock folder">
<LockOpen />
</Button>
)}
</DataTable.Cell>
</DataTable.Row>
);
const renderBodyRow = (item: any) => (
<DataTable.Row key={String(item.key)}>
<DataTable.Cell>
<div className="flex items-center gap-2">
<TypeIcon type={String(item.type)} />
<span className={item.type === 'Folder' ? 'font-semibold' : ''}>{String(item.name)}</span>
</div>
</DataTable.Cell>
<DataTable.Cell>
<span className="text-sm text-surface-500 dark:text-surface-400">{String(item.size)}</span>
</DataTable.Cell>
<DataTable.Cell>
<Tag severity={typeSeverity[String(item.type)] ?? 'secondary'}>{String(item.type)}</Tag>
</DataTable.Cell>
<DataTable.Cell style={{ width: '4rem', textAlign: 'center' }}>
{item._treeLevel === 0 && (
<Button
variant="text"
severity="secondary"
size="small"
iconOnly
disabled={lockedKeys.length >= 1}
onClick={() => toggleLock(String(item.key))}
aria-label="Lock folder"
>
<Lock />
</Button>
)}
</DataTable.Cell>
</DataTable.Row>
);
return (
<div className="w-full">
<DataTable.Root data={unlockedFolders} dataKey="key" treeMode scrollable scrollHeight="400px">
<DataTable.TableContainer>
<DataTable.Table style={{ minWidth: '50rem' }}>
<DataTable.THead>
<DataTable.THeadRow>
<DataTable.THeadCell style={{ width: '50%' }}>
<DataTable.THeadTitle>Name</DataTable.THeadTitle>
</DataTable.THeadCell>
<DataTable.THeadCell style={{ width: '20%' }}>
<DataTable.THeadTitle>Size</DataTable.THeadTitle>
</DataTable.THeadCell>
<DataTable.THeadCell style={{ width: '20%' }}>
<DataTable.THeadTitle>Type</DataTable.THeadTitle>
</DataTable.THeadCell>
<DataTable.THeadCell style={{ width: '4rem' }} />
</DataTable.THeadRow>
</DataTable.THead>
<DataTable.FrozenTBody>
{lockedFolders.flatMap((folder) => flattenBranch(folder).map((node) => renderFrozenRow(node)))}
</DataTable.FrozenTBody>
<DataTable.TBody>{({ item }: { item: any }) => renderBodyRow(item)}</DataTable.TBody>
<DataTable.EmptyTBody>
<DataTable.Row>
<DataTable.Cell colSpan={4}>
<div className="flex flex-col items-center justify-center gap-3 py-24 text-center">
<div className="relative">
<div className="w-16 h-16 rounded-full bg-surface-100 dark:bg-surface-800 flex items-center justify-center">
<Database className="w-8 h-8 text-surface-400 dark:text-surface-500" />
</div>
<span className="absolute -bottom-0.5 -right-0.5 w-4 h-4 rounded-full border-2 border-surface-0 dark:border-surface-900 bg-primary animate-pulse" />
</div>
<div>
<p className="m-0 font-semibold text-surface-900 dark:text-surface-0">Loading tree</p>
<p className="mt-1 text-sm text-surface-500 dark:text-surface-400">Hang tight, the data is on its way.</p>
</div>
</div>
</DataTable.Cell>
</DataTable.Row>
</DataTable.EmptyTBody>
</DataTable.Table>
</DataTable.TableContainer>
</DataTable.Root>
</div>
);
}
Editing#
Both editMode="cell" and editMode="row" compose with treeMode. Editing state is keyed by the row key, so collapsing or expanding other branches never drops edits in flight.
Cell#
'use client';
import { ChevronDown } from '@primeicons/react/chevron-down';
import { Database } from '@primeicons/react/database';
import { ChevronRight } from '@primeicons/react/chevron-right';
import { File } from '@primeicons/react/file';
import { Folder } from '@primeicons/react/folder';
import { Image as ImageIcon } from '@primeicons/react/image';
import { Video } from '@primeicons/react/video';
import type { DataTableExpansionEvent } from '@primereact/ui/datatable';
import { DataTable } from '@primereact/ui/datatable';
import { InputText } from '@primereact/ui/inputtext';
import { Tag } from '@primereact/ui/tag';
import * as React from 'react';
const typeSeverity: Record<string, 'info' | 'warn' | 'success' | 'secondary'> = {
Folder: 'warn',
Document: 'info',
Text: 'secondary',
Picture: 'success',
Video: 'success'
};
function TypeIcon({ type }: { type: string }) {
if (type === 'Picture') return <ImageIcon size={16} />;
if (type === 'Video') return <Video size={16} />;
if (type === 'Folder') return <Folder size={16} />;
return <File size={16} />;
}
interface TreeNode {
key: string;
data: {
name: string;
size: string;
type: string;
};
children?: TreeNode[];
}
const initialNodes: TreeNode[] = [
{
key: '0',
data: { name: 'Documents', size: '75kb', type: 'Folder' },
children: [
{
key: '0-0',
data: { name: 'Work', size: '55kb', type: 'Folder' },
children: [
{ key: '0-0-0', data: { name: 'Expenses.doc', size: '30kb', type: 'Document' } },
{ key: '0-0-1', data: { name: 'Resume.doc', size: '25kb', type: 'Document' } }
]
},
{
key: '0-1',
data: { name: 'Home', size: '20kb', type: 'Folder' },
children: [{ key: '0-1-0', data: { name: 'Invoices.txt', size: '20kb', type: 'Text' } }]
}
]
},
{
key: '1',
data: { name: 'Pictures', size: '150kb', type: 'Folder' },
children: [
{ key: '1-0', data: { name: 'barcelona.jpg', size: '90kb', type: 'Picture' } },
{ key: '1-1', data: { name: 'primeui.png', size: '30kb', type: 'Picture' } },
{ key: '1-2', data: { name: 'optimus.jpg', size: '30kb', type: 'Picture' } }
]
}
];
function updateNode(nodes: TreeNode[], key: string, field: string, value: unknown): TreeNode[] {
return nodes.map((node) => {
if (node.key === key) {
return { ...node, data: { ...node.data, [field]: value } };
}
if (node.children) {
return { ...node, children: updateNode(node.children, key, field, value) };
}
return node;
});
}
export default function TreeTableCellEditDemo() {
const [nodes, setNodes] = React.useState<TreeNode[]>(initialNodes);
const [expandedKeys, setExpandedKeys] = React.useState<Record<string, boolean>>({ '0': true, '0-0': true });
const pendingValueRef = React.useRef<Record<string, string>>({});
const handleCellEditComplete = React.useCallback((e: { rowIndex: number; field: string; rowData?: Record<string, unknown> }) => {
const key = e.rowData?.key as string | undefined;
if (!key) return;
const editKey = `${key}-${e.field}`;
const newValue = pendingValueRef.current[editKey];
if (newValue !== undefined) {
setNodes((prev) => updateNode(prev, key, e.field, newValue));
delete pendingValueRef.current[editKey];
}
}, []);
return (
<div className="w-full">
<DataTable.Root
data={nodes}
dataKey="key"
treeMode
expandedKeys={expandedKeys}
onExpandedChange={(e: DataTableExpansionEvent) => setExpandedKeys(e.value)}
editMode="cell"
onCellEditComplete={handleCellEditComplete}
>
<DataTable.TableContainer>
<DataTable.Table style={{ minWidth: '50rem' }}>
<DataTable.THead>
<DataTable.THeadRow>
<DataTable.THeadCell style={{ width: '50%' }}>
<DataTable.THeadTitle>Name</DataTable.THeadTitle>
</DataTable.THeadCell>
<DataTable.THeadCell style={{ width: '25%' }}>
<DataTable.THeadTitle>Size</DataTable.THeadTitle>
</DataTable.THeadCell>
<DataTable.THeadCell style={{ width: '25%' }}>
<DataTable.THeadTitle>Type</DataTable.THeadTitle>
</DataTable.THeadCell>
</DataTable.THeadRow>
</DataTable.THead>
<DataTable.TBody>
{({ item, index }: { item: any; index: number }) => (
<DataTable.Row key={String(item.key)}>
<DataTable.Cell>
<div className="flex items-center gap-2">
<DataTable.RowToggle>
<DataTable.RowToggleIndicator match="expanded">
<ChevronDown />
</DataTable.RowToggleIndicator>
<DataTable.RowToggleIndicator match="collapsed">
<ChevronRight />
</DataTable.RowToggleIndicator>
</DataTable.RowToggle>
<TypeIcon type={String(item.type)} />
<DataTable.CellEditor field="name" rowIndex={index} rowData={item} style={{ flex: 1 }}>
<DataTable.CellEditorDisplay>{String(item.name)}</DataTable.CellEditorDisplay>
<DataTable.CellEditorContent>
<InputText
defaultValue={String(item.name)}
onChange={(e: any) => {
pendingValueRef.current[`${item.key}-name`] = e.target.value;
}}
size="small"
fluid
/>
</DataTable.CellEditorContent>
</DataTable.CellEditor>
</div>
</DataTable.Cell>
<DataTable.Cell>
<DataTable.CellEditor field="size" rowIndex={index} rowData={item}>
<DataTable.CellEditorDisplay>{String(item.size)}</DataTable.CellEditorDisplay>
<DataTable.CellEditorContent>
<InputText
defaultValue={String(item.size)}
onChange={(e: any) => {
pendingValueRef.current[`${item.key}-size`] = e.target.value;
}}
size="small"
fluid
/>
</DataTable.CellEditorContent>
</DataTable.CellEditor>
</DataTable.Cell>
<DataTable.Cell>
<DataTable.CellEditor field="type" rowIndex={index} rowData={item}>
<DataTable.CellEditorDisplay>
<Tag severity={typeSeverity[String(item.type)] ?? 'secondary'}>{String(item.type)}</Tag>
</DataTable.CellEditorDisplay>
<DataTable.CellEditorContent>
<InputText
defaultValue={String(item.type)}
onChange={(e: any) => {
pendingValueRef.current[`${item.key}-type`] = e.target.value;
}}
size="small"
fluid
/>
</DataTable.CellEditorContent>
</DataTable.CellEditor>
</DataTable.Cell>
</DataTable.Row>
)}
</DataTable.TBody>
<DataTable.EmptyTBody>
<DataTable.Row>
<DataTable.Cell colSpan={3}>
<div className="flex flex-col items-center justify-center gap-3 py-24 text-center">
<div className="relative">
<div className="w-16 h-16 rounded-full bg-surface-100 dark:bg-surface-800 flex items-center justify-center">
<Database className="w-8 h-8 text-surface-400 dark:text-surface-500" />
</div>
<span className="absolute -bottom-0.5 -right-0.5 w-4 h-4 rounded-full border-2 border-surface-0 dark:border-surface-900 bg-primary animate-pulse" />
</div>
<div>
<p className="m-0 font-semibold text-surface-900 dark:text-surface-0">Loading tree</p>
<p className="mt-1 text-sm text-surface-500 dark:text-surface-400">Hang tight, the data is on its way.</p>
</div>
</div>
</DataTable.Cell>
</DataTable.Row>
</DataTable.EmptyTBody>
</DataTable.Table>
</DataTable.TableContainer>
</DataTable.Root>
</div>
);
}
Cell editing composes with row selection.
'use client';
import { Check } from '@primeicons/react/check';
import { Database } from '@primeicons/react/database';
import { ChevronDown } from '@primeicons/react/chevron-down';
import { ChevronRight } from '@primeicons/react/chevron-right';
import { Minus } from '@primeicons/react/minus';
import { NodeService, type TreeTableNode } from '@/shared/services/node.service';
import type { DataTableExpansionEvent, DataTableSelectionEvent } from '@primereact/ui/datatable';
import type { CheckboxRootChangeEvent } from '@primereact/ui/checkbox';
import type { DataTableSelectionInstance } from '@primereact/ui/datatable';
import { Badge } from '@primereact/ui/badge';
import { Checkbox } from '@primereact/ui/checkbox';
import { DataTable } from '@primereact/ui/datatable';
import { InputText } from '@primereact/ui/inputtext';
import { Tag } from '@primereact/ui/tag';
import * as React from 'react';
import { File } from '@primeicons/react/file';
import { Folder } from '@primeicons/react/folder';
import { Image as ImageIcon } from '@primeicons/react/image';
import { Video } from '@primeicons/react/video';
const typeSeverity: Record<string, 'info' | 'warn' | 'success' | 'secondary'> = {
Folder: 'warn',
Document: 'info',
Resume: 'info',
Text: 'secondary',
Application: 'info',
Picture: 'success',
Video: 'success',
Zip: 'secondary',
PDF: 'info',
Link: 'secondary'
};
function TypeIcon({ type }: { type: string }) {
if (type === 'Picture') return <ImageIcon size={16} />;
if (type === 'Video') return <Video size={16} />;
if (type === 'Folder') return <Folder size={16} />;
return <File size={16} />;
}
function updateNode(nodes: TreeTableNode[], key: string, field: string, value: unknown): TreeTableNode[] {
return nodes.map((n) => {
if (n.key === key) return { ...n, data: { ...n.data, [field]: value } };
if (n.children) return { ...n, children: updateNode(n.children, key, field, value) };
return n;
});
}
export default function CellEditSelectionDemo() {
const [nodes, setNodes] = React.useState<TreeTableNode[]>([]);
const [selectionKeys, setSelectionKeys] = React.useState<Record<string, boolean>>({});
const [expandedKeys, setExpandedKeys] = React.useState<Record<string, boolean>>({ '0': true });
const pendingValueRef = React.useRef<Record<string, string>>({});
React.useEffect(() => {
setNodes(NodeService.getTreeTableNodesData().slice(0, 5));
}, []);
const handleCellEditComplete = React.useCallback((e: { rowIndex: number; field: string; rowData?: Record<string, unknown> }) => {
const key = e.rowData?.key as string | undefined;
if (!key) return;
const editKey = `${key}-${e.field}`;
const newValue = pendingValueRef.current[editKey];
if (newValue !== undefined) {
setNodes((prev) => updateNode(prev, key, e.field, newValue));
delete pendingValueRef.current[editKey];
}
}, []);
const selectedCount = Object.values(selectionKeys).filter(Boolean).length;
return (
<div className="w-full">
<div className="flex items-center gap-2 mb-3">
<span className="text-sm font-medium">Selected</span>
<Badge severity={selectedCount ? 'info' : 'secondary'}>{selectedCount}</Badge>
</div>
<DataTable.Root
data={nodes}
dataKey="key"
treeMode
expandedKeys={expandedKeys}
onExpandedChange={(e: DataTableExpansionEvent) => setExpandedKeys(e.value)}
selectionMode="multiple"
selectionKeys={selectionKeys}
onSelectionChange={(e: DataTableSelectionEvent) => setSelectionKeys(e.value)}
editMode="cell"
onCellEditComplete={handleCellEditComplete}
>
<DataTable.TableContainer>
<DataTable.Table style={{ minWidth: '40rem' }}>
<DataTable.THead>
<DataTable.THeadRow>
<DataTable.THeadCell style={{ width: '3rem' }}>
<DataTable.Selection>
{({ isAllSelected, isSomeSelected, toggleAll }: DataTableSelectionInstance) => (
<Checkbox.Root
checked={isAllSelected}
indeterminate={isSomeSelected && !isAllSelected}
onCheckedChange={(e: CheckboxRootChangeEvent) => toggleAll(e.originalEvent)}
>
<Checkbox.Box>
<Checkbox.Indicator match="checked">
<Check />
</Checkbox.Indicator>
<Checkbox.Indicator match="indeterminate">
<Minus />
</Checkbox.Indicator>
</Checkbox.Box>
</Checkbox.Root>
)}
</DataTable.Selection>
</DataTable.THeadCell>
<DataTable.THeadCell style={{ width: '50%' }}>
<DataTable.THeadTitle>Name</DataTable.THeadTitle>
</DataTable.THeadCell>
<DataTable.THeadCell style={{ width: '25%' }}>
<DataTable.THeadTitle>Size</DataTable.THeadTitle>
</DataTable.THeadCell>
<DataTable.THeadCell style={{ width: '25%' }}>
<DataTable.THeadTitle>Type</DataTable.THeadTitle>
</DataTable.THeadCell>
</DataTable.THeadRow>
</DataTable.THead>
<DataTable.TBody>
{({ item, index }: { item: any; index: number }) => (
<DataTable.Row key={String(item.key)}>
<DataTable.Cell>
<DataTable.Selection>
{({ isSelected, isPartiallySelected, toggle }: DataTableSelectionInstance) => (
<Checkbox.Root
checked={isSelected}
indeterminate={isPartiallySelected}
onCheckedChange={(e: CheckboxRootChangeEvent) => toggle(e.originalEvent)}
>
<Checkbox.Box>
<Checkbox.Indicator match="checked">
<Check />
</Checkbox.Indicator>
<Checkbox.Indicator match="indeterminate">
<Minus />
</Checkbox.Indicator>
</Checkbox.Box>
</Checkbox.Root>
)}
</DataTable.Selection>
</DataTable.Cell>
<DataTable.Cell>
<div className="flex items-center gap-2">
<DataTable.RowToggle>
<DataTable.RowToggleIndicator match="expanded">
<ChevronDown />
</DataTable.RowToggleIndicator>
<DataTable.RowToggleIndicator match="collapsed">
<ChevronRight />
</DataTable.RowToggleIndicator>
</DataTable.RowToggle>
<TypeIcon type={String(item.type)} />
<DataTable.CellEditor field="name" rowIndex={index} rowData={item} style={{ flex: 1 }}>
<DataTable.CellEditorDisplay>{String(item.name)}</DataTable.CellEditorDisplay>
<DataTable.CellEditorContent>
<InputText
defaultValue={String(item.name)}
onChange={(e: any) => {
pendingValueRef.current[`${item.key}-name`] = e.target.value;
}}
size="small"
fluid
/>
</DataTable.CellEditorContent>
</DataTable.CellEditor>
</div>
</DataTable.Cell>
<DataTable.Cell>
<DataTable.CellEditor field="size" rowIndex={index} rowData={item}>
<DataTable.CellEditorDisplay>
<span className="text-sm text-surface-500 dark:text-surface-400">{String(item.size)}</span>
</DataTable.CellEditorDisplay>
<DataTable.CellEditorContent>
<InputText
defaultValue={String(item.size)}
onChange={(e: any) => {
pendingValueRef.current[`${item.key}-size`] = e.target.value;
}}
size="small"
fluid
/>
</DataTable.CellEditorContent>
</DataTable.CellEditor>
</DataTable.Cell>
<DataTable.Cell>
<Tag severity={typeSeverity[String(item.type)] ?? 'secondary'}>{String(item.type)}</Tag>
</DataTable.Cell>
</DataTable.Row>
)}
</DataTable.TBody>
<DataTable.EmptyTBody>
<DataTable.Row>
<DataTable.Cell colSpan={4}>
<div className="flex flex-col items-center justify-center gap-3 py-24 text-center">
<div className="relative">
<div className="w-16 h-16 rounded-full bg-surface-100 dark:bg-surface-800 flex items-center justify-center">
<Database className="w-8 h-8 text-surface-400 dark:text-surface-500" />
</div>
<span className="absolute -bottom-0.5 -right-0.5 w-4 h-4 rounded-full border-2 border-surface-0 dark:border-surface-900 bg-primary animate-pulse" />
</div>
<div>
<p className="m-0 font-semibold text-surface-900 dark:text-surface-0">Loading tree</p>
<p className="mt-1 text-sm text-surface-500 dark:text-surface-400">Hang tight, the data is on its way.</p>
</div>
</div>
</DataTable.Cell>
</DataTable.Row>
</DataTable.EmptyTBody>
</DataTable.Table>
</DataTable.TableContainer>
</DataTable.Root>
</div>
);
}
Row#
'use client';
import { Check } from '@primeicons/react/check';
import { Database } from '@primeicons/react/database';
import { ChevronDown } from '@primeicons/react/chevron-down';
import { ChevronRight } from '@primeicons/react/chevron-right';
import { File } from '@primeicons/react/file';
import { Folder } from '@primeicons/react/folder';
import { Image as ImageIcon } from '@primeicons/react/image';
import { Video } from '@primeicons/react/video';
import { Pencil } from '@primeicons/react/pencil';
import { Times } from '@primeicons/react/times';
import type { DataTableExpansionEvent, DataTableRowEditEvent } from '@primereact/ui/datatable';
import { Button } from '@primereact/ui/button';
import { DataTable } from '@primereact/ui/datatable';
import { InputText } from '@primereact/ui/inputtext';
import { Tag } from '@primereact/ui/tag';
import * as React from 'react';
const typeSeverity: Record<string, 'info' | 'warn' | 'success' | 'secondary'> = {
Folder: 'warn',
Document: 'info',
Text: 'secondary',
Picture: 'success'
};
function TypeIcon({ type }: { type: string }) {
if (type === 'Picture') return <ImageIcon size={16} />;
if (type === 'Video') return <Video size={16} />;
if (type === 'Folder') return <Folder size={16} />;
return <File size={16} />;
}
interface TreeNode {
key: string;
data: {
name: string;
size: string;
type: string;
};
children?: TreeNode[];
}
const initialNodes: TreeNode[] = [
{
key: '0',
data: { name: 'Documents', size: '75kb', type: 'Folder' },
children: [
{
key: '0-0',
data: { name: 'Work', size: '55kb', type: 'Folder' },
children: [
{ key: '0-0-0', data: { name: 'Expenses.doc', size: '30kb', type: 'Document' } },
{ key: '0-0-1', data: { name: 'Resume.doc', size: '25kb', type: 'Document' } }
]
},
{
key: '0-1',
data: { name: 'Home', size: '20kb', type: 'Folder' },
children: [{ key: '0-1-0', data: { name: 'Invoices.txt', size: '20kb', type: 'Text' } }]
}
]
},
{
key: '1',
data: { name: 'Pictures', size: '150kb', type: 'Folder' },
children: [
{ key: '1-0', data: { name: 'barcelona.jpg', size: '90kb', type: 'Picture' } },
{ key: '1-1', data: { name: 'primeui.png', size: '30kb', type: 'Picture' } }
]
}
];
function applyNodeUpdate(nodes: TreeNode[], key: string, patch: Partial<TreeNode['data']>): TreeNode[] {
return nodes.map((node) => {
if (node.key === key) {
return { ...node, data: { ...node.data, ...patch } };
}
if (node.children) {
return { ...node, children: applyNodeUpdate(node.children, key, patch) };
}
return node;
});
}
export default function TreeTableRowEditDemo() {
const [nodes, setNodes] = React.useState<TreeNode[]>(initialNodes);
const [expandedKeys, setExpandedKeys] = React.useState<Record<string, boolean>>({ '0': true });
const [editingKeys, setEditingKeys] = React.useState<Record<string, boolean>>({});
const draftRef = React.useRef<Record<string, Partial<TreeNode['data']>>>({});
const handleSave = React.useCallback((e: DataTableRowEditEvent) => {
const key = e.data.key as string | undefined;
if (!key) return;
const patch = draftRef.current[key];
if (patch && Object.keys(patch).length > 0) {
setNodes((prev) => applyNodeUpdate(prev, key, patch));
}
delete draftRef.current[key];
}, []);
const handleCancel = React.useCallback((e: DataTableRowEditEvent) => {
const key = e.data.key as string | undefined;
if (key) delete draftRef.current[key];
}, []);
return (
<div className="w-full">
<DataTable.Root
data={nodes}
dataKey="key"
treeMode
expandedKeys={expandedKeys}
onExpandedChange={(e: DataTableExpansionEvent) => setExpandedKeys(e.value)}
editMode="row"
editingKeys={editingKeys}
onEditingKeysChange={(e: any) => setEditingKeys(e.value)}
onRowEditSave={handleSave}
onRowEditCancel={handleCancel}
>
<DataTable.TableContainer>
<DataTable.Table style={{ minWidth: '50rem' }}>
<DataTable.THead>
<DataTable.THeadRow>
<DataTable.THeadCell style={{ width: '50%' }}>
<DataTable.THeadTitle>Name</DataTable.THeadTitle>
</DataTable.THeadCell>
<DataTable.THeadCell style={{ width: '20%' }}>
<DataTable.THeadTitle>Size</DataTable.THeadTitle>
</DataTable.THeadCell>
<DataTable.THeadCell style={{ width: '30%' }}>
<DataTable.THeadTitle>Type</DataTable.THeadTitle>
</DataTable.THeadCell>
<DataTable.THeadCell style={{ width: '8rem' }}>
<DataTable.THeadTitle>Actions</DataTable.THeadTitle>
</DataTable.THeadCell>
</DataTable.THeadRow>
</DataTable.THead>
<DataTable.TBody>
{({ item, index }: { item: any; index: number }) => {
const key = String(item.key);
return (
<DataTable.Row key={key}>
<DataTable.RowEditor rowKey={key} rowData={item}>
<DataTable.Cell>
<div className="flex items-center gap-2">
<DataTable.RowToggle>
<DataTable.RowToggleIndicator match="expanded">
<ChevronDown />
</DataTable.RowToggleIndicator>
<DataTable.RowToggleIndicator match="collapsed">
<ChevronRight />
</DataTable.RowToggleIndicator>
</DataTable.RowToggle>
<TypeIcon type={String(item.type)} />
<DataTable.CellEditor field="name" rowIndex={index} rowData={item} style={{ flex: 1 }}>
<DataTable.CellEditorDisplay>{String(item.name)}</DataTable.CellEditorDisplay>
<DataTable.CellEditorContent>
<InputText
defaultValue={String(item.name)}
onChange={(e: any) => {
draftRef.current[key] = { ...draftRef.current[key], name: e.target.value };
}}
size="small"
fluid
/>
</DataTable.CellEditorContent>
</DataTable.CellEditor>
</div>
</DataTable.Cell>
<DataTable.Cell>
<DataTable.CellEditor field="size" rowIndex={index} rowData={item}>
<DataTable.CellEditorDisplay>{String(item.size)}</DataTable.CellEditorDisplay>
<DataTable.CellEditorContent>
<InputText
defaultValue={String(item.size)}
onChange={(e: any) => {
draftRef.current[key] = { ...draftRef.current[key], size: e.target.value };
}}
size="small"
fluid
/>
</DataTable.CellEditorContent>
</DataTable.CellEditor>
</DataTable.Cell>
<DataTable.Cell>
<DataTable.CellEditor field="type" rowIndex={index} rowData={item}>
<DataTable.CellEditorDisplay>
<Tag severity={typeSeverity[String(item.type)] ?? 'secondary'}>{String(item.type)}</Tag>
</DataTable.CellEditorDisplay>
<DataTable.CellEditorContent>
<InputText
defaultValue={String(item.type)}
onChange={(e: any) => {
draftRef.current[key] = { ...draftRef.current[key], type: e.target.value };
}}
size="small"
fluid
/>
</DataTable.CellEditorContent>
</DataTable.CellEditor>
</DataTable.Cell>
<DataTable.Cell>
<div style={{ display: 'flex', gap: '0.25rem' }}>
<DataTable.RowEditorInit as={Button} variant="text" severity="secondary" size="small">
<Pencil />
</DataTable.RowEditorInit>
<DataTable.RowEditorSave as={Button} variant="text" severity="success" size="small">
<Check />
</DataTable.RowEditorSave>
<DataTable.RowEditorCancel as={Button} variant="text" severity="danger" size="small">
<Times />
</DataTable.RowEditorCancel>
</div>
</DataTable.Cell>
</DataTable.RowEditor>
</DataTable.Row>
);
}}
</DataTable.TBody>
<DataTable.EmptyTBody>
<DataTable.Row>
<DataTable.Cell colSpan={4}>
<div className="flex flex-col items-center justify-center gap-3 py-24 text-center">
<div className="relative">
<div className="w-16 h-16 rounded-full bg-surface-100 dark:bg-surface-800 flex items-center justify-center">
<Database className="w-8 h-8 text-surface-400 dark:text-surface-500" />
</div>
<span className="absolute -bottom-0.5 -right-0.5 w-4 h-4 rounded-full border-2 border-surface-0 dark:border-surface-900 bg-primary animate-pulse" />
</div>
<div>
<p className="m-0 font-semibold text-surface-900 dark:text-surface-0">Loading tree</p>
<p className="mt-1 text-sm text-surface-500 dark:text-surface-400">Hang tight, the data is on its way.</p>
</div>
</div>
</DataTable.Cell>
</DataTable.Row>
</DataTable.EmptyTBody>
</DataTable.Table>
</DataTable.TableContainer>
</DataTable.Root>
</div>
);
}
Column Resize#
Resize works in tree mode via the resizableColumns prop with columnResizeMode (fit or expand).
Fit Mode#
'use client';
import { ChevronDown } from '@primeicons/react/chevron-down';
import { Database } from '@primeicons/react/database';
import { ChevronRight } from '@primeicons/react/chevron-right';
import { NodeService, type TreeTableNode } from '@/shared/services/node.service';
import type { DataTableExpansionEvent } from '@primereact/ui/datatable';
import { DataTable } from '@primereact/ui/datatable';
import { Tag } from '@primereact/ui/tag';
import * as React from 'react';
import { File } from '@primeicons/react/file';
import { Folder } from '@primeicons/react/folder';
import { Image as ImageIcon } from '@primeicons/react/image';
import { Video } from '@primeicons/react/video';
const typeSeverity: Record<string, 'info' | 'warn' | 'success' | 'secondary'> = {
Folder: 'warn',
Document: 'info',
Resume: 'info',
Text: 'secondary',
Application: 'info',
Picture: 'success',
Video: 'success',
Zip: 'secondary',
PDF: 'info',
Link: 'secondary'
};
function TypeIcon({ type }: { type: string }) {
if (type === 'Picture') return <ImageIcon size={16} />;
if (type === 'Video') return <Video size={16} />;
if (type === 'Folder') return <Folder size={16} />;
return <File size={16} />;
}
export default function ResizeDemo() {
const [nodes, setNodes] = React.useState<TreeTableNode[]>([]);
const [expandedKeys, setExpandedKeys] = React.useState<Record<string, boolean>>({ '0': true });
React.useEffect(() => {
setNodes(NodeService.getTreeTableNodesData().slice(0, 5));
}, []);
return (
<div className="w-full">
<DataTable.Root
data={nodes}
dataKey="key"
treeMode
expandedKeys={expandedKeys}
onExpandedChange={(e: DataTableExpansionEvent) => setExpandedKeys(e.value)}
resizableColumns
columnResizeMode="fit"
showGridlines
>
<DataTable.TableContainer>
<DataTable.ColumnResizeIndicator />
<DataTable.Table style={{ width: '100%', tableLayout: 'fixed' }}>
<DataTable.THead>
<DataTable.THeadRow>
<DataTable.THeadCell>
<DataTable.THeadTitle>Name</DataTable.THeadTitle>
<DataTable.ColumnResizer />
</DataTable.THeadCell>
<DataTable.THeadCell>
<DataTable.THeadTitle>Size</DataTable.THeadTitle>
<DataTable.ColumnResizer />
</DataTable.THeadCell>
<DataTable.THeadCell>
<DataTable.THeadTitle>Type</DataTable.THeadTitle>
<DataTable.ColumnResizer />
</DataTable.THeadCell>
</DataTable.THeadRow>
</DataTable.THead>
<DataTable.TBody>
{({ item }: { item: any }) => (
<DataTable.Row key={String(item.key)}>
<DataTable.Cell>
<div className="flex items-center gap-2">
<DataTable.RowToggle>
<DataTable.RowToggleIndicator match="expanded">
<ChevronDown />
</DataTable.RowToggleIndicator>
<DataTable.RowToggleIndicator match="collapsed">
<ChevronRight />
</DataTable.RowToggleIndicator>
</DataTable.RowToggle>
<TypeIcon type={String(item.type)} />
<span className={item.type === 'Folder' ? 'font-medium' : ''}>{String(item.name)}</span>
</div>
</DataTable.Cell>
<DataTable.Cell>
<span className="text-sm text-surface-500 dark:text-surface-400">{String(item.size)}</span>
</DataTable.Cell>
<DataTable.Cell>
<Tag severity={typeSeverity[String(item.type)] ?? 'secondary'}>{String(item.type)}</Tag>
</DataTable.Cell>
</DataTable.Row>
)}
</DataTable.TBody>
<DataTable.EmptyTBody>
<DataTable.Row>
<DataTable.Cell colSpan={3}>
<div className="flex flex-col items-center justify-center gap-3 py-24 text-center">
<div className="relative">
<div className="w-16 h-16 rounded-full bg-surface-100 dark:bg-surface-800 flex items-center justify-center">
<Database className="w-8 h-8 text-surface-400 dark:text-surface-500" />
</div>
<span className="absolute -bottom-0.5 -right-0.5 w-4 h-4 rounded-full border-2 border-surface-0 dark:border-surface-900 bg-primary animate-pulse" />
</div>
<div>
<p className="m-0 font-semibold text-surface-900 dark:text-surface-0">Loading tree</p>
<p className="mt-1 text-sm text-surface-500 dark:text-surface-400">Hang tight, the data is on its way.</p>
</div>
</div>
</DataTable.Cell>
</DataTable.Row>
</DataTable.EmptyTBody>
</DataTable.Table>
</DataTable.TableContainer>
</DataTable.Root>
</div>
);
}
Expand Mode#
'use client';
import { ChevronDown } from '@primeicons/react/chevron-down';
import { Database } from '@primeicons/react/database';
import { ChevronRight } from '@primeicons/react/chevron-right';
import { NodeService, type TreeTableNode } from '@/shared/services/node.service';
import type { DataTableExpansionEvent } from '@primereact/ui/datatable';
import { DataTable } from '@primereact/ui/datatable';
import { Tag } from '@primereact/ui/tag';
import * as React from 'react';
import { File } from '@primeicons/react/file';
import { Folder } from '@primeicons/react/folder';
import { Image as ImageIcon } from '@primeicons/react/image';
import { Video } from '@primeicons/react/video';
const typeSeverity: Record<string, 'info' | 'warn' | 'success' | 'secondary'> = {
Folder: 'warn',
Document: 'info',
Resume: 'info',
Text: 'secondary',
Application: 'info',
Picture: 'success',
Video: 'success',
Zip: 'secondary',
PDF: 'info',
Link: 'secondary'
};
function TypeIcon({ type }: { type: string }) {
if (type === 'Picture') return <ImageIcon size={16} />;
if (type === 'Video') return <Video size={16} />;
if (type === 'Folder') return <Folder size={16} />;
return <File size={16} />;
}
export default function ResizeExpandDemo() {
const [nodes, setNodes] = React.useState<TreeTableNode[]>([]);
const [expandedKeys, setExpandedKeys] = React.useState<Record<string, boolean>>({ '0': true });
React.useEffect(() => {
setNodes(NodeService.getTreeTableNodesData().slice(0, 5));
}, []);
return (
<div className="w-full">
<DataTable.Root
data={nodes}
dataKey="key"
treeMode
expandedKeys={expandedKeys}
onExpandedChange={(e: DataTableExpansionEvent) => setExpandedKeys(e.value)}
resizableColumns
columnResizeMode="expand"
scrollable
showGridlines
>
<DataTable.TableContainer>
<DataTable.ColumnResizeIndicator />
<DataTable.Table style={{ minWidth: '40rem' }}>
<DataTable.THead>
<DataTable.THeadRow>
<DataTable.THeadCell>
<DataTable.THeadTitle>Name</DataTable.THeadTitle>
<DataTable.ColumnResizer />
</DataTable.THeadCell>
<DataTable.THeadCell>
<DataTable.THeadTitle>Size</DataTable.THeadTitle>
<DataTable.ColumnResizer />
</DataTable.THeadCell>
<DataTable.THeadCell>
<DataTable.THeadTitle>Type</DataTable.THeadTitle>
<DataTable.ColumnResizer />
</DataTable.THeadCell>
</DataTable.THeadRow>
</DataTable.THead>
<DataTable.TBody>
{({ item }: { item: any }) => (
<DataTable.Row key={String(item.key)}>
<DataTable.Cell>
<div className="flex items-center gap-2">
<DataTable.RowToggle>
<DataTable.RowToggleIndicator match="expanded">
<ChevronDown />
</DataTable.RowToggleIndicator>
<DataTable.RowToggleIndicator match="collapsed">
<ChevronRight />
</DataTable.RowToggleIndicator>
</DataTable.RowToggle>
<TypeIcon type={String(item.type)} />
<span className={item.type === 'Folder' ? 'font-medium' : ''}>{String(item.name)}</span>
</div>
</DataTable.Cell>
<DataTable.Cell>
<span className="text-sm text-surface-500 dark:text-surface-400">{String(item.size)}</span>
</DataTable.Cell>
<DataTable.Cell>
<Tag severity={typeSeverity[String(item.type)] ?? 'secondary'}>{String(item.type)}</Tag>
</DataTable.Cell>
</DataTable.Row>
)}
</DataTable.TBody>
<DataTable.EmptyTBody>
<DataTable.Row>
<DataTable.Cell colSpan={3}>
<div className="flex flex-col items-center justify-center gap-3 py-24 text-center">
<div className="relative">
<div className="w-16 h-16 rounded-full bg-surface-100 dark:bg-surface-800 flex items-center justify-center">
<Database className="w-8 h-8 text-surface-400 dark:text-surface-500" />
</div>
<span className="absolute -bottom-0.5 -right-0.5 w-4 h-4 rounded-full border-2 border-surface-0 dark:border-surface-900 bg-primary animate-pulse" />
</div>
<div>
<p className="m-0 font-semibold text-surface-900 dark:text-surface-0">Loading tree</p>
<p className="mt-1 text-sm text-surface-500 dark:text-surface-400">Hang tight, the data is on its way.</p>
</div>
</div>
</DataTable.Cell>
</DataTable.Row>
</DataTable.EmptyTBody>
</DataTable.Table>
</DataTable.TableContainer>
</DataTable.Root>
</div>
);
}
Column Reorder#
Drag and drop column headers to reorder columns.
'use client';
import { NodeService, type TreeTableNode } from '@/shared/services/node.service';
import { ArrowDown } from '@primeicons/react/arrow-down';
import { ArrowUp } from '@primeicons/react/arrow-up';
import { Bars } from '@primeicons/react/bars';
import { ChevronDown } from '@primeicons/react/chevron-down';
import { ChevronRight } from '@primeicons/react/chevron-right';
import { Database } from '@primeicons/react/database';
import { File } from '@primeicons/react/file';
import { Folder } from '@primeicons/react/folder';
import { Image as ImageIcon } from '@primeicons/react/image';
import { Video } from '@primeicons/react/video';
import type { DataTableColumnReorderEvent, DataTableExpansionEvent } from '@primereact/ui/datatable';
import { DataTable } from '@primereact/ui/datatable';
import { Tag } from '@primereact/ui/tag';
import * as React from 'react';
const typeSeverity: Record<string, 'info' | 'warn' | 'success' | 'secondary'> = {
Folder: 'warn',
Document: 'info',
Resume: 'info',
Text: 'secondary',
Application: 'info',
Picture: 'success',
Video: 'success',
Zip: 'secondary',
PDF: 'info',
Link: 'secondary'
};
function TypeIcon({ type }: { type: string }) {
if (type === 'Picture') return <ImageIcon size={16} />;
if (type === 'Video') return <Video size={16} />;
if (type === 'Folder') return <Folder size={16} />;
return <File size={16} />;
}
interface Column {
field: 'name' | 'size' | 'type';
header: string;
}
const initialColumns: Column[] = [
{ field: 'name', header: 'Name' },
{ field: 'size', header: 'Size' },
{ field: 'type', header: 'Type' }
];
function renderCell(field: Column['field'], item: any) {
if (field === 'name') {
return (
<div className="flex items-center gap-2">
<DataTable.RowToggle>
<DataTable.RowToggleIndicator match="expanded">
<ChevronDown />
</DataTable.RowToggleIndicator>
<DataTable.RowToggleIndicator match="collapsed">
<ChevronRight />
</DataTable.RowToggleIndicator>
</DataTable.RowToggle>
<TypeIcon type={String(item.type)} />
<span className={item.type === 'Folder' ? 'font-medium' : ''}>{String(item.name)}</span>
</div>
);
}
if (field === 'type') {
return <Tag severity={typeSeverity[String(item.type)] ?? 'secondary'}>{String(item.type)}</Tag>;
}
return <span className="text-sm text-surface-500 dark:text-surface-400">{String(item[field])}</span>;
}
export default function ReorderDemo() {
const [nodes, setNodes] = React.useState<TreeTableNode[]>([]);
const [expandedKeys, setExpandedKeys] = React.useState<Record<string, boolean>>({ '0': true });
const [columns, setColumns] = React.useState<Column[]>(initialColumns);
React.useEffect(() => {
setNodes(NodeService.getTreeTableNodesData().slice(0, 5));
}, []);
return (
<div className="w-full">
<DataTable.Root
data={nodes}
dataKey="key"
treeMode
expandedKeys={expandedKeys}
onExpandedChange={(e: DataTableExpansionEvent) => setExpandedKeys(e.value)}
reorderableColumns
onColumnReorder={(e: DataTableColumnReorderEvent) => {
const next = [...columns];
const [moved] = next.splice(e.dragIndex, 1);
next.splice(e.dropIndex, 0, moved);
setColumns(next);
}}
>
<DataTable.TableContainer>
<DataTable.Table style={{ minWidth: '40rem' }}>
<DataTable.THead>
<DataTable.THeadRow>
{columns.map((col) => (
<DataTable.THeadCell key={col.field}>
<DataTable.ColumnReorderTarget className="flex items-center gap-2 select-none">
<DataTable.ColumnReorder className="text-surface-400 transition-colors">
<Bars />
</DataTable.ColumnReorder>
<DataTable.THeadTitle>{col.header}</DataTable.THeadTitle>
</DataTable.ColumnReorderTarget>
</DataTable.THeadCell>
))}
</DataTable.THeadRow>
</DataTable.THead>
<DataTable.TBody key={columns.map((c) => c.field).join(',')}>
{({ item }: { item: any }) => (
<DataTable.Row key={String(item.key)}>
{columns.map((col) => (
<DataTable.Cell key={col.field}>{renderCell(col.field, item)}</DataTable.Cell>
))}
</DataTable.Row>
)}
</DataTable.TBody>
<DataTable.EmptyTBody>
<DataTable.Row>
<DataTable.Cell colSpan={columns.length}>
<div className="flex flex-col items-center justify-center gap-3 py-24 text-center">
<div className="relative">
<div className="w-16 h-16 rounded-full bg-surface-100 dark:bg-surface-800 flex items-center justify-center">
<Database className="w-8 h-8 text-surface-400 dark:text-surface-500" />
</div>
<span className="absolute -bottom-0.5 -right-0.5 w-4 h-4 rounded-full border-2 border-surface-0 dark:border-surface-900 bg-primary animate-pulse" />
</div>
<div>
<p className="m-0 font-semibold text-surface-900 dark:text-surface-0">Loading tree</p>
<p className="mt-1 text-sm text-surface-500 dark:text-surface-400">Hang tight, the data is on its way.</p>
</div>
</div>
</DataTable.Cell>
</DataTable.Row>
</DataTable.EmptyTBody>
</DataTable.Table>
</DataTable.TableContainer>
<DataTable.ColumnReorderIndicatorUp>
<ArrowDown />
</DataTable.ColumnReorderIndicatorUp>
<DataTable.ColumnReorderIndicatorDown>
<ArrowUp />
</DataTable.ColumnReorderIndicatorDown>
</DataTable.Root>
</div>
);
}
Row Reorder#
Root-level row reordering via the drag handle on DataTable.RowReorder. Children stay attached to their parents.
'use client';
import { Bars } from '@primeicons/react/bars';
import { Database } from '@primeicons/react/database';
import { ChevronDown } from '@primeicons/react/chevron-down';
import { ChevronRight } from '@primeicons/react/chevron-right';
import { NodeService, type TreeTableNode } from '@/shared/services/node.service';
import type { DataTableExpansionEvent, DataTableRowReorderEvent } from '@primereact/ui/datatable';
import { DataTable } from '@primereact/ui/datatable';
import { Tag } from '@primereact/ui/tag';
import * as React from 'react';
import { File } from '@primeicons/react/file';
import { Folder } from '@primeicons/react/folder';
import { Image as ImageIcon } from '@primeicons/react/image';
import { Video } from '@primeicons/react/video';
const typeSeverity: Record<string, 'info' | 'warn' | 'success' | 'secondary'> = {
Folder: 'warn',
Document: 'info',
Resume: 'info',
Text: 'secondary',
Application: 'info',
Picture: 'success',
Video: 'success',
Zip: 'secondary',
PDF: 'info',
Link: 'secondary'
};
function TypeIcon({ type }: { type: string }) {
if (type === 'Picture') return <ImageIcon size={16} />;
if (type === 'Video') return <Video size={16} />;
if (type === 'Folder') return <Folder size={16} />;
return <File size={16} />;
}
export default function RowReorderDemo() {
const [nodes, setNodes] = React.useState<TreeTableNode[]>([]);
const [expandedKeys, setExpandedKeys] = React.useState<Record<string, boolean>>({});
React.useEffect(() => {
setNodes(NodeService.getTreeTableNodesData().slice(0, 6));
}, []);
const handleReorder = React.useCallback((e: DataTableRowReorderEvent) => {
const flat = e.value as unknown as Array<{ key: string; _treeLevel?: number }>;
const fromNode = flat[e.dragIndex];
const toNode = flat[e.dropIndex];
if (!fromNode || !toNode) return;
if ((fromNode._treeLevel ?? 0) !== 0 || (toNode._treeLevel ?? 0) !== 0) return;
setNodes((prev) => {
const fromIdx = prev.findIndex((n) => n.key === fromNode.key);
const toIdx = prev.findIndex((n) => n.key === toNode.key);
if (fromIdx < 0 || toIdx < 0 || fromIdx === toIdx) return prev;
const next = prev.slice();
const [moved] = next.splice(fromIdx, 1);
next.splice(toIdx, 0, moved);
return next;
});
}, []);
return (
<div className="w-full">
<p className="mb-3 text-sm text-surface-500 dark:text-surface-400">
Only root-level rows are reorderable; expand a node to see its children.
</p>
<DataTable.Root
data={nodes}
dataKey="key"
treeMode
expandedKeys={expandedKeys}
onExpandedChange={(e: DataTableExpansionEvent) => setExpandedKeys(e.value)}
reorderableRows
onRowReorder={handleReorder}
>
<DataTable.TableContainer>
<DataTable.Table style={{ minWidth: '40rem' }}>
<DataTable.THead>
<DataTable.THeadRow>
<DataTable.THeadCell style={{ width: '3rem' }} />
<DataTable.THeadCell style={{ width: '50%' }}>
<DataTable.THeadTitle>Name</DataTable.THeadTitle>
</DataTable.THeadCell>
<DataTable.THeadCell style={{ width: '20%' }}>
<DataTable.THeadTitle>Size</DataTable.THeadTitle>
</DataTable.THeadCell>
<DataTable.THeadCell style={{ width: '30%' }}>
<DataTable.THeadTitle>Type</DataTable.THeadTitle>
</DataTable.THeadCell>
</DataTable.THeadRow>
</DataTable.THead>
<DataTable.TBody>
{({ item, index }: { item: any; index: number }) => (
<DataTable.Row key={String(item.key)}>
<DataTable.Cell>
{Number(item._treeLevel ?? 0) === 0 ? (
<DataTable.RowReorder rowIndex={index} className="cursor-grab text-surface-400 hover:text-surface-600">
<Bars />
</DataTable.RowReorder>
) : null}
</DataTable.Cell>
<DataTable.Cell>
<div className="flex items-center gap-2">
<DataTable.RowToggle>
<DataTable.RowToggleIndicator match="expanded">
<ChevronDown />
</DataTable.RowToggleIndicator>
<DataTable.RowToggleIndicator match="collapsed">
<ChevronRight />
</DataTable.RowToggleIndicator>
</DataTable.RowToggle>
<TypeIcon type={String(item.type)} />
<span className={item.type === 'Folder' ? 'font-medium' : ''}>{String(item.name)}</span>
</div>
</DataTable.Cell>
<DataTable.Cell>
<span className="text-sm text-surface-500 dark:text-surface-400">{String(item.size)}</span>
</DataTable.Cell>
<DataTable.Cell>
<Tag severity={typeSeverity[String(item.type)] ?? 'secondary'}>{String(item.type)}</Tag>
</DataTable.Cell>
</DataTable.Row>
)}
</DataTable.TBody>
<DataTable.EmptyTBody>
<DataTable.Row>
<DataTable.Cell colSpan={4}>
<div className="flex flex-col items-center justify-center gap-3 py-24 text-center">
<div className="relative">
<div className="w-16 h-16 rounded-full bg-surface-100 dark:bg-surface-800 flex items-center justify-center">
<Database className="w-8 h-8 text-surface-400 dark:text-surface-500" />
</div>
<span className="absolute -bottom-0.5 -right-0.5 w-4 h-4 rounded-full border-2 border-surface-0 dark:border-surface-900 bg-primary animate-pulse" />
</div>
<div>
<p className="m-0 font-semibold text-surface-900 dark:text-surface-0">Loading tree</p>
<p className="mt-1 text-sm text-surface-500 dark:text-surface-400">Hang tight, the data is on its way.</p>
</div>
</div>
</DataTable.Cell>
</DataTable.Row>
</DataTable.EmptyTBody>
</DataTable.Table>
</DataTable.TableContainer>
</DataTable.Root>
</div>
);
}
Column Toggle#
Show/hide columns and drag to reorder them in a popover.
'use client';
import { Bars } from '@primeicons/react/bars';
import { Database } from '@primeicons/react/database';
import { Check } from '@primeicons/react/check';
import { ChevronDown } from '@primeicons/react/chevron-down';
import { ChevronRight } from '@primeicons/react/chevron-right';
import { Cog } from '@primeicons/react/cog';
import { Refresh } from '@primeicons/react/refresh';
import { NodeService, type TreeTableNode } from '@/shared/services/node.service';
import { useSortableList } from '@primereact/hooks/use-sortable-list';
import type { DataTableExpansionEvent } from '@primereact/ui/datatable';
import { Button } from '@primereact/ui/button';
import { Checkbox } from '@primereact/ui/checkbox';
import { DataTable } from '@primereact/ui/datatable';
import { Popover } from '@primereact/ui/popover';
import { Tag } from '@primereact/ui/tag';
import * as React from 'react';
import { File } from '@primeicons/react/file';
import { Folder } from '@primeicons/react/folder';
import { Image as ImageIcon } from '@primeicons/react/image';
import { Video } from '@primeicons/react/video';
const typeSeverity: Record<string, 'info' | 'warn' | 'success' | 'secondary'> = {
Folder: 'warn',
Document: 'info',
Resume: 'info',
Text: 'secondary',
Application: 'info',
Picture: 'success',
Video: 'success',
Zip: 'secondary',
PDF: 'info',
Link: 'secondary'
};
function TypeIcon({ type }: { type: string }) {
if (type === 'Picture') return <ImageIcon size={16} />;
if (type === 'Video') return <Video size={16} />;
if (type === 'Folder') return <Folder size={16} />;
return <File size={16} />;
}
interface ColumnDef {
field: 'name' | 'size' | 'type' | 'key' | 'level' | 'hasChildren';
header: string;
}
const initialColumns: ColumnDef[] = [
{ field: 'name', header: 'Name' },
{ field: 'size', header: 'Size' },
{ field: 'type', header: 'Type' },
{ field: 'key', header: 'Key' },
{ field: 'level', header: 'Level' },
{ field: 'hasChildren', header: 'Has Children' }
];
const defaultVisibleFields: ColumnDef['field'][] = ['name', 'size', 'type'];
function renderCell(field: ColumnDef['field'], item: any) {
if (field === 'name') {
return (
<div className="flex items-center gap-2">
<DataTable.RowToggle>
<DataTable.RowToggleIndicator match="expanded">
<ChevronDown />
</DataTable.RowToggleIndicator>
<DataTable.RowToggleIndicator match="collapsed">
<ChevronRight />
</DataTable.RowToggleIndicator>
</DataTable.RowToggle>
<TypeIcon type={String(item.type)} />
<span className={item.type === 'Folder' ? 'font-medium' : ''}>{String(item.name)}</span>
</div>
);
}
if (field === 'type') return <Tag severity={typeSeverity[String(item.type)] ?? 'secondary'}>{String(item.type)}</Tag>;
if (field === 'size') return <span className="text-sm text-surface-500 dark:text-surface-400">{String(item.size)}</span>;
if (field === 'key') return <code className="text-xs text-surface-500 dark:text-surface-400">{String(item.key)}</code>;
if (field === 'level') return Number(item._treeLevel ?? 0);
if (field === 'hasChildren') return item._treeHasChildren ? 'Yes' : 'No';
return null;
}
const LIST_ID = 'columns';
function ColumnsPopover({
columns,
visibleFields,
onColumnsChange,
onVisibleFieldsChange,
onReset
}: {
columns: ColumnDef[];
visibleFields: string[];
onColumnsChange: (next: ColumnDef[]) => void;
onVisibleFieldsChange: (next: string[]) => void;
onReset: () => void;
}) {
const { dragState, getListHandlers, getItemHandlers } = useSortableList<typeof LIST_ID, ColumnDef>({
onReorder: (_listId, from, to) => {
const next = [...columns];
const [moved] = next.splice(from, 1);
next.splice(to, 0, moved);
onColumnsChange(next);
}
});
const toggleVisible = (field: string) => {
const set = new Set(visibleFields);
if (set.has(field)) set.delete(field);
else set.add(field);
onVisibleFieldsChange(Array.from(set));
};
const listHandlers = getListHandlers(LIST_ID);
return (
<Popover.Root>
<Popover.Trigger as={Button} variant="outlined" severity="secondary" size="small">
<Cog />
Columns
</Popover.Trigger>
<Popover.Portal>
<Popover.Positioner sideOffset={4} side="bottom" align="end">
<Popover.Popup className="w-72 p-0">
<div className="flex items-center justify-between gap-2 px-4 py-3 border-b border-surface-200 dark:border-surface-700">
<span className="text-sm font-semibold">Columns</span>
<Button variant="text" size="small" severity="secondary" onClick={onReset}>
<Refresh /> Reset
</Button>
</div>
<div className="py-2 max-h-80 overflow-auto" onDragOver={listHandlers.onDragOver} onDrop={listHandlers.onDrop}>
{columns.map((col, idx) => {
const checked = visibleFields.includes(col.field);
const itemHandlers = getItemHandlers(LIST_ID, idx, col);
const isDragging = dragState.draggedIndex === idx;
const isDragOver = dragState.dragOverIndex === idx && dragState.draggedIndex !== idx;
return (
<div
key={col.field}
{...itemHandlers}
className={[
'flex items-center gap-2 px-3 py-1.5 mx-1 rounded-md cursor-move select-none transition',
isDragging ? 'opacity-40' : '',
isDragOver
? 'bg-primary-50 dark:bg-primary-900/30 ring-1 ring-primary-400'
: 'hover:bg-surface-100 dark:hover:bg-surface-800'
].join(' ')}
>
<Bars className="w-3.5 h-3.5 text-surface-400 dark:text-surface-500" />
<label className="flex-1 flex items-center gap-2 cursor-pointer" onClick={(e) => e.stopPropagation()}>
<Checkbox.Root checked={checked} onCheckedChange={() => toggleVisible(col.field)}>
<Checkbox.Box>
<Checkbox.Indicator match="checked">
<Check />
</Checkbox.Indicator>
</Checkbox.Box>
</Checkbox.Root>
<span className="text-sm">{col.header}</span>
</label>
</div>
);
})}
</div>
</Popover.Popup>
</Popover.Positioner>
</Popover.Portal>
</Popover.Root>
);
}
export default function ColumnToggleDemo() {
const [nodes, setNodes] = React.useState<TreeTableNode[]>([]);
const [expandedKeys, setExpandedKeys] = React.useState<Record<string, boolean>>({ '0': true });
const [columns, setColumns] = React.useState<ColumnDef[]>(initialColumns);
const [visibleFields, setVisibleFields] = React.useState<string[]>(defaultVisibleFields);
React.useEffect(() => {
setNodes(NodeService.getTreeTableNodesData().slice(0, 5));
}, []);
const resetOptions = () => {
setColumns(initialColumns);
setVisibleFields(defaultVisibleFields);
};
const visibleColumns = columns.filter((col) => visibleFields.includes(col.field));
return (
<div className="w-full">
<div className="mb-3 flex items-center justify-end">
<ColumnsPopover
columns={columns}
visibleFields={visibleFields}
onColumnsChange={setColumns}
onVisibleFieldsChange={setVisibleFields}
onReset={resetOptions}
/>
</div>
<DataTable.Root
data={nodes}
dataKey="key"
treeMode
expandedKeys={expandedKeys}
onExpandedChange={(e: DataTableExpansionEvent) => setExpandedKeys(e.value)}
>
<DataTable.TableContainer>
<DataTable.Table style={{ minWidth: '40rem' }}>
<DataTable.THead>
<DataTable.THeadRow>
{visibleColumns.map((col) => (
<DataTable.THeadCell key={col.field}>
<DataTable.THeadTitle>{col.header}</DataTable.THeadTitle>
</DataTable.THeadCell>
))}
</DataTable.THeadRow>
</DataTable.THead>
<DataTable.TBody key={visibleFields.join(',') + '|' + columns.map((c) => c.field).join(',')}>
{({ item }: { item: any }) => (
<DataTable.Row key={String(item.key)}>
{visibleColumns.map((col) => (
<DataTable.Cell key={col.field}>{renderCell(col.field, item)}</DataTable.Cell>
))}
</DataTable.Row>
)}
</DataTable.TBody>
<DataTable.EmptyTBody>
<DataTable.Row>
<DataTable.Cell colSpan={visibleColumns.length}>
<div className="flex flex-col items-center justify-center gap-3 py-24 text-center">
<div className="relative">
<div className="w-16 h-16 rounded-full bg-surface-100 dark:bg-surface-800 flex items-center justify-center">
<Database className="w-8 h-8 text-surface-400 dark:text-surface-500" />
</div>
<span className="absolute -bottom-0.5 -right-0.5 w-4 h-4 rounded-full border-2 border-surface-0 dark:border-surface-900 bg-primary animate-pulse" />
</div>
<div>
<p className="m-0 font-semibold text-surface-900 dark:text-surface-0">Loading tree</p>
<p className="mt-1 text-sm text-surface-500 dark:text-surface-400">Hang tight, the data is on its way.</p>
</div>
</div>
</DataTable.Cell>
</DataTable.Row>
</DataTable.EmptyTBody>
</DataTable.Table>
</DataTable.TableContainer>
</DataTable.Root>
</div>
);
}
Column Group#
Multi-level headers with rowSpan and colSpan, plus an aggregate TFoot.
'use client';
import { ArrowDown } from '@primeicons/react/arrow-down';
import { Database } from '@primeicons/react/database';
import { ArrowUp } from '@primeicons/react/arrow-up';
import { ChevronDown } from '@primeicons/react/chevron-down';
import { ChevronRight } from '@primeicons/react/chevron-right';
import type { DataTableExpansionEvent } from '@primereact/ui/datatable';
import { DataTable } from '@primereact/ui/datatable';
import { Tag } from '@primereact/ui/tag';
import * as React from 'react';
interface SalesNode {
key: string;
data: {
product: string;
lastYearSale: number;
thisYearSale: number;
lastYearProfit: number;
thisYearProfit: number;
};
children?: SalesNode[];
}
const nodes: SalesNode[] = [
{
key: 'europe',
data: { product: 'Europe', lastYearSale: 60, thisYearSale: 45, lastYearProfit: 766_537, thisYearProfit: 659_964 },
children: [
{ key: 'e-1', data: { product: 'Bamboo Watch', lastYearSale: 51, thisYearSale: 40, lastYearProfit: 54_406, thisYearProfit: 43_342 } },
{ key: 'e-2', data: { product: 'Black Watch', lastYearSale: 83, thisYearSale: 9, lastYearProfit: 423_132, thisYearProfit: 312_122 } },
{ key: 'e-3', data: { product: 'Blue T-Shirt', lastYearSale: 49, thisYearSale: 22, lastYearProfit: 288_999, thisYearProfit: 304_500 } }
]
},
{
key: 'americas',
data: { product: 'Americas', lastYearSale: 28, thisYearSale: 42, lastYearProfit: 655_563, thisYearProfit: 508_832 },
children: [
{ key: 'a-1', data: { product: 'Blue Band', lastYearSale: 38, thisYearSale: 5, lastYearProfit: 12_321, thisYearProfit: 8_500 } },
{ key: 'a-2', data: { product: 'Bracelet', lastYearSale: 17, thisYearSale: 79, lastYearProfit: 643_242, thisYearProfit: 500_332 } }
]
}
];
function Trend({ current, previous }: { current: number; previous: number }) {
const up = current >= previous;
return (
<div className="inline-flex items-center gap-1">
<span>{current}%</span>
{up ? <ArrowUp className="w-3 h-3 text-green-500" /> : <ArrowDown className="w-3 h-3 text-red-500" />}
</div>
);
}
export default function ColumnGroupDemo() {
const [expandedKeys, setExpandedKeys] = React.useState<Record<string, boolean>>({ europe: true });
const totals = React.useMemo(() => {
return nodes.reduce(
(acc, n) => ({
lastYear: acc.lastYear + n.data.lastYearProfit,
thisYear: acc.thisYear + n.data.thisYearProfit
}),
{ lastYear: 0, thisYear: 0 }
);
}, []);
return (
<div className="w-full">
<DataTable.Root
data={nodes}
dataKey="key"
treeMode
expandedKeys={expandedKeys}
onExpandedChange={(e: DataTableExpansionEvent) => setExpandedKeys(e.value)}
showGridlines
>
<DataTable.TableContainer>
<DataTable.Table style={{ minWidth: '50rem' }}>
<DataTable.THead>
<DataTable.THeadRow>
<DataTable.THeadCell rowSpan={2}>
<DataTable.THeadTitle>Product</DataTable.THeadTitle>
</DataTable.THeadCell>
<DataTable.THeadCell colSpan={2} style={{ textAlign: 'center' }}>
<DataTable.THeadTitle>Sale Rate</DataTable.THeadTitle>
</DataTable.THeadCell>
<DataTable.THeadCell colSpan={2} style={{ textAlign: 'center' }}>
<DataTable.THeadTitle>Profits</DataTable.THeadTitle>
</DataTable.THeadCell>
</DataTable.THeadRow>
<DataTable.THeadRow>
<DataTable.THeadCell>
<DataTable.THeadTitle>Last Year</DataTable.THeadTitle>
</DataTable.THeadCell>
<DataTable.THeadCell>
<DataTable.THeadTitle>This Year</DataTable.THeadTitle>
</DataTable.THeadCell>
<DataTable.THeadCell>
<DataTable.THeadTitle>Last Year</DataTable.THeadTitle>
</DataTable.THeadCell>
<DataTable.THeadCell>
<DataTable.THeadTitle>This Year</DataTable.THeadTitle>
</DataTable.THeadCell>
</DataTable.THeadRow>
</DataTable.THead>
<DataTable.TBody>
{({ item }: { item: any }) => (
<DataTable.Row key={String(item.key)}>
<DataTable.Cell>
<div className="flex items-center gap-2">
<DataTable.RowToggle>
<DataTable.RowToggleIndicator match="expanded">
<ChevronDown />
</DataTable.RowToggleIndicator>
<DataTable.RowToggleIndicator match="collapsed">
<ChevronRight />
</DataTable.RowToggleIndicator>
</DataTable.RowToggle>
<span className={item._treeHasChildren ? 'font-medium' : ''}>{String(item.product)}</span>
</div>
</DataTable.Cell>
<DataTable.Cell>{Number(item.lastYearSale)}%</DataTable.Cell>
<DataTable.Cell>
<Trend current={Number(item.thisYearSale)} previous={Number(item.lastYearSale)} />
</DataTable.Cell>
<DataTable.Cell>${Number(item.lastYearProfit).toLocaleString()}</DataTable.Cell>
<DataTable.Cell>
<Tag severity={Number(item.thisYearProfit) >= Number(item.lastYearProfit) ? 'success' : 'danger'}>
${Number(item.thisYearProfit).toLocaleString()}
</Tag>
</DataTable.Cell>
</DataTable.Row>
)}
</DataTable.TBody>
<DataTable.EmptyTBody>
<DataTable.Row>
<DataTable.Cell colSpan={3}>
<div className="flex flex-col items-center justify-center gap-3 py-24 text-center">
<div className="relative">
<div className="w-16 h-16 rounded-full bg-surface-100 dark:bg-surface-800 flex items-center justify-center">
<Database className="w-8 h-8 text-surface-400 dark:text-surface-500" />
</div>
<span className="absolute -bottom-0.5 -right-0.5 w-4 h-4 rounded-full border-2 border-surface-0 dark:border-surface-900 bg-primary animate-pulse" />
</div>
<div>
<p className="m-0 font-semibold text-surface-900 dark:text-surface-0">Loading tree</p>
<p className="mt-1 text-sm text-surface-500 dark:text-surface-400">Hang tight, the data is on its way.</p>
</div>
</div>
</DataTable.Cell>
</DataTable.Row>
</DataTable.EmptyTBody>
<DataTable.TFoot>
<DataTable.TFootRow>
<DataTable.TFootCell colSpan={3} style={{ textAlign: 'right' }}>
<span className="font-semibold">Totals</span>
</DataTable.TFootCell>
<DataTable.TFootCell>
<span className="font-semibold">${totals.lastYear.toLocaleString()}</span>
</DataTable.TFootCell>
<DataTable.TFootCell>
<span className="font-semibold">${totals.thisYear.toLocaleString()}</span>
</DataTable.TFootCell>
</DataTable.TFootRow>
</DataTable.TFoot>
</DataTable.Table>
</DataTable.TableContainer>
</DataTable.Root>
</div>
);
}
Sort and filter work on any leaf header cell in a grouped layout, wrap the header in DataTable.Sort and add a filter row below the leaf row.
'use client';
import { ArrowDown } from '@primeicons/react/arrow-down';
import { ArrowUp } from '@primeicons/react/arrow-up';
import { ChevronDown } from '@primeicons/react/chevron-down';
import { ChevronRight } from '@primeicons/react/chevron-right';
import { Database } from '@primeicons/react/database';
import { SortAlt } from '@primeicons/react/sort-alt';
import { SortAmountDown } from '@primeicons/react/sort-amount-down';
import { SortAmountUpAlt } from '@primeicons/react/sort-amount-up-alt';
import type { DataTableExpansionEvent, DataTableFilterInstance } from '@primereact/ui/datatable';
import { DataTable, FilterMatchMode } from '@primereact/ui/datatable';
import { InputText } from '@primereact/ui/inputtext';
import { Tag } from '@primereact/ui/tag';
import * as React from 'react';
interface SalesNode {
key: string;
data: {
product: string;
lastYearSale: number;
thisYearSale: number;
lastYearProfit: number;
thisYearProfit: number;
};
children?: SalesNode[];
}
const nodes: SalesNode[] = [
{
key: 'europe',
data: { product: 'Europe', lastYearSale: 60, thisYearSale: 45, lastYearProfit: 766_537, thisYearProfit: 659_964 },
children: [
{ key: 'e-1', data: { product: 'Bamboo Watch', lastYearSale: 51, thisYearSale: 40, lastYearProfit: 54_406, thisYearProfit: 43_342 } },
{ key: 'e-2', data: { product: 'Black Watch', lastYearSale: 83, thisYearSale: 9, lastYearProfit: 423_132, thisYearProfit: 312_122 } },
{ key: 'e-3', data: { product: 'Blue T-Shirt', lastYearSale: 49, thisYearSale: 22, lastYearProfit: 288_999, thisYearProfit: 304_500 } }
]
},
{
key: 'americas',
data: { product: 'Americas', lastYearSale: 28, thisYearSale: 42, lastYearProfit: 655_563, thisYearProfit: 508_832 },
children: [
{ key: 'a-1', data: { product: 'Blue Band', lastYearSale: 38, thisYearSale: 5, lastYearProfit: 12_321, thisYearProfit: 8_500 } },
{ key: 'a-2', data: { product: 'Bracelet', lastYearSale: 17, thisYearSale: 79, lastYearProfit: 643_242, thisYearProfit: 500_332 } }
]
}
];
function Trend({ current, previous }: { current: number; previous: number }) {
const up = current >= previous;
return (
<div className="inline-flex items-center gap-1">
<span>{current}%</span>
{up ? <ArrowUp className="w-3 h-3 text-green-500" /> : <ArrowDown className="w-3 h-3 text-red-500" />}
</div>
);
}
function SortableHeader({ field, children }: { field: string; children: React.ReactNode }) {
return (
<DataTable.Sort field={field} className="inline-flex items-center gap-1">
<DataTable.THeadTitle>{children}</DataTable.THeadTitle>
<DataTable.SortIndicator match="unsorted">
<SortAlt />
</DataTable.SortIndicator>
<DataTable.SortIndicator match="asc">
<SortAmountUpAlt />
</DataTable.SortIndicator>
<DataTable.SortIndicator match="desc">
<SortAmountDown />
</DataTable.SortIndicator>
</DataTable.Sort>
);
}
export default function ColumnGroupFilterSortDemo() {
const [expandedKeys, setExpandedKeys] = React.useState<Record<string, boolean>>({ europe: true, americas: true });
const [filters, setFilters] = React.useState<Record<string, { value: unknown; matchMode: FilterMatchMode }>>({
product: { value: null, matchMode: FilterMatchMode.Contains },
thisYearSale: { value: null, matchMode: FilterMatchMode.Gte }
});
return (
<div className="w-full">
<DataTable.Root
data={nodes}
dataKey="key"
treeMode
expandedKeys={expandedKeys}
onExpandedChange={(e: DataTableExpansionEvent) => setExpandedKeys(e.value)}
removableSort
filters={filters}
onFilter={(e: { filters: Record<string, unknown> }) => setFilters(e.filters as typeof filters)}
showGridlines
>
<DataTable.TableContainer>
<DataTable.Table style={{ minWidth: '50rem' }}>
<DataTable.THead>
<DataTable.THeadRow>
<DataTable.THeadCell rowSpan={2}>
<SortableHeader field="product">Product</SortableHeader>
</DataTable.THeadCell>
<DataTable.THeadCell colSpan={2} style={{ textAlign: 'center' }}>
<DataTable.THeadTitle>Sale Rate</DataTable.THeadTitle>
</DataTable.THeadCell>
<DataTable.THeadCell colSpan={2} style={{ textAlign: 'center' }}>
<DataTable.THeadTitle>Profits</DataTable.THeadTitle>
</DataTable.THeadCell>
</DataTable.THeadRow>
<DataTable.THeadRow>
<DataTable.THeadCell>
<SortableHeader field="lastYearSale">Last Year</SortableHeader>
</DataTable.THeadCell>
<DataTable.THeadCell>
<SortableHeader field="thisYearSale">This Year</SortableHeader>
</DataTable.THeadCell>
<DataTable.THeadCell>
<SortableHeader field="lastYearProfit">Last Year</SortableHeader>
</DataTable.THeadCell>
<DataTable.THeadCell>
<SortableHeader field="thisYearProfit">This Year</SortableHeader>
</DataTable.THeadCell>
</DataTable.THeadRow>
<DataTable.THeadRow>
<DataTable.THeadCell>
<DataTable.Filter field="product" display="row" dataType="text">
{({ value, onChange }: DataTableFilterInstance) => (
<InputText
value={(value as string) ?? ''}
onChange={(e: any) => onChange(e, e.target.value)}
placeholder="Search"
size="small"
fluid
/>
)}
</DataTable.Filter>
</DataTable.THeadCell>
<DataTable.THeadCell />
<DataTable.THeadCell>
<DataTable.Filter field="thisYearSale" display="row" dataType="numeric">
{({ value, onChange }: DataTableFilterInstance) => (
<InputText
value={(value as string) ?? ''}
onChange={(e: any) => onChange(e, e.target.value ? Number(e.target.value) : null, 'gte')}
placeholder="≥"
size="small"
fluid
/>
)}
</DataTable.Filter>
</DataTable.THeadCell>
<DataTable.THeadCell />
<DataTable.THeadCell />
</DataTable.THeadRow>
</DataTable.THead>
<DataTable.TBody>
{({ item }: { item: any }) => (
<DataTable.Row key={String(item.key)}>
<DataTable.Cell>
<div className="flex items-center gap-2">
<DataTable.RowToggle>
<DataTable.RowToggleIndicator match="expanded">
<ChevronDown />
</DataTable.RowToggleIndicator>
<DataTable.RowToggleIndicator match="collapsed">
<ChevronRight />
</DataTable.RowToggleIndicator>
</DataTable.RowToggle>
<span className={item._treeHasChildren ? 'font-medium' : ''}>{String(item.product)}</span>
</div>
</DataTable.Cell>
<DataTable.Cell>{Number(item.lastYearSale)}%</DataTable.Cell>
<DataTable.Cell>
<Trend current={Number(item.thisYearSale)} previous={Number(item.lastYearSale)} />
</DataTable.Cell>
<DataTable.Cell>${Number(item.lastYearProfit).toLocaleString()}</DataTable.Cell>
<DataTable.Cell>
<Tag severity={Number(item.thisYearProfit) >= Number(item.lastYearProfit) ? 'success' : 'danger'}>
${Number(item.thisYearProfit).toLocaleString()}
</Tag>
</DataTable.Cell>
</DataTable.Row>
)}
</DataTable.TBody>
<DataTable.EmptyTBody>
<DataTable.Row>
<DataTable.Cell colSpan={5}>
<div className="flex flex-col items-center justify-center gap-3 py-24 text-center">
<div className="relative">
<div className="w-16 h-16 rounded-full bg-surface-100 dark:bg-surface-800 flex items-center justify-center">
<Database className="w-8 h-8 text-surface-400 dark:text-surface-500" />
</div>
<span className="absolute -bottom-0.5 -right-0.5 w-4 h-4 rounded-full border-2 border-surface-0 dark:border-surface-900 bg-primary animate-pulse" />
</div>
<div>
<p className="m-0 font-semibold text-surface-900 dark:text-surface-0">Loading tree</p>
<p className="mt-1 text-sm text-surface-500 dark:text-surface-400">Hang tight, the data is on its way.</p>
</div>
</div>
</DataTable.Cell>
</DataTable.Row>
</DataTable.EmptyTBody>
</DataTable.Table>
</DataTable.TableContainer>
</DataTable.Root>
</div>
);
}
Filter#
Filtering uses the same filters + onFilter API as DataTable; treeMode honours it against the flattened tree. For the standalone hook used outside DataTable, see useTreeFilter.
Basic#
display="row" renders inline inputs directly under each column header and applies as the user types.
'use client';
import { ChevronDown } from '@primeicons/react/chevron-down';
import { Database } from '@primeicons/react/database';
import { ChevronRight } from '@primeicons/react/chevron-right';
import { File } from '@primeicons/react/file';
import { Folder } from '@primeicons/react/folder';
import { Image as ImageIcon } from '@primeicons/react/image';
import { Search } from '@primeicons/react/search';
import { Video } from '@primeicons/react/video';
import { NodeService, type TreeTableNode } from '@/shared/services/node.service';
import { FilterMatchMode } from '@primereact/ui/datatable';
import type { DataTableExpansionEvent } from '@primereact/ui/datatable';
import type { DataTableFilterInstance } from '@primereact/ui/datatable';
import { DataTable } from '@primereact/ui/datatable';
import { IconField } from '@primereact/ui/iconfield';
import { InputText } from '@primereact/ui/inputtext';
import { Select, type SelectValueChangeEvent } from '@primereact/ui/select';
import { Tag } from '@primereact/ui/tag';
import * as React from 'react';
const typeSeverity: Record<string, 'info' | 'warn' | 'success' | 'secondary'> = {
Folder: 'warn',
Document: 'info',
Resume: 'info',
Text: 'secondary',
Application: 'info',
Picture: 'success',
Video: 'success',
Zip: 'secondary',
PDF: 'info',
Link: 'secondary'
};
function TypeIcon({ type }: { type: string }) {
if (type === 'Picture') return <ImageIcon size={16} />;
if (type === 'Video') return <Video size={16} />;
if (type === 'Folder') return <Folder size={16} />;
return <File size={16} />;
}
const typeOptions = [
{ label: 'Any', value: '' },
{ label: 'Folder', value: 'Folder' },
{ label: 'Document', value: 'Document' },
{ label: 'Text', value: 'Text' },
{ label: 'Picture', value: 'Picture' },
{ label: 'Video', value: 'Video' }
];
export default function FilterBasicDemo() {
const [nodes, setNodes] = React.useState<TreeTableNode[]>([]);
const [globalFilter, setGlobalFilter] = React.useState('');
const [expandedKeys, setExpandedKeys] = React.useState<Record<string, boolean>>({});
const [filters, setFilters] = React.useState<Record<string, { value: unknown; matchMode: FilterMatchMode }>>({
name: { value: null, matchMode: FilterMatchMode.Contains },
type: { value: null, matchMode: FilterMatchMode.Equals }
});
React.useEffect(() => {
setNodes(NodeService.getTreeTableNodesData());
}, []);
return (
<div className="w-full">
<div className="mb-3 flex justify-end">
<IconField.Root className="w-full sm:max-w-xs">
<IconField.Inset>
<Search />
</IconField.Inset>
<InputText
type="search"
placeholder="Keyword search"
value={globalFilter}
onChange={(e: any) => setGlobalFilter(e.target.value)}
className="w-full"
/>
</IconField.Root>
</div>
<DataTable.Root
data={nodes}
dataKey="key"
treeMode
expandedKeys={expandedKeys}
onExpandedChange={(e: DataTableExpansionEvent) => setExpandedKeys(e.value)}
filters={filters}
onFilter={(e: { filters: Record<string, unknown> }) => setFilters(e.filters as typeof filters)}
globalFilter={globalFilter}
globalFilterFields={['name', 'type']}
>
<DataTable.TableContainer>
<DataTable.Table style={{ minWidth: '40rem' }}>
<DataTable.THead>
<DataTable.THeadRow>
<DataTable.THeadCell style={{ width: '50%' }}>
<DataTable.THeadTitle>Name</DataTable.THeadTitle>
</DataTable.THeadCell>
<DataTable.THeadCell style={{ width: '20%' }}>
<DataTable.THeadTitle>Size</DataTable.THeadTitle>
</DataTable.THeadCell>
<DataTable.THeadCell style={{ width: '30%' }}>
<DataTable.THeadTitle>Type</DataTable.THeadTitle>
</DataTable.THeadCell>
</DataTable.THeadRow>
<DataTable.THeadRow>
<DataTable.THeadCell>
<DataTable.Filter field="name" display="row" dataType="text">
{({ value, onChange }: DataTableFilterInstance) => (
<InputText
value={(value as string) ?? ''}
onChange={(e: any) => onChange(e, e.target.value)}
placeholder="Search name..."
size="small"
fluid
/>
)}
</DataTable.Filter>
</DataTable.THeadCell>
<DataTable.THeadCell />
<DataTable.THeadCell>
<DataTable.Filter field="type" display="row">
{({ value, onChange }: DataTableFilterInstance) => (
<Select.Root
value={(value as string) ?? ''}
onValueChange={(e: SelectValueChangeEvent) => onChange({} as React.SyntheticEvent, e.value || null)}
options={typeOptions}
optionLabel="label"
optionValue="value"
size="small"
fluid
>
<Select.Trigger className="w-full">
<Select.Value placeholder="Any" />
<Select.Indicator>
<ChevronDown />
</Select.Indicator>
</Select.Trigger>
<Select.Portal>
<Select.Positioner>
<Select.Popup>
<Select.List />
</Select.Popup>
</Select.Positioner>
</Select.Portal>
</Select.Root>
)}
</DataTable.Filter>
</DataTable.THeadCell>
</DataTable.THeadRow>
</DataTable.THead>
<DataTable.TBody>
{({ item }: { item: any }) => (
<DataTable.Row key={String(item.key)}>
<DataTable.Cell>
<div className="flex items-center gap-2">
<DataTable.RowToggle>
<DataTable.RowToggleIndicator match="expanded">
<ChevronDown />
</DataTable.RowToggleIndicator>
<DataTable.RowToggleIndicator match="collapsed">
<ChevronRight />
</DataTable.RowToggleIndicator>
</DataTable.RowToggle>
<TypeIcon type={String(item.type)} />
<span className={item.type === 'Folder' ? 'font-medium' : ''}>{String(item.name)}</span>
</div>
</DataTable.Cell>
<DataTable.Cell>
<span className="text-sm text-surface-500 dark:text-surface-400">{String(item.size)}</span>
</DataTable.Cell>
<DataTable.Cell>
<Tag severity={typeSeverity[String(item.type)] ?? 'secondary'}>{String(item.type)}</Tag>
</DataTable.Cell>
</DataTable.Row>
)}
</DataTable.TBody>
<DataTable.EmptyTBody>
<DataTable.Row>
<DataTable.Cell colSpan={3}>
<div className="flex flex-col items-center justify-center gap-3 py-24 text-center">
<div className="relative">
<div className="w-16 h-16 rounded-full bg-surface-100 dark:bg-surface-800 flex items-center justify-center">
<Database className="w-8 h-8 text-surface-400 dark:text-surface-500" />
</div>
<span className="absolute -bottom-0.5 -right-0.5 w-4 h-4 rounded-full border-2 border-surface-0 dark:border-surface-900 bg-primary animate-pulse" />
</div>
<div>
<p className="m-0 font-semibold text-surface-900 dark:text-surface-0">Loading tree</p>
<p className="mt-1 text-sm text-surface-500 dark:text-surface-400">Hang tight, the data is on its way.</p>
</div>
</div>
</DataTable.Cell>
</DataTable.Row>
</DataTable.EmptyTBody>
</DataTable.Table>
</DataTable.TableContainer>
</DataTable.Root>
</div>
);
}
Advanced#
display="menu" swaps the inline input for a trigger icon that opens a popover with multiple constraints, match mode, operator, and Apply/Clear buttons.
'use client';
import { ChevronDown } from '@primeicons/react/chevron-down';
import { Database } from '@primeicons/react/database';
import { ChevronRight } from '@primeicons/react/chevron-right';
import { File } from '@primeicons/react/file';
import { Filter as FilterIcon } from '@primeicons/react/filter';
import { FilterSlash } from '@primeicons/react/filter-slash';
import { Folder } from '@primeicons/react/folder';
import { Image as ImageIcon } from '@primeicons/react/image';
import { Plus } from '@primeicons/react/plus';
import { Search } from '@primeicons/react/search';
import { Trash } from '@primeicons/react/trash';
import { Video } from '@primeicons/react/video';
import { NodeService, type TreeTableNode } from '@/shared/services/node.service';
import { FilterMatchMode, FilterOperator } from '@primereact/ui/datatable';
import type { DataTableExpansionEvent } from '@primereact/ui/datatable';
import type { DataTableFilterInstance } from '@primereact/ui/datatable';
import { Button } from '@primereact/ui/button';
import { DataTable } from '@primereact/ui/datatable';
import { IconField } from '@primereact/ui/iconfield';
import { InputText } from '@primereact/ui/inputtext';
import { Popover, type PopoverRootOpenChangeEvent } from '@primereact/ui/popover';
import { Select, type SelectValueChangeEvent } from '@primereact/ui/select';
import { Tag } from '@primereact/ui/tag';
import * as React from 'react';
const typeSeverity: Record<string, 'info' | 'warn' | 'success' | 'secondary'> = {
Folder: 'warn',
Document: 'info',
Resume: 'info',
Text: 'secondary',
Application: 'info',
Picture: 'success',
Video: 'success',
Zip: 'secondary',
PDF: 'info',
Link: 'secondary'
};
function TypeIcon({ type }: { type: string }) {
if (type === 'Picture') return <ImageIcon size={16} />;
if (type === 'Video') return <Video size={16} />;
if (type === 'Folder') return <Folder size={16} />;
return <File size={16} />;
}
const operatorOptions = [
{ label: 'Match All (AND)', value: 'and' },
{ label: 'Match Any (OR)', value: 'or' }
];
const typeOptions = [
{ label: 'Any', value: '' },
{ label: 'Folder', value: 'Folder' },
{ label: 'Document', value: 'Document' },
{ label: 'Text', value: 'Text' },
{ label: 'Picture', value: 'Picture' },
{ label: 'Video', value: 'Video' }
];
function FilterSelect<T extends string>({
value,
onChange,
options,
placeholder
}: {
value: T;
onChange: (value: T) => void;
options: { label: string; value: T }[];
placeholder?: string;
}) {
return (
<Select.Root
value={value}
onValueChange={(e: SelectValueChangeEvent) => onChange(e.value as T)}
options={options}
optionLabel="label"
optionValue="value"
size="small"
style={{ width: '100%' }}
>
<Select.Trigger>
<Select.Value placeholder={placeholder} />
<Select.Indicator>
<ChevronDown />
</Select.Indicator>
</Select.Trigger>
<Select.Portal>
<Select.Positioner>
<Select.Popup>
<Select.List />
</Select.Popup>
</Select.Positioner>
</Select.Portal>
</Select.Root>
);
}
function FilterPopover({
isActive,
open,
onOpenChange,
children
}: {
isActive: boolean;
open: boolean;
onOpenChange: (event: PopoverRootOpenChangeEvent) => void;
children: React.ReactNode;
}) {
return (
<Popover.Root open={open} onOpenChange={onOpenChange}>
<Popover.Trigger as={Button} variant="text" severity={isActive ? 'primary' : 'secondary'} size="small" iconOnly aria-label="Filter">
{isActive ? <FilterIcon /> : <FilterSlash />}
</Popover.Trigger>
<Popover.Portal>
<Popover.Positioner sideOffset={4} side="bottom" align="end">
<Popover.Popup className="w-80 p-4">{children}</Popover.Popup>
</Popover.Positioner>
</Popover.Portal>
</Popover.Root>
);
}
function toggleFilterOverlay(
e: PopoverRootOpenChangeEvent,
overlayVisible: boolean,
onToggleOverlay: (event: React.SyntheticEvent) => void,
onHideOverlay: () => void
) {
if (e.value === overlayVisible) return;
if (e.value) {
const nativeEvent = e.originalEvent;
const fakeSynthetic = {
preventDefault: () => nativeEvent?.preventDefault(),
stopPropagation: () => nativeEvent?.stopPropagation(),
nativeEvent
} as unknown as React.SyntheticEvent;
onToggleOverlay(fakeSynthetic);
} else {
onHideOverlay();
}
}
const DEFAULT_FILTERS = {
name: { operator: FilterOperator.And, constraints: [{ value: null, matchMode: FilterMatchMode.Contains }] },
type: { value: null as string | null, matchMode: FilterMatchMode.Equals }
};
export default function FilterAdvancedDemo() {
const [nodes, setNodes] = React.useState<TreeTableNode[]>([]);
const [globalFilter, setGlobalFilter] = React.useState('');
const [filters, setFilters] = React.useState(DEFAULT_FILTERS);
const [expandedKeys, setExpandedKeys] = React.useState<Record<string, boolean>>({});
React.useEffect(() => {
setNodes(NodeService.getTreeTableNodesData());
}, []);
const clearAll = () => {
setFilters(DEFAULT_FILTERS);
setGlobalFilter('');
};
return (
<div className="w-full">
<div className="mb-3 flex items-center justify-between gap-3">
<Button variant="outlined" size="small" onClick={clearAll}>
<FilterSlash /> Clear Filters
</Button>
<IconField.Root className="w-full sm:max-w-xs">
<IconField.Inset>
<Search />
</IconField.Inset>
<InputText
type="search"
placeholder="Keyword search"
value={globalFilter}
onChange={(e: any) => setGlobalFilter(e.target.value)}
className="w-full"
/>
</IconField.Root>
</div>
<DataTable.Root
data={nodes}
dataKey="key"
treeMode
expandedKeys={expandedKeys}
onExpandedChange={(e: DataTableExpansionEvent) => setExpandedKeys(e.value)}
filters={filters}
onFilter={(e: { filters: Record<string, unknown> }) => setFilters(e.filters as typeof filters)}
globalFilter={globalFilter}
globalFilterFields={['name', 'type']}
>
<DataTable.TableContainer>
<DataTable.Table style