dnd-kit-sortable-tree

Tree component for React with ability to drag-and-drop items throughout the tree. Based on an example from dnd-kit (https://github.com/clauderic/dnd-kit).

MIT License

Downloads
34.4K
Stars
87
Committers
2

dnd-kit-sortable-tree

This is a Tree component extracted from dndkit examples and abstracted a bit. Here's how it could look like (visuals are completely customizable via css though)

Play around in examples to check the API and see what it can do.

Install

npm install dnd-kit-sortable-tree @dnd-kit/core @dnd-kit/sortable @dnd-kit/utilities

How-to use

Check out the Storybook for code samples and play around. You could also play with it on playcode

Shortly, you need to render:

<SortableTree
  items={/* array of your tree items */}
  onItemsChanged={/* callback when items are reordered */}
  TreeItemComponent={/* component that renders a single tree item */}
/>

And TreeItemComponent is usually your data wrapped in SimpleTreeItemWrapper or FolderTreeItemWrapper:

React.forwardRef((props, ref) => (
  <SimpleTreeItemWrapper {...props} ref={ref}>
    <div>{props.item.value}</div>
  </SimpleTreeItemWrapper>
));

Note that wrapping in forwardRef and passing ref to SimpleTreeItemWrapper is very important!

Examples

  1. Here's the very minimal code to add a Sortable Tree. You shouldn't use it as is in your project, but it could be easier to grasp what's going on.

    export const Minimal = () => {
      const [items, setItems] = useState(initialMinimalData);
      return (
        <SortableTree
          items={items}
          onItemsChanged={setItems}
          {
            /*
             * You need to pass the component rendering a single item via TreeItemComponent props.
             * This component will receive the data via `props.item`.
             * In this example we inline the component, but in reality you should extract it into a const.
             */ ...{}
          }
          TreeItemComponent={React.forwardRef((props, ref) => (
            <SimpleTreeItemWrapper {...props} ref={ref}>
              {/* HERE GOES THE ACTUAL CONTENT OF YOUR COMPONENT */}
              <div>{props.item.id}</div>
            </SimpleTreeItemWrapper>
          ))}
        />
      );
    };
    /*
     * Configure the tree data.
     */
    const initialMinimalData = [
      { id: '1', children: [{ id: '4' }, { id: '5' }] },
      { id: '2' },
      { id: '3' },
    ];
    
  2. Here's the minimal viable example that you could potentially copy&paste to your project to start from.

    export const MinimalViable = () => {
      const [items, setItems] = useState(initialViableMinimalData);
      return (
        <SortableTree
          items={items}
          onItemsChanged={setItems}
          TreeItemComponent={MinimalTreeItemComponent}
        />
      );
    };
    type MinimalTreeItemData = {
      value: string;
    };
    /*
     * Here's the component that will render a single row of your tree
     */
    const MinimalTreeItemComponent = React.forwardRef<
      HTMLDivElement,
      TreeItemComponentProps<MinimalTreeItemData>
    >((props, ref) => (
      /* you could also use FolderTreeItemWrapper if you want to show vertical lines.  */
      <SimpleTreeItemWrapper {...props} ref={ref}>
        <div>{props.item.value}</div>
      </SimpleTreeItemWrapper>
    ));
    
    /*
     * Configure the tree data.
     */
    const initialViableMinimalData: TreeItems<MinimalTreeItemData> = [
      {
        id: '1',
        value: 'Jane',
        children: [
          { id: '4', value: 'John' },
          { id: '5', value: 'Sally' },
        ],
      },
      { id: '2', value: 'Fred', children: [{ id: '6', value: 'Eugene' }] },
      { id: '3', value: 'Helen', canHaveChildren: false },
    ];
    

API

Data configuration (each TreeItem element could define them):

  • canHaveChildren - Default: true.

    If set to false, prevents any node from being dragged into the current one.

    Also accepts a function: (dragItem) => bool which could conditionally determine if a certain item could be a children of a node

  • disableSorting - Default: false. If set to true, prevents node from being dragged (i.e. it can't be sorted or moved to another node)

Tree configuration (props of <SortableTree>)

  • items - mandatory, items shown in a tree

  • onItemsChanged - mandatory, callback that is called when dragging of certain item is finished. You should preserve new state and adjust the value of items prop as needed.

  • TreeItemComponent - mandatory, component that renders a single tree row.

  • indentationWidth - optional, padding used for children

  • pointerSensorOptions - optional, configures the condition when item dragging starts. Defaults to:

    {
      "activationConstraint": {
        "distance": 3
      }
    }
    
  • disableSorting - optional, you could set this to true to completely disable the sorting

  • keepGhostInPlace - optional, you could set this to true to keep the Node that you are dragging in it's original place in a Tree. Check VSCode sample to see it in action.

  • dndContextProps - optional, override any prop of underlying DndContext.

  • sortableProps - optional, override any prop that is passed to underlying useSortable hook.

TreeItemWrapper configuration (props of <SimpleTreeItemWrapper> and <FolderTreeItemWrapper>)

  • manualDrag - Default: false. Set to true if you want tree item to be draggable ONLY from dragHandle.
  • showDragHandle - optional, set to false if you want to hide default dragHandle and show your own instead. Use <div {...props.handleProps}>DRAG_ME</div> for your own drag handle.

FAQ

  1. If you want to disable animation completely, you need to do the following:
    1. Pass null as dropAnimation prop (this disables the actual 'drop' animation for the Node that was dragged).
    2. Pass { animateLayoutChanges: () => false } to sortableProps (this disables the animation of all other nodes that were not dragged)

Troubleshooting

  1. If your dragged item is shown at the end of a list, make sure you that:
    1. You wrapped your TreeItem component in React.forwardRef and passing the ref to SimpleTreeItemWrapper
    2. You pass the styles prop from TreeItem to SimpleTreeItemWrapper