List views are a classic way to present data. I used to work with them in PHP, back in the 2010s. As my experience accumulated, I got a better sense of what worked, what didn't, what scaled, and what sounded like a good idea and just wasn't in practice.
With the help of jQuery, I'd like to walk you through some of the regular features of a list view that I worked over and over with.
To facilitate this Web Tutorial, I have also created a database of sample records in Oracle
APEX and created an API endpoint to retrieve those records. It's a collection of records from a hypothetical library, with imaginary titles, authors and people. The categories, though, are real - they're from
BISAC.
Here's what a sample of the data looks like in CSV. At least, this was what I imported into Oracle
APEX. In this Web Tutorial, we wont be dealing with the API endpoint creation at all - we'll just call the endpoint and grab the data.
Title,Category,Author,Name,BorrowDate,ReturnBy,ReturnDate,Fine
Help my program just crashed,Computers,RJ Neville,Olive Yao,2025-03-01,2025-04-01,2025-03-22,0
The Saraville Murders,True Crime,Lillian Yang,Faith Wentz,2025-03-01,2025-05-01,2025-04-21,0
The Moon's Reflection in the Padi Fields,Fiction,Rudy Ismail,Charles Wong,2025-03-01,2025-04-01,2025-03-30,0
It's Now or Never!,Young Adult Fiction,Kara Leow,Lin Yiheng,2025-03-01,2025-04-01,2025-04-02,0.1
Dreaded Doberman: Man's Best Friend... or are they?,Pets,Selina Perez,Teo Teng Hwee,2025-03-01,202
...
Here's a sample API endpoint for these records. In this URL, "2026" and "3" are the year and month that the books were borrowed. Do note that we're not doing security for this - it's a purely display feature.
https://oracleapex.com/ords/teochewthunder/borrowing/2026/3
Here's some boilerplate HTML. We want to include jQuery, and the starting object,
lv. In the styles, the outline of divs has been set to
red. This will help us with layout until we're ready to turn it off.
<!DOCTYPE html>
<html>
<head>
<title>List View</title>
<style>
div {outline: 1px solid red;}
</style>
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<script>
const lv =
{
};
</script>
</head>
<body>
</body>
</html>
In the
body tag, we have a
div tag with the id
listContent. Within it, here are four different divs with the ids
listFilters,
listHeader,
listRows and listPaging. The first and last div tag have a
hr tag within. You may not think much about it now, but that horizontal rule will be a Godsend later.
<body>
<div id="listContent">
<div id="listFilters">
<hr />
</div>
<div id="listHeader">
</div>
<div id="listRows">
</div>
<div id="listPaging">
<hr />
</div>
</div>
</body>
listContent is a thousand pixels wide, with the
height property set to
auto. We center it using the
margin property, and set a font. The other divs within will just inherit their parent's width.
<style>
div {outline: 1px solid red;}
#listContent
{
width: 1000px;
height: auto;
margin: 0 auto 0 auto;
font-size: 12px;
font-family: verdana;
}
#listHeader
{
width: 100%
}
#listRows
{
width: 100%
}
#listPaging
{
width: 100%;
}
</style>
Now for every column we'll have, we add a div...
<div id="listHeader">
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
</div>
...and insert a button with that column's name in it. Note that there is one more button at the beginning - that is just a column header for serialization of records. Note that each of these buttons, save the first one, has an unique id.
<div id="listHeader">
<div><button>No</button></div>
<div><button id="btnSortTitle">Title</button></div>
<div><button id="btnSortAuthor">Author</button></div>
<div><button id="btnSortCategory">Category</button></div>
<div><button id="btnSortName">Name</button></div>
<div><button id="btnSortBorrowdate">Borrowed</button></div>
<div><button id="btnSortReturnBy">Return By</button></div>
<div><button id="btnSortReturndate">Returned</button></div>
<div><button id="btnSortFine">Fine</button></div>
</div>
Now you see a bunch of buttons. These are supposed to be clickable headers.
So let's style them accordingly. Each of these buttons is styled using the
sortButton CSS class. Only the first button uses
noButton.
<div id="listHeader">
<div><button class="noButton">No</button></div>
<div><button id="btnSortTitle" class="sortButton">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">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>
Here's the styling. Visually,
sortButton and
noButton share the same visuals. No background or border - it just looks like
black text wth a fixed height.
#listPaging
{
width: 100%;
}
.sortButton, .noButton
{
width: auto;
padding: 0px;
height: 1.5em;
font-size: 1em;
background: transparent;
border: none;
color: rgb(0, 0, 0);
}
Now they no longer look like buttons.
Time for a bit of layout magic. Add the CSS class
listViewColumns to the
listHeader div.
<div id="listHeader" class="listViewColumns">
<div><button class="noButton">No</button></div>
<div><button id="btnSortTitle" class="sortButton">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">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>
In the CSS, we set the
display property to
grid, and then specify the widths of 8 columns, with a 5 pixel gap between columns.
#listRows
{
width: 100%
}
.listViewColumns
{
display: grid;
grid-template-columns: 1fr 10fr 6fr 6fr 6fr 5fr 5fr 5fr 3fr;
gap: 5px;
}
#listPaging
{
width: 100%;
}
There, it's in one line now.
Time for JavaScript!
Now we're going to populate those rows. We'll first need to flesh out the lv object. We first have some properties.
-
pageSize: How many records are shown on a page at any time. We'll set it at 50 as a default, but this won't affect anything right now.
-
pageNo: The current page being shown. The default is 1. The value may change as pages are gone through.
-
sort: An array which we'll use to carry out sorting later. Leave it empty for now.
-
search: A string that will be used for searching values. Leave empty for now.
-
records: An array of the current records being worked on. Leave it empty for now.
<script>
const lv =
{
pageSize: 50,
pageNo: 1,
sort: [],
search: "",
records: []
};
</script>
The next two are methods which will be relevant immediately.
getRecords() will populate the
records array. By default, it will fetch records in March 2025, so its two parameters,
year and
month, are set accordingly.
filterRecords() will display the contents of records.
<script>
const lv =
{
pageSize: 50,
pageNo: 1,
sort: [],
search: "",
records: [],
getRecords: function(year = 2025, month = 3)
{
},
filterRecords: function()
{
}
};
</script>
In
getRecords(), we have a jQuery AJAX call.
getRecords: function(year = 2025, month = 3)
{
$.ajax(
{
}
);
},
In it, we specify the URL and that it will be a
GET operation. The URL is basically a template string that incorporates
year and
month.
getRecords: function(year = 2025, month = 3)
{
$.ajax(
{
url: `https://oracleapex.com/ords/teochewthunder/borrowing/${year}/${month}`,
method: "GET",
success: function(data)
{
},
error: function (xhr, status, error)
{
console.log(xhr);
}
}
);
},
On success, we set
records to the
items array of
data, and then run the
filterRecords() method.
getRecords: function(year = 2025, month = 3)
{
$.ajax(
{
url: `https://oracleapex.com/ords/teochewthunder/borrowing/${year}/${month}`,
method: "GET",
success: function(data)
{
lv.records = data.items;
lv.filterRecords();
return;
},
error: function (xhr, status, error)
{
console.log(xhr);
}
}
);
},
We begin the method with a
return statement at the end. At the start, we clear the contents of the
listRows div. Then we declare
filtered. It's an independent copy of
records, using the
structuredClone() function.
filterRecords: function()
{
$("#listRows").html("");
var filtered = structuredClone(this.records);
return;
},
Then we iterate through
filtered using a
forEach() loop.
x is the element and
i is the index. If you're wondering why the array's named "filtered", that's because later on we'll be putting it through filtering.
filterRecords: function()
{
$("#listRows").html("");
var filtered = structuredClone(this.records);
filtered.forEach(
(x, i) =>
{
}
);
return;
},
Here, we create a div element,
row. It will be styled using the CSS class
listViewColumns, which means it will be a single-line grid just like the header! Then we append
row to
listRows.
filterRecords: function()
{
$("#listRows").html("");
var filtered = structuredClone(this.records);
filtered.forEach(
(x, i) =>
{
const row = $("<div></div>");
row.addClass("listViewColumns");
$("#listRows").append(row);
}
);
return;
},
But before that, we declare
col. Repeatedly, we define
col as a div element, use the
text() method to insert that element's data, and append it to
row. We begin with the serial number, which is just 1 added to
i so we don't have the 0 value at the start. The 0 would make sense to programmers. To everyone else, not so much.
filterRecords: function()
{
$("#listRows").html("");
var filtered = structuredClone(this.records);
filtered.forEach(
(x, i) =>
{
const row = $("<div></div>");
row.addClass("listViewColumns");
let col;
col = $("<div></div>");
col.text(i + 1);
row.append(col);
col = $("<div></div>");
col.text(x.title);
row.append(col);
col = $("<div></div>");
col.text(x.author);
row.append(col);
col = $("<div></div>");
col.text(x.category);
row.append(col);
col = $("<div></div>");
col.text(x.name);
row.append(col);
col = $("<div></div>");
col.text(x.borrowdate);
row.append(col);
col = $("<div></div>");
col.text(x.returnby);
row.append(col);
col = $("<div></div>");
col.text(x.returndate);
row.append(col);
col = $("<div></div>");
col.text(x.fine);
row.append(col);
$("#listRows").append(row);
}
);
return;
},
Note that for all numerical and date data, we'll also style
col using the
figure CSS class.
filterRecords: function()
{
$("#listRows").html("");
var filtered = structuredClone(this.records);
filtered.forEach(
(x, i) =>
{
const row = $("<div></div>");
row.addClass("listViewColumns");
let col;
col = $("<div></div>");
col.text(i + 1);
col.addClass("figure");
row.append(col);
col = $("<div></div>");
col.text(x.title);
row.append(col);
col = $("<div></div>");
col.text(x.author);
row.append(col);
col = $("<div></div>");
col.text(x.category);
row.append(col);
col = $("<div></div>");
col.text(x.name);
row.append(col);
col = $("<div></div>");
col.text(x.borrowdate);
col.addClass("figure");
row.append(col);
col = $("<div></div>");
col.text(x.returnby);
col.addClass("figure");
row.append(col);
col = $("<div></div>");
col.text(x.returndate);
col.addClass("figure");
row.append(col);
col = $("<div></div>");
col.text(x.fine);
col.addClass("figure");
row.append(col);
$("#listRows").append(row);
}
);
return;
},
figure is just setting the text to align right.
#listPaging
{
width: 100%;
}
.figure
{
text-align: right;
}
.sortButton, .noButton
{
width: auto;
padding: 0px;
height: 1.5em;
font-size: 1em;
background: transparent;
border: none;
color: rgb(0, 0, 0);
}
After the
lv object, set
getRecords() to run as soon as the document loads.
col.text(x.fine);
col.addClass("figure");
row.append(col);
$("#listRows").append(row);
}
);
return;
}
};
$(document).ready(
function()
{
lv.getRecords();
}
);
</script>
You see here, we have the records! But the dates look weird...
No matter. Let's run all the date values through the
prettifyDate() method.
filterRecords: function()
{
$("#listRows").html("");
var filtered = structuredClone(this.records);
filtered.forEach(
(x, i) =>
{
const row = $("<div></div>");
row.addClass("listViewColumns");
let col;
col = $("<div></div>");
col.text(i + 1);
col.addClass("figure");
row.append(col);
col = $("<div></div>");
col.text(x.title);
row.append(col);
col = $("<div></div>");
col.text(x.author);
row.append(col);
col = $("<div></div>");
col.text(x.category);
row.append(col);
col = $("<div></div>");
col.text(x.name);
row.append(col);
col = $("<div></div>");
col.text(this.prettifyDate(x.borrowdate));
col.addClass("figure");
row.append(col);
col = $("<div></div>");
col.text(this.prettifyDate(x.returnby));
col.addClass("figure");
row.append(col);
col = $("<div></div>");
col.text(this.prettifyDate(x.returndate));
col.addClass("figure");
row.append(col);
col = $("<div></div>");
col.text(x.fine);
col.addClass("figure");
row.append(col);
$("#listRows").append(row);
}
);
return;
},
Create this method. It basically just cleans up the value by using the
split() method to divide up the string by the "T" character, and then returning only the first element of the resultant array.
col.text(x.fine);
col.addClass("figure");
row.append(col);
$("#listRows").append(row);
}
);
return;
},
prettifyDate: function(dt)
{
var arr = dt.split("T");
return arr[0];
}
};
</script>
Now it's looking much better.
It's a small thing, but let's get this out of the way. Create CSS class
shade. Its background color is a very translucent
black.
.listViewColumns
{
display: grid;
grid-template-columns: 1fr 10fr 6fr 6fr 6fr 5fr 5fr 5fr 3fr;
gap: 5px;
}
.shade
{
background-color: rgba(0, 0, 0, 0.1);
}
#listPaging
{
width: 100%;
}
Then in
filterRecords(), when styling row using the
listViewColumns CSS class, we check to see if index
i is even, using the
modulus operator. If so, also style
row using
shade.
const row = $("<div></div>");
row.addClass("listViewColumns");
if (i % 2 == 0) row.addClass("shade");
let col;
col = $("<div></div>");
col.text(i + 1);
col.addClass("figure");
row.append(col);
More presentable and readable now.
That's about it for now. We'll do paging through records next.
Next
Implementing paging.