Skip to content

dsimushkin/react-draggable-hoc

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

React Draggable Higher Order Component

A set of react hooks, hocs and components used to gain full control on DnD process.

Browser realization supports touch and mouse events. Prevents from scrolling only on drag (NOTE: when delay > 0).

Browser basic usage

Package provides a realization of the useDraggable hook, called Draggable, which can be used for simple case scenarios.

import { Draggable } from "react-draggable-hoc";

<Draggable>
  <div className="draggable">...</div>
</Draggable>;

or if you need a handle, use a prop of a functional child component called handleRef as a ref to your handle

import { Draggable } from "react-draggable-hoc";

<Draggable>
  {({ handleRef, isDetached }) => (
    <div className={`draggable${isDetached ? " dragged" : ""}`} ref={handleRef}>
      ...
    </div>
  )}
</Draggable>;

Additionally Draggable component supports the following properties:

property name type description
dragProps [required] any Used as a key of the draggable component. Used to attach/re-attach listeners and other manipulations. i.e. it can be used to handle drop by droppable targets.
className = "draggable" string class name
children JSX or functional component Rendered twice: as a node inplace and as a detached element. In case of a functional component handleRef will be passed only for the node inplace
postProcess = defaultPostProcessor (drag props, ref) Used to inject custom properties into drag props. Useful for changes in positioning
detachDelta = 20 number A delta that is used to detach (influences isDetached for functional children)
delay = 30 number A delay in ms. If a move event is fired within a delay, from the moment the drag is started, the drag will be canceled.
detachedParent = document.body HTMLNode HTML node, where the detached element will be rendered
onDragStart () => any A function fired when the drag is started (after delay)
onDragEnd () => any A function fired when the drag is finished
throttleMs = 10 number Throttling in ms. If equal to 0, throttling is disabled
disabled = false Boolean Flag for disabling draggability

If you want the node to be dragged only inside a container, use DragDropContainer

import { Draggable, DragDropContainer } from "react-draggable-hoc";

<DragDropContainer>
  <div>
    ...
    <Draggable />
    ...
  </div>
</DragDropContainer>;

If you need a droppable target, you can use a realization of DragDropContainer context usage called Droppable with a functional child component.

NOTE: don't forget to provide dragProps to the Draggable.

import { Draggable, DragDropContainer, Droppable } from "react-draggable-hoc";

<DragDropContainer>
  <div>
    ...
    <Draggable dragProps="this is a prop that will be passed to onDrop" />
    ...
    <Droppable
      onDrop={({ dragProps }) => {
        console.log(`Dropped: ${dragProps}`);
      }}
    >
      {({ isHovered, ref }) => (
        <div className={isHovered ? "hovered" : undefined} ref={ref}>
          ...
        </div>
      )}
    </Droppable>
    ...
  </div>
</DragDropContainer>;

NOTE: if you use a lot of droppables, optional flag withDragProps of Droppable should be set to false to enhance performance and prevent re-rendering the element on drag start. To inject dragProps use WithDragProps as a parent of all droppables like this:

import { Draggable, DragDropContainer, Droppable, WithDragProps } from "react-draggable-hoc";

<DragDropContainer>
  <div>
    <WithDragProps>
      {({dragProps}) => (
        ...
        <Draggable dragProps="this is a prop that will be passed to onDrop" />
        ...
        <Droppable
          withDragProps={false}
          onDrop={({dragProps}) => {
            console.log(`Dropped: ${dragProps}`);
          }}
        >
          {({ isHovered, ref }) => (
            <div className={isHovered ? "hovered" : undefined} ref={ref}>
              {dragProps ? "Drop it here" : null}
            </div>
          )}
        </Droppable>
        ...
      )}
    </WithDragProps>
  </div>
</DragDropContainer>;

Nested (with respect to HTML DOM) droppables are supported from the box.

NOTE: when droppables are not nested from DOM perspective but nodes overlap should be handled, one can utilize priority prop/setting.


Browser advanced usage

While all of the above implementation are helpful to achieve fast results, the main purpose of the packase is provide capabilites to implement draggable components in just a couple of lines of code. For this purpose a useDraggable should be used

import { useDraggable } form "react-draggable-hoc"

function MyDraggable({dragProps}) {
    const ref = React.useRef();  // create a reference for the DOM node
    const { isDragged, deltaX, deltaY, state, container } = useDraggable(
        ref,
        {
          dragProps,
          delay: 100, // mobile browsers can trigger text selection for big values, small values are useful when a parent is scrollable
          onDragStart: (state) => {},
          onDrag: (state) => {},
          onDrop: (state) => {},
          onDelayedDrag: (state) => {},
          onDragCancel: (state) => {},
          disabled: false,
        }
    );

    return (
        <div
            ref={ref}
            style={
                // add drag deltas to the DOM node position
                isDragged ? {
                    transform: `translate3d(${deltaX}px, ${deltaY}px, 0)`
                } : undefined
            }
        >
            ...
        </div>
    )
}

If instead you need a component that will handle situations, when smth is dropped on it, you might use a useDroppable

(NOTE: works only for draggables with dragProps)

import { useDroppable } form "react-draggable-hoc"

function MyDropable({doSmthOnDrop}) {
    const ref = React.useRef();  // create a reference for the DOM node
    const { isHovered } = useDroppable(
        ref,
        {
          method: (state, ref, defaultMethod),
          onDrop: (state) => {
            if (typeof doSmthOnDrop === "function") {
              doSmthOnDrop();
            }
          },
          disabled = false
        }
    );

    return (
        <div
            ref={ref}
            style={
              isHovered ? {
                  color: "red"
              } : undefined
            }
        >
            ...
        </div>
    )
}

if you need to enforce update on dragProps change during drag start, drop or drag cancel you can use useDragProps hook

import { useDragProps } form "react-draggable-hoc"

function MyComponent() {
  const dragProps = useDragProps({disabled: false});

  ...
}

or a synthetic sugar called WithDragProps, which is a Component wrapper of the useDragProps with functional children.

Most of the hooks lifecycle methods take state as a parameter. It represents the state of the Dnd observer events cache and calculated values:

interface ISharedState<T, E, N> {
  dragProps: T; // dragProps of the current draggable
  cancel: () => void; // helper method to cancel the drag
  initial?: { x: number; y: number; event: E }; // drag start cache
  current?: { x: number; y: number; event: E }; // cache of the current event
  history: { x: number; y: number; event: E }[]; // dnd history
  deltaX: number; // change of the pageX during Dnd
  deltaY: number; // change of the pageY during Dnd
  node: N; // dragged element
  wasDetached: Boolean; // if at least one drag event was fired (useful for detecting click events)
}

Browser's HtmlDndObserver additionally provides a getter for elementsFromPoint, which can be useful in droppable's hover method implementation.

export interface IHtmlDndObserverState<T>
  extends ISharedState<T, DndEvent, HTMLElement> {
  readonly elementsFromPoint: Element[];
}

Experimental features

Currently, context observer supports changing dragProps on the fly. It can be used in cases when a pointerdown produces DOM modifications and a new element, that should be considered dragged, is created. But due to component lifecycle with hooks, swapping dragProps should be done in useEffect hook or before the DOM is rendered/created.


Ninja usage

The whole idea behind the library was to create a single realization to quickly handle both touch and mouse devices in cases when a developer needs full controll of the Dnd elements. Therefore an implementation of the Browser Dnd Observer was written. Hooks are only a synthetic sugar around this implementation.

Hooks utilize DragContext (React context API) behind the curtains, which injects the following observer

// drag phases
type DnDPhases =
  | "dragStart"
  | "drag"
  | "cancel"
  | "drop"
  | "delayedDrag"
  | "dragPropsChange";

interface IDndObserver<T, E, N> {
  makeDraggable(
    node: N,
    config?: {
      delay?: number;
      dragProps?: T;
      onDragStart?: (state: ISharedState<T, E, N>) => void;
      onDelayedDrag?: (state: ISharedState<T, E, N>) => void;
      onDrop?: (state: ISharedState<T, E, N>) => void;
      onDrag?: (state: ISharedState<T, E, N>) => void;
      onDragCancel?: (...args: any) => void;
    },
  ): () => void; // make an element draggable, returns a function to destroy the draggable.
  init(): void; // lazy initialization (auto performed when makeDraggable is used)
  destroy(): void; // lazy destruction
  cancel(): void;

  dragProps?: T;
  dragged?: N;
  wasDetached: Boolean;
  history: ISharedState<T, E, N>["history"];

  on: PubSub<DnDPhases, (state: ISharedState<T, E, N>) => void>["on"]; // subscribe a listener to Dnd phase
  off: PubSub<DnDPhases, (state: ISharedState<T, E, N>) => void>["off"]; // unsubscribe a listener from Dnd phase

  // calculated shared state
  readonly state: ISharedState<T, E, N>;
}

NOTE: dragPropsChange is currently an experimental feature.

The interface does not depend on the browser API and can be implemented for other platform usage.

Thus, a vanila js implementation of a draggable Node might look like:

const observer = HtmlDndObserver();
const draggable = document.getElementById(draggableId);
// attach to dragStart events (mousedown, touchstart) on the HTMLElement
const destroyDraggable = observer.makeDraggable(draggable, {
  dragProps,
  onDrag: ({ deltaX, deltaY }) => {
    draggable.style.transform = `translate3d(${deltaX}px, ${deltaY}px, 0)`;
  },
  onDrop: () => {
    draggable.style.transform = "initial";
  },
});
const droppable = document.getElementById(droppableId);
const onDrop = state => {
  if (droppable.contains(state.current.target)) {
    console.log(`Dropped ${state.dragProps} on droppable`);
  }
};
const destroyOnDropListener = observer.on("drop", onDrop);

...
// finally, a cleanup example
destroyDraggable();
destroyOnDropListener(); // or observer.off("drop", onDrop);
observer.destroy();

Or, if you can't use hooks, but only a class is available for you

import { withDndContext, DragContext } from "react-draggable-hoc";

const MyComponentWithDndContext = withDndContext(DragContext)(
  class MyComponent extends React.Component<{ dragProps: any }> {
    componentDidMount() {
      this.destroy = this.props.observer.makeDraggable(this.c, {
        dragProps: this.props.dragProps,
      });
    }

    componentWillUnmount() {
      this.destroy(); // this is actually not necessary, since the node will be removed anyway.
    }

    render() {
      <div ref={(c) => (this.c = c)} />;
    }
  },
);

THIS IS THE TITLE PAGE OF THE DOCUMENTATION. FULL API DESCRIPTION WILL FOLLOW. THE DEMO MIGHT ALREADY CONTAIN SOME EXAMPLES.

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Packages

No packages published