Search code examples
javascriptreactjsnext.jsreact-grid-layout

Toggling the static key boolean in layout object of a react-grid-layout causes all grid items to move incorrectly


tl;dr:

React-grid-layout incorrectly moves all grid items around when their static option is enabled and disabled (toggled). I want the grid items to be frozen in place when static, and move if and only if the user moves them with static mode disabled. An example of the problem can be found here (CodeSandbox), with a GIF example here (Imgur).


Background

I am making an interactive grid layout for my app using react-grid-layout. I have a button to toggle between non-static (draggable and resizeable) and static (non-interactive) mode for all items in the grid (info about static in react-grid-layout). I am doing so with useState and with tips from this answer:

var [layout, setLayout] = useState([
  { i: "a", x: 0, y: 0, w: 10, h: 4, static: false },
  { i: "b", x: 1, y: 0, w: 3, h: 2, static: false },
  { i: "c", x: 4, y: 0, w: 1, h: 2, static: false },
]);

function toggleStaticMode() {
  setLayout(
    layout.map((prevLayout) => {
      return { ...prevLayout, static: !prevLayout.static };
    })
  );
}

Expected behavior:

Each element in the grid has unique properties that define its location, size, etc... on the grid layout. Those are defined in the layout variable shown above. When toggling between static and non-static mode, all values should stay the same, except for the static key in each object. The positions and sizes on-screen should stay the same as well. This enables a user to toggle static mode off to change the position/size of grid items, and on to "freeze" them in the place/size they left them.

Current (wrong) behavior:

My problem is when I am in non-static mode, I drag the grid items around to where I want them, and toggle static mode on, they reset to where they were before. i.e.:

I drag Element A to the bottom of the stack, and when I turn on static mode, it shifts to the middle of the stack, and turning it back off returns Element A to the top of the stack. Element A will then loop between the middle and top, depending on whether static mode is on or off.

Here is an example of what I mean:


What I have tried:

  • As shown in the snippet (the commented code), I tried re-creating the toggleStaticMode function to provide more detail. I made the isStatic state, and set the new value of it every click.
  • I also tried to change the positions to see if I could see a position change on click, which I could. Without any modification to the sizes and positions, the values would stay the same, but the positions and/or sizes on-screen would change.
  • I tried also changing the static value for each object to stay as true or false to note changes. When object a and c's static value was true and b's was isStatic, toggling static mode changed the position of b, as well as a and c. The values printed in the console said they did not change.

None of the above showed me what was happening or why it was. The docs also did not provide much information about this.

One "hackish" solution that may work is to use local storage/cookies from this example, but I do not want to do this every time a user toggles static mode. This solution might not actually fix this problem.

This may be a bug, but I would like to find a better work-around if possible.


Full code used:

import React, {
  useState
} from "react";
import GridLayout from "react-grid-layout";

export default function Home() {
  let [layout, setLayout] = useState([{
      i: "a",
      x: 1,
      y: 0,
      w: 8,
      h: 4,
      static: false
    },
    {
      i: "b",
      x: 3,
      y: 2,
      w: 3,
      h: 2,
      static: false
    },
    {
      i: "c",
      x: 5,
      y: 4,
      w: 1,
      h: 2,
      static: false
    },
  ]);

  function printLayoutItem(val) {
    console.log(layout[val].i + ": (w: " + layout[val].w + " h: " + layout[val].h + " x: " + layout[val].x + " y: " + layout[val].y + ")");
  }

  function toggleStaticMode() {
    setLayout(
      layout.map((l) => {
        return { ...l,
          static: !l.static
        };
      })
    );
  }

  // let [isStatic, setStatic] = useState(false);

  // function toggleStaticMode() {
  //    if (isStatic) setStatic(false) 
  //    else setStatic(true);
  //    printLayoutItem(0);
  //    printLayoutItem(1);
  //    printLayoutItem(2);
  //    setLayout([
  //        { i: "a", x: 3, y: 0, w: 8, h: 4, static: isStatic },
  //        { i: "b", x: 0, y: 2, w: 3, h: 2, static:  isStatic},
  //        { i: "c", x: 5, y: 4, w: 1, h: 2, static: isStatic },
  //    ]);
  // }

  return ( <div>
    <main>{/* thanks for this beautiful Tidy, StackOverflow */}
    <button onClick ={toggleStaticMode}>
    Toggle Static Mode
    </button> 
    <GridLayout 
        className="layout"
        layout={layout}
        cols={4}
        rowHeight={100}
        width={1200}>
    <div key="a">Hello A </div> 
    <div key="b">Hello B </div> 
    <div key="c">Hello C </div> 
    </GridLayout > 
   </main> 
   </div>
  );
}
<!-- Snippet does not run correctly, please see the example below for a working example of the error occuring -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>

I also uploaded this code to CodeSandbox to show a full, working example of my current problem. This example is different in the way that it makes use of react classes/states and not useState, but the same problem occurs. This example shows the size/position values changing when toggling static mode.

EDIT:

Someone Special's answer got me in the right direction, however, when the columns rearrange to a smaller value than the minimum size, the items scatter upon toggle again. An example of this can be viewed here.


Solution

  • Updated: Found your problem. You updated the lg array but u did not update the sm array.

    toggleStaticMode() {
        this.setState({
          layouts: {
            lg: this.state.layouts.lg.map((l) => {
              return { ...l, static: !l.static };
            }),
            sm: this.state.layouts.sm.map((l) => {
              return { ...l, static: !l.static };
            })
          }
        });
      }
    

    By updating the sm array in togglestaticmode, it works on my side. Sandbox


    Previously:

    I haven't worked with this react-grid-layout before, however I see your onLayout function as follows.

      onLayoutChange(layout, layouts) {
        this.props.onLayoutChange(layout, layouts);
      }
    

    You update your layout using the props, and you set the initialprops, but that only sets the initialProps - it doesn't listen to changes to props from your parent component.

    Meaning, when you use props.onLayoutChange (from your parent component), your parent changes, but your component state did not change.

    You can either,

    1. Update your state in onLayoutChange in addition to props.onLayoutChange
    2. Update your state with componentDidUpdate so it synchronises with your parent component.

    componentDidUpdate