You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

9.5 KiB

Tables

Tables are essential for displaying structured data. Concept provides both basic Bootstrap tables and advanced DataTables integration.

Basic Tables

Simple Table

<table class="table">
  <thead>
    <tr>
      <th scope="col">#</th>
      <th scope="col">Name</th>
      <th scope="col">Email</th>
      <th scope="col">Status</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th scope="row">1</th>
      <td>John Doe</td>
      <td>john@example.com</td>
      <td><span class="badge bg-success">Active</span></td>
    </tr>
  </tbody>
</table>

Table Variants

<!-- Striped rows -->
<table class="table table-striped">
  <!-- table content -->
</table>

<!-- Bordered table -->
<table class="table table-bordered">
  <!-- table content -->
</table>

<!-- Hover effect -->
<table class="table table-hover">
  <!-- table content -->
</table>

<!-- Small table -->
<table class="table table-sm">
  <!-- table content -->
</table>

Responsive Tables

<!-- Always responsive -->
<div class="table-responsive">
  <table class="table">
    <!-- table content -->
  </table>
</div>

<!-- Responsive at specific breakpoints -->
<div class="table-responsive-lg">
  <table class="table">
    <!-- table content -->
  </table>
</div>

DataTables Integration

Basic DataTable

<table id="dataTable" class="table table-striped">
  <thead>
    <tr>
      <th>Name</th>
      <th>Position</th>
      <th>Office</th>
      <th>Age</th>
      <th>Start date</th>
      <th>Salary</th>
    </tr>
  </thead>
  <tbody>
    <!-- table rows -->
  </tbody>
</table>

<script>
import DataTable from 'datatables.net-bs5';

document.addEventListener('DOMContentLoaded', function() {
  new DataTable('#dataTable');
});
</script>

DataTable with Options

new DataTable('#advancedTable', {
  // Pagination
  pageLength: 25,
  lengthMenu: [[10, 25, 50, -1], [10, 25, 50, "All"]],
  
  // Sorting
  order: [[1, 'asc']],
  
  // Search
  searching: true,
  
  // Info
  info: true,
  
  // Responsive
  responsive: true,
  
  // Language
  language: {
    search: "Search:",
    lengthMenu: "Show _MENU_ entries",
    info: "Showing _START_ to _END_ of _TOTAL_ entries",
    paginate: {
      first: "First",
      last: "Last",
      next: "Next",
      previous: "Previous"
    }
  }
});

Server-Side Processing

new DataTable('#serverTable', {
  processing: true,
  serverSide: true,
  ajax: {
    url: '/api/data',
    type: 'POST',
    data: function(d) {
      // Add custom parameters
      d.customParam = 'value';
    }
  },
  columns: [
    { data: 'name' },
    { data: 'email' },
    { data: 'status' },
    {
      data: 'id',
      render: function(data) {
        return `
          <button class="btn btn-sm btn-primary" onclick="editRecord(${data})">
            <i class="fa-solid fa-edit"></i>
          </button>
          <button class="btn btn-sm btn-danger" onclick="deleteRecord(${data})">
            <i class="fa-solid fa-trash"></i>
          </button>
        `;
      }
    }
  ]
});

Table Patterns

Table with Actions

<table class="table">
  <thead>
    <tr>
      <th>
        <input type="checkbox" class="form-check-input" id="selectAll">
      </th>
      <th>Name</th>
      <th>Email</th>
      <th>Role</th>
      <th>Actions</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>
        <input type="checkbox" class="form-check-input row-checkbox">
      </td>
      <td>John Doe</td>
      <td>john@example.com</td>
      <td>Admin</td>
      <td>
        <div class="btn-group btn-group-sm">
          <button class="btn btn-outline-primary">
            <i class="fa-solid fa-eye"></i>
          </button>
          <button class="btn btn-outline-secondary">
            <i class="fa-solid fa-edit"></i>
          </button>
          <button class="btn btn-outline-danger">
            <i class="fa-solid fa-trash"></i>
          </button>
        </div>
      </td>
    </tr>
  </tbody>
</table>

<script>
// Select all functionality
document.getElementById('selectAll').addEventListener('change', function() {
  const checkboxes = document.querySelectorAll('.row-checkbox');
  checkboxes.forEach(cb => cb.checked = this.checked);
});
</script>

Expandable Rows

<table class="table">
  <tbody>
    <tr class="expandable-row" data-target="#details1">
      <td><i class="fa-solid fa-chevron-right"></i></td>
      <td>Order #1234</td>
      <td>$150.00</td>
      <td>Completed</td>
    </tr>
    <tr id="details1" class="collapse">
      <td colspan="4">
        <div class="p-3">
          <h6>Order Details</h6>
          <p>Additional information about the order...</p>
        </div>
      </td>
    </tr>
  </tbody>
</table>

<script>
document.querySelectorAll('.expandable-row').forEach(row => {
  row.addEventListener('click', function() {
    const target = document.querySelector(this.dataset.target);
    const icon = this.querySelector('i');
    
    if (target.classList.contains('show')) {
      target.classList.remove('show');
      icon.classList.replace('fa-chevron-down', 'fa-chevron-right');
    } else {
      target.classList.add('show');
      icon.classList.replace('fa-chevron-right', 'fa-chevron-down');
    }
  });
});
</script>

Editable Table

<table class="table" id="editableTable">
  <thead>
    <tr>
      <th>Product</th>
      <th>Price</th>
      <th>Quantity</th>
      <th>Total</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td contenteditable="true">Product Name</td>
      <td contenteditable="true" class="price">10.00</td>
      <td contenteditable="true" class="quantity">1</td>
      <td class="total">10.00</td>
    </tr>
  </tbody>
</table>

<script>
// Calculate totals on edit
document.querySelectorAll('#editableTable td[contenteditable]').forEach(cell => {
  cell.addEventListener('input', function() {
    const row = this.closest('tr');
    const price = parseFloat(row.querySelector('.price').textContent) || 0;
    const quantity = parseInt(row.querySelector('.quantity').textContent) || 0;
    row.querySelector('.total').textContent = (price * quantity).toFixed(2);
  });
});
</script>

Advanced Features

Export Buttons

new DataTable('#exportTable', {
  dom: 'Bfrtip',
  buttons: [
    'copy',
    'csv',
    'excel',
    'pdf',
    {
      extend: 'print',
      text: '<i class="fa-solid fa-print"></i> Print',
      className: 'btn btn-secondary'
    }
  ]
});

Column Visibility

new DataTable('#columnToggleTable', {
  dom: 'Bfrtip',
  buttons: [
    {
      extend: 'colvis',
      text: 'Columns',
      className: 'btn btn-secondary'
    }
  ],
  columnDefs: [
    {
      targets: [2, 3],
      visible: false
    }
  ]
});

Custom Filtering

<div class="row mb-3">
  <div class="col-md-4">
    <select class="form-select" id="statusFilter">
      <option value="">All Status</option>
      <option value="Active">Active</option>
      <option value="Inactive">Inactive</option>
    </select>
  </div>
</div>

<table id="filterTable" class="table">
  <!-- table content -->
</table>

<script>
const table = new DataTable('#filterTable');

// Custom filter
document.getElementById('statusFilter').addEventListener('change', function() {
  table.column(3).search(this.value).draw();
});

// Date range filter
$.fn.dataTable.ext.search.push(function(settings, data, dataIndex) {
  const min = $('#minDate').val();
  const max = $('#maxDate').val();
  const date = data[4]; // Date column
  
  if (!min && !max) return true;
  if (!min && date <= max) return true;
  if (min <= date && !max) return true;
  if (min <= date && date <= max) return true;
  
  return false;
});
</script>

Table Styling

Custom Table Styles

// Compact table
.table-compact {
  td, th {
    padding: 0.5rem;
    font-size: 0.875rem;
  }
}

// Fixed header
.table-fixed-header {
  thead {
    position: sticky;
    top: 0;
    background-color: $white;
    z-index: 10;
    box-shadow: 0 2px 2px -1px rgba(0, 0, 0, 0.1);
  }
}

// Highlight on hover
.table-highlight tbody tr:hover {
  background-color: rgba($primary, 0.05);
  cursor: pointer;
}

Performance Optimization

Virtual Scrolling

new DataTable('#largeTable', {
  scrollY: '400px',
  scrollCollapse: true,
  scroller: true,
  deferRender: true,
  ajax: 'large-dataset.json'
});

Lazy Loading

let page = 1;
const pageSize = 50;

function loadMoreRows() {
  fetch(`/api/data?page=${page}&size=${pageSize}`)
    .then(response => response.json())
    .then(data => {
      const tbody = document.querySelector('#infiniteTable tbody');
      data.forEach(row => {
        tbody.insertAdjacentHTML('beforeend', createTableRow(row));
      });
      page++;
    });
}

// Intersection Observer for infinite scroll
const observer = new IntersectionObserver(entries => {
  if (entries[0].isIntersecting) {
    loadMoreRows();
  }
});

observer.observe(document.querySelector('#loadMoreTrigger'));

Accessibility

  • Use proper table structure with thead, tbody
  • Include scope attributes on th elements
  • Provide table captions when needed
  • Ensure keyboard navigation works
  • Use aria-label for action buttons
  • Maintain focus management

Best Practices

DO:

  • Use semantic HTML table elements
  • Make tables responsive
  • Provide sorting/filtering for large datasets
  • Include loading states
  • Show empty states when no data
  • Use appropriate column widths

DON'T:

  • Don't use tables for layout
  • Don't load thousands of rows at once
  • Don't forget mobile experience
  • Don't make cells too cramped
  • Don't use tables for simple lists