Skip to main content

Tree view

With TreeView you can render a nested data structure. You can also use filtering and a lot of other features.

Import

import { TreeView, TreeNode, TreeProps } from '@jutro/components';

Nodes

You can add elements to the tree view as JSX children components or as an array of objects passed to the nodes prop.

The nodes prop

If you want to use the nodes prop, pass an array of objects with the following keys:

  • id - a unique ID for the node. It is used as the ID in HTML
  • label - the text to display
  • nodes (optional) - an array of child nodes
const foods = [
{
id: 'fruit',
label: { id: 'foods.Fruit', defaultMessage: 'Fruit' },
nodes: [
{
id: 'berries',
label: { id: 'foods.Berries', defaultMessage: 'Berries' },
nodes: [
{
id: 'cranberry',
label: {
id: 'foods.Cranberry',
defaultMessage: 'Cranberry',
},
},
{
id: 'banana',
label: { id: 'foods.Banana', defaultMessage: 'Banana' },
},
],
},
{
id: 'apple',
label: { id: 'foods.Apple', defaultMessage: 'Apple' },
},
],
},
{
id: 'sandwiches',
label: { id: 'foods.Sandwiches', defaultMessage: 'Sandwiches' },
nodes: [
{
id: 'hotdog',
label: { id: 'foods.hot dog', defaultMessage: 'Hot dog' },
},
{
id: 'ham',
label: {
id: 'foods.ham sandwich',
defaultMessage: 'Ham sandwich',
},
},
],
},
{
id: 'soup',
label: { id: 'foods.soups', defaultMessage: 'soups' },
nodes: [
{
id: 'tomato',
label: {
id: 'foods.tomato soup',
defaultMessage: 'tomato soup',
},
},
{
id: 'potato',
label: {
id: 'foods.potato soup',
defaultMessage: 'potato soup',
},
},
],
},
];

return <TreeView nodes={foods} />;

renderNodes

If you want to override something about your nodes when using the nodes props, you can do it with renderNodes.

The props object of renderNodes contains the following:

  • nodeId - the id assigned to the tree node
  • nodes - the nodes object passed to the tree node

The nodes object contains nested tree nodes, so you need to iterate recursively to render the entire tree.

For example, imagine you want to apply some nice styles to each node. Create a custom component that takes the list of nodes and renders them as TreeNodes recursively while applying the style to each one.

const TomatoNodes = ({ nodes }) => {
return (
<>
{nodes.map(({ id, label, nodes: childNodes }) => (
<TreeNode
id={id}
label={label}
style={{
backgroundColor: 'tomato',
color: 'blue',
border: '1px solid yellow',
}}>
{childNodes && <TomatoNodes nodes={childNodes} />}
</TreeNode>
))}
</>
);
};

Then use the component in renderNodes like so:

<TreeView
nodes={foods}
renderNodes={TomatoNodes}
/>

JSX children

If you prefer to use JSX children, you can do it like this:

<TreeView>
<TreeNode
id="1"
label="Fruit">
<TreeNode
id="2"
label="Banana"
/>
<TreeNode
id="3"
label="Apple"
/>
</TreeNode>
<TreeNode
id="4"
label="Vegetables (coming soon!)"
/>
</TreeView>

A more complex example available in Storybook.

Custom tree nodes

The best way to implement custom tree nodes is to wrap the TreeNode element.

Let's create a tree node with a link in the header:

TreeNodeLink.jsx
import React, { useMemo } from 'react';
import { Link } from '@jutro/components';
import { TreeNode, TreeNodeHeader } from '@jutro/components';

const TreeNodeLink = ({ to, ...otherProps }) => {
const renderHeader = useMemo(
() => (props) =>
(
<Link to={to}>
<TreeNodeHeader {...props} />
</Link>
),
[to]
);

return (
<TreeNode
{...otherProps}
renderHeader={renderHeader}
/>
);
};

This component does a few things, so let's break it down:

  1. The component returns a TreeNode and we pass the props down to it.
  2. The component also takes an additional prop, to, which is our link target.
  3. To display the link, our component uses the renderHeader callback. The callback returns a TreeNodeHeader wrapped in a Jutro Link.
  4. The custom header is additionally wrapped in a useMemo, so that it is not re-mounted on every render.

You can now use the component like so:

<TreeView>
<TreeNodeLink
id="entrees"
label="Entrees"
to="/menu/entrees">
<TreeNodeLink
id="pasta"
label="Pasta"
to="/menu/entrees/pasta"
/>
<TreeNodeLink
id="steaks"
label="Steaks"
to="/menu/entrees/steaks"
/>
</TreeNodeLink>
</TreeView>

defaultRenderHeader

Each item has a header component which you can override using the defaultRenderHeader callback function. The props object the following:

  • id - the id of the header element. By default, is the node ID with a -header suffix
  • nodeId - the id assigned to the tree node
  • label - the label assigned to the tree node
<TreeView
nodes={nodes}
defaultRenderHeader={({ is, nodeId, label }) => (
<MyComponent id={id}>{label}</MyComponent>
)}
/>

defaultRenderChildren

The defaultRenderChildren callback allows you to wrap the collection of child nodes in a custom wrapper. The props object contains the following:

  • nodeId - the id assigned to the tree node
  • expanded - true if the node is currently expanded
  • children - child nodes of the node component

Using defaultRenderChildren on the TreeView works on all nested groups of children.

<TreeView
nodes={foods}
defaultRenderChildren={({ nodeId, children }) => {
// using Card imported from @jutro/components
return <Card id={`${nodeId}-card`}>{children}</Card>;
}}></TreeView>

onClick

You can assign a custom onClick function which is triggered when a user clicks any of the nodes in the tree. The function takes a single object which contains the following:

  • hasNodes - true if the node has child nodes
  • id - the id assigned to the tree node
  • expanded (optional) - true if the node is currently expanded

Filter

If you want to include a filter field in your tree view, add the showFilter prop. The field uses a default filtering function, and if you want to override it, use the filterCallback prop.

The filterCallback function takes two arguments:

  • the translated label of the tree element
  • the filtering query that the user entered in the filter field

The function needs to return an array of indices that show ranges where the label matches the query, for example [{ start: 2, end: 4 }, { start: 7, end: 9 }].

If the query does not match, the function needs to return null.

The example below matches items only if the label starts with the query (case sensitive).

<TreeView
showFilter
filterCallback={(value, query) => {
if (value.startsWith(query)) {
return [{ start: 0, end: query.length }];
}

return null;
}}
/>

The filter field is a standard Jutro InputField. You have two options to customize it:

  • Pass additional props using the filterProps prop

    <TreeView
    showFilter
    filterProps={{
    icon: 'gw-bathtub',
    placeholder: 'Item starts with...',
    }}
    />
  • Render a custom filter field using renderFilter. The callback allows you to pass the onChange event from the tree view.

    <TreeView
    showFilter
    renderFilter={(onChange) => (
    <input onChange={(event) => onChange(event.target.value)} />
    )}
    />

Icons

You can control icons displayed in the tree view using the following props:

  • defaultParentIcon - the default icon for a component which has children

  • defaultEndIcon - the default icon for a component without any children

    <TreeView
    defaultParentIcon="gw-business-center"
    defaultEndIcon="gw-account-box"
    />
  • defaultRenderIcon - the functional component to render icons, the props contain the following:

    • nodeId - the id assigned to the tree node
    • icon - the string denoting the name of the icon, for example the string passed to defaultParentIcon.
    <TreeView
    defaultRenderIcon={({ icon, nodeId }) => (
    <Link>
    {nodeId}: {icon}
    </span>
    )}
    />
  • defaultRenderExpandIcon - the function to render the "expand" chevron. The first argument is an object which contains the following parameters:

    • nodeId - the id assigned to the tree node
    • expanded - true if the node is currently expanded
    • isEndNode - true if the node does not have any child nodes

    You can override the default chevron like this:

    <TreeView
    defaultRenderExpandIcon={({ expanded, isEndNode }) => {
    if (isEndNode) {
    return <span>🤔</span>;
    }

    return <span>{expanded ? '👇' : '👉'}</span>;
    }}
    />

Accessibility

Tree view is fully accessible and supports keyboard navigation.

Usage in metadata

If you use this component only in metadata and you don't use it in JSX, you need to import this module explicitly.

import '@jutro/components';

Learn more

To see examples of what you can do, visit Storybook.