Most Angular admin dashboards need a sortable, filterable, paginated table. Here's how to build one properly using Angular 21 signals and Bootstrap 5 for styling.
The Component
// data-table.component.ts import { Component, Input, computed, signal } from '@angular/core' import { CommonModule } from '@angular/common' import { FormsModule } from '@angular/forms' export interface TableColumn { key: string label: string sortable?: boolean } @Component({ selector: 'app-data-table', standalone: true, imports: [CommonModule, FormsModule], templateUrl: './data-table.component.html' }) export class DataTableComponent { @Input() columns: TableColumn[] = [] @Input() set data(value: any[]) { this._data.set(value) } private _data = signal<any[]>([]) // Search searchQuery = signal('') // Sort sortColumn = signal('') sortDirection = signal<'asc' | 'desc'>('asc') // Pagination currentPage = signal(1) pageSize = signal(10) // Filtered + sorted data processedData = computed(() => { let rows = [...this._data()] // Filter const query = this.searchQuery().toLowerCase() if (query) { rows = rows.filter(row => Object.values(row).some(val => String(val).toLowerCase().includes(query) ) ) } // Sort const col = this.sortColumn() if (col) { const dir = this.sortDirection() rows.sort((a, b) => { const av = a[col], bv = b[col] if (av < bv) return dir === 'asc' ? -1 : 1 if (av > bv) return dir === 'asc' ? 1 : -1 return 0 }) } return rows }) // Paginated slice paginatedData = computed(() => { const start = (this.currentPage() - 1) * this.pageSize() return this.processedData().slice(start, start + this.pageSize()) }) totalPages = computed(() => Math.ceil(this.processedData().length / this.pageSize()) ) pages = computed(() => Array.from({ length: this.totalPages() }, (_, i) => i + 1) ) // Sort handler sort(column: string) { if (this.sortColumn() === column) { this.sortDirection.update(d => d === 'asc' ? 'desc' : 'asc') } else { this.sortColumn.set(column) this.sortDirection.set('asc') } this.currentPage.set(1) } // Page handler goToPage(page: number) { if (page >= 1 && page <= this.totalPages()) { this.currentPage.set(page) } } // Search handler onSearch(query: string) { this.searchQuery.set(query) this.currentPage.set(1) // Reset to page 1 on search } }
The Template
<!-- data-table.component.html --> <div> <!-- Search + Info bar --> <div class="d-flex justify-content-between align-items-center mb-3 flex-wrap gap-2"> <div class="d-flex align-items-center gap-2"> <span class="text-muted small"> Showing {{ (currentPage()-1)*pageSize()+1 }}–{{ [currentPage()*pageSize(), processedData().length] | min }} of {{ processedData().length }} results </span> </div> <input type="search" class="form-control form-control-sm" style="max-width:240px;" placeholder="Search..." [value]="searchQuery()" (input)="onSearch($any($event.target).value)"> </div> <!-- Table --> <div class="table-responsive"> <table class="table table-hover align-middle"> <thead class="table-dark"> <tr> <th *ngFor="let col of columns" [style.cursor]="col.sortable ? 'pointer' : 'default'" (click)="col.sortable ? sort(col.key) : null"> {{ col.label }} <span *ngIf="col.sortable && sortColumn() === col.key"> {{ sortDirection() === 'asc' ? '↑' : '↓' }} </span> <span *ngIf="col.sortable && sortColumn() !== col.key" class="text-muted opacity-50">↕</span> </th> </tr> </thead> <tbody> <tr *ngFor="let row of paginatedData()"> <td *ngFor="let col of columns">{{ row[col.key] }}</td> </tr> <tr *ngIf="paginatedData().length === 0"> <td [colSpan]="columns.length" class="text-center text-muted py-4"> No results found </td> </tr> </tbody> </table> </div> <!-- Pagination --> <div class="d-flex justify-content-between align-items-center mt-3 flex-wrap gap-2" *ngIf="totalPages() > 1"> <div class="d-flex align-items-center gap-2"> <span class="small text-muted">Rows per page:</span> <select class="form-select form-select-sm" style="width:auto;" [value]="pageSize()" (change)="pageSize.set(+$any($event.target).value); currentPage.set(1)"> <option value="5">5</option> <option value="10">10</option> <option value="25">25</option> <option value="50">50</option> </select> </div> <nav> <ul class="pagination pagination-sm mb-0"> <li class="page-item" [class.disabled]="currentPage() === 1"> <a class="page-link" (click)="goToPage(currentPage()-1)">«</a> </li> <li class="page-item" *ngFor="let page of pages()" [class.active]="page === currentPage()"> <a class="page-link" (click)="goToPage(page)">{{ page }}</a> </li> <li class="page-item" [class.disabled]="currentPage() === totalPages()"> <a class="page-link" (click)="goToPage(currentPage()+1)">»</a> </li> </ul> </nav> </div> </div>
Using the Component
// parent.component.ts import { DataTableComponent } from './data-table.component' @Component({ standalone: true, imports: [DataTableComponent], template: ` <app-data-table [columns]="columns" [data]="templates"> </app-data-table> ` }) export class ParentComponent { columns = [ { key: 'name', label: 'Template', sortable: true }, { key: 'framework', label: 'Framework', sortable: true }, { key: 'price', label: 'Price', sortable: true }, { key: 'downloads', label: 'Downloads', sortable: true }, { key: 'status', label: 'Status', sortable: false }, ] templates = [ { name: 'Marvel Dashboard', framework: 'Angular', price: 29, downloads: 1240, status: 'Active' }, { name: 'PORTO Template', framework: 'Bootstrap', price: 19, downloads: 3580, status: 'Active' }, { name: 'Proctu Medical', framework: 'React', price: 39, downloads: 890, status: 'Draft' }, ] }
The table handles search, sort and pagination automatically. Pass data and column definitions — everything else is handled internally.
Custom Cell Rendering
For custom cells like badges, links or action buttons, extend with a slot or template projection approach. The simplest is using ng-content with named outlets or @ContentChild for a cell template. For most admin dashboards, the inline approach of handling specific columns in the parent template is simpler:
<app-data-table [columns]="standardColumns" [data]="filteredData"> </app-data-table>
And keep action columns in the parent template as a separate column outside the reusable component. Clean separation between the data table logic and page-specific actions.
Frequently Asked Questions
Related
Need a Full Angular + Bootstrap Admin Dashboard?
Marvel Angular Dashboard — Angular 21 + Bootstrap 5 with 50+ components and dark mode.
Browse Templates →Use code FIRST30 for 30% off.