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.

Query:idle
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