Skip to content

Commit 4d03577

Browse files
committed
Add server-side table sorting
1 parent 6489e85 commit 4d03577

9 files changed

Lines changed: 117 additions & 10 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# CHANGELOG.md
22

3+
## Unreleased
4+
5+
- The table component supports `server_sort_column`. Columns named by this property reload the current page with a `sort_<column>=ASCENDING|DESCENDING` URL parameter when their headers are clicked, enabling database-side sorting before pagination.
6+
37
## v0.46.1
48

59
- Fixed a regression introduced in v0.46 that could replace a variable with `NULL` while building a value that also used database expressions and `sqlpage.*` functions. For example, this API request could lose `john.doe` and produce a URL ending at `https://api.example.com/`:

examples/official-site/sqlpage/migrations/01_documentation.sql

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -949,7 +949,8 @@ Advanced users can apply custom styles to table columns using a CSS class with t
949949

950950
INSERT INTO parameter(component, name, description, type, top_level, optional) SELECT 'table', * FROM (VALUES
951951
-- top level
952-
('sort', 'Make the columns clickable to let the user sort by the value contained in the column.', 'BOOLEAN', TRUE, TRUE),
952+
('sort', 'Make the columns clickable to let the user sort by the value contained in the column in the browser.', 'BOOLEAN', TRUE, TRUE),
953+
('server_sort_column', 'A column name, or JSON array of column names, whose headers reload the current page with a sort_<column>=ASCENDING or sort_<column>=DESCENDING URL parameter. Use this for database-side sorting, particularly with pagination. Your SQL query must read the parameter and apply the corresponding ORDER BY safely.', 'JSON', TRUE, TRUE),
953954
('search', 'Add a search bar at the top of the table, letting users easily filter table rows by value.', 'BOOLEAN', TRUE, TRUE),
954955
('initial_search_value', 'Pre-fills the search bar used to filter the table. The user will still be able to edit the value to display table rows that will initially be filtered out.', 'TEXT', TRUE, TRUE),
955956
('search_placeholder', 'Customizes the placeholder text shown in the search input field. Replaces the default "Search..." with text that better describes what users should search for.', 'TEXT', TRUE, TRUE),
@@ -1005,6 +1006,10 @@ INSERT INTO example(component, description, properties) VALUES
10051006
'{"Person": "Jane Doe", "Height": 150},' ||
10061007
'{"Person": "John Doe", "Height": 200},' ||
10071008
'{"_sqlpage_footer":true, "_sqlpage_color": "green", "Person": "Average", "Height": 180}]')),
1009+
('table', 'A table whose Name header asks the server to sort the data. Clicking it reloads the page with sort_Name=ASCENDING or sort_Name=DESCENDING; the page SQL should use that parameter in its ORDER BY.',
1010+
json('[{"component":"table", "server_sort_column":"Name"}, ' ||
1011+
'{"Name": "Rudolph Lingens", "Height": 190},' ||
1012+
'{"Name": "Jane Doe", "Height": 150}]')),
10081013
(
10091014
'table',
10101015
'A table with column sorting. Sorting sorts numbers in numeric order, and strings in alphabetical order.

examples/simple-website-example/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ This website illustrates how to create a basic Create-Read-Update-Delete (CRUD)
66
It has the following bsic features:
77

88
- Displays a list of user names using the [list component](https://sql-page.com/documentation.sql?component=list#component) (in [`index.sql`](./index.sql#L14-L20))
9+
- Displays an admin table whose user-name column uses [server-side table sorting](https://sql-page.com/documentation.sql?component=table#component) (in [`table.sql`](./table.sql))
910
- Add a new user name to the list through a [form](https://sql-page.com/documentation.sql?component=form#component) (in [`index.sql`](./index.sql#L1-L9))
1011
- View a user's personal page by clicking on a name in the list (in [`user.sql`](./user.sql))
1112
- Delete a user from the list by clicking on the delete button in the user's personal page (in [`delete.sql`](./delete.sql))
Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,13 @@
1-
select 'table' as component, 'action' as markdown;
1+
-- Sorting this column reloads the current page with sort_username=ASCENDING
2+
-- or sort_username=DESCENDING. The query below applies that choice before
3+
-- rendering the table, so sorting remains correct when the result is paginated.
4+
select 'table' as component,
5+
'username' as server_sort_column,
6+
'action' as markdown;
27
select *,
38
format('[Edit](edit.sql?id=%s)', id) as action
4-
from users;
9+
from users
10+
order by
11+
case when $sort_username = 'ASCENDING' then username end asc,
12+
case when $sort_username = 'DESCENDING' then username end desc,
13+
id;

examples/simple-website-example/test.hurl

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,26 @@ body contains "Hurl User"
1414
body contains "user.sql?id"
1515
body not contains "An error occurred"
1616

17+
POST http://localhost:8080/
18+
[FormParams]
19+
Username: Hurl Alpha
20+
HTTP 200
21+
[Asserts]
22+
body contains "Hurl Alpha"
23+
body not contains "An error occurred"
24+
25+
GET http://localhost:8080/table.sql
26+
HTTP 200
27+
[Asserts]
28+
xpath "string(//button[@data-server-sort-column='username']/@data-server-sort-column)" == "username"
29+
body not contains "An error occurred"
30+
31+
GET http://localhost:8080/table.sql?sort_username=ASCENDING
32+
HTTP 200
33+
[Asserts]
34+
xpath "normalize-space(string(//tbody/tr[1]/td[2]))" == "Hurl Alpha"
35+
body not contains "An error occurred"
36+
1737
GET http://localhost:8080/user.sql?id=1
1838
HTTP 200
1939
[Asserts]

sqlpage/sqlpage.js

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,17 +31,24 @@ function setup_table(root_el) {
3131
const table_el = root_el.querySelector("table");
3232
if (!table_el) return;
3333
/** @type {NodeListOf<HTMLElement>} */
34-
const sort_button_els = table_el.querySelectorAll("button.sort[data-sort]");
34+
const sort_button_els = table_el.querySelectorAll("button.sort");
3535
const sort_buttons = [...sort_button_els];
36+
const client_sort_buttons = sort_buttons.filter(
37+
(button) => !button.dataset.serverSortColumn,
38+
);
39+
const server_sort_buttons = sort_buttons.filter(
40+
(button) => !!button.dataset.serverSortColumn,
41+
);
3642
const item_parent = table_el.querySelector("tbody");
37-
const has_sort = sort_buttons.length > 0;
43+
const has_client_sort = client_sort_buttons.length > 0;
3844

39-
if (search_input || has_sort) {
40-
const items = table_parse_data(table_el, sort_buttons);
45+
if (search_input || has_client_sort) {
46+
const items = table_parse_data(table_el, client_sort_buttons);
4147
if (search_input) setup_table_search_behavior(search_input, items);
42-
if (has_sort && item_parent)
43-
setup_sort_behavior(sort_buttons, items, item_parent);
48+
if (has_client_sort && item_parent)
49+
setup_sort_behavior(client_sort_buttons, items, item_parent);
4450
}
51+
setup_server_sort_behavior(server_sort_buttons);
4552

4653
// Change number format AFTER parsing and storing the sort keys
4754
apply_number_formatting(table_el);
@@ -159,6 +166,36 @@ function setup_sort_behavior(sort_buttons, items, item_parent) {
159166
});
160167
}
161168

169+
/**
170+
* Reloads the current page with the selected server-side sort column and direction.
171+
* @param {HTMLElement[]} sort_buttons
172+
*/
173+
function setup_server_sort_behavior(sort_buttons) {
174+
const current_url = new URL(window.location.href);
175+
for (const button of sort_buttons) {
176+
const column = button.dataset.serverSortColumn;
177+
if (!column) continue;
178+
179+
const parameter = `sort_${column}`;
180+
const direction = current_url.searchParams.get(parameter);
181+
if (direction === "ASCENDING") button.classList.add("asc");
182+
if (direction === "DESCENDING") button.classList.add("desc");
183+
184+
button.addEventListener("click", () => {
185+
const url = new URL(window.location.href);
186+
const next_direction =
187+
url.searchParams.get(parameter) === "ASCENDING"
188+
? "DESCENDING"
189+
: "ASCENDING";
190+
for (const key of [...url.searchParams.keys()]) {
191+
if (key.startsWith("sort_")) url.searchParams.delete(key);
192+
}
193+
url.searchParams.set(parameter, next_direction);
194+
window.location.assign(url.toString());
195+
});
196+
}
197+
}
198+
162199
function sqlpage_table() {
163200
/** @type {NodeListOf<HTMLElement>} */
164201
const tables = document.querySelectorAll("[data-pre-init=table]");

sqlpage/templates/table.handlebars

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,9 @@
4545
{{~#if (array_contains_case_insensitive ../../raw_numbers @key)}} data-raw_number="1"{{/if~}}
4646
{{~#if (array_contains_case_insensitive ../../money @key)}} data-money="1"{{/if~}}
4747
>
48-
{{~#if ../../sort~}}
48+
{{~#if (array_contains_case_insensitive (to_array ../../server_sort_column) @key)~}}
49+
<button class="table-sort sort d-inline" data-server-sort-column="{{@key}}">{{@key}}</button>
50+
{{~else if ../../sort~}}
4951
<button class="table-sort sort d-inline" data-sort="{{@key}}">{{@key}}</button>
5052
{{~else~}}
5153
{{~@key~}}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
SELECT 'table' AS component,
2+
'name' AS server_sort_column;
3+
4+
SELECT name
5+
FROM (
6+
SELECT 'Zulu' AS name
7+
UNION ALL SELECT 'Alpha'
8+
UNION ALL SELECT 'Mike'
9+
)
10+
ORDER BY
11+
CASE WHEN $sort_name = 'ASCENDING' THEN name END ASC,
12+
CASE WHEN $sort_name = 'DESCENDING' THEN name END DESC;
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { expect, test } from "../../fixture";
2+
3+
test("server-sort columns reload the page in ascending and descending order", async ({
4+
page,
5+
}) => {
6+
const sortButton = page.getByRole("button", { name: "name" });
7+
8+
await sortButton.click();
9+
await expect(page).toHaveURL(/sort_name=ASCENDING/);
10+
await expect(page.locator("tbody tr").first()).toHaveText("Alpha");
11+
await expect(sortButton).toHaveClass(/\basc\b/);
12+
13+
await sortButton.click();
14+
await expect(page).toHaveURL(/sort_name=DESCENDING/);
15+
await expect(page.locator("tbody tr").first()).toHaveText("Zulu");
16+
await expect(sortButton).toHaveClass(/\bdesc\b/);
17+
});

0 commit comments

Comments
 (0)