Skip to content

Added an option to format numbers using toLocaleString() for table columns #807

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Feb 11, 2025
Merged
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
16 changes: 13 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,25 @@
- 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.
- 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.
- New `options_source` parameter in the form component. This allows to dynamically load options for dropdowns from a different SQL file.
- This allows easily implementing autocomplete for form fields with a large number of possible options.
- In the map component, add support for map pins with a description but no title.
- This allows easily implementing autocomplete for form fields with a large number of possible options.
- In the map component, add support for map pins with a description but no title.
- 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.
- Make the `headers` field of the `sqlpage.fetch` function parameter optional. It defaults to sending a User-Agent header containing the SQLPage version.
- Make custom layout creations with the `card` component easier and less error-prone:
- The `embed` property now automatically adds the `_sqlpage_embed` parameter to the embedded page URL to render it as an embeddable fragment.
- 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.
- Update Tabler Icons to version [3.30.0](https://tabler.io/changelog#/changelog/tabler-icons-3.30), with many new icons.
- 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.
- 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.
- This is better than formatting numbers inside the database, because
- columns are sorted correctly in the numeric order by default, instead of being sorted in the alphabetic order of the formatted string.
- the formatted numbers are more readable for the user by default, without requiring any additional code.
- 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.
- 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.
- Add new customization properties to the table component:
- Switch back to displaying raw numbers without formatting using the `raw_numbers` property.
- Format monetary values using the `money` property to specify columns and `currency` to set the currency.
- Control decimal places with `number_format_digits` property.

## 0.32.1 (2025-01-03)

Expand Down Expand Up @@ -118,7 +128,7 @@ This is a bugfix release.
- Added support for:
- advanced `JSON_TABLE` usage in MySQL for working with JSON arrays.
- `EXECUTE` statements with parameters in MSSQL for running stored procedures.
- MSSQLs `TRY_CONVERT` function for type conversion.
- MSSQL's `TRY_CONVERT` function for type conversion.
- `ANY`, `ALL`, and `SOME` subqueries (e.g., `SELECT * FROM t WHERE a = ANY (SELECT b FROM t2)`).
- `LIMIT max_rows, offset` syntax in SQLite.
- Assigning column names aliases using `=` in MSSQL (e.g., `SELECT col_name = value`).
Expand Down
22 changes: 16 additions & 6 deletions examples/official-site/sqlpage/migrations/01_documentation.sql
Original file line number Diff line number Diff line change
Expand Up @@ -780,10 +780,14 @@ INSERT INTO parameter(component, name, description, type, top_level, optional) S
('border', 'Whether to draw borders on all sides of the table and cells.', 'BOOLEAN', TRUE, TRUE),
('overflow', 'Whether to to let "wide" tables overflow across the right border and enable browser-based horizontal scrolling.', 'BOOLEAN', TRUE, TRUE),
('small', 'Whether to use compact table.', 'BOOLEAN', TRUE, TRUE),
('description','Description of the table content and helps users with screen readers to find a table and understand what it’s.','TEXT',TRUE,TRUE),
('description','Description of the table contents. Helps users with screen readers to find a table and understand what it’s about.','TEXT',TRUE,TRUE),
('empty_description', 'Text to display if the table does not contain any row. Defaults to "no data".', 'TEXT', TRUE, TRUE),
('freeze_columns', 'Whether to freeze the leftmost column of the table.', 'BOOLEAN', TRUE, TRUE),
('freeze_headers', 'Whether to freeze the top row of the table.', 'BOOLEAN', TRUE, TRUE),
('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),
('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),
('currency', 'The ISO 4217 currency code (e.g., USD, EUR, GBP, etc.) to use when formatting monetary values.', 'TEXT', TRUE, TRUE),
('number_format_digits', 'Maximum number of decimal digits to display for numeric values.', 'INTEGER', TRUE, TRUE),
-- row level
('_sqlpage_css_class', 'For advanced users. Sets a css class on the table row. Added in v0.8.0.', 'TEXT', FALSE, TRUE),
('_sqlpage_color', 'Sets the background color of the row. Added in v0.8.0.', 'COLOR', FALSE, TRUE),
Expand All @@ -804,12 +808,18 @@ INSERT INTO example(component, description, properties) VALUES
]')),
(
'table',
'A table with column sorting. Sorting sorts numbers in numeric order, and strings in alphabetical order.',
'A table with column sorting. Sorting sorts numbers in numeric order, and strings in alphabetical order.

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,
- as numbers with a fixed maximum number of decimal digits using the `number_format_digits` property.
',
json(
'[{"component":"table", "sort": true, "align_right": ["Price ($)", "Amount in stock"], "align_center": ["part_no"] },
{"id": 31456, "part_no": "SQL-TABLE-856-G", "Price ($)": 12, "Amount in stock": 5},
{"id": 996, "part_no": "SQL-FORMS-86-M", "Price ($)": 1, "Amount in stock": 15},
{"id": 131456, "part_no": "SQL-CARDS-56-K", "Price ($)": 127, "Amount in stock": 9}
'[{"component":"table", "sort": true, "align_right": ["Price", "Amount in stock"], "align_center": ["part_no"], "raw_numbers": ["id"], "currency": "USD", "money": ["Price"] },
{"id": 31456, "part_no": "SQL-TABLE-856-G", "Price": 12, "Amount in stock": 5},
{"id": 996, "part_no": "SQL-FORMS-86-M", "Price": 1, "Amount in stock": 1234},
{"id": 131456, "part_no": "SQL-CARDS-56-K", "Price": 127, "Amount in stock": 98}
]'
)),
(
Expand Down
43 changes: 33 additions & 10 deletions sqlpage/sqlpage.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,19 +23,42 @@ function sqlpage_card() {
}
}

/** @param {HTMLElement} el */
function table_search_sort(el) {
/** @param {HTMLElement} root_el */
function table_search_sort(root_el) {
/** @type {HTMLInputElement | null} */
const search_input = el.querySelector("input.search");
const sort_buttons = [...el.querySelectorAll("button.sort[data-sort]")];
const item_parent = el.querySelector("tbody");
const items = [...item_parent.querySelectorAll("tr")].map((el) => {
const cells = el.getElementsByTagName("td");
const search_input = root_el.querySelector("input.search");
const table_el = root_el.querySelector("table");
const sort_buttons = [...table_el.querySelectorAll("button.sort[data-sort]")];
const item_parent = table_el.querySelector("tbody");
const number_format_locale = table_el.dataset.number_format_locale;
const number_format_digits = table_el.dataset.number_format_digits;
const currency = table_el.dataset.currency;
const header_els = table_el.querySelectorAll("thead > tr > th");
const col_types = [...header_els].map((el) => el.dataset.column_type);
const col_rawnums = [...header_els].map((el) => !!el.dataset.raw_number);
const col_money = [...header_els].map((el) => !!el.dataset.money);
const items = [...item_parent.querySelectorAll("tr")].map((tr_el) => {
const cells = tr_el.getElementsByTagName("td");
return {
el,
sort_keys: sort_buttons.map((b, idx) => {
el: tr_el,
sort_keys: sort_buttons.map((btn_el, idx) => {
const sort_key = cells[idx]?.textContent;
return { num: Number.parseFloat(sort_key), str: sort_key };
const column_type = col_types[idx];
const num = Number.parseFloat(sort_key);
const is_raw_number = col_rawnums[idx];
if (column_type === "number" && !is_raw_number) {
const cell_el = cells[idx];
const is_money = col_money[idx];
cell_el.textContent = num.toLocaleString(number_format_locale, {
maximumFractionDigits: number_format_digits,
currency,
style: is_money ? "currency" : undefined,
});
}
return {
num,
str: sort_key,
};
}),
};
});
Expand Down
12 changes: 10 additions & 2 deletions sqlpage/templates/table.handlebars
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,11 @@
{{~#if hover}} table-hover {{/if~}}
{{~#if border}} table-bordered {{/if~}}
{{~#if small}} table-sm {{/if~}}
">
"
{{~#if number_format_locale}} data-number_format_locale="{{number_format_locale}}"{{/if~}}
{{~#if number_format_digits}} data-number_format_digits="{{number_format_digits}}"{{/if~}}
{{~#if currency}} data-currency="{{currency}}"{{/if~}}
>
{{#if description}}<caption>{{description}}</caption>{{/if}}
{{#each_row}}
{{#if (eq @row_index 0)}}
Expand All @@ -33,7 +37,11 @@
{{~@key~}}
{{~#if (array_contains_case_insensitive ../../align_right @key)}} text-end {{/if~}}
{{~#if (array_contains_case_insensitive ../../align_center @key)}} text-center {{/if~}}
">
"
data-column_type="{{typeof this}}"
{{~#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~}}
<button class="table-sort sort d-inline" data-sort="{{@key}}">{{@key}}</button>
{{~else~}}
Expand Down
4 changes: 3 additions & 1 deletion tests/end-to-end/official-site.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,9 @@ test("table sorting", async ({ page }) => {
// Test amount in stock column sorting
await tableSection.getByRole("button", { name: "Amount in stock" }).click();
const amounts = await tableSection.locator("td.Amount").allInnerTexts();
const numericAmounts = amounts.map((amount) => Number.parseInt(amount));
const numericAmounts = amounts.map((amount) =>
Number.parseInt(amount.replace(/[^0-9]/g, "")),
);
const sortedAmounts = [...numericAmounts].sort((a, b) => a - b);
expect(numericAmounts).toEqual(sortedAmounts);
});
Expand Down
28 changes: 14 additions & 14 deletions tests/end-to-end/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading