Ausnahme gefangen: SSL certificate problem: certificate is not yet valid 📌 Build a reorderable list in react

🏠 Team IT Security News

TSecurity.de ist eine Online-Plattform, die sich auf die Bereitstellung von Informationen,alle 15 Minuten neuste Nachrichten, Bildungsressourcen und Dienstleistungen rund um das Thema IT-Sicherheit spezialisiert hat.
Ob es sich um aktuelle Nachrichten, Fachartikel, Blogbeiträge, Webinare, Tutorials, oder Tipps & Tricks handelt, TSecurity.de bietet seinen Nutzern einen umfassenden Überblick über die wichtigsten Aspekte der IT-Sicherheit in einer sich ständig verändernden digitalen Welt.

16.12.2023 - TIP: Wer den Cookie Consent Banner akzeptiert, kann z.B. von Englisch nach Deutsch übersetzen, erst Englisch auswählen dann wieder Deutsch!

Google Android Playstore Download Button für Team IT Security



📚 Build a reorderable list in react


💡 Newskategorie: Programmierung
🔗 Quelle: dev.to

What are we building?

Hello all! In this tutorial I want to take you through the process of building a reorderable list in react. The list we are gonna be building is probably far too simple for usage in real-world apps, but it should serve as a good starting point.

Here is a demo of what we are gonna be building:

Reorderable list demo

Here is a code sandbox demo aswell for you to play around in:

(In the demo, we are reordering the lyrics of the song tiptoe through the true bits by Los Campesinos! it's a great song, so go give it a listen!)

Writing the HTML

Ok, before we start writing some logic, let's get some HTML up and running:

const App = () => {
  const items = [
    "You asked if",
    "you could see me",
    "before I went to",
    "Spain, you didn't",
    "give a reason didn't",
    "know what you would",
    "say. But I was hoping",
    "that my breath on your",
    "face would blow every",
    "last thing into place",
  ];

  return (
    <div className='list'>
      {items.map((value, index) => (
        <div key={value} className="list-item">
          {value}
        </div>
      ))}
    </div>
  );
};

I put the list of items in a constant variable, but let's actually move it into a useState hook so we can edit it later:

const App = () => {
  const [items, setItems] = useState([
    "You asked if",
    "you could see me",
    "before I went to",
    "Spain, you didn't",
    "give a reason didn't",
    "know what you would",
    "say. But I was hoping",
    "that my breath on your",
    "face would blow every",
    "last thing into place",
  ]);
  ...
};

Adding some CSS

As you can see, I already added the classes .list and .list-item, so let's use those to style our items:

.list {
  display: flex;
  flex-direction: column;

  justify-content: center;
  text-align: center;

  border: 1px black solid;

  width: min-content;
  margin: auto;
}

.list-item {
  padding: 0.3rem;
  white-space: nowrap;
  cursor: pointer;
  height: 1.5rem;
  background-color: white;
}

We now have something that looks like the demo, but with no functionality. Let's get started with some of the actual logic:

Adding drag logic.

Ok, for our drag-n-drop behaviour, we are gonna first have to be able to drag any given item in our list, this should be simple enough, first, we have to keep track of a dragged item inside some variable, so let's make such a variable:

const App = () => {
  const [items, setItems] = useState([...]);
  const [dragged, setDragged] = useState<number | null>(null); // storing the dragged item as an index

  ...
};

Now, we have to do three things:

  1. Set the currently dragged item when clicking on an object.
  2. Not render the dragged item
  3. Make a copy of the dragged item that follows the mouse.

The first and second are pretty straight forward, we have to use the onMouseDown event for the first one.
For the second one, we just need to use good ol' conditional rendering:

{items.map((value, index) => (
        <>
          {dragged !== index && (
            <div
              key={value}
              className="list-item"
              onMouseDown={(e) => {
                e.preventDefault();
                setDragged(index);
              }}
            >
              {value}
            </div>
          )}
        </>
      ))}

Now clicking in any given item makes it disappear, in order to render a floating item, let's first add another div with an absolute position, we'll worry about how to make it follow the mouse later

return (
    <>
      {/* ----------FLOATING ITEM---------- */}
      {dragged !== null && (
        <div className="floating list-item">{items[dragged]}</div>
      )}

      {/* ----------MAIN LIST---------- */}
      <div className="list">
        ...
      </div>

The .floating css class just looks like this:

.floating {
  position: absolute;
  box-shadow: 5px 5px 10px rgba(0,0,0,0.5);
}

Now, in order to properly render the item below the mouse, we are gonna have to listen to the mousemove event on the entire document. We can use a useEffect for that:

  const [items, setItems] = useState([...]);
  const [dragged, setDragged] = useState<number | null>(null);

  const [mouse, setMouse] = useState<[number, number]>([0, 0]);

  // get mouse coordenates
  useEffect(() => {
    const handler = (e: MouseEvent) => {
      setMouse([e.x, e.y]);
    };

    document.addEventListener("mousemove", handler);

    return () => document.removeEventListener("mousemove", handler);
  }, []);
...

Now that we are tracking the mouse, we can set the floating div's top and left properties to make it follow the mouse:

      {/* ----------FLOATING ITEM---------- */}
      {dragged !== null && (
        <div className="floating list-item"
        style={{
          left: `${mouse[0]}px`,
          top: `${mouse[1]}px`,
        }}
        >{items[dragged]}</div>
      )}

Amazing, we have now a dragging functionality, but no way to drop any item, so let's work on that now:

Putting the 'drop' in 'drag-n-drop'

When we drop an item, we are given a place to drop it between two different items, for example, here:

Drag-n-drop demonstration

We are putting 'you could see me' between 'say. But I was hoping' and 'that my breath on your', so we need to have some sort of dropbox element between each item:

{/* ----------MAIN LIST---------- */}
<div className="list">
  <div className="list-item drop-zone" /> {/* Drop zone before all items */}
  {items.map((value, index) => (
    <>
      {dragged !== index && (
        <>
          <div
            key={value}
            className="list-item"
            onMouseDown={(e) => {
              e.preventDefault();
              setDragged(index);
            }}
          >
            {value}
          </div>
          <div className="list-item drop-zone" /> {/* drop zone after every item */}
        </>
      )}
    </>
  ))}
</div>

This boxes kinda break the app because they are visible all of the time, so let's make them only visible while we are dragging something, to do that, we can replace their className with the following line:

className={`list-item drop-zone ${dragged === null ? "hidden" : ""}`}

Now it should look like this:

<div
  className={`list-item drop-zone ${dragged === null ? "hidden" : ""}`}
/>
  {items.map((value, index) => (
    <>
      {dragged !== index && (
        <>
          <div
            ...
          >
            {value}
          </div>
          <div
            className={`list-item drop-zone ${
              dragged === null ? "hidden" : ""
            }`}
          />
        </>
      )}
    </>
  ))}
</div>

Now we should write some CSS for .drop-zone and .drop-zone.hidden

.list-item.drop-zone {
  background-color: #ccc;
  transition-property: height padding;
  transition-duration: 250ms;
  transition-timing-function: cubic-bezier(0.075, 0.82, 0.165, 1);
  overflow: hidden;
}

.list-item.drop-zone.hidden {
  height: 0px;
  padding: 0px
}

That would be good, except for the fact that all of the drop-zones are visible at the same time, in reality, only the closest one to the mouse should be.

The simplest way to fix that is to use some good ol' javascript to figure out which of the items is closest to us, so inside a new useEffect hook, we'll check the distance to the mouse for every drop-zone element:

  const [dropZone, setDropZone] = useState(0);

  // get closest drop zone
  useEffect(() => {
    if (dragged !== null) {
      // get all drop-zones
      const elements = Array.from(document.getElementsByClassName("drop-zone"));
      // get all drop-zones' y-axis position
      // if we were using a horizontally-scrolling list, we would get the .left property
      const positions = elements.map((e) => e.getBoundingClientRect().top);
      // get the difference with the mouse's y position
      const absDifferences = positions.map((v) => Math.abs(v - mouse[1]));

      // get the item closest to the mouse
      let result = absDifferences.indexOf(Math.min(...absDifferences));

      // if the item is below the dragged item, add 1 to the index
      if (result > dragged) result += 1;

      setDropZone(result);
    }
  }, [dragged, mouse]);

The code and the comments should be pretty self-explanatory, with, maybe, the exception of this line:

// if the item is below the dragged item, add 1 to the index
if (result > dragged) result += 1;

We have to add 1 to the items dragged because this code does not account for the box right below the currently dragged item, as that box is currently non-existant (because of the conditional rendering we added earlier), our index will be 1 number too low if we are above the currently dragged item.

Now that we have figured out the closest item, we can make sure only said item is rendered, so we are gonna change the className line again, this time, from this:

className={`list-item drop-zone ${dragged === null ? "hidden" : ""}`}

to this:

className={`list-item drop-zone ${dragged === null || dropZone !== index+1 ? "hidden" : ""}`}

Now, our code looks like this:

<div className="list">
  <div
    className={`list-item drop-zone ${
      dragged === null || dropZone !== 0 ? "hidden" : ""
    }`}
  />
  {items.map((value, index) => (
    <>
      {dragged !== index && (
        <>
          <div
            ...
          >
            {value}
          </div>
          <div
            className={`list-item drop-zone ${dragged === null || dropZone !== index + 1 ? "hidden" : ""}`}
          />
        </>
      )}
    </>
  ))}
</div>

As you can see, I used index+1 to account for the '0th' zone before all items.

Ok, this leaves us with the following:

Demo of dragging

Yep, it's still impossible to drop an item, but the rendering works as expected, now, to drop an item, we just need to add a mouseup listener to the document to know when we are dropping the item, so let's do just that inside a new useEffect

// drop item
  useEffect(() => {
    const handler = (e: MouseEvent) => {
      if (dragged !== null) {
        e.preventDefault();
        setDragged(null);
      }
    };

    document.addEventListener("mouseup", handler);
    return () => document.removeEventListener("mouseup", handler);
  });

This allows us to drop items, but it doesn't really reorder the list just yet, once we figure out how to reorder the list, the component will be finished.

Reordering the list.

Before we can write a function for reordering the list, we need to figure some stuff out:

  1. When should we reorder the list
  2. What information about the list do we need.
  3. Where we can get that information
  4. How to actually use that information to reorder the list.

Ok, so number one is pretty simple, we should reorder the list after we drop an item, so inside the mouseup handler.

Number two should be pretty simple too, we should just need two numbers, the index of the item's original position and the index of the item's final position.

Number 3 has also already been solved, the dragged variable has the original index and the dropZone variable has the new index.

So now, let's solve number 4 by looking at two examples:

Example 1, simple movement:

If we have a list like this:

  • Start of the list
    • dropzone 0
  • [0] ITEM A
    • dropzone 1
  • [1] ITEM B
    • dropzone 2
  • [2] ITEM C
    • dropzone 3
  • [3] ITEM D
    • dropzone 4
  • [4] ITEM E
    • dropzone 5

If we drag 'ITEM B' to 'dropzone 4', our dragged variable will be 1 and our dropZone variable will be 4.
The new order should be:

  • [0] ITEM A
  • [1] ITEM C
  • [2] ITEM D
  • [3] ITEM B
  • [4] ITEM E

So index 1 now has what was on index 2, index 2 now has what was on index 3 and index 3 now has what was on index 1, knowing what our variables should be, this function should do the trick:

const reorderList = <T,>(l: T[], start: number, end: number) => {
  const temp = l[start];

  for (let i=start; i<end; i++) {
    l[i] + l[i+1];
  }
  l[end - 1] = temp;

  return l;
};

We can plug this item into our handler like so:

// drop item
  useEffect(() => {
    const handler = (e: MouseEvent) => {
      if (dragged !== null) {
        e.preventDefault();
        setDragged(null);

        setItems((items) => reorderList([...items], dragged, dropZone));
      }
    };

    document.addEventListener("mouseup", handler);
    return () => document.removeEventListener("mouseup", handler);
  });

Ok, we can now push items forward on the list, except in a very specific case, let's look at example number 2 for that case:

Example 2: Backward movement

Now, let's look at the exact opposite situation, let's say, using the example list we defined above, that we want to move "ITEM D" into "dropzone 1" (dragged=3 and dropZone=1). The resulting list should look like this:

  • [0] ITEM A
  • [1] ITEM D
  • [2] ITEM B
  • [3] ITEM C
  • [4] ITEM E

The item in index 1 has what was in index 3, item in index 2 has what was in index 1 and item in index 3 has what was in index 2.

So it is similar but not quite the same, let's separate the algorithm into moving the items backward and forward.

First, move all of the previous reordering code into a new function and call it _reorderListForward (or something), now we'll redefine our reorderList function like so:

const reorderList = <T,>(l: T[], start: number, end: number) => {
  if (start < end) return _reorderListForward([...l], start, end);
  else if (start > end) return _reorderListBackward([...l], start, end);

  return l; // if start == end
};

Now, for the _reorderListBackward function, as I said, any item in a given index should be equal to what used to be in the list on the previous index:

const _reorderListBackward = <T,>(l: T[], start: number, end: number) => {
  for (let i = start; i > end; i--) { // backward for-loop for backward movement
    l[i] = l[i - 1];
  }

  return l;
};

Except for the end index, which should have the same value as start.

const _reorderListBackward = <T,>(l: T[], start: number, end: number) => {
  const temp = l[start];

  for (let i = start; i > end; i--) {
    l[i] = l[i - 1];
  }

  l[end] = temp;

  return l;
};

Amazing! We can now reorder any item in any direction at any time! Yay!

Conclusion

Now the element is complete, it isn't perfect and I wouldn't call if production ready, but I think it's a really good starting place.

If you wanted to make this into a bigger project, why not try adding drag-handles? Or packaging this whole thing into a reusable component?

Or, you know, stop reinventing the wheel and just use a premade library that can do all that and more? Yeah, probably should have done that from the beginning... ¯\_(ツ)_/¯

...



📌 Build a reorderable list in react


📈 61.28 Punkte

📌 This Week In React #127: Nextra, React-Query, React Documentary, Storybook, Remix, Tamagui, Solito, TC39, Rome...


📈 26.62 Punkte

📌 This Week In React #131: useReducer, Controlled Inputs, Async React, DevTools, React-Query, Storybook, Remix, RN , Expo...


📈 26.62 Punkte

📌 This Week In React #139: React.dev, Remix, Server Components, Error Boundary, Wakuwork, React-Native, Bottom Sheet...


📈 26.62 Punkte

📌 This Week In React #146: Concurrency, Server Components, Next.js, React-Query, Remix, Expo Router, Skia, React-Native...


📈 26.62 Punkte

📌 List Within a List in Python – How to Initialize a Nested List


📈 24.07 Punkte

📌 File List Export 2.8.8 - Export folder contents to a list (was File list to Excel).


📈 24.07 Punkte

📌 Build a 3D World in React with ThreeJS and React Three Fiber


📈 23.15 Punkte

📌 Build complex PDFs using React: react-print-pdf


📈 23.15 Punkte

📌 Build complex PDFs using React: react-print-pdf


📈 23.15 Punkte

📌 How to Build a Dynamic Dropdown Component in React – React Compound Component Pattern Explained


📈 23.15 Punkte

📌 React Fiber: Facebook baut Javascript-Bibliothek React fundamental um


📈 17.74 Punkte

📌 The State of React Native Tooling (React Native CLI - The Ultimate Guide)


📈 17.74 Punkte

📌 heise+ | Externe Bibliotheken für React: Material-UI und React Router einbinden


📈 17.74 Punkte

📌 How to Test Your React App Effectively with React Testing Library


📈 17.74 Punkte

📌 Fix the "React Hook is Called Conditionally" Error in React


📈 17.74 Punkte

📌 Typescript for React Components (or How To Write Components in React The Right Way)


📈 17.74 Punkte

📌 React Redux Tutorial #1 - Was ist React Redux


📈 17.74 Punkte

📌 React Redux Tutorial #1 - Was ist React Redux


📈 17.74 Punkte

📌 This Week In React #126: Perf, Progressive Enhancement, Remix, Storybook, React-Native, FlashList, Nitro, TC39...


📈 17.74 Punkte

📌 React Testing Library Tutorial – How to Write Unit Tests for React Apps


📈 17.74 Punkte

📌 This Week In React #129: useEffectEvent, Storybook, OpenNEXT, React Email, Remix, Next.js, Pointer-Events, Expo-MDX...


📈 17.74 Punkte

📌 Оптимизация React приложения с помощью React.lazy


📈 17.74 Punkte

📌 Create a project in React without create-react-app/vite 2023 (Spanish)


📈 17.74 Punkte

📌 One-Click Code Block Copying in React with react-copy-to-clipboard


📈 17.74 Punkte

📌 React Native Networking – How To Perform API Requests In React Native using the FetchAPI


📈 17.74 Punkte

📌 Building Pagination in React with React Paginate


📈 17.74 Punkte

📌 How To Create Custom Alerts in React Using React-Notifications-Component


📈 17.74 Punkte

📌 How To Make Login Page Like Twitter Using React Js | Sign In Page Design With React Js


📈 17.74 Punkte

📌 Building a Modern Document Website for React Native Library Like React Native ECharts


📈 17.74 Punkte

📌 Mastering React Router: The Ultimate Guide to Navigation and Routing in React Apps!


📈 17.74 Punkte

📌 How to Manage State in React and React Native with the PullState Library


📈 17.74 Punkte











matomo