Skip to content

Commit c57d82c

Browse files
Added an option to format numbers using toLocaleString() for table columns (#807)
* Added an option to format numbers using `toLocaleString()` for table columns * format numbers in tables --------- Co-authored-by: lovasoa <[email protected]>
1 parent 9279004 commit c57d82c

File tree

6 files changed

+89
-36
lines changed

6 files changed

+89
-36
lines changed

CHANGELOG.md

+13-3
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,25 @@
1414
- Add a new `auto_submit` parameter to the form component. When set to true, the form will be automatically submitted when the user changes any of its fields, and the page will be reloaded with the new value. The validation button is removed.
1515
- This is useful to quickly create filters at the top of a dashboard or report page, that will be automatically applied when the user changes them.
1616
- New `options_source` parameter in the form component. This allows to dynamically load options for dropdowns from a different SQL file.
17-
- This allows easily implementing autocomplete for form fields with a large number of possible options.
18-
- In the map component, add support for map pins with a description but no title.
17+
- This allows easily implementing autocomplete for form fields with a large number of possible options.
18+
- In the map component, add support for map pins with a description but no title.
1919
- Improved error messages when a parameter of a sqlpage function is invalid. Error traces used to be truncated, and made it hard to understand the exact cause of the error in some cases. In particular, calls to `sqlpage.fetch` would display an unhelpful error message when the HTTP request definition was invalid. `sqlpage.fetch` now also throws an error if the HTTP request definition contains unknown fields.
2020
- Make the `headers` field of the `sqlpage.fetch` function parameter optional. It defaults to sending a User-Agent header containing the SQLPage version.
2121
- Make custom layout creations with the `card` component easier and less error-prone:
2222
- The `embed` property now automatically adds the `_sqlpage_embed` parameter to the embedded page URL to render it as an embeddable fragment.
2323
- When an embedded page is rendered, the `shell` component is automatically replaced by a `shell-empty` component, to avoid displaying a duplicate shell and creating invalid duplicated page metadata in the response.
2424
- Update Tabler Icons to version [3.30.0](https://tabler.io/changelog#/changelog/tabler-icons-3.30), with many new icons.
2525
- Update the CSS framework to [Tabler 1.0.0](https://github.com/tabler/tabler/releases/tag/v1.0.0), with many small UI consistency improvements.
26+
- Add native number formatting to the table component. Numeric values in tables are now formatted in the visitor's locale by default, using country-specific thousands separators and decimal points.
27+
- This is better than formatting numbers inside the database, because
28+
- columns are sorted correctly in the numeric order by default, instead of being sorted in the alphabetic order of the formatted string.
29+
- the formatted numbers are more readable for the user by default, without requiring any additional code.
30+
- it adapts to the visitor's preferred locale, for instance using `.` as a decimal point and a space as a thousands separator if the visitor is in France.
31+
- less data is sent from the database to sqlpage, and from sqlpage to the client, because the numbers are not formatted directly in the database.
32+
- Add new customization properties to the table component:
33+
- Switch back to displaying raw numbers without formatting using the `raw_numbers` property.
34+
- Format monetary values using the `money` property to specify columns and `currency` to set the currency.
35+
- Control decimal places with `number_format_digits` property.
2636

2737
## 0.32.1 (2025-01-03)
2838

@@ -118,7 +128,7 @@ This is a bugfix release.
118128
- Added support for:
119129
- advanced `JSON_TABLE` usage in MySQL for working with JSON arrays.
120130
- `EXECUTE` statements with parameters in MSSQL for running stored procedures.
121-
- MSSQLs `TRY_CONVERT` function for type conversion.
131+
- MSSQL's `TRY_CONVERT` function for type conversion.
122132
- `ANY`, `ALL`, and `SOME` subqueries (e.g., `SELECT * FROM t WHERE a = ANY (SELECT b FROM t2)`).
123133
- `LIMIT max_rows, offset` syntax in SQLite.
124134
- Assigning column names aliases using `=` in MSSQL (e.g., `SELECT col_name = value`).

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

+16-6
Original file line numberDiff line numberDiff line change
@@ -781,10 +781,14 @@ INSERT INTO parameter(component, name, description, type, top_level, optional) S
781781
('border', 'Whether to draw borders on all sides of the table and cells.', 'BOOLEAN', TRUE, TRUE),
782782
('overflow', 'Whether to to let "wide" tables overflow across the right border and enable browser-based horizontal scrolling.', 'BOOLEAN', TRUE, TRUE),
783783
('small', 'Whether to use compact table.', 'BOOLEAN', TRUE, TRUE),
784-
('description','Description of the table content and helps users with screen readers to find a table and understand what it’s.','TEXT',TRUE,TRUE),
784+
('description','Description of the table contents. Helps users with screen readers to find a table and understand what it’s about.','TEXT',TRUE,TRUE),
785785
('empty_description', 'Text to display if the table does not contain any row. Defaults to "no data".', 'TEXT', TRUE, TRUE),
786786
('freeze_columns', 'Whether to freeze the leftmost column of the table.', 'BOOLEAN', TRUE, TRUE),
787787
('freeze_headers', 'Whether to freeze the top row of the table.', 'BOOLEAN', TRUE, TRUE),
788+
('raw_numbers', 'Name of a column whose values are numeric, but should be displayed as raw numbers without any formatting (no thousands separators, decimal separator is always a dot). This argument can be repeated multiple times.', 'TEXT', TRUE, TRUE),
789+
('money', 'Name of a numeric column whose values should be displayed as currency amounts, in the currency defined by the `currency` property. This argument can be repeated multiple times.', 'TEXT', TRUE, TRUE),
790+
('currency', 'The ISO 4217 currency code (e.g., USD, EUR, GBP, etc.) to use when formatting monetary values.', 'TEXT', TRUE, TRUE),
791+
('number_format_digits', 'Maximum number of decimal digits to display for numeric values.', 'INTEGER', TRUE, TRUE),
788792
-- row level
789793
('_sqlpage_css_class', 'For advanced users. Sets a css class on the table row. Added in v0.8.0.', 'TEXT', FALSE, TRUE),
790794
('_sqlpage_color', 'Sets the background color of the row. Added in v0.8.0.', 'COLOR', FALSE, TRUE),
@@ -805,12 +809,18 @@ INSERT INTO example(component, description, properties) VALUES
805809
]')),
806810
(
807811
'table',
808-
'A table with column sorting. Sorting sorts numbers in numeric order, and strings in alphabetical order.',
812+
'A table with column sorting. Sorting sorts numbers in numeric order, and strings in alphabetical order.
813+
814+
Numbers can be displayed
815+
- as raw digits without formatting using the `raw_numbers` property,
816+
- as currency using the `money` property to define columns that contain monetary values and `currency` to define the currency,
817+
- as numbers with a fixed maximum number of decimal digits using the `number_format_digits` property.
818+
',
809819
json(
810-
'[{"component":"table", "sort": true, "align_right": ["Price ($)", "Amount in stock"], "align_center": ["part_no"] },
811-
{"id": 31456, "part_no": "SQL-TABLE-856-G", "Price ($)": 12, "Amount in stock": 5},
812-
{"id": 996, "part_no": "SQL-FORMS-86-M", "Price ($)": 1, "Amount in stock": 15},
813-
{"id": 131456, "part_no": "SQL-CARDS-56-K", "Price ($)": 127, "Amount in stock": 9}
820+
'[{"component":"table", "sort": true, "align_right": ["Price", "Amount in stock"], "align_center": ["part_no"], "raw_numbers": ["id"], "currency": "USD", "money": ["Price"] },
821+
{"id": 31456, "part_no": "SQL-TABLE-856-G", "Price": 12, "Amount in stock": 5},
822+
{"id": 996, "part_no": "SQL-FORMS-86-M", "Price": 1, "Amount in stock": 1234},
823+
{"id": 131456, "part_no": "SQL-CARDS-56-K", "Price": 127, "Amount in stock": 98}
814824
]'
815825
)),
816826
(

sqlpage/sqlpage.js

+33-10
Original file line numberDiff line numberDiff line change
@@ -23,19 +23,42 @@ function sqlpage_card() {
2323
}
2424
}
2525

26-
/** @param {HTMLElement} el */
27-
function table_search_sort(el) {
26+
/** @param {HTMLElement} root_el */
27+
function table_search_sort(root_el) {
2828
/** @type {HTMLInputElement | null} */
29-
const search_input = el.querySelector("input.search");
30-
const sort_buttons = [...el.querySelectorAll("button.sort[data-sort]")];
31-
const item_parent = el.querySelector("tbody");
32-
const items = [...item_parent.querySelectorAll("tr")].map((el) => {
33-
const cells = el.getElementsByTagName("td");
29+
const search_input = root_el.querySelector("input.search");
30+
const table_el = root_el.querySelector("table");
31+
const sort_buttons = [...table_el.querySelectorAll("button.sort[data-sort]")];
32+
const item_parent = table_el.querySelector("tbody");
33+
const number_format_locale = table_el.dataset.number_format_locale;
34+
const number_format_digits = table_el.dataset.number_format_digits;
35+
const currency = table_el.dataset.currency;
36+
const header_els = table_el.querySelectorAll("thead > tr > th");
37+
const col_types = [...header_els].map((el) => el.dataset.column_type);
38+
const col_rawnums = [...header_els].map((el) => !!el.dataset.raw_number);
39+
const col_money = [...header_els].map((el) => !!el.dataset.money);
40+
const items = [...item_parent.querySelectorAll("tr")].map((tr_el) => {
41+
const cells = tr_el.getElementsByTagName("td");
3442
return {
35-
el,
36-
sort_keys: sort_buttons.map((b, idx) => {
43+
el: tr_el,
44+
sort_keys: sort_buttons.map((btn_el, idx) => {
3745
const sort_key = cells[idx]?.textContent;
38-
return { num: Number.parseFloat(sort_key), str: sort_key };
46+
const column_type = col_types[idx];
47+
const num = Number.parseFloat(sort_key);
48+
const is_raw_number = col_rawnums[idx];
49+
if (column_type === "number" && !is_raw_number) {
50+
const cell_el = cells[idx];
51+
const is_money = col_money[idx];
52+
cell_el.textContent = num.toLocaleString(number_format_locale, {
53+
maximumFractionDigits: number_format_digits,
54+
currency,
55+
style: is_money ? "currency" : undefined,
56+
});
57+
}
58+
return {
59+
num,
60+
str: sort_key,
61+
};
3962
}),
4063
};
4164
});

sqlpage/templates/table.handlebars

+10-2
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,11 @@
2121
{{~#if hover}} table-hover {{/if~}}
2222
{{~#if border}} table-bordered {{/if~}}
2323
{{~#if small}} table-sm {{/if~}}
24-
">
24+
"
25+
{{~#if number_format_locale}} data-number_format_locale="{{number_format_locale}}"{{/if~}}
26+
{{~#if number_format_digits}} data-number_format_digits="{{number_format_digits}}"{{/if~}}
27+
{{~#if currency}} data-currency="{{currency}}"{{/if~}}
28+
>
2529
{{#if description}}<caption>{{description}}</caption>{{/if}}
2630
{{#each_row}}
2731
{{#if (eq @row_index 0)}}
@@ -33,7 +37,11 @@
3337
{{~@key~}}
3438
{{~#if (array_contains_case_insensitive ../../align_right @key)}} text-end {{/if~}}
3539
{{~#if (array_contains_case_insensitive ../../align_center @key)}} text-center {{/if~}}
36-
">
40+
"
41+
data-column_type="{{typeof this}}"
42+
{{~#if (array_contains_case_insensitive ../../raw_numbers @key)~}} data-raw_number="1"{{/if~}}
43+
{{~#if (array_contains_case_insensitive ../../money @key)~}} data-money="1"{{/if~}}
44+
>
3745
{{~#if ../../sort~}}
3846
<button class="table-sort sort d-inline" data-sort="{{@key}}">{{@key}}</button>
3947
{{~else~}}

tests/end-to-end/official-site.spec.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,9 @@ test("table sorting", async ({ page }) => {
121121
// Test amount in stock column sorting
122122
await tableSection.getByRole("button", { name: "Amount in stock" }).click();
123123
const amounts = await tableSection.locator("td.Amount").allInnerTexts();
124-
const numericAmounts = amounts.map((amount) => Number.parseInt(amount));
124+
const numericAmounts = amounts.map((amount) =>
125+
Number.parseInt(amount.replace(/[^0-9]/g, "")),
126+
);
125127
const sortedAmounts = [...numericAmounts].sort((a, b) => a - b);
126128
expect(numericAmounts).toEqual(sortedAmounts);
127129
});

tests/end-to-end/package-lock.json

+14-14
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)