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
...
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>
<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>
<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>
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>
<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>
<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>
<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);
}
{
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>
<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%;
}
{
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>
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>
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(
{
}
);
},
{
$.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);
}
}
);
},
{
$.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);
}
}
);
},
{
$.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;
},
{
$("#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;
},
{
$("#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;
},
{
$("#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;
},
{
$("#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;
},
{
$("#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);
}
{
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>
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;
},
{
$("#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>
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%;
}
{
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);
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.





































