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 |
|---|---|---|---|---|---|
| Fabiola.Goldner87 | admin | pending | 02 Apr 2025 14:06:09 PDT | 09 Aug 2025 05:40:45 PDT | 83 |
| Sabryna.Cole8 | user | suspended | 19 Feb 2025 03:02:34 PDT | 11 Jul 2025 14:32:59 PDT | 530 |
| Emma_Klocko89 | admin | active | 13 Apr 2024 02:35:07 PDT | 23 Jun 2025 13:43:01 PDT | 815 |
| Ferne_Beatty | admin | pending | 05 Jan 2024 14:40:25 PDT | 08 Sep 2024 16:35:17 PDT | 234 |
| Garret.Collins-Schaden40 | moderator | pending | 29 Jun 2025 22:31:12 PDT | 14 Oct 2025 06:09:26 PDT | 212 |
| Mable_Koch86 | admin | active | 13 Mar 2024 01:24:42 PDT | 21 Oct 2025 08:50:36 PDT | 554 |
| Leanne_Sawayn61 | user | pending | 01 Nov 2024 05:39:59 PDT | 08 Mar 2025 07:28:20 PDT | 628 |
| Reid_Predovic-Hamill | admin | active | 06 Oct 2025 14:49:53 PDT | 28 Oct 2025 04:01:13 PDT | 705 |
| Joanie_Pagac | moderator | pending | 11 Sep 2025 13:34:34 PDT | 09 Oct 2025 18:16:46 PDT | 784 |
| Joey_Gibson | moderator | suspended | 17 Sep 2024 18:42:04 PDT | 09 May 2025 16:03:16 PDT | 592 |
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 {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 {provideIcons} from "@qualcomm-ui/angular-core/lucide"
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
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