All four sorting arrows point to the same direction if only one arrow is clicked

My HTML table now displays four (4) arrow table sorter (arrow icons). The problem now is that all the four icons point in the same direction once I clicked one of them. I get you some examples:

  1. Sort in ascending order
    I want to sort the table by ID in ascending order, so I clicked the “#” on the header.

I wanted the leftmost chevron pointing downwards (v) and the other three chevrons pointing up (^). As the screenshot above, you see that all the chevrons are pointing downwards when the table is sorted by “#.”

  1. Sort in descending order
    Now I have sorted the table in descending order by clicking the “#” once again.

I wanted the leftmost chevron pointing upwards and all the other ones pointing downwards, but all of them are pointing upwards as per screenshot above.

I need some hints to fix this problem again, not the solution code. Any tutorial, tips, insights much appreciated.
My current codes are below for your reference:

HTML

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <!--JQuery-->
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
    <!--bootstrap v5.1-->
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
    <!--Vue.js-->
    <script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
    <!--Axios-->
    <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
    <!--Custom CSS-->
    <link rel="stylesheet" href="style.css"> 
    <!--fontawesome icons-->
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.13.1/css/all.min.css" />
    <title>CONFIDENCIAL - users</title>
</head>
<body>
    <div class="mt-4 container" id="app">
        <table class="table table-success table-striped">
        <thead>
            <tr>
              <th v-on:click=" collapsed = !collapsed" class="collapse-link" style="width: 4%;">
                # <span style="float:right;"><i :class="[collapsed ? 'fa-chevron-up' : 'fa-chevron-down', 'fa']"></i></span>
                
              </th>
              <th v-on:click=" collapsed = !collapsed" class="collapse-link" style="width: 8%;">
                Name
                <span style="float:right;"><i :class="[collapsed ? 'fa-chevron-up' : 'fa-chevron-down', 'fa']"></i></span>
              </th>
              <th v-on:click=" collapsed = !collapsed" class="collapse-link">
                Title
                <span style="float:right;"><i :class="[collapsed ? 'fa-chevron-up' : 'fa-chevron-down', 'fa']"></i></span>
              </th>
              <th v-on:click=" collapsed = !collapsed" class="collapse-link">
                Body
                <span style="float:right;"><i :class="[collapsed ? 'fa-chevron-up' : 'fa-chevron-down', 'fa']"></i></span>
              </th>
            </tr>
        </thead>
          <tbody>
            <tr v-for="post, index in posts" :key = "post.userId">
              <td scope="row">{{ index + 1 }}</td>
              <td>{{ post.id }}</td>
              <td>{{ post.title }}</td>
              <td>{{ post.body }}</td>
            </tr>
          </tbody>
        </table>
    </div>
    <script>
        new Vue({
            el:"#app",
            data: {
                posts: [],
                loading: false,
                collapsed: true
            },
            created(){
                axios.get('https://jsonplaceholder.typicode.com/posts')
                .then(response => 
                    this.posts = response.data
                )
                .catch(error => 
                    console.log(error)
                )
            }
        }) 
    </script>
    <!--custom js-->
    <script src="main.js"></script>
</body>
</html>

CSS

tr:nth-child(even){
    background-color: var(--bs-table-bg);
}

tr:nth-child(odd){
    background-color: var(--bs-table-active-bg);
}

.is-collapsed{
    display: none;
}

JS

//I know this code can be more simple, but I didn't have time to find a way to do that.
javascript: (function () {
  const isEmptyOrNaN = (obj) => obj === "" || isNaN(obj);
  
  const getCellValueInColumn = (tr, columnIdx) =>
    tr.children[columnIdx].innerText || tr.children[columnIdx].textContent;
  
  const compareCellValues = (cellValue1, cellValue2) => {
    return isEmptyOrNaN(cellValue1) || isEmptyOrNaN(cellValue2)
      ? cellValue1.toString().localeCompare(cellValue2)
      : cellValue1 - cellValue2;
  };
  
  const compareFnFactory = (columnIdx, ascending) => (firstEl, secondEl) => {
    const cellValue1 = getCellValueInColumn(firstEl, columnIdx);
    const cellValue2 = getCellValueInColumn(secondEl, columnIdx);
    return ascending
      ? compareCellValues(cellValue1, cellValue2)
      : compareCellValues(cellValue2, cellValue1);
  };
  
  document.querySelectorAll("th").forEach((th) =>
    th.addEventListener("click", () => {
      const table = th.closest("table");
      const tbody = table.querySelector("tbody");
      const columnIdx = Array.from(th.parentNode.children).indexOf(th);
      const compareFn = compareFnFactory(columnIdx, (this.ascending = !this.ascending));
      Array.from(tbody.querySelectorAll("tr"))
        .sort(compareFn)
        .forEach((tr) => tbody.appendChild(tr));
    })
  );
    })();

Hello.

You are using one collapsed variable for all columns, so when you change it by clicking one column, it will change for all columns. In addition to the sort direction, you have to store the name of the column your table is sorted by and update the arrows accordingly.

Hope, it will help.

Thank you for the response! I understood that I need to store the name of the column to give it a sort. I am stuck as to how I store that name. Do you mean something like this one? javascript - Vue.js - on click collapse the nearest div - Stack Overflow

Hi!

You just need to use two variables in Vue data, the first for the column name and the second for the sort direction.

One of the solutions:

  1. When the user clicks a heading and a table already sorted by that column, change direction. Otherwise, change the value of column name variable.
  2. In a sorted column, display an arrow with the correct direction, in other columns, hide the arrows or display a double-sided arrow.

And about Vue-way:

  1. Use “methods” in the Vue config object for functions: Introduction — Vue.js.
  2. With Vue, you don’t have to manually sort trs. You can simply sort the original posts array and Vue will automatically re-render the HTML. See Array.prototype.sort() - JavaScript | MDN.

And there is a problem in your current code. In HTML, you use post.userId as the key for tr. But for v-for to work correctly, the key must be unique. Therefore, it is more correct to use post.id as a key. Always open developer console in your browser when working with JavaScript so you can see error messages.

PS And sorting by index is not needed. It will always be ascendant.

Hi there! Thank you so much for the clarification. I have tried the method and I think I am getting closer to achieving what I want.

With regards to displaying an arrow with the correct direction, I think I can try something like this

<th style="width: 4%;">
                # 
                <span style="float:right;"><i :class="[sortByFirstNameAsc ? 'fa-chevron-up' : 'fa-chevron-down', 'fa']"></i></span>
              </th>

In order to make this work, I must initialise sortByFirstNameAsc. Right now I only coded the sorting function with pure JS, not the Vue-Way at all.

If I understood your instruction correctly, I may be able to connect the sortByFirstNameAsc in the Vue-Way sorting. Am I right?

Hi!

Yes, if you introduce a Vue method like sort(columnName) you can call it on click with the column name as a parameter. Inside the method, sort the original data.posts array and save the new sort column and direction into data (by this keyword in method) object.

And instead of one variable per column like sortByFirstNameAsc, I recommend using just two variables sortColumn and sortDirection, so you only need two variables for any number of columns. And the condition for :class will look like sortColumn === 'firstName' && sortDirection === 'asc'.

Hello @9509706156 thank you for the clarification again!

I found a solution that applies the method variable in Vue.js, but I still need to fix one thing: the problem is that the sorting does not work on the specific # column, but it does work on all the other columns. I show you what I mean.

I have the leftmost # column which does not sort the data. The arrow buttons work, once I clicked the down arrow it displays the down arrow. But the numbers on the table does not get sorted in descending order.

All the other columns sort the data just fine, I just need to fix the # one.

I leave my current code now for your reference, could you kindly point out where I did wrong?

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <!--JQuery-->
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
    <!--bootstrap v5.1-->
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
    <!--Vue.js-->
    <script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
    <!--Axios-->
    <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
    <!--Custom CSS-->
    <link rel="stylesheet" href="style.css"> 
    <!--fontawesome icons-->
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.13.1/css/all.min.css" />
    <script src="https://cdn.jsdelivr.net/npm/vue-column-sortable@0.0.1/dist/vue-column-sortable.js"></script>
    <title>CONFIDENCIAL - users</title>
</head>
<body>
    <div class="mt-4 container" id="app">
      <table class="table table-success table-striped">
        <thead>
            <tr>
              <th v-column-sortable:userid style="width: 4%;">
                # 
              </th>
              <th v-column-sortable:id style="width: 8%;">
                Name
              </th>
              <th v-column-sortable:title>
                Title
              </th>
              <th v-column-sortable:body>
                Body
              </th>
            </tr>
        </thead>
          <tbody>
            <tr v-for="(post, index) in posts" :key = "post.userid">
              <td scope="row">{{ index + 1 }}</td>
              <td>{{ post.id }}</td>
              <td>{{ post.title }}</td>
              <td>{{ post.body }}</td>
            </tr>
          </tbody>
        </table>
    </div>
    <script>
        new Vue({
            el:"#app",
            data: {
                posts: [],
            },
            directives: {columnSortable},
            methods: {
             orderBy(sortFn){
               this.posts.sort(sortFn);
             }
            },
            created(){
                axios.get('https://jsonplaceholder.typicode.com/posts')
                .then(response => 
                    this.posts = response.data
             
                )
                .catch(error => 
                    console.log(error)
                )
            }
        }) 
    </script>
    <!--custom js-->
    <script src="main.js"></script>
</body>
</html>
<!--once done with replicating, display arrows :-) -->

Hello.

As I pointed out above, sorting by index is meaningless because the array index is always ascending (this is row number). It makes sense to sort only by object fields (id, title, etc.). Also, you’re trying to sort by userid, but you’re printing {{ index + 1 }} in the first column.

And see what I wrote above about :key = "post.userid" in your code.

Alright, thank you so much for the feedback! I want to keep the index + 1 as my json-placeholder looks like this

The userId property is not ascending here by default (e.g. “userId” stays “1” for multiple data rows). In order to make this property increment, I put index. But I totally understand sorting by index does not help with the other task, which is to sort the table data. I feel overwhelmed with this personal project again, will come back once I feel more relaxed.

I’m gonna mark this as a solution as my original question was how to give a unique sorting direction to every single arrow icon. I hope this will help anyone like me!

This method does display the double direction arrows which I wanted to achieve. I still have a sorting problem which it only sorts the table in descending order, but I will create another post for that specific question.