table: make the columns swappable
Tags: webdev, javascript, beginners, tutorial
Published: 2022-11-26 12:23:31

In the previous tutorial, we make a table that can take any kind of data and display them. Displaying information is the most important thing. But the order of the columns is fixed. It won't be a problem until our audiences grow and their opinions conflict where we can't just change the order manually.
 
In this tutorial, we will use `addEventListener` to make the columns swappable. So people can move the columns to any order they want. If you are not familiar with `addEventListener`, you can checkout out my verbose [article](https://dev.to/gohomewho/addeventlistener-3o6k) on the topic. 

Here is the [code](https://github.com/ga676005/table/tree/05759f676d33389037ca3123f7c3cb0940810546) from the previous tutorial.


To make the columns swappable, we need to know which two targets to swap. It's not complex if you think about it. Every time we move a column, we are swapping its position with either its closest left or right column. The first target will be the column element we want to move, and the second target will be either its left or right element. With this approach, we could move columns around.

To get the first target, we can `addEventListener` to each column elements. we can also use the event delegation and `addEventListener` to their parent. We are going to use event delegation.

We created column elements inside `createTableHead` function, we can `addEventListener` to their parent `tr`. We choose `'pointerdown'` over `'click'` event so our code will work well with both touch and mouse devices. You can learn more about pointer  events on [JavaScript.info](https://javascript.info/pointer-events).
```js
function createTableHead(columns) {
  const thead = document.createElement('thead')
  const tr = document.createElement('tr')

  columns.forEach(columnName => {
    const th = document.createElement('th')
    th.textContent = columnName
    tr.appendChild(th)
  });

  // `addEventListener` to column elements' parent 
  tr.addEventListener('pointerdown', e => {
    const firstTarget = e.target
    console.log(firstTarget)
  })

  thead.appendChild(tr)
  return thead
}
```

Because of event propagation, `e.target` can be any element inside `tr`. It's not quite obvious because currently we don't have any style and other elements columns, so clicking on a column always gives us a column element. Since we want to move column elements, we need to make sure we get the correct column elements but not their descendants. We can check if `e.target` is in the `tr.children`. 
```js
// ...

  tr.addEventListener('pointerdown', e => {
    // `tr.children` gives us a HTMLCollection not an array
    // but we can convert it to an array like this `[...tr.children]`
    // so we can use `.indexOf()`
    const columnElements = [...tr.children]
    const firstTarget = e.target
    // `.indexOf()` returns `-1` if it can't find the item
    const firstTargetIndex = columnElements.indexOf(firstTarget)
    
    // if `e.target` is not a column element
    // we want to early return and do nothing
    if (firstTargetIndex === -1)
      return

  })

// ...
```

We have the first target. Now we want to get the second target when moving. Add a `'pointermove'` event listener to `tr` and get the second target. It's similar as the steps above.
```js
  tr.addEventListener('pointerdown', e => {
    const columnElements = [...tr.children]
    const firstTarget = e.target
    const firstTargetIndex = columnElements.indexOf(firstTarget)

    if (firstTargetIndex === -1)
      return

    // add a pointermove event listener here 
    tr.addEventListener('pointermove', e2 => {
      const secondTarget = e2.target
      const secondTargetIndex = columnElements.indexOf(secondTarget)

      if (secondTargetIndex === -1)
        return
    })
  })
```

After we have both first target and second target, We can start writing the logic to move them. We can use their indexes as a clue to indicate we are moving a column to left or right.
```js
// this is inside tr.addEventListener('pointerdown', e => {
// ...

    tr.addEventListener('pointermove', e2 => {
      const secondTarget = e2.target
      const secondTargetIndex = columnElements.indexOf(secondTarget)

      if (secondTargetIndex === -1)
        return

      // if `firstTarget` and `secondTarget` are the same element
      // we want to early return and do nothing
      if (firstTarget === secondTarget)
        return
      
      // if firstTargetIndex > secondTargetIndex
      // it means the second target is on the left side of first target
      // it also means that we want to move the first target to left
      const isMoveToLeft = firstTargetIndex > secondTargetIndex
      // same logic apply to moving to right
      const isMoveToRight = firstTargetIndex < secondTargetIndex

      if (isMoveToLeft) {
        // insert `firstTarget` before `secondTarget`
        // which moves `firstTarget` to the left side of `secondTarget`
        secondTarget.insertAdjacentElement('beforebegin', firstTarget)
      } else if (isMoveToRight) {
        // insert `firstTarget` after `secondTarget`
        // which moves `firstTarget` to the right side of `secondTarget`
        secondTarget.insertAdjacentElement('afterend', firstTarget)
      }
//...
```

We can move columns now, but it never stops. We need to stop it at some point. We can do that by adding a `'pointerup'` event listener to `tr` and remove the `'pointermove'`.
```js
  tr.addEventListener('pointerdown', e => {
    //...
    tr.addEventListener('pointermove', e2 => {
      //...
    })

    // We want to stop the 'pointermove' when 'pointerup'
    tr.addEventListener('pointerup', e3 => {

    })
  })
```

The callback function inside `'pointermove'` is inline. We have to extract the function out in order to reference it at `removeEventListener`.
```js
  tr.addEventListener('pointerdown', e => {
    //...

    // extract the inline function from 'pointermove' to here
    function handleMove(e) {
      const secondTarget = e.target
      const secondTargetIndex = columnElements.indexOf(secondTarget)

      if (secondTargetIndex === -1)
        return

      if (firstTarget === secondTarget)
        return

      const isMoveToLeft = firstTargetIndex > secondTargetIndex
      const isMoveToRight = firstTargetIndex < secondTargetIndex

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

    // use the extracted function `handleMove` on 'pointermove'
    tr.addEventListener('pointermove', handleMove)

    // stop moving on 'pointerup'
    tr.addEventListener('pointerup', () => {
      // remove the 'pointermove' event listener on `tr`
      // which references the function `handleMove`
      tr.removeEventListener('pointermove', handleMove)
    })
  })
```

We add other event listeners `'pointermove'` and `'pointerup'` to `tr` on `'pointerdown'`. We clean up `'pointermove'` on `'pointerup'`, but we didn't clean up `'pointerup'`. This means that every time we `'pointerdown'`, we add an extra `'pointerup'` event listener. We can make `'pointerup'` only fire once.
```js
//...
    tr.addEventListener('pointerup', () => {
      tr.removeEventListener('pointermove', handleMove)
      // add configuration { once: true }
    }, { once: true })
//...
```
You can add a `console.log('hi')` to `'pointerup'` and click on a column many times to see what's the difference without `{ once: true }`.

Now we can start moving and stop moving. If you've followed the tutorial, you might notice that the moving isn't quite right. You are correct! And we are going to fix it.

The problem is that we didn't update some variables after a move. 
```js
  tr.addEventListener('pointerdown', e => {
    // `tr.children` is a HTMLCollection, it can automatically update
    // but we convert it to an array, it becomes a fix order
    // we need to update this array after a move
    // to get the correct index of elements
    const columnElements = [...tr.children]
    // the `firstTarget` is always the 'pointerdown' one
    const firstTarget = e.target
    // `firstTargetIndex` will change after a move
    // so we should update this
    const firstTargetIndex = columnElements.indexOf(firstTarget)

    if (firstTargetIndex === -1)
      return

    function handleMove(e) {
      // secondTarget will change
      // but we always get the latest secondTarget on `'pointermove'`
      // so this won't be a problem
      const secondTarget = e.target
      // getting `secondTargetIndex` has a problem
      // because we don't update `columnElements` after a move
      // so the index would be incorrect
      const secondTargetIndex = columnElements.indexOf(secondTarget)

      if (secondTargetIndex === -1)
        return

      if (firstTarget === secondTarget)
        return

      // indexes become incorrect
      // so `isMoveToLeft` and `isMoveToRight ` become incorrect
      const isMoveToLeft = firstTargetIndex > secondTargetIndex
      const isMoveToRight = firstTargetIndex < secondTargetIndex

      // These also become incorrect
      if (isMoveToLeft) {
        secondTarget.insertAdjacentElement('beforebegin', firstTarget)
      } else if (isMoveToRight) {
        secondTarget.insertAdjacentElement('afterend', firstTarget)
      }

      // so after a move
      // we need to update columnElements and firstTargetIndex 
    }

    tr.addEventListener('pointermove', handleMove)

    tr.addEventListener('pointerup', () => {
      tr.removeEventListener('pointermove', handleMove)
    }, { once: true })
  })
```

Fix the problem.
```js
  tr.addEventListener('pointerdown', e => {
    // change this to let
    let columnElements = [...tr.children]
    const firstTarget = e.target
    // change this to let
    let firstTargetIndex = columnElements.indexOf(firstTarget)

    if (firstTargetIndex === -1)
      return

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

      if (secondTargetIndex === -1)
        return

      if (firstTarget === secondTarget)
        return

      const isMoveToLeft = firstTargetIndex > secondTargetIndex
      const isMoveToRight = firstTargetIndex < secondTargetIndex

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

      // update `columnElements` and `firstTargetIndex` after a move
      columnElements = [...tr.children]
      firstTargetIndex = columnElements.indexOf(firstTarget)
    }

    tr.addEventListener('pointermove', handleMove)

    tr.addEventListener('pointerup', () => {
      tr.removeEventListener('pointermove', handleMove)
    }, { once: true })
  })
```

We should be able to move columns like this.
![move table header columns](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jhtdn1yofztter63k3c4.gif)

Great! Now we can move the columns in the table header, we should also apply that change to the table body, otherwise columns won't match the data!

How do we do that? If you think about it, the structure of the table header and a table row is very similar. They both have a container that wraps the column elements. So the logic to move the elements might be the same. Let's try it out. First, extract the logic of moving columns to a function. 
```js
    function handleMove(e) {
      const secondTarget = e.target
      const secondTargetIndex = columnElements.indexOf(secondTarget)

      if (secondTargetIndex === -1)
        return

      if (firstTarget === secondTarget)
        return

      // use the function
      swapColumns(tr, firstTargetIndex, secondTargetIndex)
      
      columnElements = [...tr.children]
      firstTargetIndex = columnElements.indexOf(firstTarget)
    }

    // make a function that can swap two columns
    function swapColumns(container, firstTargetIndex, secondTargetIndex) {
      // get column elements of the container
      const columns = container.children

      // get `firstTarget` and `secondTarget` with the indexes
      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)
      }
    }
```
Note that we don't pass `firstTarget` and `secondTarget` directly to `swapColumns`. First, we can get them through `container, firstTargetIndex, secondTargetIndex`. Second, `container, firstTargetIndex, secondTargetIndex` is the common point between the table header and a table row.

Next, we can use the function `swapColumns` to update the table body rows. 
```js
function handleMove(e) {
  const secondTarget = e.target
  const secondTargetIndex = columnElements.indexOf(secondTarget)

  if (secondTargetIndex === -1)
    return

  if (firstTarget === secondTarget)
    return

  swapColumns(tr, firstTargetIndex, secondTargetIndex)

  // we don't have access to the rows 
  // so we get them here
  const rows = [...table.querySelectorAll('tbody > tr')]

  // update the rows
  rows.forEach((columnsContainer) => {
    swapColumns(columnsContainer, firstTargetIndex, secondTargetIndex)
  })

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

Move table columns altogether.
![move columns altogether](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pp3qrum4sde2zrqnd2h9.gif) 

When we are moving columns, we don't intent to select text. I found that selection quite annoying, so let's disable it.
```js
tr.addEventListener('pointerdown', e => {
  let columnElements = [...tr.children]
  const firstTarget = e.target
  let firstTargetIndex = columnElements.indexOf(firstTarget)

  // create a function so we can reference it
  function preventDefault(e) {
    e.preventDefault()
  }
  
  // disable text selection
  document.addEventListener('selectstart', preventDefault)

  if (firstTargetIndex === -1)
    return

  function handleMove(e) { 
    //...
  }

  function swapColumns(container, firstTargetIndex, secondTargetIndex) { 
    // ...
  }

  tr.addEventListener('pointermove', handleMove)

  tr.addEventListener('pointerup', () => {
    tr.removeEventListener('pointermove', handleMove)

    // remove the text selection disable
    // so we can still select text and copy the data 
    document.removeEventListener('selectstart', preventDefault)
  }, { once: true })
})
```


We can still select text when we are not moving columns.
![disable text selection](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/igj3lnv35qegotpo8cii.gif)

We also want to change the target we add `'pointerup'` from `tr` to `document`, so we don't have to `'pointerup'` in the `tr`.
```js
//...
    // change `tr.addEventListener` to `document.addEventListener`
    document.addEventListener('pointerup', () => {
      tr.removeEventListener('pointermove', handleMove)
      document.removeEventListener('selectstart', preventDefault)
    }, { once: true })
//...
```

Throughout this tutorial, we add the logic directly to `tr` in the `createTableHead` function. We extract the logic that moves columns to the function `swapColumns`, so we can update any other columns which is the most important part. This means that we can extract the whole logic, so it can work on any container that follows this structure. 

Let's create a function `makeColumnsSwappable` to the bottom of the file and copy everything from `'pointerdown'`. Rseplace all references from `tr` to `columnsContainer`, so it can work on any container.
```js
function makeColumnsSwappable(columnsContainer) {
  // tr -> columnsContainer
  columnsContainer.addEventListener('pointerdown', e => {
    // tr -> columnsContainer
    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

      // tr -> columnsContainer
      swapColumns(columnsContainer, firstTargetIndex, secondTargetIndex)

      const rows = [...table.querySelectorAll('tbody > tr')]
      rows.forEach((columnsContainer) => {
        swapColumns(columnsContainer, firstTargetIndex, secondTargetIndex)
      })

      // tr -> columnsContainer
      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)
      }
    }

    // tr -> columnsContainer
    columnsContainer.addEventListener('pointermove', handleMove)

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

Use the `makeColumnsSwappable` function. It should works as before.
```js
function createTableHead(columns) {
  const thead = document.createElement('thead')
  const tr = document.createElement('tr')

  columns.forEach(columnName => {
    const th = document.createElement('th')
    th.textContent = columnName
    tr.appendChild(th)
  });

  // use it here
  makeColumnsSwappable(tr)

  thead.appendChild(tr)
  return thead
}
```

Since we extract the logic, `makeColumnsSwappable` doesn't have to be called inside `createTableHead`. Maybe we don't want every table to have the moving feature. We can use it as a plugin. Remove `makeColumnsSwappable(tr)` inside `createTableHead` and use it at where we created the table.
```js
// This is where we created the table.
const table = createTable(nameOfDefaultColumns, users, columnFormatter)
document.body.appendChild(table)
// remember that the container of table header columns 
// is structured as thead > tr 
const columnsContainer = table.querySelector('thead').firstElementChild
// only make this table header columns to be swappable
makeColumnsSwappable(columnsContainer)
```

There is one last thing we haven't handled in the `makeColumnsSwappable` function. We used to get the table body rows and update them. Now we can't do that anymore. We can't assume there are other columns to update. We need that information to be passed from outside.
```js
// add a paremeter `elementsToPatch` and defaults to empty array
// so we can call .forEach() directly
function makeColumnsSwappable(columnsContainer, elementsToPatch = []) {
  // ...
    function handleMove(e) {
      // ...
      // change these
        const rows = [...table.querySelectorAll('tbody > tr')]
        rows.forEach((columnsContainer) => {
          swapColumns(columnsContainer, firstTargetIndex, secondTargetIndex)
        })
      
      // to this 
      elementsToPatch.forEach((columnsContainer) => {
        swapColumns(columnsContainer, firstTargetIndex, secondTargetIndex)
      })
      
      // ...
    }
}
```
`makeColumnsSwappable` by default only update the `columnsContainer` that it takes directly, unless `elementsToPatch` is specified.

Lastly, we need to pass `elementsToPatch` to `makeColumnsSwappable` to make the columns in the table body rows swap again. 
```js
const table = createTable(nameOfDefaultColumns, users, columnFormatter)
document.body.appendChild(table)
const columnsContainer = table.querySelector('thead').firstElementChild
// get `elementsToPatch`
const elementsToPatch = table.querySelectorAll('tbody > tr')
// pass `elementsToPatch` 
// if we comment out this line
// the table will remain the same as the previous tutorial
makeColumnsSwappable(columnsContainer, elementsToPatch)
```

Our result
![result gif](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/molxell9vlaiq78mp1vo.gif)

If you have noticed that sometimes two columns swap really fast and flash, that's a flaw in our current logic. I'll give you a clue if you want to fix it. We need to **actually** check the pointer is moving left or right rather than using the elements' indexes.

The code we make in this tutorial.
```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

      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 })
  })
}
```