Provides named parameters and strong typing & declarative usage to dragula in React

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.

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 !