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
Okey_Christiansen-Swift user active 13 Jan 2025 04:17:49 PDT 05 Jun 2025 07:42:28 PDT 262
Taylor_Padberg-Blanda16 user suspended 13 Jun 2024 18:09:31 PDT 12 Jan 2025 03:58:35 PDT 113
Elvie.Bahringer15 admin suspended 04 Nov 2024 10:26:25 PDT 04 Sep 2025 11:06:15 PDT 6
Cole.Bergnaum69 moderator pending 25 Apr 2025 13:58:29 PDT 03 Oct 2025 07:17:33 PDT 899
Dameon.Raynor-Kuvalis2 moderator suspended 21 Jul 2025 01:40:08 PDT 18 Aug 2025 07:41:26 PDT 991
Cheyanne_Emmerich user suspended 24 Mar 2025 15:10:12 PDT 01 Jun 2025 09:55:13 PDT 24
Terrell.Will user suspended 13 May 2025 01:00:06 PDT 28 Jun 2025 05:16:14 PDT 3
Jaime.Doyle user suspended 21 May 2024 00:59:17 PDT 19 Feb 2025 19:05:42 PDT 220
Nathanial_Mann user active 12 Jul 2024 10:44:22 PDT 26 Nov 2024 13:41:46 PDT 908
Mose_Heller admin suspended 30 May 2025 13:31:16 PDT 09 Oct 2025 16:15:25 PDT 430
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