CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-material-design-lite

Material Design Components in CSS, JS and HTML providing a comprehensive implementation of Google's Material Design specification for web applications.

Pending
Quality

Pending

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

SecuritybySnyk

Pending

The risk profile of this skill

Overview
Eval results
Files

data-display-components.mddocs/

Data Display Components

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.

Capabilities

Material Data Table

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';
      }
    }
  });
}

Dynamic Data Table Management

// 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 });

Table Sorting Implementation

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);
});

Table Filtering and Search

// 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);
  });
}

Bulk Actions

// 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);
}

Data Table Constants

/**
 * 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';
}

Accessibility Features

// 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();
  }
}

docs

component-management.md

data-display-components.md

feedback-components.md

form-components.md

index.md

layout-components.md

navigation-components.md

visual-effects.md

tile.json