Skip to content

Commit e0d5ac6

Browse files
committed
feat(Datatable): added sorting for header cell
1 parent 6e623b5 commit e0d5ac6

12 files changed

Lines changed: 725 additions & 55 deletions

File tree

playground/DataTable.html

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,111 @@ <h3>Expandable + Multi-select</h3>
272272
</sgds-data-table>
273273
</div>
274274

275+
<!-- Custom footer text -->
276+
<div class="container">
277+
<h3>Custom Footer Text (footerText)</h3>
278+
<sgds-data-table currentPage="1" dataLength="4" itemsPerPage="2" footerText="Showing records fetched from API cache">
279+
<sgds-data-table-row>
280+
<sgds-data-table-head>ID</sgds-data-table-head>
281+
<sgds-data-table-head>Name</sgds-data-table-head>
282+
</sgds-data-table-row>
283+
<sgds-data-table-row>
284+
<sgds-data-table-cell>1</sgds-data-table-cell>
285+
<sgds-data-table-cell>Amy</sgds-data-table-cell>
286+
</sgds-data-table-row>
287+
<sgds-data-table-row>
288+
<sgds-data-table-cell>2</sgds-data-table-cell>
289+
<sgds-data-table-cell>Ben</sgds-data-table-cell>
290+
</sgds-data-table-row>
291+
<sgds-data-table-row>
292+
<sgds-data-table-cell>3</sgds-data-table-cell>
293+
<sgds-data-table-cell>Cara</sgds-data-table-cell>
294+
</sgds-data-table-row>
295+
<sgds-data-table-row>
296+
<sgds-data-table-cell>4</sgds-data-table-cell>
297+
<sgds-data-table-cell>Dan</sgds-data-table-cell>
298+
</sgds-data-table-row>
299+
</sgds-data-table>
300+
</div>
301+
302+
<!-- Hide footer -->
303+
<div class="container">
304+
<h3>Hide Footer (hideFooter)</h3>
305+
<sgds-data-table currentPage="1" dataLength="3" itemsPerPage="5" hideFooter>
306+
<sgds-data-table-row>
307+
<sgds-data-table-head>ID</sgds-data-table-head>
308+
<sgds-data-table-head>Status</sgds-data-table-head>
309+
</sgds-data-table-row>
310+
<sgds-data-table-row>
311+
<sgds-data-table-cell>1001</sgds-data-table-cell>
312+
<sgds-data-table-cell>Pending</sgds-data-table-cell>
313+
</sgds-data-table-row>
314+
<sgds-data-table-row>
315+
<sgds-data-table-cell>1002</sgds-data-table-cell>
316+
<sgds-data-table-cell>Approved</sgds-data-table-cell>
317+
</sgds-data-table-row>
318+
<sgds-data-table-row>
319+
<sgds-data-table-cell>1003</sgds-data-table-cell>
320+
<sgds-data-table-cell>Rejected</sgds-data-table-cell>
321+
</sgds-data-table-row>
322+
</sgds-data-table>
323+
</div>
324+
325+
<!-- Server mode -->
326+
<div class="container">
327+
<h3>Server Mode (mode="server")</h3>
328+
<sgds-data-table id="server-table" currentPage="1" dataLength="50" itemsPerPage="10" mode="server" isLoading>
329+
<sgds-data-table-row>
330+
<sgds-data-table-head>ID</sgds-data-table-head>
331+
<sgds-data-table-head>Name</sgds-data-table-head>
332+
<sgds-data-table-head>Region</sgds-data-table-head>
333+
</sgds-data-table-row>
334+
</sgds-data-table>
335+
</div>
336+
337+
<!-- Header props: sortable, sortKey, width, colspan, rowspan, textAlign -->
338+
<div class="container">
339+
<h3>Header Props (sortable, sortKey, width, colspan, rowspan, textAlign)</h3>
340+
<sgds-data-table currentPage="1" dataLength="4" itemsPerPage="4">
341+
<sgds-data-table-row>
342+
<sgds-data-table-head rowspan="2" width="88">ID</sgds-data-table-head>
343+
<sgds-data-table-head colspan="2">Profile</sgds-data-table-head>
344+
<sgds-data-table-head rowspan="2" sortable sortKey="score" width="120" textAlign="right"
345+
>Score</sgds-data-table-head
346+
>
347+
</sgds-data-table-row>
348+
<sgds-data-table-row>
349+
<sgds-data-table-head sortable sortKey="name">Name</sgds-data-table-head>
350+
<sgds-data-table-head sortable sortKey="role">Role</sgds-data-table-head>
351+
</sgds-data-table-row>
352+
353+
<sgds-data-table-row id="sort-row-1">
354+
<sgds-data-table-cell>1</sgds-data-table-cell>
355+
<sgds-data-table-cell>Lina</sgds-data-table-cell>
356+
<sgds-data-table-cell>Engineer</sgds-data-table-cell>
357+
<sgds-data-table-cell>82</sgds-data-table-cell>
358+
</sgds-data-table-row>
359+
<sgds-data-table-row id="sort-row-2">
360+
<sgds-data-table-cell>2</sgds-data-table-cell>
361+
<sgds-data-table-cell>Adam</sgds-data-table-cell>
362+
<sgds-data-table-cell>Designer</sgds-data-table-cell>
363+
<sgds-data-table-cell>90</sgds-data-table-cell>
364+
</sgds-data-table-row>
365+
<sgds-data-table-row id="sort-row-3">
366+
<sgds-data-table-cell>3</sgds-data-table-cell>
367+
<sgds-data-table-cell>Nora</sgds-data-table-cell>
368+
<sgds-data-table-cell>Manager</sgds-data-table-cell>
369+
<sgds-data-table-cell>75</sgds-data-table-cell>
370+
</sgds-data-table-row>
371+
<sgds-data-table-row id="sort-row-4">
372+
<sgds-data-table-cell>4</sgds-data-table-cell>
373+
<sgds-data-table-cell>Ben</sgds-data-table-cell>
374+
<sgds-data-table-cell>Engineer</sgds-data-table-cell>
375+
<sgds-data-table-cell>88</sgds-data-table-cell>
376+
</sgds-data-table-row>
377+
</sgds-data-table>
378+
</div>
379+
275380
<script type="module">
276381
// Set rowData on multi-select rows
277382
document.getElementById("ms-row-1").rowData = { id: 1, name: "John Doe" };
@@ -282,4 +387,72 @@ <h3>Expandable + Multi-select</h3>
282387
document.getElementById("ems-row-1").rowData = { id: 1, name: "John Doe" };
283388
document.getElementById("ems-row-2").rowData = { id: 2, name: "Jane Doe" };
284389
document.getElementById("ems-row-3").rowData = { id: 3, name: "Bob Smith" };
390+
391+
// Set rowData for sortable table head demo (sortKey)
392+
document.getElementById("sort-row-1").rowData = { id: 1, name: "Lina", role: "Engineer", score: 82 };
393+
document.getElementById("sort-row-2").rowData = { id: 2, name: "Adam", role: "Designer", score: 90 };
394+
document.getElementById("sort-row-3").rowData = { id: 3, name: "Nora", role: "Manager", score: 75 };
395+
document.getElementById("sort-row-4").rowData = { id: 4, name: "Ben", role: "Engineer", score: 88 };
396+
397+
// Mock API-driven pagination demo for server mode table
398+
const serverTable = document.getElementById("server-table");
399+
const totalRows = 50;
400+
const regions = ["North", "South", "East", "West", "Central"];
401+
402+
const mockApi = ({ page, itemsPerPage }) => {
403+
const start = (page - 1) * itemsPerPage;
404+
const end = Math.min(start + itemsPerPage, totalRows);
405+
406+
const rows = Array.from({ length: Math.max(0, end - start) }, (_, index) => {
407+
const id = start + index + 1;
408+
return {
409+
id,
410+
name: `Citizen ${id}`,
411+
region: regions[id % regions.length]
412+
};
413+
});
414+
415+
return new Promise(resolve => {
416+
setTimeout(() => {
417+
resolve({ total: totalRows, rows });
418+
}, 350);
419+
});
420+
};
421+
422+
const renderServerRows = rows => {
423+
const headerRow = serverTable.querySelector("sgds-data-table-row");
424+
if (!headerRow) return;
425+
426+
const bodyMarkup = rows
427+
.map(
428+
row => `
429+
<sgds-data-table-row>
430+
<sgds-data-table-cell>${row.id}</sgds-data-table-cell>
431+
<sgds-data-table-cell>${row.name}</sgds-data-table-cell>
432+
<sgds-data-table-cell>${row.region}</sgds-data-table-cell>
433+
</sgds-data-table-row>`
434+
)
435+
.join("");
436+
437+
serverTable.innerHTML = `${headerRow.outerHTML}${bodyMarkup}`;
438+
};
439+
440+
const loadServerPage = async page => {
441+
const itemsPerPage = Number(serverTable.itemsPerPage || 10);
442+
serverTable.isLoading = true;
443+
444+
try {
445+
const response = await mockApi({ page, itemsPerPage });
446+
serverTable.dataLength = response.total;
447+
renderServerRows(response.rows);
448+
} finally {
449+
serverTable.isLoading = false;
450+
}
451+
};
452+
453+
serverTable.addEventListener("sgds-page-change", async () => {
454+
await loadServerPage(Number(serverTable.currentPage || 1));
455+
});
456+
457+
loadServerPage(1);
285458
</script>

skills/sgds-components/SKILL.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
name: "sgds-components"
3-
description: "Complete reference for all SGDS web components including installation and framework integration. Use when users ask about any <sgds-*> component — accordion, alert, badge, breadcrumb, button, card, checkbox, close-button, combo-box, datepicker, description-list, divider, drawer, dropdown, file-upload, footer, icon, icon-button, icon-card, icon-list, image-card, input, link, mainnav, masthead, modal, overflow-menu, pagination, progress-bar, quantity-toggle, radio, select, sidebar, sidenav, skeleton, spinner, stepper, subnav, switch, system-banner, tab, table, table-of-contents, textarea, thumbnail-card, toast, or tooltip. Also covers React 19+, React ≤18, Vue, Angular, and Next.js integration."
3+
description: "Complete reference for all SGDS web components including installation and framework integration. Use when users ask about any <sgds-*> component — accordion, alert, badge, breadcrumb, button, card, checkbox, close-button, combo-box, data-table, datepicker, description-list, divider, drawer, dropdown, file-upload, footer, icon, icon-button, icon-card, icon-list, image-card, input, link, mainnav, masthead, modal, overflow-menu, pagination, progress-bar, quantity-toggle, radio, select, sidebar, sidenav, skeleton, spinner, stepper, subnav, switch, system-banner, tab, table, table-of-contents, textarea, thumbnail-card, toast, or tooltip. Also covers React 19+, React ≤18, Vue, Angular, and Next.js integration."
44
metadata:
55
author: singapore-design-system
66
version: "0.0.0"
@@ -210,6 +210,7 @@ When a user reports unexpected component behaviour (wrong event, property not re
210210
| **Content** | Image Card | [→ reference/image-card.md](reference/image-card.md) |
211211
| **Content** | Thumbnail Card | [→ reference/thumbnail-card.md](reference/thumbnail-card.md) |
212212
| **Content** | Description List | [→ reference/description-list.md](reference/description-list.md) |
213+
| **Content** | Data Table | [→ reference/data-table.md](reference/data-table.md) |
213214
| **Content** | Icon | [→ reference/icon.md](reference/icon.md) |
214215
| **Content** | Icon List | [→ reference/icon-list.md](reference/icon-list.md) |
215216
| **Content** | Table | [→ reference/table.md](reference/table.md) |
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
# SGDS Data Table Component Skill
2+
3+
`<sgds-data-table>` displays structured tabular data using composed row, header, and cell elements.
4+
It supports client-side pagination, server-driven pagination, sortable headers, row selection, expandable rows, and server loading state.
5+
6+
## Usage Guideline
7+
8+
### When to use
9+
10+
- When your data needs table semantics with SGDS-consistent interactions and styling.
11+
- When you need sortable columns based on slotted cells or rowData keys.
12+
- When you need server-driven paging with loading feedback.
13+
- When rows require selection (checkboxes) or expandable detail panels.
14+
15+
### When NOT to use
16+
17+
- For very simple key-value presentation with no tabular interactions.
18+
- For dense data visualisation where charts are more appropriate.
19+
20+
## Quick Decision Guide
21+
22+
- Static list with local pagination and sorting: use `mode="client"` (default).
23+
- Data from API with backend pagination: use `mode="server"` and listen to `sgds-page-change`.
24+
- Need loading indicator while fetching server data: set `isLoading` to `true`.
25+
- Need numeric columns aligned right: set `textAlign="right"` on the corresponding `sgds-data-table-head`.
26+
27+
```html
28+
<sgds-data-table dataLength="3" itemsPerPage="5" currentPage="1">
29+
<sgds-data-table-row>
30+
<sgds-data-table-head sortable sortKey="id">ID</sgds-data-table-head>
31+
<sgds-data-table-head sortable sortKey="name">Name</sgds-data-table-head>
32+
<sgds-data-table-head textAlign="right">Amount</sgds-data-table-head>
33+
</sgds-data-table-row>
34+
<sgds-data-table-row>
35+
<sgds-data-table-cell>1</sgds-data-table-cell>
36+
<sgds-data-table-cell>Amy</sgds-data-table-cell>
37+
<sgds-data-table-cell>125.00</sgds-data-table-cell>
38+
</sgds-data-table-row>
39+
</sgds-data-table>
40+
```
41+
42+
## API Summary
43+
44+
### `<sgds-data-table>`
45+
46+
| Attribute | Type | Default | Purpose |
47+
| --- | --- | --- | --- |
48+
| `multiSelect` | boolean | `false` | Shows a checkbox column for row selection |
49+
| `dataLength` | number | `0` | Total number of rows (especially for server mode) |
50+
| `currentPage` | number | `1` | Current page number |
51+
| `itemsPerPage` | number | `5` | Rows per page |
52+
| `footerText` | string | `""` | Replaces default summary text in footer |
53+
| `hideFooter` | boolean | `false` | Hides summary and pagination footer |
54+
| `mode` | `"client" \| "server"` | `"client"` | Pagination mode |
55+
| `isLoading` | boolean | `false` | Shows loading state in server mode |
56+
57+
### `<sgds-data-table-head>`
58+
59+
| Attribute | Type | Default | Purpose |
60+
| --- | --- | --- | --- |
61+
| `sortable` | boolean | `true` | Enables sort toggle on click |
62+
| `sortKey` | string | `""` | Row data key used for sorting |
63+
| `textAlign` | `"left" \| "right"` | `"left"` | Alignment for header and same body column |
64+
| `width` | string || Column width |
65+
| `colspan` | number || Number of columns spanned |
66+
| `rowspan` | number || Number of rows spanned |
67+
68+
## Slots
69+
70+
### Data Table Slots
71+
72+
| Slot | Purpose |
73+
| --- | --- |
74+
| default | Accepts `sgds-data-table-row` entries for header and body rows |
75+
76+
### Data Table Row Slots
77+
78+
| Slot | Purpose |
79+
| --- | --- |
80+
| default | Accepts `sgds-data-table-head` and `sgds-data-table-cell` |
81+
| `expandable-content` | Content displayed in expanded detail row |
82+
83+
## Events
84+
85+
| Event | When |
86+
| --- | --- |
87+
| `sgds-page-change` | Pagination page changes |
88+
| `sgds-row-select` | Selection state changes for row checkboxes |
89+
| `sgds-sort` | A sortable header changes sort direction |
90+
| `sgds-show` | Expandable row starts opening |
91+
| `sgds-after-show` | Expandable row finishes opening |
92+
| `sgds-hide` | Expandable row starts closing |
93+
| `sgds-after-hide` | Expandable row finishes closing |
94+
95+
## Server Mode Example
96+
97+
```html
98+
<sgds-data-table id="users-table" mode="server" dataLength="100" itemsPerPage="10" currentPage="1">
99+
<sgds-data-table-row>
100+
<sgds-data-table-head>ID</sgds-data-table-head>
101+
<sgds-data-table-head>Name</sgds-data-table-head>
102+
</sgds-data-table-row>
103+
</sgds-data-table>
104+
105+
<script type="module">
106+
const table = document.getElementById("users-table");
107+
108+
async function loadPage(page) {
109+
table.isLoading = true;
110+
try {
111+
const response = await fetch(`/api/users?page=${page}`);
112+
const { total, rows } = await response.json();
113+
table.dataLength = total;
114+
115+
const headerRow = table.querySelector("sgds-data-table-row");
116+
table.innerHTML = `${headerRow.outerHTML}${rows
117+
.map(
118+
row => `
119+
<sgds-data-table-row>
120+
<sgds-data-table-cell>${row.id}</sgds-data-table-cell>
121+
<sgds-data-table-cell>${row.name}</sgds-data-table-cell>
122+
</sgds-data-table-row>`
123+
)
124+
.join("")}`;
125+
} finally {
126+
table.isLoading = false;
127+
}
128+
}
129+
130+
table.addEventListener("sgds-page-change", () => {
131+
loadPage(Number(table.currentPage || 1));
132+
});
133+
134+
loadPage(1);
135+
</script>
136+
```
137+
138+
---
139+
140+
**For AI agents**:
141+
142+
1. Always provide `dataLength`, `currentPage`, and `itemsPerPage` together for predictable pagination.
143+
2. In server mode, manage `isLoading` around fetch calls and update row markup externally.
144+
3. Use `textAlign` on header cells only; body alignment is inherited by column.
145+
4. Keep non-sortable control columns (expand/checkbox) unsortable.
146+
5. Prefer setting `sortKey` when rowData is provided for stable sorting behavior.

src/components/Checkbox/sgds-checkbox.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { defaultValue } from "../../utils/defaultvalue";
99
import { SgdsFormControl } from "../../utils/formSubmitController";
1010
import { SgdsFormValidatorMixin } from "../../utils/validatorMixin";
1111
import { watch } from "../../utils/watch";
12+
import { HasSlotController } from "../../utils/slot";
1213
import checkboxStyle from "./checkbox.css";
1314
import formCheckStyles from "../../styles/form-check.css";
1415

@@ -28,6 +29,8 @@ import formCheckStyles from "../../styles/form-check.css";
2829
export class SgdsCheckbox extends SgdsFormValidatorMixin(FormControlElement) implements SgdsFormControl {
2930
static styles = [...FormControlElement.styles, formCheckStyles, checkboxStyle];
3031

32+
private readonly hasSlotController = new HasSlotController(this, "[default]");
33+
3134
/**@internal */
3235
static dependencies = {
3336
"sgds-icon": SgdsIcon
@@ -183,6 +186,7 @@ export class SgdsCheckbox extends SgdsFormValidatorMixin(FormControlElement) imp
183186
render() {
184187
const displayFeedbackStyle = this.hasFeedback === "both" || this.hasFeedback === "style";
185188
const displayFeedbackText = this.hasFeedback === "both" || this.hasFeedback === "text";
189+
const hasDefaultSlot = this.hasSlotController.test("[default]");
186190

187191
return html`
188192
<div class="form-check">
@@ -208,7 +212,9 @@ export class SgdsCheckbox extends SgdsFormValidatorMixin(FormControlElement) imp
208212
@blur=${this._handleBlur}
209213
@focus=${this._handleFocus}
210214
/>
211-
<label for="${this._controlId}" class="form-check-label" id="${this._labelId}"><slot></slot></label>
215+
${hasDefaultSlot
216+
? html`<label for="${this._controlId}" class="form-check-label" id="${this._labelId}"><slot></slot></label>`
217+
: nothing}
212218
</div>
213219
${displayFeedbackText && this.invalid
214220
? html`

0 commit comments

Comments
 (0)