DEV Community

Cover image for How to change numeric input by dragging in React?
Sharad Chand for Graftini

Posted on • Originally published at blog.graftini.com

How to change numeric input by dragging in React?

The above GIF is one of the interactions that I love about Figma . To change the position of an item, I do not have to use a keyboard at all. Just a mouse is enough.

So in this article we are going to create a numeric input field whose value can be changed by dragging on its label from scratch. We needed this for Graftini because we think a visual tool should not require keyboard for most interactions.

If you are eager to see the code and try it out for yourself, then jump to CodeSandbox at https://codesandbox.io/s/drag-number-input-z2rnj.

Let us first create a simple input

We can start by creating an input which reads from and writes the values to a state. If you have already created forms this should be simple enough to understand.

function Input() {
  const [value, setValue] = useState(0);

 const onInputChange = useCallback(
    (ev) => setValue(parseInt(ev.target.value, 10)),
    []
  );

  return (
    <input
        value={value}
        onChange={onInputChange}
        style={{
          padding: 8,
        }}
     />
  );
}
Enter fullscreen mode Exit fullscreen mode

We are creating a state which stores the current value of the input. This state will be updated when the input changes via the keyboard.

Now we need a label that fits right in with the input

We need the label to be an anchor on which the mouse interactions can be added. The input itself cannot be the anchor because then it will ruin how input fields normally are expected to behave. The code that captures that idea can look like:

<div
    style={{
      display: "flex",
      border: "1px solid #CCC",
      alignItems: "center",
      borderRadius: 4,
      fontFamily: "sans-serif",
      width: 300,
    }}
>
   <span
      style={{
        padding: 8,
        color: "gray",
        cursor: "ew-resize",
        userSelect: "none",
      }}
    >
      Count
    </span>
   <input
      value={value}
      onChange={onInputChange}
      style={{
        flex: 1,
        padding: 8,
        border: "none",
        outline: "none",
      }}
   />
</div>
Enter fullscreen mode Exit fullscreen mode

The above code is just visual cosmetics. You can make it to look however you see fit. Now the input should look something like:
image.png

Adding mouse interactions on the label

We will extract the label into its own component to make it easier to write & understand the code. Then we will add three mouse interactions in it. One on the label itself and two on the document. Why? We will discuss it along side the code.

function DragLabel({ value, setValue }) {
  // We are creating a snapshot of the values when the drag starts
  // because the [value] will itself change & we need the original
  // [value] to calculate during a drag.
  const [snapshot, setSnapshot] = useState(value);

  // This captures the starting position of the drag and is used to 
  // calculate the diff in positions of the cursor.
  const [startVal, setStartVal] = useState(0);

  // Start the drag to change operation when the mouse button is down.
  const onStart = useCallback(
    (event) => {
      setStartVal(event.clientX);
      setSnapshot(value);
    },
    [value]
  );

  // We use document events to update and end the drag operation
  // because the mouse may not be present over the label during
  // the operation..
  useEffect(() => {
    // Only change the value if the drag was actually started.
    const onUpdate = (event) => {
      if (startVal) {
        setValue(event.clientX - snapshot);
      }
    };

    // Stop the drag operation now.
    const onEnd = () => {
      setStartVal(0);
    };

    document.addEventListener("mousemove", onUpdate);
    document.addEventListener("mouseup", onEnd);
    return () => {
      document.removeEventListener("mousemove", onUpdate);
      document.removeEventListener("mouseup", onEnd);
    };
  }, [startVal, setValue, snapshot]);

  return (
    <span
      onMouseDown={onStart}
      style={{
        padding: 8,
        color: "gray",
        cursor: "ew-resize",
        userSelect: "none",
      }}
    >
      Count
    </span>
  );
}
Enter fullscreen mode Exit fullscreen mode

Now try running it up and voila 🎉🎊 you have your own drag to change numeric input.

2021-06-29 16.27.09.gif

It looks awesome doesn't it? Though the cursor during the operation does not look good. This could be something that you can fix. 😋

The full code is at CodeSandbox for you to try it out https://codesandbox.io/s/drag-number-input-z2rnj?file=/src/App.js.

Fork it & make improvements to it. Till then ✌️.

Top comments (3)

Collapse
 
tombalev profile image
Tom Balev • Edited

Change:

if (startVal) {
     setValue(event.clientX - snapshot);
}
Enter fullscreen mode Exit fullscreen mode

to:

if (startVal) {
     setValue(snapshot + event.clientX - startVal);
}
Enter fullscreen mode Exit fullscreen mode

to prevent zeroing out the input on next drag

Collapse
 
dakotys profile image
Jaroslav Rojík • Edited

You forgot to remove mousemove listener on mouse release:
const onMouseUp = () => {
document.removeEventListener("mousemove", onMouseMove);
document.removeEventListener("mouseup", onMouseUp);
setStartPos(0);
};

Collapse
 
dariansampare profile image
Darian Sampare

Very nice start but you'll have to do some math if you want a default value other than 0!