Plugins
You can customize or expand the core behavior of Headless Tree and its features by defining custom plugins that can be added to your tree configuration similar to the built-in features.
Features are loaded one after another as they are defined in the configuration, and behavior implemented by a subsequent features can expand or override the behavior of the previous ones (see Order of features for details on the order). In the most extreme case, you can just completely copy the implementation for a single feature (you can find the source for each feature, by following the "View Source" link in one of the feature docs pages) into your project and customizing the implementation.
In the following sample, a new tree instance method is defined in a custom feature, and invoked in custom rendered buttons.
You can also write a feature, that wraps the original feature and modifies its behavior. When multiple features
are included in the tree configuration, that define an instance method with the same name, the last feature
in the list will be the one called when the method is invoked, and its implementation will be passed a method
called prev
that can be used in the wrapper implementation to call the previous implementation. Don't
forget to pass any arguments that the previous implementation expects.
The Typescript type FeatureImplementation
outlines how a feature should be structured.
Order of features
By default, features are loaded left-to-right, with each feature overwriting the previous one. This means
that prev()
will always refer to next feature to left of the overwriting one, which also implements the
method that prev()
is called inside.
The order can additionally be controlled with the overwrites
property of a feature, which defines the names
of other features that should be overwritten by the current feature. Headless Tree will reorder features so that
overwriting features are always loaded after features being overwritten, but will leave the order unchanged
otherwise. In reality, this means that you can define HT core plugins in any arbitrary order, HT will always
load them in the correct order. The order of any custom features added by you will be respected however.
Prop generation
Props are generated with the getProps
method of the itemInstance
property of a feature, as well as some
other prop-related functions defined on the tree instance. Like any other method provided by a feature,
its return value can be expanded by subsequent features.
As example, note the implementation of itemInstance.getProps()
in the selection feature, that adds new DOM properties
and modifies the behavior of the onClick
handler in cases where Shift or Ctrl is pressed, while still
respecting the behavior of other features generating props:
export const selectionFeature: FeatureImplementation = {
// ... other feature properties
itemInstance: {
getProps: ({ tree, item, prev }) => ({
...prev?.(), // include other props generated by other features
"aria-selected": item.isSelected() ? "true" : "false",
onClick: (e: MouseEvent) => { // override onClick handler
if (e.shiftKey) {
item.selectUpTo(e.ctrlKey || e.metaKey);
} else if (e.ctrlKey || e.metaKey) {
item.toggleSelect();
} else {
tree.setSelectedItems([item.getItemMeta().itemId]);
}
// call the implementation of onClick of the
// previous feature that implements this method
prev?.()?.onClick?.(e);
},
}),
}
};
Adding custom state
You can add custom state information to Headless Tree, that can be managed like every other HT state
entity with all three ways described in the state management guide. The convention is
to name the a config value setMyCustomState
for a state value myCustomState
.
First, define them in typescript:
import { SetStateFn } from "@headless-tree/core";
declare module "@headless-tree/core" {
export interface TreeState<T> {
myCustomState: CustomType;
}
export interface TreeConfig<T> {
setMyCustomState?: SetStateFn<CustomType>;
}
}
Then, implement it in your feature:
import { FeatureImplementation, makeStateUpdater } from "@headless-tree/core";
const customFeature: FeatureImplementation = {
// initialize the state with a default value
getInitialState: (initialState) => ({
myCustomState: {},
...initialState,
}),
// Set the default state setter to something that hooks it up
// to the `setState` config so that consumers can manage
// the entire state with one variable.
getDefaultConfig: (defaultConfig, tree) => ({
setMyCustomState: makeStateUpdater("myCustomState", tree),
...defaultConfig,
}),
// Let HT know the state setter and state value belong together
stateHandlerNames: {
myCustomState: "setMyCustomState",
},
};
Then, implement the behavior of your feature in tree-instance methods and item-instance
methods and control the state with tree.getState().myCustomState
and tree.setMyCustomState()
.
Remapping Prop Names for different Frameworks other than React
All prop names generated by features are created as React-compliant prop names. You can remap them
by implementing a custom feature that overwrites prop-related methods, and rewrites their output in
a way that is compatible with your framework. In the following sample, the aria-label
prop is remapped
to data-label
for tree item instances. This can be expanded to every prop if you want.