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
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