TreeView

A tree view is a component based on nodes that are shown in a hierarchical structure.

installyarn add @clayui/core
versionNPM Version
useimport {TreeView} from '@clayui/core';

Example

import {Provider, TreeView} from '@clayui/core';
import Icon from '@clayui/icon';

import '@clayui/css/lib/css/atlas.css';

const spritemap = '/public/icons.svg';

export default function FileExplorer() {
	const items = [
		{
			children: [
				{
					children: [
						{
							children: [{name: 'Research 1'}],
							name: 'Research',
						},
						{
							children: [{name: 'News 1'}],
							name: 'News',
						},
					],
					name: 'Blogs',
				},
				{
					children: [
						{
							children: [
								{
									name: 'Instructions.pdf',
									status: 'success',
									type: 'pdf',
								},
							],
							name: 'PDF',
						},
						{
							children: [
								{
									name: 'Treeview review.docx',
									status: 'success',
									type: 'document',
								},
								{
									name: 'Heuristics Evaluation.docx',
									status: 'success',
									type: 'document',
								},
							],
							name: 'Word',
						},
					],
					name: 'Documents and Media',
				},
			],
			name: 'Liferay Drive',
			type: 'cloud',
		},
		{
			children: [{name: 'Blogs'}, {name: 'Documents and Media'}],
			name: 'Repositories',
			type: 'repository',
		},
		{
			children: [{name: 'PDF'}, {name: 'Word'}],
			name: 'Documents and Media',
			status: 'warning',
		},
	];

	const TYPES_TO_SYMBOLS = {
		document: 'document-text',
		pdf: 'document-pdf',
		success: 'check-circle-full',
		warning: 'warning-full',
	};

	const TYPES_TO_COLORS = {
		document: 'text-primary',
		pdf: 'text-danger',
		success: 'text-success',
		warning: 'text-warning',
	};

	return (
		<Provider spritemap={spritemap}>
			<TreeView defaultItems={items} dragAndDrop nestedKey="children">
				{(item) => (
					<TreeView.Item>
						<TreeView.ItemStack>
							<Icon symbol={item.type ? item.type : 'folder'} />
							{item.name}
							{item.status && (
								<Icon
									className={TYPES_TO_COLORS[item.status]}
									symbol={TYPES_TO_SYMBOLS[item.status]}
								/>
							)}
						</TreeView.ItemStack>
						<TreeView.Group items={item.children}>
							{({name, status, type}) => (
								<TreeView.Item>
									{type && (
										<Icon
											className={TYPES_TO_COLORS[type]}
											symbol={TYPES_TO_SYMBOLS[type]}
										/>
									)}
									{name}
									{status && (
										<Icon
											className={TYPES_TO_COLORS[status]}
											symbol={TYPES_TO_SYMBOLS[status]}
										/>
									)}
								</TreeView.Item>
							)}
						</TreeView.Group>
					</TreeView.Item>
				)}
			</TreeView>
		</Provider>
	);
}

Introduction

The TreeView component is designed to display data using a hierarchical structure. It was built with performance in mind, which influences API design and feature implementations.

This component follows a slightly different thought from our low-level and high-level philosophy, we consider this to be a middle-level component that allows you to build the two in one, allowing flexibility and adding features that you wouldn’t have in a low-level. It’s also easy to move from low-level to high-level quickly and the APIs are more consistent.

Content

As TreeView is middle-level, it allows you to build static or dynamic content if you need to consume data from some service.

It uses a very common technique in React.js components called render props whose value is a function that will receive some data. This is essential for the component to be data agnostic. It allows more flexibility with the names of the properties to be rendered and avoids creating APIs to change those properties.

<Mouse render={(mouse) => <Cat mouse={mouse} />} />

Another benefit is using children as a render prop which allows you to quickly adapt the static component to dynamic just by changing the syntax.

<Mouse>{(mouse) => <Cat mouse={mouse} />}</Mouse>

The main components of the TreeView are <TreeView.Item />, <TreeView.Group /> and <TreeView.ItemStack /> which abstract all the necessary markup complexity and allow the granular composition to build different styles of a TreeView. For example: File Explorer, Users, Page Navigation, or Page Structure.

Static

<TreeView>
	<TreeView.Item>
		<TreeView.ItemStack>
			<Sticker displayType="primary" shape="user-icon" size="sm">
				NS
			</Sticker>
			nisi quis eleifend
		</TreeView.ItemStack>
		<TreeView.Group>
			<TreeView.Item key="Victor Valle">
				<TreeView.ItemStack>
					<Sticker displayType="primary" shape="user-icon" size="sm">
						FP
					</Sticker>
					fusce ut placerat
				</TreeView.ItemStack>
				<TreeView.Group>
					<TreeView.Item key="susana-vázquez">
						<Sticker
							displayType="primary"
							shape="user-icon"
							size="sm"
						>
							UT
						</Sticker>
						ultrices dui sapien
					</TreeView.Item>
				</TreeView.Group>
			</TreeView.Item>
			<TreeView.Item key="emily-young">
				<Sticker displayType="primary" shape="user-icon" size="sm">
					MC
				</Sticker>
				maecenas pharetra convallis
			</TreeView.Item>
		</TreeView.Group>
	</TreeView.Item>
</TreeView>

Dynamic

The TreeView and Group components when used with dynamic items are populated from hierarchical data. The component consumes the data using the items property.

For the component to be dynamic it is necessary to convert children to render prop, which must be a function that will receive the item of the current iterator.

function Example() {
	const items = [
		{
			children: [
				{
					children: [{name: 'ultrices dui sapien'}],
					name: 'fusce ut placerat',
				},
				{name: 'maecenas pharetra convallis'},
			],
			name: 'nisi quis eleifend',
		},
	];

	return (
		<TreeView defaultItems={items} nestedKey="children">
			{(item) => (
				<TreeView.Item key={item.name}>
					<TreeView.ItemStack>{item.name}</TreeView.ItemStack>

					{item.children && (
						<TreeView.Group items={item.children}>
							{(item) => (
								<TreeView.Item key={item.name}>
									{item.name}
								</TreeView.Item>
							)}
						</TreeView.Group>
					)}
				</TreeView.Item>
			)}
		</TreeView>
	);
}

Asynchronous Item

When a tree is very large, loading items (nodes) asynchronously is preferred to decrease the initial payload and memory space. TreeView provides two ways to load asynchronous data:

  • Load the children of an item when clicking on the item
  • Load paginated data from an item

Load the children of an item

The TreeView doesn’t know when an item is asynchronous, so the developer must specify whether the item is asynchronous or not. The onLoadMore property is called every time the item is a leaf node of the tree and depending on the method’s return value it will behave differently.

  • When returning void , null or undefined the TreeView will do nothing.
  • When returning the item will add to the tree.

When adding a new asynchronous item to the tree, the onItemsChange method is respectively called to update the tree with a new value if the items prop is controlled.

Warning If you have an error in the asynchronous call of the onLoadMore method, only the suppression is done and an error is thrown on the console.

<TreeView
	onLoadMore={async (item) => {
		return await fetch(`example.com/tree/item?parent_id=${item.id}`);
	}}
>
	...
</TreeView>

Load paginated data from an item

The onLoadMore API can also be used to load paginated data for a specific item. The signature just needs to be followed so the TreeView can know what to do.

<TreeView
	onLoadMore={async (item, cursor) => {
		const result = await fetch(
			cursor ?? `example.com/tree/item?parent_id=${item.id}`
		);

		return {
			cursor: result.next,
			items: result.data,
		};
	}}
>
	...
</TreeView>

The cursor helps to store the data that will be used to identify which will be the next request, this can be an offset, cursor, id the URL or any other data that represents the item cursor, when there is no more data just sets the cursor to null. The TreeView also exposes a public API that can invoke loadMore and find out if there is a cursor for a specific item.

<TreeView>
	{(item, selection, expand, load) => (
		<TreeView.Item>
			<TreeView.ItemStack>{item.name}</TreeView.ItemStack>
			<TreeView.Group items={item.children}>
				{(item) => <TreeView.Item>{item.name}</TreeView.Item>}
			</TreeView.Group>

			{expand.has(item.id) && load.has(item.id) !== null && (
				<Button
					borderless
					displayType="secondary"
					onClick={() => load.loadMore(item.id, item)}
				>
					Load more results
				</Button>
			)}
		</TreeView.Item>
	)}
</TreeView>

Item

Item is very flexible and can behave in different ways to simplify usage for different use cases depending on the data.

Item also allows defining the unique key of the component across the tree, which is also used to tell which items are expanded and selected.

Key

In a dynamic content, if the data has the property id it is used as key, otherwise, a key is generated for each item at the time of rendering.

In static content, it is also possible to define the key to be used in the selection and expanding, if not defined it is also generated.

{
	(item) => <TreeView.Item key={item.name}>{item.name}</TreeView.Item>;
}

<TreeView.Item key="Drive">Drive</TreeView.Item>;

String

<TreeView.Item>Drive</TreeView.Item>

Single level

<TreeView.Item>
	<Checkbox />
	<Icon symbol="folder" />
	Drive
</TreeView.Item>

Nested

Creating a nested item is necessary to declare it inside the <TreeView.Item> to differentiate the nested items from the current item, it is necessary to wrap the item elements in a <TreeView.ItemStack />. To declare the items of an item, declare the same components nested inside a <TreeView.Group /> component.

<TreeView.Item>
	<TreeView.ItemStack>
		<Checkbox />
		<Icon symbol="folder" />
		Drive
	</TreeView.ItemStack>
	<TreeView.Group>
		<TreeView.Item disabled={true}>
			<Checkbox />
			<Icon symbol="folder" />
			Documents
		</TreeView.Item>
	</TreeView.Group>
</TreeView.Item>

Actions

Actions are added using the actions property on each item. The component abstracts some of the complexities of markup and class usage to make composing components easier without having to add explicitly classes. This works great for Clay’s Button and DropDownWithItems components, other components should consider adding the class component-action explicitly.

<TreeView>
	<TreeView.Item
		actions={
			<>
				<Button displayType={null} monospaced>
					<Icon symbol="times" />
				</Button>
				<DropDownWithItems
					items={[{label: 'One'}, {label: 'Two'}, {label: 'Three'}]}
					trigger={
						<Button displayType={null} monospaced>
							<Icon symbol="ellipsis-v" />
						</Button>
					}
				/>
			</>
		}
	>
		<Icon symbol="folder" />
		Folder
	</TreeView.Item>
</TreeView>

Disabled

An item can be disabled by setting the disabled prop in the <TreeView.ItemStack /> and <TreeView.Item /> components. By default, the Expander and Checkbox are also disabled, and any other clickable elements other than these need to be set to disabled in the composition.

<TreeView.Item>
	<TreeView.ItemStack disabled={true}>
		<Checkbox />
		<Icon symbol="folder" />
		Drive
	</TreeView.ItemStack>
	<TreeView.Group>
		<TreeView.Item disabled={true}>
			<Checkbox />
			<Icon symbol="folder" />
			Documents
		</TreeView.Item>
	</TreeView.Group>
</TreeView.Item>

Expander

The expansion of an item in the TreeView is controlled internally. It’s also possible to control the state and set an initial value with the keys of the items that are initially expanded.

The TreeView exposes the expandedKeys property and the onExpandedChange callback to control the state of expanded items. The expandedKeys property must be a collection of values using ia Set().

function Example() {
	const [expandedKeys, setExpandedKeys] = useState(new Set(['1', '2', '3']));

	return (
		<TreeView
			expandedKeys={expandedKeys}
			onExpandedChange={setExpandedKeys}
		>
			...
		</TreeView>
	);
}

Custom expander

Customizing the expander is possible using the expanderIcons property. Changing the icons is applied the entire tree, and it’s not possible to change them exclusively for a single item.

<TreeView
	expanderIcons={{
		close: <Icon symbol="hr" />,
		open: <Icon symbol="plus" />,
	}}
>
	...
</TreeView>

Disabled

The Expander is automatically disabled when the item is disabled but you can optionally control its state.

<TreeView.Item>
	<TreeView.ItemStack disabled={true} expanderDisabled={false}>
		<Checkbox />
		<Icon symbol="folder" />
		Drive
	</TreeView.ItemStack>
	<TreeView.Group>
		<TreeView.Item disabled={true}>
			<Checkbox />
			<Icon symbol="folder" />
			Documents
		</TreeView.Item>
	</TreeView.Group>
</TreeView.Item>

The expanderDisabled property is only available in the <TreeView.ItemStack /> component where the Expander is visible and has children.

Manual Expand

Some more advanced use cases want to change the behavior of expanding the item when the user interacts with the item differently, e.g single click selects, double click expands the item.

The expand method is available via render props only when the content is dynamic, the same as selection, expose two methods, toggle and has.

<TreeView>
	{(item, selection, expand) => (
		<TreeView.Item
			onClick={(event) => {
				clearTimeout(clickTimerRef.current);

				// Ignores the component's default behavior, clicking the
				// item expands the item if it has any child items.
				event.preventDefault();

				switch (event.detail) {
					case 1:
						clickTimerRef.current = setTimeout(() => {
							if (!selection.has(item.id)) {
								selection.toggle(item.id);
							}
						}, 200);
						break;
					case 2:
						expand.toggle(item.id);
					default:
						break;
				}
			}}
		>
			Item
		</TreeView.Item>
	)}
</TreeView>

Selection

The selection allows the user to select one or more items in the TreeView, there are three main interactions that can be configured and are defined using the selectionMode prop.

  • single select only one item.
  • multiple select multiple items.
  • multiple-recursive selects multiple items and the item’s children recursively. When all children are selected, the parent will be marked as selected, otherwise it will be marked as intermediate.

The selection can be configured and composed in different ways depending on each use case. Setting the selectionMode and using a <Checkbox /> in the item will respect the configuration of the selection. It is also possible to configure the selection manually to customize what will trigger the selection or modify the look of the selected state. For more information check the manual selection section.

Selection can be controlled and uncontrolled, this means you can keep the state of selectedKeys at the implementation level or let the TreeView keep the state controlled internally.

const [selectedKeys, setSelectionChange] = useState(new Set());

return (
	<TreeView
		onSelectionChange={(keys) => setSelectionChange(keys)}
		selectedKeys={selectedKeys}
	>
		...
	</TreeView>
);

Rendering the TreeView with pre-selected items will cause their parent items to be expanded so that the selected item is visible on the first render. In recursive multiple selection, the parents items will marked as intermediate.

Info Expanding selected items in the first render implies performance degradation if the TreeView has too many items; but there are some possibilities to work around depending on the use case, read the Selection hydration section for more information to mitigate this when viewing a noticeable slowdown on the first render.

Single Selection

The single selection state works independently of adding the <Checkbox /> and this state is set by default.

Multiple Selection

The multi-selection in TreeView is a achieved by composing with the <Checkbox /> component that must be added explicitly in the item rendering.

<TreeView.Item>
	<Checkbox />
	Drive
</TreeView.Item>
<TreeView.Item>
	<TreeView.ItemStack>
		<Checkbox />
		Drive
	</TreeView.ItemStack>
	<TreeView.Group>...</TreeView.Group>
</TreeView.Item>

It isn’t necessary to add the onChange event or the checked property. The TreeView adds the methods to the component under the covers. This allows you to just add the Checkbox in any order you want.

Multi-selection is also controlled in the same way as for the expander. It uses the key of the item to select it. The selectedKeys property and the onSelectionChange event are used for this purpose.

function Example() {
	const [selectedKeys, setSelectionChange] = useState(
		new Set(['1', '2', '3'])
	);

	return (
		<TreeView
			onSelectionChange={setSelectionChange}
			selectedKeys={selectedKeys}
			selectionMode="multiple"
		>
			...
		</TreeView>
	);
}

Recursive Multiple Selection

By default, when selecting an item with nested items, its children are recursively selected.

There are some limitations: for static content the recursive selection only works if the item is expanded, (i.e visible). For dynamic content, it works independently.

Warning Recursive selection will not select items from an asynchronous Item.

Manual Selection

Manual selection is the possibility to trigger the selection without using the <Checkbox /> or visually changing the state of the selection types, such as the single selection state.

The selection method is available via render props when the content is dynamic, it exposes two main methods, toggle and has that allow you to customize who will trigger and check if the item is selected.

<TreeView>
	{(item, selection) => (
		<TreeView.Item
			active={selection.has(item.id)}
			onClick={() => selection.toggle(item.id)}
		>
			Drive
		</TreeView.Item>
	)}
</TreeView>

The default behavior of clicking the item is to expand and load the item asynchronously if this is not needed when the user selects an item you can prevent this default behavior in the same way you would prevent the default behavior of the browser.

<TreeView>
	{(item) => (
		<TreeView.Item
			onClick={(event) => {
				event.preventDefault();

				// You can do anything after that, for example in a Modal,
				// close and select.
				onClose(item);
			}}
		>
			Drive
		</TreeView.Item>
	)}
</TreeView>

Drag and Drop

The drag and drop implementation handles items internally and works only for TreeView with dynamic content because it depends on the items property and must be enabled using the dragAndDrop property.

The TreeView handles items immutably but follows a partial tree immutability implementation that is more optimized to do at runtime, To access these changes and be able to save in some service, you must control the state using the items property and the onItemsChange event.

function Example() {
	const [items, setItems] = useState([]);

	return (
		<TreeView dragAndDrop items={items} onItemsChange={setItems}>
			...
		</TreeView>
	);
}

The component allows you to add rules in Drag and Drop like disabling an item to be draggable and dropable, for this you need to set the draggable property of the component to TreeView.Item or TreeView.ItemStack.

<TreeView dragAndDrop>
	{(item) => (
		<TreeView.Item>
			<TreeView.ItemStack draggable={false}>
				{item.name}
			</TreeView.ItemStack>
			<TreeView.Item.Group items={item.children}>
				{(item) => (
					<TreeView.Item draggable={false}>{item.name}</TreeView.Item>
				)}
			</TreeView.Item.Group>
		</TreeView.Item>
	)}
</TreeView>

In some cases it is necessary to check if the item can be dropped in another item, for example a TreeView from a file explorer, does not allow moving a file into another file. You need to use onItemMove to decide if the item can be droppable on a specific item.

<TreeView
	dragAndDrop
	onItemMove={(item, parentItem) => {
		// your logic here

		// Returning false does not allow the item to be dropped in the parentItem.
		return false;
	}}
>
	...
</TreeView>

Shortcuts

The TreeView implements shortcuts and manages the focus. Some shortcuts and focus trigger some actions like renaming, removing or asynchronous loading if any.

Warning If an Item is asynchronous the onLoadMore method will be called as described in the Asynchronous Item section.

Rename Item

The user can press the shortcut key R or F2 to rename whatever property. In that case, feel free to open a Modal or any other user interface element that allows the user to change the item. For this to work, the onRenameItem method must be passed as a property to the component and must be an asynchronous method.

The onRenameItem property receives the item object corresponding to the current item that the user wants to rename and the method needs to return a new immutable object with the new data.

<TreeView
	dragAndDrop
	onRenameItem={async (item) => {
		return await openRenameModal(item);
	}}
>
	...
</TreeView>

Remove Item

If the user presses the Backspace or Delete key the TreeView removes the item from the items structure. In which case onItemsChange is called with the tree is updated.

Performance

The TreeView component was designed with performance in mind. Internally, the component handles everything without the developer having to worry about making implementation adjustments, but in some scenarios, it may be necessary for the developer to follow good practices to avoid a performance degradation.

This section helps with some good practices that help the TreeView remain responsive.

Selection hydration

Selection hydration is the ability for the TreeView to expand the parent items of the selected items when the component performs the first render.

This implies some performance problems because the component only knows about the selectedKeys and the items, and will have to traverse the entire tree to find the selected items and the path to the parent item to expand it.

This can lead to a performance problem on the first render if you have too many items or too many selected items. It’s possible to mitigate this on the first render by setting the selectionHydrationMode prop: it supports two rendering phases, render-first and hydrate-first, both have trade-offs that depend on the number of items being rendered:

  • render-first will render first and then hydrate. It doesn’t block the initial rendering but it is possible to see the items being expanded.
  • hydrate-first will hydrate first and then render. This blocks rendering first until it traverses the tree, when rendered the items are already expanded.

Info The tree traversal implementation internally has some performance optimizations. Depending on the number of selected items, the TreeView stops traversing the tree when it finds all the selected items. In most scenarios, the TreeView will almost never traverse the entire tree.

API Reference

TreeView

View Source

displayType

'light' | 'dark'= 'light'

Flag to determine which style the TreeView will display.

dragAndDrop

boolean= false

Flag to enable Drag and Drop of Nodes over the Tree.

dragAndDropContext

Window & typeof globalThis= window

Optional drag and drop context: this is to avoid errors when using various drag and drop contexts on the same page.

expandDoubleClick

boolean= false

Flag to expand the node's children when double-clicking the node.

expandOnCheck

boolean= false

Flag to expand child nodes when a parent node is checked.

expanderClassName

string

Extra classes passed to the expander button.

expanderIcons

Icons

Flag to modify Node expansion state icons.

itemNameKey

string= 'name'

Flag to indicate which key name matches the item name to be displayed in drag preview.

messages

DragAndDropMessages

Messages that the TreeView uses to announce to the screen reader. Use this to handle internationalization.

onItemHover

(item: T, parentItem: T, index: MoveItemIndex) => boolean

The callback is called whenever there is an item dragging over another item.

onItemMove

(item: T, parentItem: T, index: MoveItemIndex) => boolean

Callback is called when an item is about to be moved elsewhere in the tree.

onLoadMore

OnLoadMore<T>

When a tree is very large, loading items (nodes) asynchronously is preferred to decrease the initial payload and memory space. The callback is called every time the item is a leaf node of the tree.

onSelect

(item: T) => void

Callback called whenever an item is selected. Similar to the onSelectionChange callback but instead of passing the selected keys it is called with the current item being selected.

onRenameItem

(item: T) => Promise<T>

Calback is called when the user presses the R or F2 hotkey.

selectionMode

'single' | 'multiple' | 'multiple-recursive' | null= 'single'

Flag changes the Node selection behavior when a checkbox is rendered on the Node.

  • single: select only node.
  • multiple: select multiple nodes.
  • multiple-recursive: selects multiple nodes and recursively.

showExpanderOnHover

boolean= true

Flag to indicate if the TreeView will show the expander in the hover in the Node.

nestedKey

string= 'children'

Flag to indicate which key name matches the nested rendering of the tree.

onItemsChange

(items: ICollectionProps<T>['items']) => void

A callback which is called when the property of items is changed.

defaultExpandedKeys

Set<Key>

Property to set the initial value of expandedKeys.

expandedKeys

Set<Key>

The currently expanded keys in the collection.

onExpandedChange

(keys: Set<Key>) => void

A callback that is called when items are expanded or collapsed.

selectionHydrationMode

'render-first' | 'hydrate-first'= 'hydrate-first'

Flag to indicate the hydration phase to expand the selected items. When selectionMode is multiple-recursive it also revalidates the indeterminate state of the items.

It supports two rendering phases, render-first and hydrate after or hydrate before rendering, both have trade-offs that depend on the number of items being rendered.

Both cases traverse the tree looking for the selected items to know which items should be expanded and which should be in the indeterminate state, this is done only the first time the component is rendered and if it has selected items. This operation can degrade the performance of the component depending on the number of items, choose the best option for your use case.

  • render-first will render first and then hydrate. It doesn't block the initial rendering but after rendering it is possible to see the items being expanded.

  • hydrate-first will hydrate first and then render. This blocks rendering first until it traverses the tree, when rendered the items are already expanded.

defaultSelectedKeys

Set<Key>

Property to set the initial value of selectedKeys.

onSelectionChange

(keys: Set<Key>) => void

Handler that is called when the selection changes.

selectedKeys

Set<Key>

The currently selected keys in the collection.

indeterminate

boolean= true

Flag to disable indeterminate state when selectionMode is multiple-recursive.

items

Array<T>

Property to inform the dynamic data of the tree.

defaultItems

Array<T>

Property to set the initial value of items.

children *

React.ReactNode | ChildrenFunction<T>