Data Table

A complete, production-ready data table with sorting, filtering, pagination, row selection, and responsive design.

Features Demonstrated

  • Two-way data binding - Search input, filters, page size

  • Computed properties - Filtered, sorted, and paginated data

  • repeat.for with keys - Efficient list rendering with tracking

  • Event handling - Sort, filter, pagination clicks

  • Conditional rendering - Empty states, loading states

  • Value converters - Date and number formatting

  • CSS class binding - Active sort, selected rows

  • Debouncing - Optimize search performance

Code

View Model (data-table.ts)

interface User {
  id: number;
  name: string;
  email: string;
  role: string;
  status: 'active' | 'inactive' | 'pending';
  lastLogin: Date;
  tasksCompleted: number;
}

type SortColumn = 'name' | 'email' | 'role' | 'status' | 'lastLogin' | 'tasksCompleted';
type SortDirection = 'asc' | 'desc';

export class DataTable {
  // Raw data (would normally come from API)
  private allUsers: User[] = [
    {
      id: 1,
      name: 'Alice Johnson',
      email: '[email protected]',
      role: 'Admin',
      status: 'active',
      lastLogin: new Date('2025-01-08'),
      tasksCompleted: 127
    },
    {
      id: 2,
      name: 'Bob Smith',
      email: '[email protected]',
      role: 'User',
      status: 'active',
      lastLogin: new Date('2025-01-09'),
      tasksCompleted: 89
    },
    {
      id: 3,
      name: 'Carol Williams',
      email: '[email protected]',
      role: 'Manager',
      status: 'inactive',
      lastLogin: new Date('2024-12-15'),
      tasksCompleted: 203
    },
    {
      id: 4,
      name: 'David Brown',
      email: '[email protected]',
      role: 'User',
      status: 'pending',
      lastLogin: new Date('2025-01-07'),
      tasksCompleted: 45
    },
    {
      id: 5,
      name: 'Eve Davis',
      email: '[email protected]',
      role: 'User',
      status: 'active',
      lastLogin: new Date('2025-01-09'),
      tasksCompleted: 156
    },
    // Add more sample data...
    {
      id: 6,
      name: 'Frank Miller',
      email: '[email protected]',
      role: 'Admin',
      status: 'active',
      lastLogin: new Date('2025-01-08'),
      tasksCompleted: 312
    },
    {
      id: 7,
      name: 'Grace Wilson',
      email: '[email protected]',
      role: 'Manager',
      status: 'active',
      lastLogin: new Date('2025-01-09'),
      tasksCompleted: 178
    },
    {
      id: 8,
      name: 'Henry Moore',
      email: '[email protected]',
      role: 'User',
      status: 'inactive',
      lastLogin: new Date('2024-11-20'),
      tasksCompleted: 67
    },
    {
      id: 9,
      name: 'Iris Taylor',
      email: '[email protected]',
      role: 'User',
      status: 'active',
      lastLogin: new Date('2025-01-09'),
      tasksCompleted: 234
    },
    {
      id: 10,
      name: 'Jack Anderson',
      email: '[email protected]',
      role: 'Manager',
      status: 'active',
      lastLogin: new Date('2025-01-08'),
      tasksCompleted: 189
    }
  ];

  // Filter state
  searchQuery = '';
  selectedRole: string = 'all';
  selectedStatus: string = 'all';

  // Sort state
  sortColumn: SortColumn = 'name';
  sortDirection: SortDirection = 'asc';

  // Pagination state
  currentPage = 1;
  pageSize = 5;

  // Selection state
  selectedRows = new Set<number>();

  // Loading state
  isLoading = false;

  // Computed: Filtered data
  get filteredUsers(): User[] {
    return this.allUsers.filter(user => {
      // Search filter
      const query = this.searchQuery.toLowerCase();
      const matchesSearch = !query ||
        user.name.toLowerCase().includes(query) ||
        user.email.toLowerCase().includes(query);

      // Role filter
      const matchesRole = this.selectedRole === 'all' ||
        user.role === this.selectedRole;

      // Status filter
      const matchesStatus = this.selectedStatus === 'all' ||
        user.status === this.selectedStatus;

      return matchesSearch && matchesRole && matchesStatus;
    });
  }

  // Computed: Sorted data
  get sortedUsers(): User[] {
    const sorted = [...this.filteredUsers];

    sorted.sort((a, b) => {
      let aVal: any = a[this.sortColumn];
      let bVal: any = b[this.sortColumn];

      // Handle dates
      if (aVal instanceof Date) {
        aVal = aVal.getTime();
        bVal = (bVal as Date).getTime();
      }

      // Handle strings (case-insensitive)
      if (typeof aVal === 'string') {
        aVal = aVal.toLowerCase();
        bVal = bVal.toLowerCase();
      }

      if (aVal < bVal) return this.sortDirection === 'asc' ? -1 : 1;
      if (aVal > bVal) return this.sortDirection === 'asc' ? 1 : -1;
      return 0;
    });

    return sorted;
  }

  // Computed: Paginated data
  get paginatedUsers(): User[] {
    const start = (this.currentPage - 1) * this.pageSize;
    const end = start + this.pageSize;
    return this.sortedUsers.slice(start, end);
  }

  // Computed: Pagination info
  get totalPages(): number {
    return Math.ceil(this.sortedUsers.length / this.pageSize);
  }

  get totalResults(): number {
    return this.sortedUsers.length;
  }

  get startResult(): number {
    if (this.totalResults === 0) return 0;
    return (this.currentPage - 1) * this.pageSize + 1;
  }

  get endResult(): number {
    return Math.min(this.currentPage * this.pageSize, this.totalResults);
  }

  pageSizeChanged(newValue: number | string) {
    const numeric = typeof newValue === 'string' ? Number(newValue) : newValue;
    if (typeof numeric === 'number' && !Number.isNaN(numeric) && numeric !== this.pageSize) {
      this.pageSize = numeric;
      return;
    }
    this.currentPage = 1;
  }

  get pages(): number[] {
    const pages: number[] = [];
    const maxVisible = 5;
    const half = Math.floor(maxVisible / 2);

    let start = Math.max(1, this.currentPage - half);
    let end = Math.min(this.totalPages, start + maxVisible - 1);

    // Adjust start if we're near the end
    if (end - start < maxVisible - 1) {
      start = Math.max(1, end - maxVisible + 1);
    }

    for (let i = start; i <= end; i++) {
      pages.push(i);
    }

    return pages;
  }

  // Computed: Selection state
  get allPageSelected(): boolean {
    if (this.paginatedUsers.length === 0) return false;
    return this.paginatedUsers.every(user => this.selectedRows.has(user.id));
  }

  get somePageSelected(): boolean {
    if (this.paginatedUsers.length === 0) return false;
    return this.paginatedUsers.some(user => this.selectedRows.has(user.id)) &&
      !this.allPageSelected;
  }

  // Actions
  sort(column: SortColumn) {
    if (this.sortColumn === column) {
      // Toggle direction
      this.sortDirection = this.sortDirection === 'asc' ? 'desc' : 'asc';
    } else {
      // New column, default to ascending
      this.sortColumn = column;
      this.sortDirection = 'asc';
    }
  }

  goToPage(page: number) {
    if (page < 1 || page > this.totalPages) return;
    this.currentPage = page;
  }

  nextPage() {
    this.goToPage(this.currentPage + 1);
  }

  previousPage() {
    this.goToPage(this.currentPage - 1);
  }

  toggleAllPageSelection() {
    if (this.allPageSelected) {
      // Deselect all on page
      this.paginatedUsers.forEach(user => this.selectedRows.delete(user.id));
    } else {
      // Select all on page
      this.paginatedUsers.forEach(user => this.selectedRows.add(user.id));
    }
  }

  clearSelection() {
    this.selectedRows.clear();
  }

  deleteSelected() {
    if (this.selectedRows.size === 0) return;

    const confirmed = confirm(`Delete ${this.selectedRows.size} user(s)?`);
    if (!confirmed) return;

    // Remove selected users
    this.allUsers = this.allUsers.filter(user => !this.selectedRows.has(user.id));

    // Clear selection
    this.selectedRows.clear();

    // Adjust page if needed
    if (this.currentPage > this.totalPages && this.totalPages > 0) {
      this.currentPage = this.totalPages;
    }
  }

  // Reset filters
  resetFilters() {
    this.searchQuery = '';
    this.selectedRole = 'all';
    this.selectedStatus = 'all';
    this.currentPage = 1;
  }

  // Watch for filter changes and reset to page 1
  searchQueryChanged() {
    this.currentPage = 1;
  }

  selectedRoleChanged() {
    this.currentPage = 1;
  }

  selectedStatusChanged() {
    this.currentPage = 1;
  }
}

Template (data-table.html)

Styles (data-table.css)

How It Works

Filtering Pipeline

Data flows through a pipeline:

  1. Raw data (allUsers) → all records

  2. Filtered (filteredUsers) → apply search and dropdown filters

  3. Sorted (sortedUsers) → apply column sorting

  4. Paginated (paginatedUsers) → slice for current page

Each computed property builds on the previous one, keeping the logic clean and testable.

Sorting

Click column headers to sort. The first click sorts ascending, the second descending, and subsequent clicks toggle between the two. The active sort column is highlighted.

Pagination

Smart pagination shows up to 5 page numbers with ellipsis for gaps. Always shows first and last pages. Automatically adjusts when filters reduce total pages.

Selection

  • Checkbox in header selects/deselects all rows on current page

  • Individual row checkboxes for granular selection

  • Selected rows track across pages

  • Delete selected button removes all selected users

Performance

  • Debounced search (300ms) prevents excessive filtering

  • Keyed repeat ensures efficient DOM updates

  • Computed properties cache results until dependencies change

Variations

Server-Side Pagination

For large datasets, move filtering/sorting to the server:

Inline Editing

Add edit mode for quick updates:

Column Visibility Toggle

Let users show/hide columns:

Last updated

Was this helpful?