0
# Data Display Components
1
2
Components for displaying structured data including data tables with sorting and selection capabilities. These components enhance standard HTML tables with Material Design styling and interactive functionality.
3
4
## Capabilities
5
6
### Material Data Table
7
8
Enhanced data table with selection capabilities, Material Design styling, and automatic row selection management.
9
10
```javascript { .api }
11
/**
12
* Material Design data table component
13
* CSS Class: mdl-js-data-table
14
* Widget: false
15
*/
16
interface MaterialDataTable {
17
// No public methods - behavior is entirely declarative via HTML/CSS
18
// Automatic functionality includes:
19
// - Row selection with checkboxes
20
// - Master checkbox for select all/none
21
// - Visual feedback for selected rows
22
// - Keyboard navigation support
23
}
24
```
25
26
**HTML Structure:**
27
28
```html
29
<!-- Basic data table -->
30
<table class="mdl-data-table mdl-js-data-table">
31
<thead>
32
<tr>
33
<th class="mdl-data-table__cell--non-numeric">Material</th>
34
<th>Quantity</th>
35
<th>Unit price</th>
36
</tr>
37
</thead>
38
<tbody>
39
<tr>
40
<td class="mdl-data-table__cell--non-numeric">Acrylic (Transparent)</td>
41
<td>25</td>
42
<td>$2.90</td>
43
</tr>
44
<tr>
45
<td class="mdl-data-table__cell--non-numeric">Plywood (Birch)</td>
46
<td>50</td>
47
<td>$1.25</td>
48
</tr>
49
<tr>
50
<td class="mdl-data-table__cell--non-numeric">Laminate (Gold on Blue)</td>
51
<td>10</td>
52
<td>$2.35</td>
53
</tr>
54
</tbody>
55
</table>
56
57
<!-- Selectable data table -->
58
<table class="mdl-data-table mdl-js-data-table mdl-data-table--selectable">
59
<thead>
60
<tr>
61
<th class="mdl-data-table__cell--non-numeric">Material</th>
62
<th>Quantity</th>
63
<th>Unit price</th>
64
</tr>
65
</thead>
66
<tbody>
67
<tr>
68
<td class="mdl-data-table__cell--non-numeric">Acrylic (Transparent)</td>
69
<td>25</td>
70
<td>$2.90</td>
71
</tr>
72
<tr>
73
<td class="mdl-data-table__cell--non-numeric">Plywood (Birch)</td>
74
<td>50</td>
75
<td>$1.25</td>
76
</tr>
77
<tr>
78
<td class="mdl-data-table__cell--non-numeric">Laminate (Gold on Blue)</td>
79
<td>10</td>
80
<td>$2.35</td>
81
</tr>
82
</tbody>
83
</table>
84
```
85
86
**Usage Examples:**
87
88
Since the Material Data Table has no programmatic API, interaction is handled through DOM events:
89
90
```javascript
91
// Listen for row selection changes
92
document.addEventListener('change', (event) => {
93
if (event.target.matches('.mdl-data-table__select')) {
94
const row = event.target.closest('tr');
95
const isSelected = event.target.checked;
96
97
console.log('Row selection changed:', {
98
row: row,
99
selected: isSelected,
100
data: getRowData(row)
101
});
102
103
updateSelectionActions();
104
}
105
});
106
107
// Get data from a table row
108
function getRowData(row) {
109
const cells = row.querySelectorAll('td:not(.mdl-data-table__cell--checkbox)');
110
return Array.from(cells).map(cell => cell.textContent.trim());
111
}
112
113
// Handle master checkbox (select all/none)
114
document.addEventListener('change', (event) => {
115
if (event.target.matches('thead .mdl-data-table__select')) {
116
const table = event.target.closest('table');
117
const allRowCheckboxes = table.querySelectorAll('tbody .mdl-data-table__select');
118
const isChecked = event.target.checked;
119
120
allRowCheckboxes.forEach(checkbox => {
121
checkbox.checked = isChecked;
122
// Trigger change event for each checkbox
123
checkbox.dispatchEvent(new Event('change', { bubbles: true }));
124
});
125
}
126
});
127
128
// Get all selected rows
129
function getSelectedRows(table) {
130
const selectedRows = [];
131
const checkboxes = table.querySelectorAll('tbody .mdl-data-table__select:checked');
132
133
checkboxes.forEach(checkbox => {
134
const row = checkbox.closest('tr');
135
selectedRows.push({
136
row: row,
137
data: getRowData(row),
138
index: Array.from(row.parentNode.children).indexOf(row)
139
});
140
});
141
142
return selectedRows;
143
}
144
145
// Update UI based on selection
146
function updateSelectionActions() {
147
const tables = document.querySelectorAll('.mdl-data-table--selectable');
148
149
tables.forEach(table => {
150
const selectedRows = getSelectedRows(table);
151
const actionBar = document.querySelector(`[data-table="${table.id}"] .selection-actions`);
152
153
if (actionBar) {
154
if (selectedRows.length > 0) {
155
actionBar.style.display = 'block';
156
actionBar.querySelector('.selection-count').textContent = selectedRows.length;
157
} else {
158
actionBar.style.display = 'none';
159
}
160
}
161
});
162
}
163
```
164
165
### Dynamic Data Table Management
166
167
```javascript
168
// Create data table dynamically
169
function createDataTable(containerId, data, options = {}) {
170
const container = document.getElementById(containerId);
171
if (!container) return null;
172
173
const table = document.createElement('table');
174
table.className = 'mdl-data-table mdl-js-data-table';
175
176
if (options.selectable) {
177
table.classList.add('mdl-data-table--selectable');
178
}
179
180
// Create header
181
if (data.length > 0) {
182
const thead = document.createElement('thead');
183
const headerRow = document.createElement('tr');
184
185
Object.keys(data[0]).forEach(key => {
186
const th = document.createElement('th');
187
188
// Determine if column is numeric
189
const isNumeric = data.every(row =>
190
typeof row[key] === 'number' ||
191
!isNaN(parseFloat(row[key]))
192
);
193
194
if (!isNumeric) {
195
th.classList.add('mdl-data-table__cell--non-numeric');
196
}
197
198
th.textContent = formatHeaderText(key);
199
headerRow.appendChild(th);
200
});
201
202
thead.appendChild(headerRow);
203
table.appendChild(thead);
204
}
205
206
// Create body
207
const tbody = document.createElement('tbody');
208
209
data.forEach(rowData => {
210
const row = document.createElement('tr');
211
212
Object.values(rowData).forEach((value, index) => {
213
const td = document.createElement('td');
214
215
// Apply non-numeric class if needed
216
const isNumeric = typeof value === 'number' || !isNaN(parseFloat(value));
217
if (!isNumeric) {
218
td.classList.add('mdl-data-table__cell--non-numeric');
219
}
220
221
td.textContent = value;
222
row.appendChild(td);
223
});
224
225
tbody.appendChild(row);
226
});
227
228
table.appendChild(tbody);
229
container.appendChild(table);
230
231
// Upgrade table
232
componentHandler.upgradeElement(table);
233
234
return table;
235
}
236
237
function formatHeaderText(key) {
238
return key.replace(/([A-Z])/g, ' $1')
239
.replace(/^./, str => str.toUpperCase())
240
.trim();
241
}
242
243
// Usage
244
const sampleData = [
245
{ material: 'Acrylic (Transparent)', quantity: 25, unitPrice: 2.90 },
246
{ material: 'Plywood (Birch)', quantity: 50, unitPrice: 1.25 },
247
{ material: 'Laminate (Gold on Blue)', quantity: 10, unitPrice: 2.35 }
248
];
249
250
createDataTable('table-container', sampleData, { selectable: true });
251
```
252
253
### Table Sorting Implementation
254
255
Since MDL data tables don't include built-in sorting, here's how to add it:
256
257
```javascript
258
// Add sorting functionality to data tables
259
function makeTableSortable(table) {
260
const headers = table.querySelectorAll('th');
261
262
headers.forEach((header, columnIndex) => {
263
// Skip checkbox column
264
if (header.classList.contains('mdl-data-table__cell--checkbox')) {
265
return;
266
}
267
268
header.style.cursor = 'pointer';
269
header.style.userSelect = 'none';
270
271
// Add sort indicator
272
const sortIcon = document.createElement('i');
273
sortIcon.className = 'material-icons sort-icon';
274
sortIcon.textContent = 'unfold_more';
275
sortIcon.style.fontSize = '16px';
276
sortIcon.style.marginLeft = '4px';
277
sortIcon.style.color = '#999';
278
header.appendChild(sortIcon);
279
280
header.addEventListener('click', () => {
281
sortTable(table, columnIndex, header);
282
});
283
});
284
}
285
286
function sortTable(table, columnIndex, header) {
287
const tbody = table.querySelector('tbody');
288
const rows = Array.from(tbody.querySelectorAll('tr'));
289
290
// Determine sort direction
291
const currentDirection = header.dataset.sortDirection || 'asc';
292
const newDirection = currentDirection === 'asc' ? 'desc' : 'asc';
293
294
// Clear all sort indicators
295
table.querySelectorAll('th .sort-icon').forEach(icon => {
296
icon.textContent = 'unfold_more';
297
icon.style.color = '#999';
298
});
299
300
// Update current header
301
const sortIcon = header.querySelector('.sort-icon');
302
sortIcon.textContent = newDirection === 'asc' ? 'keyboard_arrow_up' : 'keyboard_arrow_down';
303
sortIcon.style.color = '#333';
304
header.dataset.sortDirection = newDirection;
305
306
// Sort rows
307
rows.sort((a, b) => {
308
const aCell = a.cells[columnIndex];
309
const bCell = b.cells[columnIndex];
310
311
// Skip checkbox cells
312
const aText = aCell.classList.contains('mdl-data-table__cell--checkbox') ?
313
'' : aCell.textContent.trim();
314
const bText = bCell.classList.contains('mdl-data-table__cell--checkbox') ?
315
'' : bCell.textContent.trim();
316
317
// Try numeric comparison first
318
const aNum = parseFloat(aText.replace(/[^0-9.-]/g, ''));
319
const bNum = parseFloat(bText.replace(/[^0-9.-]/g, ''));
320
321
let comparison = 0;
322
323
if (!isNaN(aNum) && !isNaN(bNum)) {
324
comparison = aNum - bNum;
325
} else {
326
comparison = aText.localeCompare(bText);
327
}
328
329
return newDirection === 'asc' ? comparison : -comparison;
330
});
331
332
// Re-append sorted rows
333
rows.forEach(row => tbody.appendChild(row));
334
335
// Update selection state if needed
336
updateSelectionActions();
337
}
338
339
// Make all data tables sortable
340
document.addEventListener('DOMContentLoaded', () => {
341
document.querySelectorAll('.mdl-data-table').forEach(makeTableSortable);
342
});
343
```
344
345
### Table Filtering and Search
346
347
```javascript
348
// Add search/filter functionality
349
function addTableSearch(table, searchInputId) {
350
const searchInput = document.getElementById(searchInputId);
351
if (!searchInput) return;
352
353
const tbody = table.querySelector('tbody');
354
const rows = Array.from(tbody.querySelectorAll('tr'));
355
356
searchInput.addEventListener('input', (event) => {
357
const searchTerm = event.target.value.toLowerCase();
358
359
rows.forEach(row => {
360
const cells = Array.from(row.cells);
361
const rowText = cells
362
.filter(cell => !cell.classList.contains('mdl-data-table__cell--checkbox'))
363
.map(cell => cell.textContent.toLowerCase())
364
.join(' ');
365
366
const matches = rowText.includes(searchTerm);
367
row.style.display = matches ? '' : 'none';
368
});
369
370
updateRowCount(table);
371
});
372
}
373
374
function updateRowCount(table) {
375
const tbody = table.querySelector('tbody');
376
const visibleRows = tbody.querySelectorAll('tr:not([style*="display: none"])');
377
const totalRows = tbody.querySelectorAll('tr');
378
379
// Update row count display if it exists
380
const countDisplay = document.querySelector(`[data-table="${table.id}"] .row-count`);
381
if (countDisplay) {
382
countDisplay.textContent = `Showing ${visibleRows.length} of ${totalRows.length} rows`;
383
}
384
}
385
386
// Column-specific filtering
387
function addColumnFilter(table, columnIndex, filterId) {
388
const filterSelect = document.getElementById(filterId);
389
if (!filterSelect) return;
390
391
const tbody = table.querySelector('tbody');
392
const rows = Array.from(tbody.querySelectorAll('tr'));
393
394
// Populate filter options
395
const uniqueValues = new Set();
396
rows.forEach(row => {
397
const cell = row.cells[columnIndex];
398
if (cell && !cell.classList.contains('mdl-data-table__cell--checkbox')) {
399
uniqueValues.add(cell.textContent.trim());
400
}
401
});
402
403
// Add "All" option
404
const allOption = document.createElement('option');
405
allOption.value = '';
406
allOption.textContent = 'All';
407
filterSelect.appendChild(allOption);
408
409
// Add unique values as options
410
Array.from(uniqueValues).sort().forEach(value => {
411
const option = document.createElement('option');
412
option.value = value;
413
option.textContent = value;
414
filterSelect.appendChild(option);
415
});
416
417
// Handle filter changes
418
filterSelect.addEventListener('change', (event) => {
419
const filterValue = event.target.value;
420
421
rows.forEach(row => {
422
const cell = row.cells[columnIndex];
423
if (cell && !cell.classList.contains('mdl-data-table__cell--checkbox')) {
424
const cellValue = cell.textContent.trim();
425
const matches = !filterValue || cellValue === filterValue;
426
row.style.display = matches ? '' : 'none';
427
}
428
});
429
430
updateRowCount(table);
431
});
432
}
433
```
434
435
### Bulk Actions
436
437
```javascript
438
// Implement bulk actions for selected rows
439
function setupBulkActions(table, actionsConfig) {
440
const actionBar = document.createElement('div');
441
actionBar.className = 'bulk-actions';
442
actionBar.style.display = 'none';
443
actionBar.innerHTML = `
444
<span class="selection-count">0</span> selected
445
<div class="action-buttons"></div>
446
`;
447
448
const buttonContainer = actionBar.querySelector('.action-buttons');
449
450
// Create action buttons
451
actionsConfig.forEach(action => {
452
const button = document.createElement('button');
453
button.className = 'mdl-button mdl-js-button mdl-button--raised';
454
button.textContent = action.label;
455
button.addEventListener('click', () => {
456
const selectedRows = getSelectedRows(table);
457
action.handler(selectedRows, table);
458
});
459
buttonContainer.appendChild(button);
460
461
// Upgrade button
462
componentHandler.upgradeElement(button);
463
});
464
465
// Insert action bar before table
466
table.parentNode.insertBefore(actionBar, table);
467
468
// Update action bar visibility based on selection
469
const originalUpdateFunction = window.updateSelectionActions || (() => {});
470
window.updateSelectionActions = function() {
471
originalUpdateFunction();
472
473
const selectedRows = getSelectedRows(table);
474
const selectionCount = actionBar.querySelector('.selection-count');
475
476
if (selectedRows.length > 0) {
477
actionBar.style.display = 'block';
478
selectionCount.textContent = selectedRows.length;
479
} else {
480
actionBar.style.display = 'none';
481
}
482
};
483
}
484
485
// Usage example
486
setupBulkActions(document.querySelector('#my-table'), [
487
{
488
label: 'Delete',
489
handler: (selectedRows, table) => {
490
if (confirm(`Delete ${selectedRows.length} items?`)) {
491
selectedRows.forEach(({ row }) => {
492
row.remove();
493
});
494
updateSelectionActions();
495
}
496
}
497
},
498
{
499
label: 'Export',
500
handler: (selectedRows) => {
501
const data = selectedRows.map(({ data }) => data);
502
exportToCSV(data);
503
}
504
}
505
]);
506
507
function exportToCSV(data) {
508
if (data.length === 0) return;
509
510
const csvContent = data.map(row =>
511
row.map(cell => `"${cell}"`).join(',')
512
).join('\n');
513
514
const blob = new Blob([csvContent], { type: 'text/csv' });
515
const url = URL.createObjectURL(blob);
516
517
const a = document.createElement('a');
518
a.href = url;
519
a.download = 'export.csv';
520
a.click();
521
522
URL.revokeObjectURL(url);
523
}
524
```
525
526
## Data Table Constants
527
528
```javascript { .api }
529
/**
530
* Material Data Table CSS classes and selectors
531
*/
532
interface DataTableConstants {
533
/** Main data table class */
534
DATA_TABLE: 'mdl-data-table';
535
536
/** Selectable data table modifier */
537
SELECTABLE: 'mdl-data-table--selectable';
538
539
/** Checkbox cell class */
540
SELECT_ELEMENT: 'mdl-data-table__select';
541
542
/** Non-numeric cell class */
543
NON_NUMERIC: 'mdl-data-table__cell--non-numeric';
544
545
/** Selected row state class */
546
IS_SELECTED: 'is-selected';
547
548
/** Upgraded component state class */
549
IS_UPGRADED: 'is-upgraded';
550
}
551
```
552
553
## Accessibility Features
554
555
```javascript
556
// Enhance table accessibility
557
function enhanceTableAccessibility(table) {
558
// Add ARIA labels
559
table.setAttribute('role', 'table');
560
561
const thead = table.querySelector('thead');
562
if (thead) {
563
thead.setAttribute('role', 'rowgroup');
564
565
thead.querySelectorAll('tr').forEach(row => {
566
row.setAttribute('role', 'row');
567
row.querySelectorAll('th').forEach(header => {
568
header.setAttribute('role', 'columnheader');
569
});
570
});
571
}
572
573
const tbody = table.querySelector('tbody');
574
if (tbody) {
575
tbody.setAttribute('role', 'rowgroup');
576
577
tbody.querySelectorAll('tr').forEach((row, index) => {
578
row.setAttribute('role', 'row');
579
row.setAttribute('aria-rowindex', index + 1);
580
581
row.querySelectorAll('td').forEach((cell, cellIndex) => {
582
cell.setAttribute('role', 'cell');
583
cell.setAttribute('aria-colindex', cellIndex + 1);
584
});
585
});
586
}
587
588
// Add keyboard navigation
589
table.addEventListener('keydown', (event) => {
590
if (event.target.matches('.mdl-data-table__select')) {
591
handleCheckboxKeyboard(event);
592
}
593
});
594
}
595
596
function handleCheckboxKeyboard(event) {
597
const checkbox = event.target;
598
const row = checkbox.closest('tr');
599
600
switch (event.key) {
601
case 'ArrowUp':
602
event.preventDefault();
603
focusPreviousCheckbox(row);
604
break;
605
606
case 'ArrowDown':
607
event.preventDefault();
608
focusNextCheckbox(row);
609
break;
610
611
case ' ':
612
event.preventDefault();
613
checkbox.click();
614
break;
615
}
616
}
617
618
function focusPreviousCheckbox(currentRow) {
619
const tbody = currentRow.parentNode;
620
const rows = Array.from(tbody.querySelectorAll('tr'));
621
const currentIndex = rows.indexOf(currentRow);
622
623
if (currentIndex > 0) {
624
const prevCheckbox = rows[currentIndex - 1].querySelector('.mdl-data-table__select');
625
if (prevCheckbox) prevCheckbox.focus();
626
}
627
}
628
629
function focusNextCheckbox(currentRow) {
630
const tbody = currentRow.parentNode;
631
const rows = Array.from(tbody.querySelectorAll('tr'));
632
const currentIndex = rows.indexOf(currentRow);
633
634
if (currentIndex < rows.length - 1) {
635
const nextCheckbox = rows[currentIndex + 1].querySelector('.mdl-data-table__select');
636
if (nextCheckbox) nextCheckbox.focus();
637
}
638
}
639
```