Skip to main content

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.

tip

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.