Sunday, 27 September 2026

Web Tutorial: The List View (Part 3/4)

Welcome back. Let's deal with sorting now, by adding to the sort array.

The default sorting for the list view will be first alphabetically by title, then by most recently borrowed. Each sorting detail is an object of its own. The sequence matters.
const lv =
{
  pageSize: 50,
  pageNo: 1,
  sort: [ {"col": "title", "dir": "asc"}, {"col": "borrowdate", "dir": "desc"} ],
  search: "",
  records: [],


Now, in filterRecords(), we want action only if there's stuff in sort. Therefore, we insert a conditional block here. Then we iterate through sort using a forEach() loop.
filterRecords: function()
{
  $("#listRows").html("");

  var filtered = structuredClone(this.records);

  if (this.sort.length > 0)
  {
    this.sort.forEach(
      (x) =>
      {

      }
    );
  }


  var startIndex = ((this.pageNo - 1) * this.pageSize);


This next If block tests if "title", "author", "name" or "category" are the columns being sorted here - they're text data, so the comparison will be string-based.
filterRecords: function()
{
  $("#listRows").html("");

  var filtered = structuredClone(this.records);

  if (this.sort.length > 0)
  {
    this.sort.forEach(
      (x) =>
      {
        if (["title", "author", "name", "category"].indexOf(x.col) > -1)
        {
                
        }

      }
    );
  }

  var startIndex = ((this.pageNo - 1) * this.pageSize);


We check for dir. If the value is "asc" or "desc", we compare accordingly using the localeCompare() method.
filterRecords: function()
{
  $("#listRows").html("");

  var filtered = structuredClone(this.records);

  if (this.sort.length > 0)
  {
    this.sort.forEach(
      (x) =>
      {
        if (["title", "author", "name", "category"].indexOf(x.col) > -1)
        {
          if (x.dir == "asc") filtered.sort((a, b) => a[x.col].localeCompare(b[x.col]));
          if (x.dir == "desc") filtered.sort((a, b) => b[x.col].localeCompare(a[x.col]));    
              
        }
      }
    );
  }

  var startIndex = ((this.pageNo - 1) * this.pageSize);


For "borrowdate", "returndate" or "returnby", these are date comparisons. We convert the strings to Date objects and compare.
filterRecords: function()
{
  $("#listRows").html("");

  var filtered = structuredClone(this.records);

  if (this.sort.length > 0)
  {
    this.sort.forEach(
      (x) =>
      {
        if (["title", "author", "name", "category"].indexOf(x.col) > -1)
        {
          if (x.dir == "asc") filtered.sort((a, b) => a[x.col].localeCompare(b[x.col]));
          if (x.dir == "desc") filtered.sort((a, b) => b[x.col].localeCompare(a[x.col]));                  
        }

        if (["borrowdate", "returndate", "returnby"].indexOf(x.col) > -1)
        {
          if (x.dir == "asc") filtered.sort((a, b) => new Date(a[x.col]) - new Date(b[x.col]));
          if (x.dir == "desc") filtered.sort((a, b) => new Date(b[x.col]) - new Date(a[x.col]));        
        }

      }
    );
  }

  var startIndex = ((this.pageNo - 1) * this.pageSize);


"fine" is currently the only numerical column sortable, and that one's the most straightforward.
filterRecords: function()
{
  $("#listRows").html("");

  var filtered = structuredClone(this.records);

  if (this.sort.length > 0)
  {
    this.sort.forEach(
      (x) =>
      {
        if (["title", "author", "name", "category"].indexOf(x.col) > -1)
        {
          if (x.dir == "asc") filtered.sort((a, b) => a[x.col].localeCompare(b[x.col]));
          if (x.dir == "desc") filtered.sort((a, b) => b[x.col].localeCompare(a[x.col]));                  
        }

        if (["borrowdate", "returndate", "returnby"].indexOf(x.col) > -1)
        {
          if (x.dir == "asc") filtered.sort((a, b) => new Date(a[x.col]) - new Date(b[x.col]));
          if (x.dir == "desc") filtered.sort((a, b) => new Date(b[x.col]) - new Date(a[x.col]));        
        }

        if (["fine"].indexOf(x.col) > -1)
        {
          if (x.dir == "asc") filtered.sort((a, b) => a[x.col] - b[x.col]);
          if (x.dir == "desc") filtered.sort((a, b) => b[x.col] - a[x.col]);        
        }

      }
    );
  }

  var startIndex = ((this.pageNo - 1) * this.pageSize);


Now take a close look - the records are ordered by the column "Borrowed", most recent dates first. Then take a look at those records borrowed on "2025-03-23". Note that the titles are sorted alphabetically in that group!


Cool! Next, you'll want to properly display what's being sorted, and have a mechanism to change the sorting. In the CSS, allow sortButton to turn red if hovered over as well.
  .sortButton, .noButton
  {
    width: auto;
    padding: 0px;
    height: 1.5em;
    font-size: 1em;
    background: transparent;
    border: none;
    color: rgb(0, 0, 0);
  }

  .pageButton:hover, .sortButton:hover
  {
    color: rgb(255, 0, 0);
  }
</style>


Then we use the after pseudoselector on sortButton. We want it to be a tiny square that has some spacing from the end of the text. By default, there is no text content.
  .pageButton:hover, .sortButton:hover
  {
    color: rgb(255, 0, 0);
  }

  .sortButton:after
  {
    width: 10px;
    height: 10px;
    margin-left: 5px;
    display: inline-block;
    content: "";
  }

</style>


But then we declare asc and desc as new CSS classes with pseudoselectors. In these cases, asc uses a upwards pointing triangle as content and desc uses a triangle pointing down.
  .pageButton:hover, .sortButton:hover
  {
    color: rgb(255, 0, 0);
  }

  .sortButton:after
  {
    width: 10px;
    height: 10px;
    margin-left: 5px;
    display: inline-block;
    content: "";
  }

  .asc:after
  {
    content: "\25B2";
  }

  .desc:after
  {
    content: "\25BC";
  }

</style>


Add asc to btnSortTitle and desc to btnSortBorrowdate.
<div id="listHeader" class="listViewColumns">
  <div><button class="noButton">No</button></div>
  <div><button id="btnSortTitle" class="sortButton asc">Title</button></div>
  <div><button id="btnSortAuthor" class="sortButton">Author</button></div>
  <div><button id="btnSortCategory" class="sortButton">Category</button></div>
  <div><button id="btnSortName" class="sortButton">Name</button></div>
  <div><button id="btnSortBorrowdate" class="sortButton desc">Borrowed</button></div>
  <div><button id="btnSortReturnBy" class="sortButton">Return By</button></div>
  <div><button id="btnSortReturndate" class="sortButton">Returned</button></div>
  <div><button id="btnSortFine" class="sortButton">Fine</button></div>
</div>


Look at the headings. Here's your visual indicator!


Make sure each heading (other than the "No" column) runs the toggleSort() method when clicked, with the actual column name and button id passed in as arguments.
<div id="listHeader" class="listViewColumns">
  <div><button class="noButton">No</button></div>
  <div><button id="btnSortTitle" class="sortButton asc" onclick="lv.toggleSort('title', this.id)">Title</button></div>
  <div><button id="btnSortAuthor" class="sortButton" onclick="lv.toggleSort('author', this.id)">Author</button></div>
  <div><button id="btnSortCategory" class="sortButton" onclick="lv.toggleSort('category', this.id)">Category</button></div>
  <div><button id="btnSortName" class="sortButton" onclick="lv.toggleSort('name', this.id)">Name</button></div>
  <div><button id="btnSortBorrowdate" class="sortButton desc" onclick="lv.toggleSort('borrowdate', this.id)">Borrowed</button></div>
  <div><button id="btnSortReturnBy" class="sortButton" onclick="lv.toggleSort('returnby', this.id)">Return By</button></div>
  <div><button id="btnSortReturndate" class="sortButton" onclick="lv.toggleSort('returndate', this.id)">Returned</button></div>
  <div><button id="btnSortFine" class="sortButton" onclick="lv.toggleSort('fine', this.id)">Fine</button></div>
</div>


Create toggleSort() with a return statement.
setPageSize: function(pageSize)
{
  this.pageSize = pageSize;
  this.pageNo = 1;
  this.filterRecords();
  this.renderPageInfo();

  return;
},
toggleSort: function(col, id)
{
  return;
},      
  
filterRecords: function()
{


We declare currentIndex and set it to -1. Then we clear the column's button of both asc and desc CSS classes, returning it to a visually neutral state.
toggleSort: function(col, id)
{
  var currentIndex = -1;
  $("#" + id)
  .removeClass("asc")
  .removeClass("desc");


  return;
},  


Then we traverse the sort array. If we find a match for the column name, we set currentIndex to the value of i, which is the matching element. This means that the current column being clicked on, is in the sort array.
toggleSort: function(col, id)
{
  var currentIndex = -1;
  $("#" + id)
  .removeClass("asc")
  .removeClass("desc");

  this.sort.forEach(
    (x, i) =>
    {
      if (x.col == col)
      {
        currentIndex = i;
      }
    }
  );


  return;
},  


Now, we have an If block to check if the current column name is represented in the sort array. If currentIndex is still -1 at this point, then it's not.
toggleSort: function(col, id)
{
  var currentIndex = -1;
  $("#" + id)
  .removeClass("asc")
  .removeClass("desc");

  this.sort.forEach(
    (x, i) =>
    {
      if (x.col == col)
      {
        currentIndex = i;
      }
    }
  );

  if (currentIndex > -1)
  {

  }
  else
  {

  }

  
  return;
},  


In that case, we'll push a new object into sort, with the current column's name and a default dir property value of "asc". We'll amend the current button's class accordingly as well.
toggleSort: function(col, id)
{
  var currentIndex = -1;
  $("#" + id)
  .removeClass("asc")
  .removeClass("desc");

  this.sort.forEach(
    (x, i) =>
    {
      if (x.col == col)
      {
        currentIndex = i;
      }
    }
  );

  if (currentIndex > -1)
  {

  }
  else
  {
    this.sort.push({ "col": col, "dir": "asc"});
    $("#" + id).addClass("asc");

  }
  
  return;
},  


Now, if the current column is already in sort, we'll want to change its direction. We first check if the current dir property value is "asc".
toggleSort: function(col, id)
{
  var currentIndex = -1;
  $("#" + id)
  .removeClass("asc")
  .removeClass("desc");

  this.sort.forEach(
    (x, i) =>
    {
      if (x.col == col)
      {
        currentIndex = i;
      }
    }
  );

  if (currentIndex > -1)
  {
    if (this.sort[currentIndex].dir == "asc")
    {

    }
    else
    {

    }

  }
  else
  {
    this.sort.push({ "col": col, "dir": "asc"});
    $("#" + id).addClass("asc");
  }
  
  return;
},  


If so, we change it to "desc" and amend the CSS class accordingly. If it's not "asc", then it can only be "desc". And if it's already "desc", we remove the entire object from the sort array accordingly, using the splice() method with currentIndex passed in. And finally, at the end of all that, we run filterRecords().
toggleSort: function(col, id)
{
  var currentIndex = -1;
  $("#" + id)
  .removeClass("asc")
  .removeClass("desc");

  this.sort.forEach(
    (x, i) =>
    {
      if (x.col == col)
      {
        currentIndex = i;
      }
    }
  );

  if (currentIndex > -1)
  {
    if (this.sort[currentIndex].dir == "asc")
    {
      this.sort[currentIndex].dir = "desc";
      $("#" + id).addClass("desc");

    }
    else
    {
      this.sort.splice(currentIndex, 1);
    }
  }
  else
  {
    this.sort.push({ "col": col, "dir": "asc"});
    $("#" + id).addClass("asc");
  }
  
  this.filterRecords();
  return;
},  


Refresh. Reduce page size to 20 for better clarity. Click on the column "Borrowed". The down-pointing triangle should disappear, and now the dataset is sorted only by "Title".


Click on "Title". Now it sorts descending.


Click on "Category". Now you see it that first sorts by "Title", then it sorts by "Category".


Click on "Title" twice, till the triangle disappears. Now it's just sorted by "Category". Click on "Title" again, so now it's sorted by "Category" first, then "Title". What a difference!


Have fun clicking on the other headers. You can sort by as many columns as you want.

Next

Searching, and getting different years' records.

No comments:

Post a Comment