Skip to main content

Drag and Drop

Drag-and-drop Capabilities for tree items and external drag objects

SourceView Source
TypesView Types
Importimport { drag-and-dropFeature } from "@headless-tree/core
Type DocumentationConfigurationStateTree InstanceItem Instance

Overview

The Drag-And-Drop Feature provides drag-and-drop capabilities. It allows to drag a single tree item (or many of the selection feature is included in the tree config) and drop it somewhere else in the tree. The feature also allows you to create interactions between the tree and external drag objects, allowing you to drag tree items out of the tree, or foreign data objects from outside the tree inside. As extension of that, this can also be used to implement drag behavior between several trees.

This page explains the general idea of how Dnd works in Headless Tree, and how to get started with dragging items within a tree. The guides following after will go into more depth about

  • TODO

Configuring Drag and Drop

The gist of configuring Drag and Drop in Headless Tree, is to

  • add the dragAndDropFeature to the tree config,
  • define the indent of your tree items in the config variable indent, and
  • handle the onDrop event in the tree config.

Note that the indent property is not required in the core tree configuration, since you can just use a custom indentation when rendering your items. For Drag and Drop it is required however to determine the drop position when the user is trying to reparent an item. (TODO link)

The Dnd Feature also exposes a Dnd State that you can handle yourself if you want to.

import {
syncDataLoaderFeature,
dragAndDropFeature,
selectionFeature,
} from "@headless-tree/core";

const tree = useTree<Item>({
indent: 20,
canReorder: true,
onDrop: (items, target) => {
// handle drop
},
features: [
syncDataLoaderFeature,
selectionFeature,
dragAndDropFeature,
],
});

The Drop Event

When the user drops tree items onto a target within the tree, the onDrop event is called with the list of items that were dragged, and a drop target. This drop target is an object that contains either

  • the item that is dropped to, if the user dragged on the title of an item with the intent to make it a child of that item

or, if the user dragged between items while the canReorder config option is set to true:

  • the item that should become the new parent
  • the childIndex, describing the index within the item where the user dropped
  • the insertionIndex, describing the index within the item where the items should be placed (see details below)
  • dragLineIndex and dragLineLevel describing the position of the drag line that is rendered while dragging
Difference between childIndex and insertionIndex

childIndex describes the visual location inside the item that is being dropped into, where the user is dragging the items to. insertionIndex on the other hand also respects the special case that the user might drag items within the same folder from further up in a folder, to further down in the same folder.

If items are dragged from a different location into a new folder, childIndex and insertionIndex will be the same. However, if the two top-most items in a folder are dragged two items down, childIndex will be 4, but insertionIndex will be 2, since removing those two items will shift the items up by two.

Rendering a Tree with Drag and Drop

The only difference between rendering a normal HT Tree, and rendering one with Drag and Drop capabilities, is that you need to render a drag line that shows the user where the items will be dropped. You just need to render an additional div with the style tree.getDragLineStyle(), and style it however you want. The computed stylings will automatically add display: none if the user is not currently dragging items, and will compute relative positioning based on the item rendered with tree.getContainerProps().

Below are examples of how to render a tree with Drag and Drop capabilities, as well as the exemplary CSS stylings used in the demo.

<div {...tree.getContainerProps()} className="tree">
{tree.getItems().map((item) => (
<MyTreeItem key={item.id} item={item} />
))}
<div style={tree.getDragLineStyle()} className="dragline" />
</div>
.dragline {
position: absolute;
height: 2px;
width: unset;
margin-top: -1px;
background-color: #00aff4;
}

/* Render the circle at the left of the dragline */
.dragline::before {
content: "";
position: absolute;
left: 0;
top: -3px;
height: 4px;
width: 4px;
background: #fff;
border: 2px solid #00aff4;
border-radius: 99px;
}

Mutating your data after the Drop

When the drop event is registered with onDrop, you will likely want to mutate your data source to reflect the new order of items after the drag. Headless Tree provides three utility methods that make this very easy:

itemIds and movedItems are arrays of item instances, and thus the same type as what the onDrop config option provides. Similarly, target is the same type as the drop target that is provided in the onDrop event.

In most situations, it is sufficient to call createOnDropHandler with a function that mutates your data source, and use that to handle drop events of items within a single tree. The other methods are useful for interactions between several trees (imagine items being removed from one tree and then inserted in another), handling cases where foreign data is dragged into a tree to create a new tree item or tree items being dragged out of the tree to external drop targets, or handling other more complicated use cases.

import { createOnDropHandler } from "@headless-tree/core";

const tree = useTree<Item>({
indent: 20,
canReorder: true,
onDrop: createOnDropHandler((item, newChildren) => {
myData[item.getId()].children = newChildren;
}),
features: [
syncDataLoaderFeature,
selectionFeature,
dragAndDropFeature,
],
});
info

In the Demo above, switch to the sample "Multiple Trees" in the selection box to see a sample of how to use the removeItemsFromParents and insertItemsAtTarget methods instead of the createOnDropHandler method.

Updated caches in async data loaders

info

You can ignore this when using removeItemsFromParents, removeItemsFromParents or createOnDropHandler to handle the drop event, as those handlers do this for you.

When you handle the drop event yourself while using the async data loader, note that the async data loader caches tree data and does not automatically update the cache when you mutate the data source.

When you update the childrens assigned to an item as response to a drop event, you can update the cache with item.updateCachedChildrenIds(children: string[]):

// Update your data source
myData[parent.getId()].children = newChildren;

// Update cache of async data loader
parent.updateCachedChildrenIds(newChildren);

// Trigger the recomputation of the internal tree structure
tree.rebuildTree();