This project is currently in Alpha. Expect breaking changes in minor version bumps and incomplete changelogs. Bug reports and contributions are welcome!
Installation
To start using Headless Tree, install it to your project as dependency via
npm install @headless-tree/core @headless-tree/react
or
yarn add @headless-tree/core @headless-tree/react
During Alpha, releases are published on an irregular basis, and changelogs may not properly reflect all changes.
The current main branch is always published to NPM under the snapshot
tag, so you can use
npm install @headless-tree/core@snapshot @headless-tree/react@snapshot
to get the latest snapshot deployment.
Quick Start
The React Bindings of Headless Tree provide a hook useTree
that creates a tree instance, which you can use
to render your tree however you like. The instance provides methods to get the tree as a flat list of nodes,
that you can easily render with custom logic. It provides accessibility props to make sure that, even if the
tree is not rendered as hierarchical structure, screen readers still read it as such even if it is just rendered
as flat list.
You can also use other frameworks than React. Use the createTree
method from the core package to create a tree
without a dependency on React. Documentation on that and bindings for other frameworks are planned for the future.
The useTree
hook (or the createTree
function from the core package, if you are not using the React bindings)
return an instance of TreeInstance. The most important
method on that instance is getItems()
, which returns a flat list of all nodes in the tree. Those nodes are of type
ItemInstance, which provides methods to interact with
each item and get the necessary props to render it.
When rendering your tree, don't forget to
- provide
tree.registerElement
as ref to the tree root, or otherwise calltree.registerElements
with the root element of your tree during mounting and withnull
during unmount if you are not using React. - provide
item.registerElement
as ref to each item element, or otherwise callitem.registerElements
with the item element during mounting and withnull
during unmount if you are not using React. - Spread the return value of
item.getProps()
as props into your tree item elements. - You probably want to indent your items based on their depth, which you can get via
item.getItemMeta().level
.
Importing Features
The architecture of Headless Tree separates individual features into distinct objects, that need to be manually
imported and added to your tree. They are exported from the @headless-tree/core
package and need to be added to
the features
property in your tree config. This is done to design a clean architecture, allow customizability
(you can easily write custom plugins that way, or overwrite parts of other plugins), and more importantly, allow
you to only import the features you need. This is especially important for tree-shaking, as you can easily
remove unused features from your bundle.
For example, if you want drag-and-drop support and hotkeys support, import the respective features and add them to your tree config:
import { useTree, dragAndDropFeature, hotkeysCoreFeature, syncDataLoaderFeature } from "@headless-tree/core";
useTree({
// ... other options
features: [
syncDataLoaderFeature,
dragAndDropFeature,
hotkeysCoreFeature,
]
})
Which features are imported and which not affects which methods are available on the tree instance and on the item instances, as well as which config options are respected by Headless Tree. However, the TypeScript Types always include all possible functions. If you are trying to use a feature which is not working or where methods are missing during runtime, check if you have included all necessary features in your tree.
All available features are documented with a dedicated guide on how to use them. In the sidebar, you can find each feature under its respective name in the "Features" category. The "Tree Core" and "Main Feature" features are special features that are always included and do not need to be imported.
Types
Headless Tree is written in TypeScript and provides full TypeScript support. The most important types to keep in mind are
- TreeConfig: The configuration object that you pass to
useTree
orcreateTree
. - TreeInstance: The instance of a tree that you get back from
useTree
orcreateTree
. - ItemInstance: The instance of an item that you get back from the tree instance.
- TreeState: The state of a tree instance, which you can access via
tree.state
(read more in the guide on managing State).
Note that some of the defined functions require that you include the respective feature in your tree config, see the section above.
Hooking up your data structure
As you saw in the previous example, we also added the syncDataLoaderFeature
feature. Then, you can define
how Headless Tree reads your tree with a dataLoader
property in the tree config.
You need either that or the asyncDataLoaderFeature
feature to hook up your data structure to Headless Tree.
Note that, when using the asyncDataLoaderFeature
, you need to provide a asyncDataLoader
instead, which has
a different interface (async methods instead of sync methods).
- In the
dataLoader.getItem
property, you define a method that returns the payload of an item. The form of the payload is up to you, and can be provided as generic type to theuseTree
orcreateTree
method. - In the
dataLoader.getChildren
property, you define a method that returns the IDs of the children of an item.
In addition to the dataLoader
property, you also need to provide
- the
rootItemId
property - a
getItemName()
method, which returns the display name of an item - a
isItemFolder()
method, which returns whether an item is a folder or not. An item being a folder means that it can be expanded.
For the latter two, the parameter provided to you is an item instance, so you can use all methods that you also have
available during rendering. Use item.getItemData()
to get the payload for the item that you provided.
const tree = useTree<ItemPayload>({
rootItemId: "root-item",
getItemName: (item) => item.getItemData().itemName,
isItemFolder: (item) => item.isFolder,
dataLoader: {
getItem: (itemId) => myDataStructure[itemId],
getChildren: (itemId) => myDataStructure[itemId].childrenIds,
},
features: [ syncDataLoaderFeature ],
});
In the prior demos, we mostly used string
as simple payload type. The following demo shows how to use
a more complex payload type, and how to use the getItemName
and isItemFolder
methods to get the
display name and folder status of an item.
TODO-DOCS: Document the concept of data adapters, and mention their availability here.