npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

react-draggable-hoc

v2.4.1

Published

drag by ref using hook

Downloads

86

Readme

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).

Demo

Demo with sources

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.