React Hook Dragula

Lightweight, strongly typed package with almost no dependencies. It uses Dragula and React Hooks to smoothly manage the DOM state. It also adds a context store which allows you to pass the data back and forth between Dragula and React.

React-Hook-Dragula

Install

yarn add react-hook-dragula

Usage

Initializing

// Typing for data which will be attached to dragula events. Yours will be different.
type Fruit = {
  itemId: number;
  itemName: string;
}

type DraggableStore = {
  fruit: Fruit;
  fruits: Fruit[];
  setFruits: Dispatch<SetStateAction<Fruit[]>>;
}

const {
  Dragula,
  DragulaContainer,
  Draggable,
  DragulaHandle,
  useDraggableStore,
  useInternalStore,
  useDrake,
} = initializeDragula<DraggableStore>();

Basic dragging

<Dragula>
  <DragulaContainer>
    <Draggable>
      You can move these elements between these two containers
    </Draggable>
  </DragulaContainer>

  <DragulaContainer>
    <Draggable>
      There's also the possibility of moving elements around in the
      same container, changing their position
    </Draggable>
  </DragulaContainer>
</Dragula>

Customizing Dragula options

<Dragula
  options={{ moves: ({ container }) => !!container?.children.length > 1 }}
  onDrop={() => console.log('dragula drop event!')}
>
  <DragulaContainer>
    <Draggable>
      You can move these elements between these two containers
    </Draggable>
    <Draggable>
      There's also the possibility of moving elements around in the
      same container, changing their position
    </Draggable>
    <Draggable>
      Anyting can be moved around. That includes images, links, or any other
      nested elements.
    </Draggable>
  </DragulaContainer>

  <DragulaContainer>
    <Draggable>
      This element can't be moved until a second one is added into it's container
    </Draggable>
  </DragulaContainer>
</Dragula>

Attaching data to Dragula store

const [fruits, setFruits] = useState<Fruit[]>([
  { itemId: 1, itemName: 'apples' },
  { itemId: 2, itemName: 'oranges' },
  { itemId: 3, itemName: 'tomatoes' },
]);

<Dragula
  onDrop={({ el, source }) => {
      setFruits([...source.children].map(
        ({ draggableStore: { fruit } }) => draggableStore,
      ))

      console.log('Dragual drop event for: ', el.draggableStore.fruit.itemName)
  }}
>
  <DragulaContainer>
    {fruits.map(fruit => (
      <Draggable
        key={fruit.id}
        draggableStore={{
          fruit,
          fruits,
          setFruits,
        }}
      >
        <div>{fruit.itemName} - id: {fruit.id}</div>
      </Draggable>
    ))}
  </DragulaContainer>
</Dragula>

Using Drake and InternalStore

<Dragula
  options={{ moves: ({ el }) => !!el?.internalStore.isMouseOverHandle }}
>
  <DragulaContainer>
    <Draggable>
      <DragulaHandle>You can only drag this item by clicking on me!</DragulaHandle>
      <div>You can move these elements between these two containers</div>
    </Draggable>
    <Draggable>
      This item doesn't have a handle.. so it can't be moved at all!
    </Draggable>
  </DragulaContainer>
</Dragula>

API

Extended typings

Each <Draggable/> node has a draggableStore and an internalStore object attached to it:

type InternalStore = {
  isMouseOverHandle: boolean;
};

type DraggableStore<T> = T;

type ExtendedDrakeElement<T> = Element & {
  draggableStore: DraggableStore<T>;
  internalStore: InternalStore;
};

type ExtendedDrakeSource<T> = Element & { children: ExtendedDrakeElement<T>[] };

Exported function

initializeDragula - - - returns object with entries:

Dragula: (props: DragulaContainerProps<T>) => <Dragula<T> {...props} />

DragulaContainer: (props: HTMLProps<HTMLDivElement>) => (<DragulaContainer {...props} />)

Draggable: (props: DraggableProps<T>) => <Draggable<T> {...props} />

DragulaHandle: (props: DragulaHandleProps) => <DragulaHandle {...props} />

useDraggableStore: () => useDraggableStore<T>()

useInternalStore: () => useInternalStore()

useDrake: () => useDrake()

Component props

For the following, we have typings:

OnEventProps<T = typeof DraggableStore> {
  clone: ExtendedDrakeElement<T>,
  container: Element,
  el: ExtendedDrakeElement<T>,
  original: ExtendedDrakeElement<T>,
  sibling: ExtendedDrakeElement<T>,
  source: ExtendedDrakeSource<T>,
  target: ExtendedDrakeElement<T>,
  type: 'mirror' | 'copy',
}

<Dragula
  options={{
    isContainer: ({ el }) => false,
    moves: ({ el, source, handle, sibling }) => true,
    accepts: ({ el, target, source, sibling }) => true,
    invalid: ({ el, handle }) => true,
    direction: "vertical",
    copy: false,
    copySortSource: false,
    revertOnSpill: false,
    removeOnSpill: false,
    mirrorContainer: document.body,
    ignoreInputTextSelection: true,
    slideFactorX: 0,
    slideFactorY: 0,
  }}
  onDrag={({ el, source }) => {}}
  onDragEnd={({ el }) => {}}
  onDrop={({ el, target, source, sibling }) => {}}
  onCancel={({ el, container, source }) => {}}
  onRemove={({ el, container, source }) => {}}
  onShadow={({ el, container, source }) => {}}
  onOver={({ el, container, source }) => {}}
  onOut={({ el, container, source }) => {}}
  onCloned={({ clone, original, type }) => {}}
  dependencyList={[]}
/>

See Dragula configuration for more info


<DragulaContainer {...props} />
  • HTMLProps<HTMLDivElement> optional
    • This component has no special props
    • Anything passed to it will be forwarded to a <div /> element

<Draggable
  draggableStore={{}}
  {...props}
/>
  • draggableStore Object, required
    • Type of store must match <T> from when you initialized const { Draggable } = initializeDragula<T>()
  • HTMLProps<HTMLDivElement> optional
    • Anything else passed to it will be forwarded to a <div /> element

<DragulaHandle {...props} />
  • HTMLProps<HTMLDivElement> optional
    • This component has no special props
    • Anything passed to it will be forwarded to a <div /> element

Custom Hooks

  useDraggableStore: () => typeof DraggableStore<T>

  useInternalStore: () => { isMouseOverHandle: boolean; }

  // If Drake type & arrow functions were generic, then it would be this:
  // useDrake: <T>() => Drake<ExtendedDrakeElement<T>>;
  // Since they're not generic, we wind up with:
  useDrake: () => ReturnType<ExtendedDragula<any>>;

Acknowledgements

  • Big thanks to all the work that Nicolás Bevacqua has done to build and maintain Dragula !