Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# CHANGELOG.md

## Unreleased

- 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.

## v0.46.1

- 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/`:
Expand Down
22 changes: 21 additions & 1 deletion examples/official-site/sqlpage/migrations/01_documentation.sql
Original file line number Diff line number Diff line change
Expand Up @@ -949,7 +949,8 @@ Advanced users can apply custom styles to table columns using a CSS class with t

INSERT INTO parameter(component, name, description, type, top_level, optional) SELECT 'table', * FROM (VALUES
-- top level
('sort', 'Make the columns clickable to let the user sort by the value contained in the column.', 'BOOLEAN', TRUE, TRUE),
('sort', 'Make the columns clickable to let the user sort by the value contained in the column in the browser.', 'BOOLEAN', TRUE, TRUE),
('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),
('search', 'Add a search bar at the top of the table, letting users easily filter table rows by value.', 'BOOLEAN', TRUE, TRUE),
('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),
('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),
Expand Down Expand Up @@ -1009,6 +1010,25 @@ INSERT INTO example(component, description, properties) VALUES
'table',
'A table with column sorting. Sorting sorts numbers in numeric order, and strings in alphabetical order.

This example uses client-side sorting: it reorders only the rows already loaded in the browser. For paginated data, use `server_sort_column` so that the database sorts before applying `LIMIT` and `OFFSET`.

## Server-side sorting

Set `server_sort_column` to the name of a result column. Clicking its header reloads the current page with `sort_<column>=ASCENDING` or `sort_<column>=DESCENDING`. Read that parameter in SQL and use fixed expressions in `ORDER BY`; do not interpolate the parameter into SQL.

```sql
SELECT ''table'' AS component,
''name'' AS server_sort_column;

SELECT id, name
FROM users
ORDER BY
CASE WHEN $sort_name = ''ASCENDING'' THEN name END ASC,
CASE WHEN $sort_name = ''DESCENDING'' THEN name END DESC,
id
LIMIT 100 OFFSET COALESCE(CAST($offset AS INTEGER), 0);
```

Numbers can be displayed
- as raw digits without formatting using the `raw_numbers` property,
- as currency using the `money` property to define columns that contain monetary values and `currency` to define the currency,
Expand Down
1 change: 1 addition & 0 deletions examples/simple-website-example/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ This website illustrates how to create a basic Create-Read-Update-Delete (CRUD)
It has the following bsic features:

- 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))
- 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))
- 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))
- View a user's personal page by clicking on a name in the list (in [`user.sql`](./user.sql))
- Delete a user from the list by clicking on the delete button in the user's personal page (in [`delete.sql`](./delete.sql))
Expand Down
13 changes: 11 additions & 2 deletions examples/simple-website-example/table.sql
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
select 'table' as component, 'action' as markdown;
-- Sorting this column reloads the current page with sort_username=ASCENDING
-- or sort_username=DESCENDING. The query below applies that choice before
-- rendering the table, so sorting remains correct when the result is paginated.
select 'table' as component,
'username' as server_sort_column,
'action' as markdown;
select *,
format('[Edit](edit.sql?id=%s)', id) as action
from users;
from users
order by
case when $sort_username = 'ASCENDING' then username end asc,
case when $sort_username = 'DESCENDING' then username end desc,
id;
20 changes: 20 additions & 0 deletions examples/simple-website-example/test.hurl
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,26 @@ body contains "Hurl User"
body contains "user.sql?id"
body not contains "An error occurred"

POST http://localhost:8080/
[FormParams]
Username: Hurl Alpha
HTTP 200
[Asserts]
body contains "Hurl Alpha"
body not contains "An error occurred"

GET http://localhost:8080/table.sql
HTTP 200
[Asserts]
xpath "string(//button[@data-server-sort-column='username']/@data-server-sort-column)" == "username"
body not contains "An error occurred"

GET http://localhost:8080/table.sql?sort_username=ASCENDING
HTTP 200
[Asserts]
xpath "normalize-space(string(//tbody/tr[1]/td[2]))" == "Hurl Alpha"
body not contains "An error occurred"

GET http://localhost:8080/user.sql?id=1
HTTP 200
[Asserts]
Expand Down
49 changes: 43 additions & 6 deletions sqlpage/sqlpage.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,17 +31,24 @@ function setup_table(root_el) {
const table_el = root_el.querySelector("table");
if (!table_el) return;
/** @type {NodeListOf<HTMLElement>} */
const sort_button_els = table_el.querySelectorAll("button.sort[data-sort]");
const sort_button_els = table_el.querySelectorAll("button.sort");
const sort_buttons = [...sort_button_els];
const client_sort_buttons = sort_buttons.filter(
(button) => !button.dataset.serverSortColumn,
);
const server_sort_buttons = sort_buttons.filter(
(button) => !!button.dataset.serverSortColumn,
);
const item_parent = table_el.querySelector("tbody");
const has_sort = sort_buttons.length > 0;
const has_client_sort = client_sort_buttons.length > 0;

if (search_input || has_sort) {
const items = table_parse_data(table_el, sort_buttons);
if (search_input || has_client_sort) {
const items = table_parse_data(table_el, client_sort_buttons);
if (search_input) setup_table_search_behavior(search_input, items);
if (has_sort && item_parent)
setup_sort_behavior(sort_buttons, items, item_parent);
if (has_client_sort && item_parent)
setup_sort_behavior(client_sort_buttons, items, item_parent);
}
setup_server_sort_behavior(server_sort_buttons);

// Change number format AFTER parsing and storing the sort keys
apply_number_formatting(table_el);
Expand Down Expand Up @@ -159,6 +166,36 @@ function setup_sort_behavior(sort_buttons, items, item_parent) {
});
}

/**
* Reloads the current page with the selected server-side sort column and direction.
* @param {HTMLElement[]} sort_buttons
*/
function setup_server_sort_behavior(sort_buttons) {
const current_url = new URL(window.location.href);
for (const button of sort_buttons) {
const column = button.dataset.serverSortColumn;
if (!column) continue;

const parameter = `sort_${column}`;
const direction = current_url.searchParams.get(parameter);
if (direction === "ASCENDING") button.classList.add("asc");
if (direction === "DESCENDING") button.classList.add("desc");

button.addEventListener("click", () => {
const url = new URL(window.location.href);
const next_direction =
url.searchParams.get(parameter) === "ASCENDING"
? "DESCENDING"
: "ASCENDING";
for (const key of [...url.searchParams.keys()]) {
if (key.startsWith("sort_")) url.searchParams.delete(key);
}
url.searchParams.set(parameter, next_direction);
window.location.assign(url.toString());
});
}
}

function sqlpage_table() {
/** @type {NodeListOf<HTMLElement>} */
const tables = document.querySelectorAll("[data-pre-init=table]");
Expand Down
4 changes: 3 additions & 1 deletion sqlpage/templates/table.handlebars
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,9 @@
{{~#if (array_contains_case_insensitive ../../raw_numbers @key)}} data-raw_number="1"{{/if~}}
{{~#if (array_contains_case_insensitive ../../money @key)}} data-money="1"{{/if~}}
>
{{~#if ../../sort~}}
{{~#if (array_contains_case_insensitive (to_array ../../server_sort_column) @key)~}}
<button class="table-sort sort d-inline" data-server-sort-column="{{@key}}">{{@key}}</button>
{{~else if ../../sort~}}
<button class="table-sort sort d-inline" data-sort="{{@key}}">{{@key}}</button>
{{~else~}}
{{~@key~}}
Expand Down
12 changes: 12 additions & 0 deletions tests/end-to-end/fixtures/table-server-sort/index.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
SELECT 'table' AS component,
'name' AS server_sort_column;

SELECT name
FROM (
SELECT 'Zulu' AS name
UNION ALL SELECT 'Alpha'
UNION ALL SELECT 'Mike'
)
ORDER BY
CASE WHEN $sort_name = 'ASCENDING' THEN name END ASC,
CASE WHEN $sort_name = 'DESCENDING' THEN name END DESC;
17 changes: 17 additions & 0 deletions tests/end-to-end/fixtures/table-server-sort/test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { expect, test } from "../../fixture";

test("server-sort columns reload the page in ascending and descending order", async ({
page,
}) => {
const sortButton = page.getByRole("button", { name: "name" });

await sortButton.click();
await expect(page).toHaveURL(/sort_name=ASCENDING/);
await expect(page.locator("tbody tr").first()).toHaveText("Alpha");
await expect(sortButton).toHaveClass(/\basc\b/);

await sortButton.click();
await expect(page).toHaveURL(/sort_name=DESCENDING/);
await expect(page.locator("tbody tr").first()).toHaveText("Zulu");
await expect(sortButton).toHaveClass(/\bdesc\b/);
});