Inspired by Downshift, a simple, data-driven, light-weight React Tree Menu component that:
- does not depend on any UI framework
- fully customizable with
render props
andcontrol props
- allows search
- support keyboard browsing
Check Storybook Demo.
Install with the following command in your React app:
npm i react-simple-tree-menu
// or
yarn add react-simple-tree-menu
To generate a TreeMenu
, you need to provide data in the following structure.
// as an array
const treeData = [
{
key: 'first-level-node-1',
label: 'Node 1 at the first level',
..., // any other props you need, e.g. url
nodes: [
{
key: 'second-level-node-1',
label: 'Node 1 at the second level',
nodes: [
{
key: 'third-level-node-1',
label: 'Last node of the branch',
nodes: [] // you can remove the nodes property or leave it as an empty array
},
],
},
],
},
{
key: 'first-level-node-2',
label: 'Node 2 at the first level',
},
];
// or as an object
const treeData = {
'first-level-node-1': { // key
label: 'Node 1 at the first level',
index: 0, // decide the rendering order on the same level
..., // any other props you need, e.g. url
nodes: {
'second-level-node-1': {
label: 'Node 1 at the second level',
index: 0,
nodes: {
'third-level-node-1': {
label: 'Node 1 at the third level',
index: 0,
nodes: {} // you can remove the nodes property or leave it as an empty array
},
},
},
},
},
'first-level-node-2': {
label: 'Node 2 at the first level',
index: 1,
},
};
And then import TreeMenu
and use it. By default you only need to provide data
. You can have more control over the behaviors of the components using the provided API.
import TreeMenu from 'react-simple-tree-menu';
...
// import default minimal styling or your own styling
import '../node_modules/react-simple-tree-menu/dist/main.css';
// Use the default minimal UI
<TreeMenu data={treeData} />
// Use any third-party UI framework
<TreeViewMenu
data={treeData}
onClickItem={({ key, label, ...props }) => {
this.navigate(props.url); // user defined prop
}}
debounceTime={125}>
{({ search, items }) => (
<>
<Input onChange={e => search(e.target.value)} placeholder="Type and search" />
<ListGroup>
{items.map(props => (
// You might need to wrap the third-party component to consume the props
// check the story as an example
// https://github.com/iannbing/react-simple-tree-menu/blob/master/stories/index.stories.js
<ListItem {...props} />
))}
</ListGroup>
</>
)}
</TreeViewMenu>
If you want to extend the minial UI components, they are exported at your disposal.
// you can import and extend the default minial UI
import TreeMenu, { defaultChildren, ItemComponent } from 'react-simple-tree-menu';
// add custom styling to the list item
<TreeViewMenu data={treeData}>
{({ search, items }) => (
<ul>
{items.map(props => (
<ItemComponent {...props, style: { background: 'pink' }} />
))}
</ul>
)}
</TreeViewMenu>
// add a button to do resetOpenNodes
<TreeViewMenu data={treeData}>
{({ search, items, resetOpenNodes }) => (
<div>
<button onClick={resetOpenNodes} />
{defaultChildren({search, items})}
</div>
)}
</TreeViewMenu>
When the tree menu is focused, you can use your keyboard to browse the tree.
- UP: move the focus onto the previous node
- DOWN: move the focus onto the next node
- LEFT: close the current node if it has children and it is open; otherwise move the focus to the parent node
- RIGHT: open the current node if it has children
- ENTER: fire
onClick
function and setactiveKey
to current node
Note the difference between the state active
and focused
. ENTER is equivalent to the onClick
event, but focus does not fire onClick
.
props | description | type | default |
---|---|---|---|
data | Data that defines the structure of the tree. You can nest it as many levels as you want, but note that it might cause performance issue. | {[string]:TreeNode} | TreeNodeInArray[] | - |
activeKey | the node matching this key will be active | string | '' |
focusKey | the node matching this key will be focused | string | '' |
initialActiveKey | set initial state of activeKey |
string | - |
initialFocusKey | set initial state of focusKey |
string | - |
onClickItem | A callback function that defines the behavior when user clicks on an node | (Item): void | console.warn |
debounceTime | debounce time for searching | number | 125 |
openNodes | you can pass an array of node names to control the open state of certain branches | string[] | - |
initialOpenNodes | you can pass an array of node names to set some branches open as initial state | string[] | - |
locale | you can provide a function that converts label into string |
({label, ...other}) => string | ({label}) => label |
hasSearch | Set to false then children will not have the prop search |
boolean | true |
matchSearch | you can define your own search function | ({label, searchTerm, ...other}) => boolean | ({label, searchTerm}) => isVisible |
children | a render props that provdes two props: search , items and resetOpenNodes |
(ChildrenProps) => React.ReactNode | - |
props | description | type | default |
---|---|---|---|
label | the rendered text of a Node | string | '' |
index | a number that defines the rendering order of this node on the same level; this is not needed if data is TreeNode[] |
number | - |
nodes | a node without this property means that it is the last child of its branch | {[string]:TreeNode} | TreeNode[] | - |
...other | User defined props | any | - |
props | description | type | default |
---|---|---|---|
key | Node name | string | - |
label | the rendered text of a Node | string | '' |
nodes | a node without this property means that it is the last child of its branch | {[string]:TreeNode} | TreeNode[] | - |
...other | User defined props | any | - |
props | description | type | default |
---|---|---|---|
hasNodes | if a TreeNode is the last node of its branch |
boolean | false |
isOpen | if it is showing its children | boolean | false |
level | the level of the current node (root is zero) | number | 0 |
key | key of a TreeNode |
string | - |
label | TreeNode label |
string | - |
...other | User defined props | any | - |
props | description | type | default |
---|---|---|---|
search | A function that takes a string to filter the label of the item (only available if hasSearch is true ) |
(value: string) => void | - |
searchTerm | the search term that is currently applied (only available if hasSearch is true ) |
string | - |
items | An array of TreeMenuItem |
TreeMenuItem[] | [] |
resetOpenNodes | A function that resets the openNodes , by default it will close all nodes |
(openNodes: string[]) => void | - |
props | description | type | default |
---|---|---|---|
hasNodes | if a TreeNode is the last node of its branch |
boolean | false |
isOpen | if it is showing its children | boolean | false |
openNodes | an array of all the open node names | string[] | - |
level | the level of the current node (root is zero) | number | 0 |
key | key of a TreeNode |
string | - |
parent | key of the parent node | string | - |
searchTerm | user provided search term | string | - |
label | TreeNode label |
string | - |
active | if current node is being selected | boolean | - |
focused | if current node is being focused | boolean | - |
onClick | a callback function that is run when the node is clicked | Function | - |
toggleNode | a function that toggles the node (only availavble if it has children) | Function | - |
...other | User defined props | {[string]: any} | - |
- lodash: some light-weight utility functions are used, e.g.
get
,isEmpty
, andmerge
. babel-plugin-import is used for only importing used modules to reduce bundle size; only the used functions are bundled.