Lesson 2
Code Page Here - PeoplePage_sort.htmlClient-Side Sorting
Quickbase has returned the records. Now the code page has control of them.
The Important Change in Lesson 2
Lessons 1A and 1B concentrated on retrieving records from Quickbase. Once those records appeared in the HTML table, the immediate objective was complete.
Lesson 2 introduces a different idea: the returned Quickbase records can remain available in JavaScript after the API request finishes.
Once we retain those records, we can begin deciding what the application should do with them.
Retrieval Is Only the Beginning
Sorting is simply the smallest operation we can use to prove that the browser now controls the returned dataset.
1. What Lesson 1B Was Doing
The REST query produced a collection of Quickbase records:
const records =
Array.isArray(parsed?.data)
? parsed.data
: [];That local variable represented the result of this particular REST request.
Lesson 1B could simply render that result:
renderRecords(records);For a read-only demonstration, that was enough.
2. Retain the Quickbase Records
Lesson 2 adds a variable outside loadPeople():
let peopleRecords = [];After the REST request succeeds, the returned records are assigned to it:
peopleRecords = records;
renderRecords(peopleRecords);| Variable | Purpose |
|---|---|
records | Local result returned by the current Quickbase REST request. |
peopleRecords | Records retained by the page for later client-side operations. |
Clarification: Why Not Replace Every Use of records?
This was an important point during development.
The REST request should still create its normal local result:
const records =
Array.isArray(parsed?.data)
? parsed.data
: [];We do not blindly replace every reference to records with peopleRecords.
The handoff occurs only after Quickbase has successfully returned and validated the dataset:
peopleRecords = records;
renderRecords(peopleRecords);That distinction keeps the REST request result and the page's retained client-side state conceptually separate.
3. Make Name an Action
The ordinary table heading becomes a button:
<th>
<button
type="button"
id="sortName"
>
Name
</button>
</th>This does not change anything in Quickbase. It simply gives the user a browser-side control that can operate on the records we already retained.
4. Sort the Retained Records
function sortByName() {
peopleRecords.sort((a, b) => {
const nameA =
getFieldValue(a, FIELD_IDS.name);
const nameB =
getFieldValue(b, FIELD_IDS.name);
const comparison =
nameA.localeCompare(nameB);
return nameSortDirection === "asc"
? comparison
: -comparison;
});
renderRecords(peopleRecords);
}Notice what is conspicuously absent from this function:
fetch()Sorting does not send another request to Quickbase.
The records are already in the browser. JavaScript changes their order and renders them again.
The New Data Flow
5. Remember the Sort Direction
let nameSortDirection = "asc";The page must remember whether the next sort should be ascending or descending.
return nameSortDirection === "asc"
? comparison
: -comparison;localeCompare() supplies the normal alphabetical comparison. Using its result normally sorts ascending; reversing its sign reverses the ordering.
Clarification: The First Click May Look Like Nothing Happened
The REST query already asks Quickbase to return the records sorted by Name ascending:
sortBy: [
{
fieldId: FIELD_IDS.name,
order: "ASC",
},
],Lesson 2 also initializes:
let nameSortDirection = "asc";Therefore the first click can sort an already-ascending dataset into ascending order again.
That does not mean the client-side sort failed.
The important test is that clicking again reverses the order while no additional REST request occurs.
6. Show the Current Sort
if (nameSortDirection === "asc") {
sortButton.textContent = "Name ↑";
nameSortDirection = "desc";
} else {
sortButton.textContent = "Name ↓";
nameSortDirection = "asc";
}After an ascending sort, the heading displays:
Name ↑The variable is then changed to desc because descending is what should happen on the next click.
Clarification: What Does nameSortDirection Mean?
This variable describes the direction that the next sort operation will use.
| Before Click | Operation | Button Shows | Next Direction |
|---|---|---|---|
asc | Sort ascending | Name ↑ | desc |
desc | Sort descending | Name ↓ | asc |
7. Connect the Button to the Client-Side Operation
document.addEventListener(
"DOMContentLoaded",
() => {
document
.getElementById("sortName")
.addEventListener(
"click",
sortByName
);
loadPeople();
}
);When the page loads, two separate responsibilities are established:
loadPeople() retrieves the data once.
sortByName() subsequently operates on the retained data.
8. What Changed from Lesson 1B?
| Stage | Code | Meaning |
|---|---|---|
| Lesson 1B | renderRecords(records) | Quickbase records are retrieved and immediately rendered. |
| Lesson 2 | peopleRecords = records | The returned Quickbase records are retained in browser memory. |
| Lesson 2 | peopleRecords.sort(...) | JavaScript manipulates the already-loaded records. |
| Lesson 2 | renderRecords(peopleRecords) | The changed client-side dataset is rendered again. |
The Bigger Idea
Sorting is not the destination of this lesson.
The important realization is that after Quickbase supplies the records, JavaScript can keep them and perform additional operations upon them.
Lesson 2 demonstrates the first of those operations with the simplest possible example: alphabetical sorting.
Why Are We Only Sorting Name?
We could immediately make Record ID, Age, and Favorite Color sortable too.
That would mostly repeat the same concept with slightly different comparison logic.
The lesson objective is already satisfied once we have proved this:
Load records once from Quickbase, retain them in JavaScript, manipulate the client-side collection, and re-render it without another API request.
Complete Lesson 2 Source
The complete training source below preserves the REST architecture from Lesson 1B and highlights the new Lesson 2 concepts around retained records and client-side sorting.
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0"
/>
<title>People Table - Lesson 2</title>
<style>
body {
margin: 0;
padding: 32px;
background: #f4f6f8;
color: #1f2933;
font-family: Arial, Helvetica, sans-serif;
}
.page {
max-width: 1000px;
margin: 0 auto;
}
h1 {
margin: 0 0 8px;
color: #003366;
}
.description {
margin: 0 0 24px;
color: #5b6775;
}
.status {
margin-bottom: 16px;
padding: 12px 14px;
border: 1px solid #cfd8e3;
border-radius: 6px;
background: #ffffff;
}
.status.error {
border-color: #a61c1c;
color: #7f0000;
background: #fff1f1;
}
.table-container {
overflow-x: auto;
border: 1px solid #d7dee7;
border-radius: 8px;
background: #ffffff;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.06);
}
table {
width: 100%;
border-collapse: collapse;
}
th,
td {
padding: 12px 14px;
border-bottom: 1px solid #e5e9ef;
text-align: left;
}
th {
background: #003366;
color: #ffffff;
}
th button {
border: 0;
background: transparent;
color: #ffffff;
font: inherit;
font-weight: bold;
cursor: pointer;
}
tbody tr:hover {
background: #f3f7fb;
}
.color-value {
display: inline-flex;
align-items: center;
gap: 8px;
}
.color-swatch {
width: 18px;
height: 18px;
border: 1px solid #7f8c99;
border-radius: 4px;
}
</style>
</head>
<body>
<main class="page">
<h1>People</h1>
<h2>
Lesson 2 — Client-Side Sorting
</h2>
<p class="description">
Load Quickbase records once, retain them in JavaScript,
then manipulate the client-side data without another API request.
</p>
<div id="status" class="status">
Loading Quickbase records...
</div>
<div class="table-container">
<table>
<thead>
<tr>
<th>Record ID</th>
<th>
<button
type="button"
id="sortName"
>
Name
</button>
</th>
<th>Age</th>
<th>Favorite Color</th>
</tr>
</thead>
<tbody id="peopleRows"></tbody>
</table>
</div>
</main>
<script>
/*
* ============================================================
* QUICKBASE CONNECTION INFORMATION
* ============================================================
*/
const REALM =
window.location.hostname.includes("quickbase.com")
? window.location.hostname
: "YOUR_REALM.quickbase.com";
const TABLE_DBID = "YOUR_TABLE_DBID";
/*
* Leave blank if the application does not require
* an Application Token.
*/
const APP_TOKEN = "";
/*
* ============================================================
* LESSON 2 — CLIENT-SIDE RECORD STATE
* ============================================================
*
* Lesson 1B retrieved records from Quickbase and rendered them.
*
* Lesson 2 needs those records to remain available AFTER
* the REST request has completed.
*
* peopleRecords becomes the browser's retained copy of the
* Quickbase records.
*
* No second Quickbase query is required when sorting.
*/
let peopleRecords = [];
/*
* ============================================================
* LESSON 2 — SORT STATE
* ============================================================
*
* This variable remembers which direction the NEXT sort
* operation should use.
*/
let nameSortDirection = "asc";
const FIELD_IDS = {
recordId: 3,
name: 6,
age: 7,
favoriteColor: 8,
};
function createCell(value) {
const cell = document.createElement("td");
cell.textContent = value;
return cell;
}
function createColorCell(colorName) {
const cell = document.createElement("td");
const wrapper = document.createElement("span");
wrapper.className = "color-value";
const swatch = document.createElement("span");
swatch.className = "color-swatch";
swatch.style.backgroundColor =
colorName || "transparent";
const label = document.createElement("span");
label.textContent = colorName;
wrapper.appendChild(swatch);
wrapper.appendChild(label);
cell.appendChild(wrapper);
return cell;
}
async function readQuickbaseResponse(response) {
const rawBody = await response.text();
try {
return {
parsed: JSON.parse(rawBody),
rawBody,
};
} catch {
return {
parsed: null,
rawBody,
};
}
}
function getFieldValue(record, fieldId) {
return record[String(fieldId)]?.value ?? "";
}
function renderRecords(records) {
const tableBody =
document.getElementById("peopleRows");
tableBody.replaceChildren();
for (const record of records) {
const recordId =
getFieldValue(
record,
FIELD_IDS.recordId
);
const name =
getFieldValue(
record,
FIELD_IDS.name
);
const age =
getFieldValue(
record,
FIELD_IDS.age
);
const favoriteColor =
getFieldValue(
record,
FIELD_IDS.favoriteColor
);
const row =
document.createElement("tr");
row.appendChild(
createCell(recordId)
);
row.appendChild(
createCell(name)
);
row.appendChild(
createCell(age)
);
row.appendChild(
createColorCell(favoriteColor)
);
tableBody.appendChild(row);
}
}
async function getTemporaryToken() {
const headers = {
"QB-Realm-Hostname": REALM,
};
if (APP_TOKEN.trim()) {
headers["QB-App-Token"] =
APP_TOKEN;
}
const response = await fetch(
`https://api.quickbase.com/v1/auth/temporary/${TABLE_DBID}`,
{
method: "GET",
credentials: "include",
headers,
},
);
const {
parsed,
rawBody,
} = await readQuickbaseResponse(
response
);
if (!response.ok) {
const details =
parsed
? JSON.stringify(parsed)
: rawBody;
throw new Error(
`HTTP ${response.status}: ${details}`
);
}
if (!parsed?.temporaryAuthorization) {
throw new Error(
"Quickbase did not return temporaryAuthorization."
);
}
return parsed.temporaryAuthorization;
}
/*
* ============================================================
* LESSON 2 — SORT THE CLIENT-SIDE RECORDS
* ============================================================
*
* IMPORTANT:
*
* There is NO fetch() inside this function.
*
* Quickbase is not contacted again.
*
* We are operating entirely upon peopleRecords, which contains
* the records that were already returned by Quickbase.
*/
function sortByName() {
peopleRecords.sort((a, b) => {
/*
* Read the Name field from each Quickbase REST record.
*/
const nameA =
getFieldValue(
a,
FIELD_IDS.name
);
const nameB =
getFieldValue(
b,
FIELD_IDS.name
);
/*
* localeCompare() gives us the normal alphabetical
* comparison between the two names.
*/
const comparison =
nameA.localeCompare(nameB);
/*
* Use the comparison normally for ascending order.
*
* Negating the comparison reverses the order.
*/
return nameSortDirection === "asc"
? comparison
: -comparison;
});
const sortButton =
document.getElementById("sortName");
/*
* Update the visible sort indicator and remember
* which direction should happen on the next click.
*/
if (nameSortDirection === "asc") {
sortButton.textContent =
"Name ↑";
nameSortDirection =
"desc";
} else {
sortButton.textContent =
"Name ↓";
nameSortDirection =
"asc";
}
/*
* The array changed in browser memory.
*
* Re-render that same client-side data.
*
* Again: no Quickbase request occurs here.
*/
renderRecords(peopleRecords);
}
async function loadPeople() {
const startTime =
performance.now();
const status =
document.getElementById("status");
try {
const temporaryToken =
await getTemporaryToken();
const query = {
from: TABLE_DBID,
select: [
FIELD_IDS.recordId,
FIELD_IDS.name,
FIELD_IDS.age,
FIELD_IDS.favoriteColor,
],
options: {
top: 200,
skip: 0,
},
sortBy: [
{
fieldId:
FIELD_IDS.name,
order: "ASC",
},
],
};
const response = await fetch(
"https://api.quickbase.com/v1/records/query",
{
method: "POST",
headers: {
"QB-Realm-Hostname":
REALM,
Authorization:
`QB-TEMP-TOKEN ${temporaryToken}`,
"Content-Type":
"application/json",
},
body:
JSON.stringify(query),
},
);
const {
parsed,
rawBody,
} = await readQuickbaseResponse(
response
);
if (!response.ok) {
const details =
parsed
? JSON.stringify(parsed)
: rawBody;
throw new Error(
`HTTP ${response.status}: ${details}`
);
}
const records =
Array.isArray(parsed?.data)
? parsed.data
: [];
if (!Array.isArray(parsed?.data)) {
throw new Error(
"REST response did not include a data array."
);
}
if (records.length === 0) {
renderRecords(records);
status.classList.remove(
"error"
);
status.textContent =
"REST query returned 0 record(s).";
return;
}
/*
* ========================================================
* LESSON 2 — THE IMPORTANT HANDOFF
* ========================================================
*
* records
* = local result from this REST request
*
* peopleRecords
* = records retained by the browser for later actions
*
* This assignment is what gives the page continued
* control over the returned Quickbase records.
*/
peopleRecords = records;
/*
* Render the retained client-side collection.
*/
renderRecords(
peopleRecords
);
const endTime =
performance.now();
const renderTime =
endTime - startTime;
status.classList.remove(
"error"
);
status.textContent =
`REST query returned ${records.length} record(s) in ${renderTime.toFixed(2)} ms.`;
} catch (error) {
console.error(error);
status.classList.add(
"error"
);
status.textContent =
`REST query failed: ${error.message}`;
}
}
/*
* ============================================================
* LESSON 2 — CONNECT THE UI TO THE CLIENT-SIDE OPERATION
* ============================================================
*
* When Name is clicked:
*
* sortByName()
*
* runs against peopleRecords.
*
* loadPeople() is NOT called again.
*/
document.addEventListener(
"DOMContentLoaded",
() => {
document
.getElementById("sortName")
.addEventListener(
"click",
sortByName
);
loadPeople();
}
);
</script>
</body>
</html>Lesson 2 Complete
You should now understand that returning records from Quickbase is not the end of the application's work.
The REST response can be retained as client-side data. JavaScript can then manipulate that data and render the changed result without contacting Quickbase again.
We retrieved the records in Lesson 1B. In Lesson 2, we started controlling them.