kenshin
kenshin

Reputation: 205

How to create a resizable component in React

As said in the title. I want to create a React component that will give me a possibility to resize its width by dragging - just like windows in Windows operating system. What is actually the best approach to handle this issue ?

EDIT
I included my current approach to the subject of the matter:

First I placed a "dragger" element in the top-right corner of my container. When i press mouse down on that element i want to create a mousemove event listener which will modify the containerWidth in respect to the X coordinate of the cursor relative to the initial X position of the edge of the container. I already have that event listener firing and logging me the coordinates after holding down the mouse button but unfortunatelly for some reason the event is not being removed after the mouse is unpressed(mouseUp event) which is not what i intended. Any suggestions appreciated, also those about some issues i might expect in the future related to this topic. Thanks.

type Props = MasterProps & LinkStateToProps & LinkDispatchToProps;
const Test3 = (Props: Props) => {

  const [containerWidth, setContainerWidth] = React.useState(640)
  const [isBeingStretched, setIsBeingStretched] = React.useState(false);
  const masterRef = React.useRef(null);


  const logMousePosition = React.useCallback((event:MouseEvent)=>{
    console.log(event.clientX);
  },[])


  const handleMouseDown=()=>{
    document.addEventListener('mousemove', logMousePosition);
    masterRef.current.addEventListener('mouseup', ()=>{
      document.removeEventListener('mouseup', logMousePosition)
    })
  }
  const handleMouseUp = () => { 
    document.removeEventListener('mousemove', logMousePosition);
  }
  return (
    <div className="master-wrapper" ref={masterRef}>
      <div className="stretchable-div" style={{ width: `${containerWidth}px` }}>
        <div className="dragger-wrapper">
          <h2>This is supposed to change width</h2>
          <div className="dragger"
            id="dragger"
            onMouseDown={handleMouseDown}
            onMouseUp={handleMouseUp}/>
        </div>
      </div>

    </div>
  );

}

export default connect(mapStateToProps, mapDispatchToProps)(Test3);

Upvotes: 3

Views: 13884

Answers (1)

lawrence-witt
lawrence-witt

Reputation: 9354

I'd never done something like this before so I decided to give it a go, and it ended up being quite straightforward to implement with React state management. I can see why you might not know where to start if you are new to React, and that's ok, although two things to note before I go through my solution:

  1. Statements such as document.getElementById or document.addEventListener are not going to function as intended anymore. With React, you are manipulating a virtual DOM, which updates the actual DOM for you, and you should aim to let it do that as much as possible.
  2. Using refs to get around this fact is bad practice. They may act in a similar way to the statements mentioned above but that is not their intended use case. Read up on what the documentation has to say about good use cases for ref.

Here's what the JSX portion of my demo looks like:

return (
    <div className="container" onMouseMove={resizeFrame} onMouseUp={stopResize}>
      <div className="box" style={boxStyle}>
        <button className="dragger" onMouseDown={startResize}>
          Size Me
        </button>
      </div>
    </div>
  );

We're going to need three different events - onMouseDown, onMouseMove and onMouseUp - to track the different stages of the resize. You already got this far in your own code. In React, we declare all these as attributes of the elements themselves, although they are not actually in-line functions. React adds them as event listeners for us in the background.

const [drag, setDrag] = useState({
    active: false,
    x: "",
    y: ""
});

const startResize = e => {
    setDrag({
      active: true,
      x: e.clientX,
      y: e.clientY
    });
};

We'll use some state to track the resize as it is in progress. I condensed everything into a single object to avoid bloat and make it more readable, although this won't always be ideal if you have other hooks like useEffect or useMemo dependent on that state. The first event simply saves the initial x and y positions of the user's mouse, and sets active to true for the next event to reference.

const [dims, setDims] = useState({
    w: 200,
    h: 200
});

const resizeFrame = e => {
    const { active, x, y } = drag;
    if (active) {
      const xDiff = Math.abs(x - e.clientX);
      const yDiff = Math.abs(y - e.clientY);
      const newW = x > e.clientX ? dims.w - xDiff : dims.w + xDiff;
      const newH = y > e.clientY ? dims.h + yDiff : dims.h - yDiff;

      setDrag({ ...drag, x: e.clientX, y: e.clientY });
      setDims({ w: newW, h: newH });
    }
};

The second piece of state will initialise and then update the dimensions of the element as its values change. This could use any measurement you want although it will have to correlate to some CSS property.

The resizeFrame function does the following:

  1. Make the properties of drag easily available via destructuring assignment. This will make the code more readable and easier to type.
  2. Check that the resize is active. onMouseMove will fire for every pixel the mouse moves over the relevant element so we want to make sure it is properly conditioned.
  3. Use Math.abs() to get the difference in value between the current mouse position and the saved mouse position as a positive integer. This will save us from having to do a second round of conditional statements.
  4. Use turnary statements to either add or subtract the difference from the dimensions, based on whether the new mouse position is greater or less than the previous on either axis.
  5. Set the states with the new values, using the spread operator ... to leave the irrelevant part of drag as it is.
const stopResize = e => {
    setDrag({ ...drag, active: false });
};

const boxStyle = {
    width: `${dims.x}px`,
    height: `${dims.y}px`
};

Then we simply set the activity of the drag state to false once the user is finished. The JS style object is passed to the element with the state variable in place so that is taken care of automatically.

Here is the codesandbox with the finished effect.


One of the drawbacks to doing things this way is that it basically requires you to have that mouseMove event listener assigned to the largest site container, because the mouse is not going to stay within the bounds of the box during the resize. That could be an issue if you want to have multiple elements with the same functionality, although nothing that you can't solve with good state management. You could probably fine tune this so that the mouse always stays on the drag element, although that would require a more complex implementation.

Upvotes: 7

Related Questions