Overview of Feature Functionality
Headless Tree is composed by a set of individual features, that can each be included or not in the tree configuration. This allows you to reduce the bundle size by including only what you need, while being able to customize the tree by overriding feature behavior or expanding the tree with custom features.
Each feature is an object that can be imported from @headless-tree/core
, and has a TypeScript type declaration
that defines the functionality that it provides as well as the configuration options that it accepts.
For instance, this is the type declaration for the selectionFeature
, which provides the ability to
select multiple items inside the tree:
export type SelectionFeatureDef<T> = {
state: {
selectedItems: string[];
};
config: {
setSelectedItems?: SetStateFn<string[]>;
};
treeInstance: {
setSelectedItems: (selectedItems: string[]) => void;
getSelectedItems: () => ItemInstance<T>[];
};
itemInstance: {
select: () => void;
deselect: () => void;
toggleSelect: () => void;
isSelected: () => boolean;
selectUpTo: (ctrl: boolean) => void;
};
hotkeys:
| "toggleSelectItem"
| "selectUpwards"
| "selectDownwards"
| "selectAll";
};
- The feature substate interface defined with
SelectionFeatureDef<T>["state"]
gets merged with the interfaceTreeState
, and allows you to manage that state in any way defined Managing State Guide. - The configuration options defined with
SelectionFeatureDef<T>["config"]
gets merged with the interfaceTreeConfig
, and the feature recognizes any of those config options passed to the tree configuration. - The
treeInstance
anditemInstance
interfaces define the methods that the feature provides to the tree instances and item instances, respectively. These methods can be called directly on the instances to interact with the feature. - The
hotkeys
type defines the hotkeys that the feature implements and respects as long as the Hotkey Core Feature is included in the tree configuration.
By including the feature, you can interact with it like follows:
const tree = useTree<string>({
// ...remaining tree config
state: { selectedItems: ["item-1", "item-2"] },
setSelectedItems: myCustomSetSelectedItems,
// alternatively manageable with setState
hotkeys: {
// Override hotkey definitions for this feature
selectAll: {
hotkey: "ctrl+q",
},
},
features: [
syncDataLoaderFeature,
selectionFeature,
hotkeysCoreFeature,
],
});
// Interact with tree instance methods
tree.setSelectedItems(["item-3", "item-4"]);
// Interact with item instance methods
tree.getItemInstance("item-1").select();
Implementing custom features
Apart from being merged into the Headless Tree core TypeScript interfaces, each feature is a completely standalone object that is defined in isolation. You can implement your own features to expand the behavior of Headless Tree, and merge the types for your feature into the global HT namespace interfaces yourself.
declare module "@headless-tree/core" {
export interface ItemInstance<T> {
alertChildren: () => void;
}
}
const customFeature: FeatureImplementation = {
itemInstance: {
alertChildren: ({ item }) => {
alert(
item
.getChildren()
.map((child) => child.getItemName())
.join(", "),
);
},
},
};
const tree = useTree<string>({
// ...remaining tree config
features: [
// ...other features
customFeature,
],
});
tree.getItemInstance("item-1").alertChildren();
You can find out more about this in the Guide on writing custom plugins.
Overwriting feature behavior
You can also overwrite HT core features, or parts of them to customize them to your use case. 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.
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.
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);
},
}),
}
};
Again, this functionality is described in more detail in the Guide on writing custom plugins.