Material Design Components in CSS, JS and HTML providing a comprehensive implementation of Google's Material Design specification for web applications.
—
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Pending
The risk profile of this skill
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.
Enhanced data table with selection capabilities, Material Design styling, and automatic row selection management.
/**
* Material Design data table component
* CSS Class: mdl-js-data-table
* Widget: false
*/
interface MaterialDataTable {
// No public methods - behavior is entirely declarative via HTML/CSS
// Automatic functionality includes:
// - Row selection with checkboxes
// - Master checkbox for select all/none
// - Visual feedback for selected rows
// - Keyboard navigation support
}HTML Structure:
<!-- Basic data table -->
<table class="mdl-data-table mdl-js-data-table">
<thead>
<tr>
<th class="mdl-data-table__cell--non-numeric">Material</th>
<th>Quantity</th>
<th>Unit price</th>
</tr>
</thead>
<tbody>
<tr>
<td class="mdl-data-table__cell--non-numeric">Acrylic (Transparent)</td>
<td>25</td>
<td>$2.90</td>
</tr>
<tr>
<td class="mdl-data-table__cell--non-numeric">Plywood (Birch)</td>
<td>50</td>
<td>$1.25</td>
</tr>
<tr>
<td class="mdl-data-table__cell--non-numeric">Laminate (Gold on Blue)</td>
<td>10</td>
<td>$2.35</td>
</tr>
</tbody>
</table>
<!-- Selectable data table -->
<table class="mdl-data-table mdl-js-data-table mdl-data-table--selectable">
<thead>
<tr>
<th class="mdl-data-table__cell--non-numeric">Material</th>
<th>Quantity</th>
<th>Unit price</th>
</tr>
</thead>
<tbody>
<tr>
<td class="mdl-data-table__cell--non-numeric">Acrylic (Transparent)</td>
<td>25</td>
<td>$2.90</td>
</tr>
<tr>
<td class="mdl-data-table__cell--non-numeric">Plywood (Birch)</td>
<td>50</td>
<td>$1.25</td>
</tr>
<tr>
<td class="mdl-data-table__cell--non-numeric">Laminate (Gold on Blue)</td>
<td>10</td>
<td>$2.35</td>
</tr>
</tbody>
</table>Usage Examples:
Since the Material Data Table has no programmatic API, interaction is handled through DOM events:
// Listen for row selection changes
document.addEventListener('change', (event) => {
if (event.target.matches('.mdl-data-table__select')) {
const row = event.target.closest('tr');
const isSelected = event.target.checked;
console.log('Row selection changed:', {
row: row,
selected: isSelected,
data: getRowData(row)
});
updateSelectionActions();
}
});
// Get data from a table row
function getRowData(row) {
const cells = row.querySelectorAll('td:not(.mdl-data-table__cell--checkbox)');
return Array.from(cells).map(cell => cell.textContent.trim());
}
// Handle master checkbox (select all/none)
document.addEventListener('change', (event) => {
if (event.target.matches('thead .mdl-data-table__select')) {
const table = event.target.closest('table');
const allRowCheckboxes = table.querySelectorAll('tbody .mdl-data-table__select');
const isChecked = event.target.checked;
allRowCheckboxes.forEach(checkbox => {
checkbox.checked = isChecked;
// Trigger change event for each checkbox
checkbox.dispatchEvent(new Event('change', { bubbles: true }));
});
}
});
// Get all selected rows
function getSelectedRows(table) {
const selectedRows = [];
const checkboxes = table.querySelectorAll('tbody .mdl-data-table__select:checked');
checkboxes.forEach(checkbox => {
const row = checkbox.closest('tr');
selectedRows.push({
row: row,
data: getRowData(row),
index: Array.from(row.parentNode.children).indexOf(row)
});
});
return selectedRows;
}
// Update UI based on selection
function updateSelectionActions() {
const tables = document.querySelectorAll('.mdl-data-table--selectable');
tables.forEach(table => {
const selectedRows = getSelectedRows(table);
const actionBar = document.querySelector(`[data-table="${table.id}"] .selection-actions`);
if (actionBar) {
if (selectedRows.length > 0) {
actionBar.style.display = 'block';
actionBar.querySelector('.selection-count').textContent = selectedRows.length;
} else {
actionBar.style.display = 'none';
}
}
});
}// Create data table dynamically
function createDataTable(containerId, data, options = {}) {
const container = document.getElementById(containerId);
if (!container) return null;
const table = document.createElement('table');
table.className = 'mdl-data-table mdl-js-data-table';
if (options.selectable) {
table.classList.add('mdl-data-table--selectable');
}
// Create header
if (data.length > 0) {
const thead = document.createElement('thead');
const headerRow = document.createElement('tr');
Object.keys(data[0]).forEach(key => {
const th = document.createElement('th');
// Determine if column is numeric
const isNumeric = data.every(row =>
typeof row[key] === 'number' ||
!isNaN(parseFloat(row[key]))
);
if (!isNumeric) {
th.classList.add('mdl-data-table__cell--non-numeric');
}
th.textContent = formatHeaderText(key);
headerRow.appendChild(th);
});
thead.appendChild(headerRow);
table.appendChild(thead);
}
// Create body
const tbody = document.createElement('tbody');
data.forEach(rowData => {
const row = document.createElement('tr');
Object.values(rowData).forEach((value, index) => {
const td = document.createElement('td');
// Apply non-numeric class if needed
const isNumeric = typeof value === 'number' || !isNaN(parseFloat(value));
if (!isNumeric) {
td.classList.add('mdl-data-table__cell--non-numeric');
}
td.textContent = value;
row.appendChild(td);
});
tbody.appendChild(row);
});
table.appendChild(tbody);
container.appendChild(table);
// Upgrade table
componentHandler.upgradeElement(table);
return table;
}
function formatHeaderText(key) {
return key.replace(/([A-Z])/g, ' $1')
.replace(/^./, str => str.toUpperCase())
.trim();
}
// Usage
const sampleData = [
{ material: 'Acrylic (Transparent)', quantity: 25, unitPrice: 2.90 },
{ material: 'Plywood (Birch)', quantity: 50, unitPrice: 1.25 },
{ material: 'Laminate (Gold on Blue)', quantity: 10, unitPrice: 2.35 }
];
createDataTable('table-container', sampleData, { selectable: true });Since MDL data tables don't include built-in sorting, here's how to add it:
// Add sorting functionality to data tables
function makeTableSortable(table) {
const headers = table.querySelectorAll('th');
headers.forEach((header, columnIndex) => {
// Skip checkbox column
if (header.classList.contains('mdl-data-table__cell--checkbox')) {
return;
}
header.style.cursor = 'pointer';
header.style.userSelect = 'none';
// Add sort indicator
const sortIcon = document.createElement('i');
sortIcon.className = 'material-icons sort-icon';
sortIcon.textContent = 'unfold_more';
sortIcon.style.fontSize = '16px';
sortIcon.style.marginLeft = '4px';
sortIcon.style.color = '#999';
header.appendChild(sortIcon);
header.addEventListener('click', () => {
sortTable(table, columnIndex, header);
});
});
}
function sortTable(table, columnIndex, header) {
const tbody = table.querySelector('tbody');
const rows = Array.from(tbody.querySelectorAll('tr'));
// Determine sort direction
const currentDirection = header.dataset.sortDirection || 'asc';
const newDirection = currentDirection === 'asc' ? 'desc' : 'asc';
// Clear all sort indicators
table.querySelectorAll('th .sort-icon').forEach(icon => {
icon.textContent = 'unfold_more';
icon.style.color = '#999';
});
// Update current header
const sortIcon = header.querySelector('.sort-icon');
sortIcon.textContent = newDirection === 'asc' ? 'keyboard_arrow_up' : 'keyboard_arrow_down';
sortIcon.style.color = '#333';
header.dataset.sortDirection = newDirection;
// Sort rows
rows.sort((a, b) => {
const aCell = a.cells[columnIndex];
const bCell = b.cells[columnIndex];
// Skip checkbox cells
const aText = aCell.classList.contains('mdl-data-table__cell--checkbox') ?
'' : aCell.textContent.trim();
const bText = bCell.classList.contains('mdl-data-table__cell--checkbox') ?
'' : bCell.textContent.trim();
// Try numeric comparison first
const aNum = parseFloat(aText.replace(/[^0-9.-]/g, ''));
const bNum = parseFloat(bText.replace(/[^0-9.-]/g, ''));
let comparison = 0;
if (!isNaN(aNum) && !isNaN(bNum)) {
comparison = aNum - bNum;
} else {
comparison = aText.localeCompare(bText);
}
return newDirection === 'asc' ? comparison : -comparison;
});
// Re-append sorted rows
rows.forEach(row => tbody.appendChild(row));
// Update selection state if needed
updateSelectionActions();
}
// Make all data tables sortable
document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('.mdl-data-table').forEach(makeTableSortable);
});// Add search/filter functionality
function addTableSearch(table, searchInputId) {
const searchInput = document.getElementById(searchInputId);
if (!searchInput) return;
const tbody = table.querySelector('tbody');
const rows = Array.from(tbody.querySelectorAll('tr'));
searchInput.addEventListener('input', (event) => {
const searchTerm = event.target.value.toLowerCase();
rows.forEach(row => {
const cells = Array.from(row.cells);
const rowText = cells
.filter(cell => !cell.classList.contains('mdl-data-table__cell--checkbox'))
.map(cell => cell.textContent.toLowerCase())
.join(' ');
const matches = rowText.includes(searchTerm);
row.style.display = matches ? '' : 'none';
});
updateRowCount(table);
});
}
function updateRowCount(table) {
const tbody = table.querySelector('tbody');
const visibleRows = tbody.querySelectorAll('tr:not([style*="display: none"])');
const totalRows = tbody.querySelectorAll('tr');
// Update row count display if it exists
const countDisplay = document.querySelector(`[data-table="${table.id}"] .row-count`);
if (countDisplay) {
countDisplay.textContent = `Showing ${visibleRows.length} of ${totalRows.length} rows`;
}
}
// Column-specific filtering
function addColumnFilter(table, columnIndex, filterId) {
const filterSelect = document.getElementById(filterId);
if (!filterSelect) return;
const tbody = table.querySelector('tbody');
const rows = Array.from(tbody.querySelectorAll('tr'));
// Populate filter options
const uniqueValues = new Set();
rows.forEach(row => {
const cell = row.cells[columnIndex];
if (cell && !cell.classList.contains('mdl-data-table__cell--checkbox')) {
uniqueValues.add(cell.textContent.trim());
}
});
// Add "All" option
const allOption = document.createElement('option');
allOption.value = '';
allOption.textContent = 'All';
filterSelect.appendChild(allOption);
// Add unique values as options
Array.from(uniqueValues).sort().forEach(value => {
const option = document.createElement('option');
option.value = value;
option.textContent = value;
filterSelect.appendChild(option);
});
// Handle filter changes
filterSelect.addEventListener('change', (event) => {
const filterValue = event.target.value;
rows.forEach(row => {
const cell = row.cells[columnIndex];
if (cell && !cell.classList.contains('mdl-data-table__cell--checkbox')) {
const cellValue = cell.textContent.trim();
const matches = !filterValue || cellValue === filterValue;
row.style.display = matches ? '' : 'none';
}
});
updateRowCount(table);
});
}// Implement bulk actions for selected rows
function setupBulkActions(table, actionsConfig) {
const actionBar = document.createElement('div');
actionBar.className = 'bulk-actions';
actionBar.style.display = 'none';
actionBar.innerHTML = `
<span class="selection-count">0</span> selected
<div class="action-buttons"></div>
`;
const buttonContainer = actionBar.querySelector('.action-buttons');
// Create action buttons
actionsConfig.forEach(action => {
const button = document.createElement('button');
button.className = 'mdl-button mdl-js-button mdl-button--raised';
button.textContent = action.label;
button.addEventListener('click', () => {
const selectedRows = getSelectedRows(table);
action.handler(selectedRows, table);
});
buttonContainer.appendChild(button);
// Upgrade button
componentHandler.upgradeElement(button);
});
// Insert action bar before table
table.parentNode.insertBefore(actionBar, table);
// Update action bar visibility based on selection
const originalUpdateFunction = window.updateSelectionActions || (() => {});
window.updateSelectionActions = function() {
originalUpdateFunction();
const selectedRows = getSelectedRows(table);
const selectionCount = actionBar.querySelector('.selection-count');
if (selectedRows.length > 0) {
actionBar.style.display = 'block';
selectionCount.textContent = selectedRows.length;
} else {
actionBar.style.display = 'none';
}
};
}
// Usage example
setupBulkActions(document.querySelector('#my-table'), [
{
label: 'Delete',
handler: (selectedRows, table) => {
if (confirm(`Delete ${selectedRows.length} items?`)) {
selectedRows.forEach(({ row }) => {
row.remove();
});
updateSelectionActions();
}
}
},
{
label: 'Export',
handler: (selectedRows) => {
const data = selectedRows.map(({ data }) => data);
exportToCSV(data);
}
}
]);
function exportToCSV(data) {
if (data.length === 0) return;
const csvContent = data.map(row =>
row.map(cell => `"${cell}"`).join(',')
).join('\n');
const blob = new Blob([csvContent], { type: 'text/csv' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'export.csv';
a.click();
URL.revokeObjectURL(url);
}/**
* Material Data Table CSS classes and selectors
*/
interface DataTableConstants {
/** Main data table class */
DATA_TABLE: 'mdl-data-table';
/** Selectable data table modifier */
SELECTABLE: 'mdl-data-table--selectable';
/** Checkbox cell class */
SELECT_ELEMENT: 'mdl-data-table__select';
/** Non-numeric cell class */
NON_NUMERIC: 'mdl-data-table__cell--non-numeric';
/** Selected row state class */
IS_SELECTED: 'is-selected';
/** Upgraded component state class */
IS_UPGRADED: 'is-upgraded';
}// Enhance table accessibility
function enhanceTableAccessibility(table) {
// Add ARIA labels
table.setAttribute('role', 'table');
const thead = table.querySelector('thead');
if (thead) {
thead.setAttribute('role', 'rowgroup');
thead.querySelectorAll('tr').forEach(row => {
row.setAttribute('role', 'row');
row.querySelectorAll('th').forEach(header => {
header.setAttribute('role', 'columnheader');
});
});
}
const tbody = table.querySelector('tbody');
if (tbody) {
tbody.setAttribute('role', 'rowgroup');
tbody.querySelectorAll('tr').forEach((row, index) => {
row.setAttribute('role', 'row');
row.setAttribute('aria-rowindex', index + 1);
row.querySelectorAll('td').forEach((cell, cellIndex) => {
cell.setAttribute('role', 'cell');
cell.setAttribute('aria-colindex', cellIndex + 1);
});
});
}
// Add keyboard navigation
table.addEventListener('keydown', (event) => {
if (event.target.matches('.mdl-data-table__select')) {
handleCheckboxKeyboard(event);
}
});
}
function handleCheckboxKeyboard(event) {
const checkbox = event.target;
const row = checkbox.closest('tr');
switch (event.key) {
case 'ArrowUp':
event.preventDefault();
focusPreviousCheckbox(row);
break;
case 'ArrowDown':
event.preventDefault();
focusNextCheckbox(row);
break;
case ' ':
event.preventDefault();
checkbox.click();
break;
}
}
function focusPreviousCheckbox(currentRow) {
const tbody = currentRow.parentNode;
const rows = Array.from(tbody.querySelectorAll('tr'));
const currentIndex = rows.indexOf(currentRow);
if (currentIndex > 0) {
const prevCheckbox = rows[currentIndex - 1].querySelector('.mdl-data-table__select');
if (prevCheckbox) prevCheckbox.focus();
}
}
function focusNextCheckbox(currentRow) {
const tbody = currentRow.parentNode;
const rows = Array.from(tbody.querySelectorAll('tr'));
const currentIndex = rows.indexOf(currentRow);
if (currentIndex < rows.length - 1) {
const nextCheckbox = rows[currentIndex + 1].querySelector('.mdl-data-table__select');
if (nextCheckbox) nextCheckbox.focus();
}
}