Guides
Drag & Drop
About this guide
This short guide will demonstrate how you can make use of specific builder store methods for seamless integration with any drag-and-drop library. Specifically, we're going to use dnd kit
for illustrative purposes.
Please note that this guide focuses on drag-and-drop functionality within a single hierarchical level to maintain simplicity.
Implementing drag & drop
First, let's define a component that will be used to wrap each arbitrary entity and make it draggable.
import { type ReactNode } from "react";
import { useSortable } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
export function DndItem(props: { id: string; children: ReactNode }) {
const { attributes, listeners, transform, transition, setNodeRef } =
useSortable({ id: props.id });
const style = {
transform: CSS.Translate.toString(transform),
transition,
};
return (
<div
ref={setNodeRef}
style={style}
{...attributes}
{...listeners}
aria-describedby="dnd"
>
{props.children}
</div>
);
}
The builder store exposes a set of useful methods, such as setEntityIndex
, to change the order of entities. For example, you can invoke this method when an entity has been dropped, and you have calculated its new desired index.
import {
DndContext,
MouseSensor,
useSensor,
type DragEndEvent,
} from "@dnd-kit/core";
import {
SortableContext,
verticalListSortingStrategy,
} from "@dnd-kit/sortable";
import {
BuilderEntities,
useBuilderStore,
useBuilderStoreData,
} from "@coltorapps/builder-react";
import {
DatePickerFieldEntity,
DndItem,
SelectFieldEntity,
TextFieldEntity,
} from "./components";
import { formBuilder } from "./form-builder";
export function FormBuilder() {
const builderStore = useBuilderStore(formBuilder);
/*
| We retrieve the `root` from the store's schema, which is
| an array that holds the top-level entities IDs in the
| hierarchy, determining their order.
|
| Note that we want for the output to refresh and
| trigger a re-render only when the store emits the
| `RootUpdated` event, signifying that the `root`
| has been updated.
*/
const {
schema: { root },
} = useBuilderStoreData(builderStore, (events) =>
events.some((event) => event.name === "RootUpdated"),
);
const mouseSensor = useSensor(MouseSensor, {
activationConstraint: {
distance: 10,
},
});
function handleDragEnd(e: DragEndEvent) {
const overId = e.over?.id;
if (!overId || typeof e.active.id !== "string") {
return;
}
const index = root.findIndex((id) => id === overId);
/*
| When an entity is dropped, we can move it
| to a new index on its hierarchical level.
*/
builderStore.setEntityIndex(e.active.id, index);
}
return (
<DndContext id="dnd" sensors={[mouseSensor]} onDragEnd={handleDragEnd}>
<SortableContext
id="sortable"
items={Array.from(root)}
strategy={verticalListSortingStrategy}
>
<BuilderEntities
builderStore={builderStore}
components={{
textField: TextFieldEntity,
selectField: SelectFieldEntity,
datePickerField: DatePickerFieldEntity,
}}
>
{/*
| We wrap each rendered entity with our `DndItem`
| component to make it draggable.
*/}
{(props) => <DndItem id={props.entity.id}>{props.children}</DndItem>}
</BuilderEntities>
</SortableContext>
</DndContext>
);
}
It's worth noting that all of the following methods accept an index for adjusting an entity's order: setEntityIndex
, addEntity
, setEntityParent
, and unsetEntityParent
.
For instance, when creating entities by dragging placeholders from a sidebar, you can utilize the index
option in the addEntity
method to position them where the user dropped the placeholder. Similarly, when moving an entity between different levels or sections, you can make use of the index
option in the setEntityParent
and unsetEntityParent
methods.