table: fix moving columns bug
Tags: webdev, javascript, beginners, tutorial
Published: 2022-12-04 15:44:30

In the previous tutorial, we make columns swappable. I mentioned that in some cases two columns can swap really fast and flash. There is a flaw in our code. In this tutorial, we are going to fix it.

---

##What
First of all, when we spot a bug, we need to identify what the bug is and try limiting the factors.

This one is working as expected.
![columns move as expected](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cead2tlg3di5yjfgkg5r.gif)


This one flashes.

![columns move and flash](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/775fm9k7621u33yj53d5.gif)


So what's the bug? When we move "website" column to the right, we expect it to swap with "company" column. The "website" column does swap with "company" column, but they somehow swap again and again when we keep moving towards right.

Let's try another column. This works as expected. When moving "company" column to left or right, "company" column only swap once with the "address" column .
![company column moves as expected](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/aqvn9fwzehwrljgvbsbj.gif)


It's not obvious where the bug occurs, but we know that "website" column has bug and "company" column does not. We can inspect on them further more. We move our cursor much slower this time. We can see that sometimes the columns swap correctly, but as soon as we move our cursor again. Even we are still moving "website" towards right, "website" column swaps back with its left column "company", which is not correct.

![website and company columns do not swap correctly](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/czhcq7n821orq9cnqm0m.gif)


So the bug occurs when we move a column to right, it should only swap with its right column, but it sometimes swaps with its left column. When we constantly moving our cursor, the columns swap repeatedly and fast. That's why they flash.

We have identified the bug. Next, we want to collect as much information as possible. Does this bug also occurs when we move a column to left? Let's test it out.

Moving "company" column to left works fine.

![Moving "company" column to left works fine](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vdgu0ixr88e8k0eccot9.gif)


Moving "website" column to left has bugs.
![Moving "website" column to left has bugs](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5j8ej7cr7h42u7xc13mq.gif)

---


## Why
After we identified the bug, we want to figure out why it occurs. We tested "company" column and "website" column. The bug only occurs when we move "website" column but not "company" column. What's the difference between them? The only difference I can tell is their sizes. The "company" column is much wider than the "website" column, but this information doesn't seem to be helpful. So we look for next clue. How do we swap the columns?

```js
function makeColumnsSwappable(columnsContainer, elementsToPatch = []) {
  columnsContainer.addEventListener('pointerdown', e => {
    let columnElements = [...columnsContainer.children]
    const firstTarget = e.target
    let firstTargetIndex = columnElements.indexOf(firstTarget)

    function preventDefault(e) {
      e.preventDefault()
    }

    document.addEventListener('selectstart', preventDefault)

    if (firstTargetIndex === -1)
      return

    function handleMove(e) {
      const secondTarget = e.target
      const secondTargetIndex = columnElements.indexOf(secondTarget)

      if (secondTargetIndex === -1)
        return

      if (firstTarget === secondTarget)
        return

      // (2) this is where we want to swap columns
      swapColumns(columnsContainer, firstTargetIndex, secondTargetIndex)

      elementsToPatch.forEach((columnsContainer) => {
        swapColumns(columnsContainer, firstTargetIndex, secondTargetIndex)
      })

      columnElements = [...columnsContainer.children]
      firstTargetIndex = columnElements.indexOf(firstTarget)
    }

    function swapColumns(container, firstTargetIndex, secondTargetIndex) {
      const columns = container.children
      const firstTarget = columns[firstTargetIndex]
      const secondTarget = columns[secondTargetIndex]
      const isMoveToLeft = firstTargetIndex > secondTargetIndex
      const isMoveToRight = firstTargetIndex < secondTargetIndex

      // (3) this is where we actually swap the column elements
      if (isMoveToLeft) {
        secondTarget.insertAdjacentElement('beforebegin', firstTarget)
      } else if (isMoveToRight) {
        secondTarget.insertAdjacentElement('afterend', firstTarget)
      }
    }

    // (1) this is where we handle 'pointermove' event
    columnsContainer.addEventListener('pointermove', handleMove)

    document.addEventListener('pointerup', () => {
      columnsContainer.removeEventListener('pointermove', handleMove)
      document.removeEventListener('selectstart', preventDefault)
    }, { once: true })
  })
}
```

We swap two column elements by getting two targets, the first target is the one on `'pointerdown'`, and the second target is the one on `'pointermove'`. We assume that we are moving first column to left if `firstTargetIndex > secondTargetIndex`, and moving first column to right if `firstTargetIndex < secondTargetIndex`. We use that information to move elements with `insertAdjacentElement`. 
```js
function swapColumns(container, firstTargetIndex, secondTargetIndex) {
  const columns = container.children
  const firstTarget = columns[firstTargetIndex]
  const secondTarget = columns[secondTargetIndex]
  const isMoveToLeft = firstTargetIndex > secondTargetIndex
  const isMoveToRight = firstTargetIndex < secondTargetIndex

  if (isMoveToLeft) {
    secondTarget.insertAdjacentElement('beforebegin', firstTarget)
  } else if (isMoveToRight) {
    secondTarget.insertAdjacentElement('afterend', firstTarget)
  }
}
```

We can probably tell that using `firstTargetIndex` and `secondTargetIndex` to get moving direction is not ideal. But the logic works sometimes. So what goes wrong? Let's look closely to the cases that works and that doesn't this time.


When we are moving "company" column, it looks like the cursor is always on top of it.
![company column moves as expected](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/aqvn9fwzehwrljgvbsbj.gif)


When we are moving "website" column, as soon as it swap with "company" column, the cursor is on top of the "company" column.
![website and company columns do not swap correctly](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/czhcq7n821orq9cnqm0m.gif)


In our swapping columns logic, we do nothing if first target and second target are the same, and we swap two columns if first target and second target are different. This reflects in the result. The reason that swapping "website" column with "company" column has bug is because "company" column is much wider than "website" column. After "website" column swaps with "company" column, the cursor becomes on top of "company" column. As soon as we move cursor again, they swap. After they swap, the cursor is still on top of "company" column. This repeats again and again on `'pointermove'`. That's why they flash.


---


## How
After we find out the reason of the bug, we can start thinking how to fix it. If you'd like to follow along, here is the [code](https://github.com/ga676005/table/tree/8e692051afd6b563a77a455ede2b77f967fc05ba) from the previous tutorial.


When we are moving a column to right, we only want it to swap with the column on its right. To do that, we need to check if cursor is moving towards right. The same logic applies to moving a column to left.

```js
function makeColumnsSwappable(columnsContainer, elementsToPatch = []) {
  columnsContainer.addEventListener('pointerdown', e => {
    // save first cursor x position on 'pointerdown'
    // e.clientX is the horizontal distance
    // from the left edge of browser window
    // to where the event is triggered
    let lastCursorX = e.clientX
    let columnElements = [...columnsContainer.children]
    const firstTarget = e.target
    let firstTargetIndex = columnElements.indexOf(firstTarget)

    function preventDefault(e) {
      e.preventDefault()
    }

    document.addEventListener('selectstart', preventDefault)

    if (firstTargetIndex === -1)
      return

    function handleMove(e) {
      // save new cursor x position on 'pointermove'
      const newCursorX = e.clientX
      const secondTarget = e.target
      const secondTargetIndex = columnElements.indexOf(secondTarget)

      if (secondTargetIndex === -1)
        return

      if (firstTarget === secondTarget)
        return

      // compare `newCursorX` and `lastCursorX` to check 
      // which direction the cursor is moving towards
      const isMoveToLeft = newCursorX < lastCursorX
      const isMoveToRight = newCursorX > lastCursorX
      // update `lastCursorX` for next comparison 
      lastCursorX = newCursorX

      swapColumns(columnsContainer, firstTargetIndex, secondTargetIndex)

      elementsToPatch.forEach((columnsContainer) => {
        swapColumns(columnsContainer, firstTargetIndex, secondTargetIndex)
      })

      columnElements = [...columnsContainer.children]
      firstTargetIndex = columnElements.indexOf(firstTarget)
    }

    function swapColumns(container, firstTargetIndex, secondTargetIndex) {
      const columns = container.children
      const firstTarget = columns[firstTargetIndex]
      const secondTarget = columns[secondTargetIndex]
      const isMoveToLeft = firstTargetIndex > secondTargetIndex
      const isMoveToRight = firstTargetIndex < secondTargetIndex

      if (isMoveToLeft) {
        secondTarget.insertAdjacentElement('beforebegin', firstTarget)
      } else if (isMoveToRight) {
        secondTarget.insertAdjacentElement('afterend', firstTarget)
      }
    }

    columnsContainer.addEventListener('pointermove', handleMove)

    document.addEventListener('pointerup', () => {
      columnsContainer.removeEventListener('pointermove', handleMove)
      document.removeEventListener('selectstart', preventDefault)
    }, { once: true })
  })
}
```

Update logic with new `isMoveToLeft` and `isMoveToRight`
```js
function handleMove(e) {
  //...
  // pass `isMoveToLeft` and `isMoveToRight`
  swapColumns(columnsContainer, firstTargetIndex, secondTargetIndex, isMoveToLeft, isMoveToRight)

  elementsToPatch.forEach((columnsContainer) => {
    // pass `isMoveToLeft` and `isMoveToRight`
    swapColumns(columnsContainer, firstTargetIndex, secondTargetIndex, isMoveToLeft, isMoveToRight)
  })
  // ...
}

// update to take two more parameter 
// `isMoveToLeft` and `isMoveToRight`
function swapColumns(container, firstTargetIndex, secondTargetIndex, isMoveToLeft, isMoveToRight) {
  const columns = container.children
  const firstTarget = columns[firstTargetIndex]
  const secondTarget = columns[secondTargetIndex]
  // no longer using `firstTargetIndex` and `secondTargetIndex`
  // to calculate `isMoveToLeft` and `isMoveToRight` from here

  if (isMoveToLeft) {
    secondTarget.insertAdjacentElement('beforebegin', firstTarget)
  } else if (isMoveToRight) {
    secondTarget.insertAdjacentElement('afterend', firstTarget)
  }
}
```

Great! By making the logic correct, the bug is fixed! 
![move "website" column is working fine now](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/g4ws9mzcginyx0y93hnf.gif)


One last thing to do is to refactor our code a little bit. function `swapColumns` now takes 5 parameters in order, if we accidently pass the arguments in a wrong order, the function won't work. The parameters are all required, so what can we do? Well, we can use object to make those parameters as a group, so the order won't matter.

```js
// `swapColumns` now only have two parameters
// the second one is an object
function swapColumns(container, {
  firstTargetIndex,
  secondTargetIndex,
  isMoveToLeft,
  isMoveToRight
}) { //...
```

Group the arguments to an object
```js
function handleMove(e) {
  //...
  swapColumns(columnsContainer, {
    // as long as we provide these in this object
    // the order of them doesn't matter at all
    firstTargetIndex,
    secondTargetIndex,
    isMoveToLeft,
    isMoveToRight
  })

  elementsToPatch.forEach((columnsContainer) => {
    swapColumns(columnsContainer, {
      // the order is different than above
      isMoveToLeft,
      isMoveToRight,
      secondTargetIndex,
      firstTargetIndex
    })
  })
  // ...
}
```

Since the two objects we pass to `swapColumns` have the exact same properties, We can also group it like this.
```js
  const swapColumnInfo = {
    firstTargetIndex,
    secondTargetIndex,
    isMoveToLeft,
    isMoveToRight
  }

  swapColumns(columnsContainer, swapColumnInfo)

  elementsToPatch.forEach((columnsContainer) => {
    swapColumns(columnsContainer, swapColumnInfo)
  })
```
The code should still work. Note that grouping all `swapColumns` parameters to one object is also possible. It will require slightly more change. You can try it out as an exercise. Just to be clear, this refactor is not necessary as we probably won't use `swapColumns` elsewhere. I merely want to show that how to modify parameters to make a function easier to use.