How to filter tables in Angular
Filtering tables lets users find records quickly without scrolling through entire datasets.
As the creator of CoreUI with over 10 years of Angular experience since 2014, I’ve implemented table filtering for enterprise dashboards managing thousands of records.
The most effective approach uses a reactive search input bound with [(ngModel)] and a getter that filters the display data.
This provides instant, responsive filtering with no extra libraries.
Add a global search filter across all columns.
import { Component } from '@angular/core'
import { CommonModule } from '@angular/common'
import { FormsModule } from '@angular/forms'
@Component({
selector: 'app-filtered-table',
standalone: true,
imports: [CommonModule, FormsModule],
template: `
<input
type="text"
[(ngModel)]="search"
placeholder="Search..."
class="search-input"
/>
<p>{{ filteredRows.length }} results</p>
<table>
<thead>
<tr>
<th>Name</th>
<th>Email</th>
<th>Role</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let row of filteredRows">
<td>{{ row.name }}</td>
<td>{{ row.email }}</td>
<td>{{ row.role }}</td>
</tr>
<tr *ngIf="filteredRows.length === 0">
<td colspan="3">No results found</td>
</tr>
</tbody>
</table>
`
})
export class FilteredTableComponent {
rows = [
{ name: 'Alice Smith', email: '[email protected]', role: 'Admin' },
{ name: 'Bob Johnson', email: '[email protected]', role: 'User' },
{ name: 'Carol White', email: '[email protected]', role: 'Editor' }
]
search = ''
get filteredRows() {
const term = this.search.toLowerCase().trim()
if (!term) return this.rows
return this.rows.filter(row =>
Object.values(row).some(val =>
String(val).toLowerCase().includes(term)
)
)
}
}
[(ngModel)] binds the input to search. The filteredRows getter evaluates on every change detection cycle. Object.values searches all columns. The result count and no-results row improve UX.
Per-Column Filters
Allow filtering individual columns independently.
import { Component } from '@angular/core'
import { CommonModule } from '@angular/common'
import { FormsModule } from '@angular/forms'
@Component({
selector: 'app-column-filtered-table',
standalone: true,
imports: [CommonModule, FormsModule],
template: `
<table>
<thead>
<tr>
<th>
Name
<input [(ngModel)]="filters.name" placeholder="Filter name" />
</th>
<th>
Role
<select [(ngModel)]="filters.role">
<option value="">All</option>
<option value="Admin">Admin</option>
<option value="User">User</option>
<option value="Editor">Editor</option>
</select>
</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let row of filteredRows">
<td>{{ row.name }}</td>
<td>{{ row.role }}</td>
</tr>
</tbody>
</table>
`
})
export class ColumnFilteredTableComponent {
rows = [
{ name: 'Alice Smith', role: 'Admin' },
{ name: 'Bob Johnson', role: 'User' },
{ name: 'Anna White', role: 'Editor' }
]
filters = { name: '', role: '' }
get filteredRows() {
return this.rows.filter(row => {
const nameMatch = row.name.toLowerCase().includes(this.filters.name.toLowerCase())
const roleMatch = !this.filters.role || row.role === this.filters.role
return nameMatch && roleMatch
})
}
}
Each column has its own filter control. Text filters use partial matching. The role filter uses exact matching via a select. All active filters apply simultaneously. Add more filter properties to the filters object as needed.
Filter with Debounce
Delay filtering to reduce CPU load on fast typing.
import { Component, OnInit, OnDestroy } from '@angular/core'
import { CommonModule } from '@angular/common'
import { FormControl, ReactiveFormsModule } from '@angular/forms'
import { Subject } from 'rxjs'
import { debounceTime, distinctUntilChanged, takeUntil } from 'rxjs/operators'
@Component({
selector: 'app-debounced-filter',
standalone: true,
imports: [CommonModule, ReactiveFormsModule],
template: `
<input [formControl]="searchControl" placeholder="Search..." />
<table>
<tbody>
<tr *ngFor="let row of filteredRows">
<td>{{ row.name }}</td>
</tr>
</tbody>
</table>
`
})
export class DebouncedFilterComponent implements OnInit, OnDestroy {
rows = Array.from({ length: 100 }, (_, i) => ({ name: `User ${i + 1}` }))
filteredRows = [...this.rows]
searchControl = new FormControl('')
private destroy$ = new Subject<void>()
ngOnInit() {
this.searchControl.valueChanges.pipe(
debounceTime(300),
distinctUntilChanged(),
takeUntil(this.destroy$)
).subscribe(term => {
const t = term?.toLowerCase() ?? ''
this.filteredRows = this.rows.filter(r =>
r.name.toLowerCase().includes(t)
)
})
}
ngOnDestroy() {
this.destroy$.next()
this.destroy$.complete()
}
}
debounceTime(300) waits 300ms after the last keystroke before filtering. distinctUntilChanged() skips filtering when value didn’t change. takeUntil cleans up the subscription when the component destroys. This is ideal for server-side filtering to reduce API calls.
Best Practice Note
This is the same filtering approach we use in CoreUI Angular tables for managing large datasets. For client-side filtering of under ~5000 rows, the getter approach works perfectly. For larger datasets, use server-side filtering via API parameters - the debounced reactive form pattern is ideal for that. Always show a result count and an empty state message. For complex filter UIs with many fields, consider a dedicated filter panel that applies filters together via an “Apply” button rather than filtering on every keystroke.



