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 |
|---|---|---|---|---|---|
| Desmond.Wyman18 | moderator | pending | 09 Jun 2024 03:37:05 PDT | 22 Jul 2024 02:15:13 PDT | 743 |
| Austyn.Johnson | user | suspended | 25 Oct 2024 00:15:09 PDT | 18 Apr 2025 19:07:06 PDT | 982 |
| Evalyn.Pacocha | user | pending | 16 Sep 2025 03:48:31 PDT | 25 Sep 2025 03:17:23 PDT | 539 |
| Olin.Bartoletti29 | moderator | active | 23 Mar 2025 16:20:04 PDT | 01 Sep 2025 11:27:17 PDT | 556 |
| Halie_Rolfson13 | moderator | pending | 02 Aug 2025 21:12:11 PDT | 13 Aug 2025 04:16:12 PDT | 657 |
| Aracely.Hansen | admin | active | 07 Jan 2024 00:16:11 PDT | 03 Dec 2024 14:03:45 PDT | 75 |
| Abdullah_Corkery | user | pending | 15 Dec 2024 14:37:06 PDT | 07 Apr 2025 15:45:07 PDT | 596 |
| Eliseo_Bins | user | active | 23 Sep 2025 00:35:16 PDT | 27 Sep 2025 02:14:21 PDT | 52 |
| Amiya_Mosciski8 | moderator | active | 28 Jun 2024 01:57:49 PDT | 28 Oct 2024 15:20:50 PDT | 624 |
| Lauretta.Parisian43 | admin | suspended | 22 Jun 2024 02:12:18 PDT | 23 Feb 2025 09:04:43 PDT | 898 |
import {Component, computed, effect, signal} from "@angular/core"
import {toObservable, toSignal} from "@angular/core/rxjs-interop"
import {FormsModule} from "@angular/forms"
import {LucideSearch} from "@lucide/angular"
import {injectQuery} from "@tanstack/angular-query-experimental"
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({LucideSearch})],
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