Filters (Server-side)
This page features a server-side filtering, pagination, and sorting demo with a simulated backend API. State is managed in the component and sent to the backend on each change. This is the standard approach for backend integrations where the server handles filtering, pagination, and sorting logic. See the Filters Guide for implementation details.
Username | Role | Account Status | Account Created On | Last Visited At | Visit Count |
|---|---|---|---|---|---|
| Adrien_Ullrich-Ritchie | user | suspended | 19 Mar 2025 18:42:51 PDT | 13 Aug 2025 14:24:39 PDT | 74 |
| Narciso_Funk | user | suspended | 18 Apr 2024 22:15:35 PDT | 18 Dec 2024 13:13:11 PDT | 124 |
| Junior_Feil61 | user | suspended | 03 Aug 2025 01:30:11 PDT | 02 Oct 2025 15:01:17 PDT | 754 |
| Okey_Daugherty47 | user | active | 16 Feb 2024 21:11:42 PDT | 29 Oct 2025 18:52:14 PDT | 797 |
| Gregorio.Bayer-Torp | moderator | suspended | 01 May 2025 00:44:31 PDT | 17 Jun 2025 10:30:18 PDT | 672 |
| Ernie.Erdman59 | admin | pending | 02 Dec 2024 11:50:42 PDT | 14 Jan 2025 11:25:16 PDT | 805 |
| Kimberly_Shanahan8 | moderator | pending | 04 Jan 2025 15:30:37 PDT | 03 Apr 2025 16:18:58 PDT | 576 |
| Theresa_Orn19 | moderator | pending | 13 Apr 2025 09:28:56 PDT | 15 Jun 2025 05:40:05 PDT | 867 |
| Jon_Boyle | user | pending | 06 Aug 2025 03:29:28 PDT | 25 Oct 2025 17:12:20 PDT | 689 |
| Burdette_Greenholt | user | pending | 15 Jun 2024 23:38:22 PDT | 12 Aug 2025 01:11:02 PDT | 391 |
import {Component, computed, effect, signal} from "@angular/core"
import {toObservable, toSignal} from "@angular/core/rxjs-interop"
import {FormsModule} from "@angular/forms"
import {injectQuery} from "@tanstack/angular-query-experimental"
import {Search} from "lucide-angular"
import {debounceTime} from "rxjs"
import {provideIcons} from "@qualcomm-ui/angular-core/lucide"
import {PaginationModule} from "@qualcomm-ui/angular/pagination"
import {PopoverModule} from "@qualcomm-ui/angular/popover"
import {ProgressRingModule} from "@qualcomm-ui/angular/progress-ring"
import {
type AngularTable,
createAngularTable,
createTablePagination,
TableModule,
} from "@qualcomm-ui/angular/table"
import {TextInputModule} from "@qualcomm-ui/angular/text-input"
import {
type ColumnFiltersState,
getCoreRowModel,
type PaginationState,
type SortingState,
} from "@qualcomm-ui/core/table"
import {fetchData, type FetchResult, type User, userColumns} from "./data"
import {TableColumnFilter} from "./table-column-filter"
@Component({
imports: [
TableModule,
TextInputModule,
FormsModule,
ProgressRingModule,
PaginationModule,
PopoverModule,
TableColumnFilter,
],
providers: [provideIcons({Search})],
selector: "filters-server-side-demo",
template: `
<div class="flex w-full flex-col gap-4 p-2">
<div q-table-root>
<div q-table-action-bar>
<q-text-input
aria-label="Search columns"
class="w-56"
placeholder="Search all columns..."
size="sm"
startIcon="Search"
[(ngModel)]="globalFilter"
/>
<div
class="text-neutral-primary font-body-sm flex items-center gap-1"
>
<span>Query:</span>
<span>{{ query.fetchStatus() }}</span>
@if (query.isFetching()) {
<div class="ml-1" q-progress-ring size="xs"></div>
}
</div>
</div>
<div q-table-scroll-container>
<table q-table-table>
<thead q-table-header>
@for (
headerGroup of table.getHeaderGroups();
track headerGroup.id
) {
<tr q-table-row>
@for (header of headerGroup.headers; track header.id) {
<th q-table-header-cell [style.width.px]="header.getSize()">
@if (!header.isPlaceholder) {
<div
class="inline-flex min-h-[28px] w-full items-center justify-between gap-2"
>
<div class="inline-flex items-center gap-1">
<ng-container *renderHeader="header; let value">
{{ value }}
</ng-container>
<button
q-table-column-sort-action
[header]="header"
[isSorted]="header.column.getIsSorted()"
></button>
</div>
@if (header.column.getCanFilter()) {
<div q-popover>
<div q-popover-anchor>
<button
q-popover-trigger
q-table-column-filter-action
[canFilter]="header.column.getCanFilter()"
[isFiltered]="header.column.getIsFiltered()"
></button>
</div>
<app-table-column-filter
[availableFilters]="
queryData().availableFilters
"
[column]="header.column"
[columnFilters]="columnFilters()"
(columnFiltersChange)="
columnFilters.set($event)
"
/>
</div>
}
</div>
}
</th>
}
</tr>
}
</thead>
<tbody q-table-body>
@for (row of table.getRowModel().rows; track row.id) {
<tr q-table-row>
@for (cell of row.getVisibleCells(); track cell.id) {
<td q-table-cell>
<ng-container *renderCell="cell; let value">
{{ value }}
</ng-container>
</td>
}
</tr>
}
</tbody>
</table>
</div>
<div
q-table-pagination
[count]="pagination.count()"
[page]="pagination.page()"
[pageSize]="pagination.pageSize()"
(pageChanged)="pagination.onPageChange($event)"
>
<div *paginationContext="let context" q-pagination-page-metadata>
@let meta = context.pageMetadata;
@if (!queryData().pageCount && query.isFetching()) {
<div q-progress-ring size="xs"></div>
} @else {
{{ meta.pageStart }}-{{ meta.pageEnd }} of
{{ meta.count }} results
}
</div>
<div q-pagination-page-buttons></div>
</div>
</div>
</div>
`,
})
export class FiltersServerSideDemo {
protected readonly paginationState = signal<PaginationState>({
pageIndex: 0,
pageSize: 10,
})
protected readonly columnFilters = signal<ColumnFiltersState>([])
protected readonly globalFilter = signal<string>("")
protected readonly sorting = signal<SortingState>([])
// Create debounced versions of filters
private readonly debouncedColumnFilters = toSignal(
toObservable(this.columnFilters).pipe(debounceTime(300)),
{initialValue: [] as ColumnFiltersState},
)
private readonly debouncedGlobalFilter = toSignal(
toObservable(this.globalFilter).pipe(debounceTime(300)),
{initialValue: ""},
)
protected readonly query = injectQuery<FetchResult>(() => ({
placeholderData: (previousData) => previousData,
queryFn: async () =>
fetchData({
columnFilters: this.debouncedColumnFilters(),
globalFilter: this.debouncedGlobalFilter(),
pageIndex: this.paginationState().pageIndex,
pageSize: this.paginationState().pageSize,
sorting: this.sorting(),
}),
queryKey: [
"data",
this.paginationState(),
this.debouncedColumnFilters(),
this.debouncedGlobalFilter(),
this.sorting(),
],
}))
readonly queryData = computed(
() =>
this.query.data() ?? {
availableFilters: {},
pageCount: 0,
totalUsers: 0,
users: [],
},
)
protected table: AngularTable<User> = createAngularTable(() => ({
columns: userColumns,
data: this.queryData().users,
getCoreRowModel: getCoreRowModel(),
manualFiltering: true,
manualPagination: true,
manualSorting: true,
onColumnFiltersChange: (updater) => {
if (typeof updater === "function") {
this.columnFilters.update(updater)
} else {
this.columnFilters.set(updater)
}
},
onGlobalFilterChange: (updater) => {
if (typeof updater === "function") {
this.globalFilter.update(updater)
} else {
this.globalFilter.set(updater ?? "")
}
},
onPaginationChange: (updater) => {
if (typeof updater === "function") {
this.paginationState.update(updater)
} else {
this.paginationState.set(updater)
}
},
onSortingChange: (updater) => {
if (typeof updater === "function") {
this.sorting.update(updater)
} else {
this.sorting.set(updater)
}
},
pageCount: this.queryData().pageCount || 0,
state: {
columnFilters: this.columnFilters(),
globalFilter: this.globalFilter(),
pagination: this.paginationState(),
sorting: this.sorting(),
},
}))
protected pagination = createTablePagination(this.table, {
totalCount: computed(() => this.queryData().totalUsers),
})
constructor() {
// Reset to first page when filters change
effect(() => {
// Read the debounced values to track them
this.debouncedColumnFilters()
this.debouncedGlobalFilter()
// Reset pagination to first page
this.paginationState.update((state) => ({...state, pageIndex: 0}))
})
}
}Last updated on by Ryan Bower