Paginated Data Loading
Paginated Row Filtering
Use the server data source to filter rows on the server and return only matching rows for each page request.
Note
This guide covers filtering on paginated server row data. For client-side filtering, start with the Filter Text guide.
Filtering Paginated Rows
To filter rows on the server, define a filter model and include it in each data request.
Include the filter model in the queryKey of the useServerDataSource hook.
Whenever the filter model changes, the grid fetches a new set of rows from the server.
The following demo passes a custom filter model to the queryKey of the server data source. Click the
funnel icon in a column header to apply a filter.
Row Pagination Filtering
1import "@1771technologies/lytenyte-pro/components.css";2import "@1771technologies/lytenyte-pro/light-dark.css";3import {4 Grid,5 usePiece,6 useServerDataSource,7 type DataResponse,8 type PieceWritable,9} from "@1771technologies/lytenyte-pro";10
11import { useCallback, useMemo, useRef, useState } from "react";12import { Server } from "./server.jsx";13import type { MovieData } from "./data.js";14import {15 GenreRenderer,16 Header,17 LinkRenderer,18 NameCellRenderer,19 RatingRenderer,20 ReleasedRenderer,21 TypeRenderer,22} from "./components.jsx";23import type { GridFilter } from "./types.js";24import { Pager } from "./pager.jsx";25
26export interface GridSpec {27 readonly data: MovieData;28 readonly api: {29 readonly filterModel: PieceWritable<Record<string, GridFilter>>;30 };31}32
33const columns: Grid.Column<GridSpec>[] = [34 {35 id: "#",36 name: "",37 width: 30,38 field: "link",39 widthMin: 30,40 widthMax: 30,41 cellRenderer: LinkRenderer,42 headerRenderer: () => <div />,43 },44 { id: "name", name: "Title", width: 250, widthFlex: 1, cellRenderer: NameCellRenderer },45 { id: "released_at", name: "Released", width: 120, cellRenderer: ReleasedRenderer, type: "date" },46 { id: "genre", name: "Genre", cellRenderer: GenreRenderer },47 { id: "type", name: "Type", width: 120, cellRenderer: TypeRenderer },48 { id: "imdb_rating", type: "number", name: "Rating", width: 120, cellRenderer: RatingRenderer },49];50
51const base: Grid.ColumnBase<GridSpec> = { headerRenderer: Header };52
53const pageSize = 10;54
55const headerHeight = 40;56const rowHeight = 40;57
58export default function PaginationDemo() {59 const [page, setPage] = useState(1);60 const [count, setCount] = useState<number | null>(null);61
62 const [filters, setFilters] = useState<Record<string, GridFilter>>({});63 const responseCache = useRef<Record<number, DataResponse[]>>({});64
65 const setFilterAndResetCache: typeof setFilters = useCallback((arg) => {66 setFilters(arg);67 setPage(1);68 responseCache.current = {};69 }, []);70
71 const model = usePiece(filters, setFilterAndResetCache);72
73 const ds = useServerDataSource({74 queryFn: async ({ requests, queryKey }) => {75 const page = queryKey[0];76 const filter = queryKey[1];77
78 if (responseCache.current[page]) {79 return responseCache.current[page].map((x) => ({ ...x, asOfTime: Date.now() }));80 }81
82 const result = await Server(requests, page - 1, pageSize, filter);83 responseCache.current[page] = result.pages;84
85 setCount(result.count);86
87 return result.pages;88 },89 queryKey: [page, filters] as const,90 });91
92 const isLoading = ds.isLoading.useValue();93 const apiExtension = useMemo(() => ({ filterModel: model }), [model]);94
95 return (96 <div>97 <div className="ln-grid" style={{ height: pageSize * rowHeight + headerHeight }}>98 <Grid99 rowSource={ds}100 apiExtension={apiExtension}101 columnBase={base}102 columns={columns}103 headerHeight={headerHeight}104 rowHeight={rowHeight}105 slotViewportOverlay={106 isLoading && (107 <div className="bg-ln-gray-20/40 top-(--ln-top-offset) absolute left-0 z-20 h-[calc(100%-var(--ln-top-offset))] w-full animate-pulse"></div>108 )109 }110 />111 </div>112 <div className="border-ln-border flex h-12 items-center justify-end gap-4 border-t px-4">113 {count && <Pager count={count} page={page} pageSize={pageSize} onPageChange={setPage} />}114 </div>115 </div>116 );117}1import { format } from "date-fns";2import type { JSX } from "react";3import { Rating, ThinRoundedStar } from "@smastrom/react-rating";4import "@smastrom/react-rating/style.css";5import { Link1Icon } from "@radix-ui/react-icons";6import type { Grid } from "@1771technologies/lytenyte-pro";7import type { GridSpec } from "./demo";8import { Popover } from "@1771technologies/lytenyte-pro/components";9import { FilterControl } from "./filter.js";10
11function SkeletonLoading() {12 return (13 <div className="h-full w-full p-2">14 <div className="bg-ln-gray-20 h-full w-full animate-pulse rounded-xl"></div>15 </div>16 );17}18
19export const NameCellRenderer = (params: Grid.T.CellRendererParams<GridSpec>) => {20 if (params.row.loading && !params.row.data) return <SkeletonLoading />;21
22 const field = params.api.columnField(params.column, params.row) as string;23
24 return <div className="overflow-hidden text-ellipsis">{field}</div>;25};26
27export const ReleasedRenderer = (params: Grid.T.CellRendererParams<GridSpec>) => {28 if (params.row.loading && !params.row.data) return <SkeletonLoading />;29 const field = params.api.columnField(params.column, params.row) as string;30
31 const formatted = field ? format(field, "dd MMM yyyy") : "-";32
33 return <div>{formatted}</div>;34};35
36export const GenreRenderer = (params: Grid.T.CellRendererParams<GridSpec>) => {37 if (params.row.loading && !params.row.data) return <SkeletonLoading />;38 const field = params.api.columnField(params.column, params.row) as string;39
40 const splits = field ? field.split(",") : [];41
42 return (43 <div className="flex h-full w-full items-center gap-1">44 {splits.map((c) => {45 return (46 <div47 className="border-(--primary-200) text-(--primary-700) dark:text-(--primary-500) bg-(--primary-200)/20 rounded border p-1 px-2 text-xs"48 key={c}49 >50 {c}51 </div>52 );53 })}54 </div>55 );56};57
58const FilmRealIcon = (props: JSX.IntrinsicElements["svg"]) => {59 return (60 <svg61 xmlns="http://www.w3.org/2000/svg"62 width="20"63 height="20"64 fill="currentcolor"65 viewBox="0 0 256 256"66 {...props}67 >68 <path d="M232,216H183.36A103.95,103.95,0,1,0,128,232H232a8,8,0,0,0,0-16ZM40,128a88,88,0,1,1,88,88A88.1,88.1,0,0,1,40,128Zm88-24a24,24,0,1,0-24-24A24,24,0,0,0,128,104Zm0-32a8,8,0,1,1-8,8A8,8,0,0,1,128,72Zm24,104a24,24,0,1,0-24,24A24,24,0,0,0,152,176Zm-32,0a8,8,0,1,1,8,8A8,8,0,0,1,120,176Zm56-24a24,24,0,1,0-24-24A24,24,0,0,0,176,152Zm0-32a8,8,0,1,1-8,8A8,8,0,0,1,176,120ZM80,104a24,24,0,1,0,24,24A24,24,0,0,0,80,104Zm0,32a8,8,0,1,1,8-8A8,8,0,0,1,80,136Z"></path>69 </svg>70 );71};72
73const MonitorPlayIcon = (props: JSX.IntrinsicElements["svg"]) => {74 return (75 <svg76 xmlns="http://www.w3.org/2000/svg"77 width="20"78 height="20"79 fill="currentcolor"80 viewBox="0 0 256 256"81 {...props}82 >83 <path d="M208,40H48A24,24,0,0,0,24,64V176a24,24,0,0,0,24,24H208a24,24,0,0,0,24-24V64A24,24,0,0,0,208,40Zm8,136a8,8,0,0,1-8,8H48a8,8,0,0,1-8-8V64a8,8,0,0,1,8-8H208a8,8,0,0,1,8,8Zm-48,48a8,8,0,0,1-8,8H96a8,8,0,0,1,0-16h64A8,8,0,0,1,168,224Zm-3.56-110.66-48-32A8,8,0,0,0,104,88v64a8,8,0,0,0,12.44,6.66l48-32a8,8,0,0,0,0-13.32ZM120,137.05V103l25.58,17Z"></path>84 </svg>85 );86};87
88export const TypeRenderer = (params: Grid.T.CellRendererParams<GridSpec>) => {89 if (params.row.loading && !params.row.data) return <SkeletonLoading />;90 const field = params.api.columnField(params.column, params.row) as string;91
92 const isMovie = field === "Movie";93 const Icon = isMovie ? FilmRealIcon : MonitorPlayIcon;94
95 return (96 <div className="flex h-full w-full items-center gap-2">97 <span className={isMovie ? "text-(--primary-500)" : "text-ln-primary-50"}>98 <Icon />99 </span>100 <span>{field}</span>101 </div>102 );103};104
105export const RatingRenderer = (params: Grid.T.CellRendererParams<GridSpec>) => {106 if (params.row.loading && !params.row.data) return <SkeletonLoading />;107 const field = params.api.columnField(params.column, params.row) as string;108 const rating = field ? Number.parseFloat(field.split("/")[0]) : null;109 if (rating == null || Number.isNaN(rating)) return "-";110
111 return (112 <div className="flex h-full w-full items-center">113 <Rating114 style={{ maxWidth: 100 }}115 halfFillMode="svg"116 value={Math.round(rating / 2)}117 itemStyles={{118 activeFillColor: "hsla(173, 78%, 34%, 1)",119 itemShapes: ThinRoundedStar,120 inactiveFillColor: "transparent",121 inactiveBoxBorderColor: "transparent",122 inactiveBoxColor: "transparent",123 inactiveStrokeColor: "transparent",124 }}125 readOnly126 />127 </div>128 );129};130
131export const LinkRenderer = (params: Grid.T.CellRendererParams<GridSpec>) => {132 if (params.row.loading && !params.row.data) return <SkeletonLoading />;133 const field = params.api.columnField(params.column, params.row) as string;134
135 return (136 <a href={field} className="text-(--primary-500)">137 <Link1Icon />138 </a>139 );140};141
142export function Header({ api, column }: Grid.T.HeaderParams<GridSpec>) {143 const label = column.name ?? column.id;144
145 const model = api.filterModel.useValue();146 const hasFilter = !!model[column.id];147 return (148 <div className="flex h-full w-full items-center justify-between">149 <div>{label}</div>150
151 <Popover>152 <Popover.Trigger data-ln-button="secondary" data-ln-icon data-ln-size="sm" className="relative">153 <div className="sr-only">Filter the {label}</div>154 <svg155 xmlns="http://www.w3.org/2000/svg"156 width="16"157 height="16"158 fill="currentcolor"159 viewBox="0 0 256 256"160 >161 <path d="M230.6,49.53A15.81,15.81,0,0,0,216,40H40A16,16,0,0,0,28.19,66.76l.08.09L96,139.17V216a16,16,0,0,0,24.87,13.32l32-21.34A16,16,0,0,0,160,194.66V139.17l67.74-72.32.08-.09A15.8,15.8,0,0,0,230.6,49.53ZM40,56h0Zm106.18,74.58A8,8,0,0,0,144,136v58.66L112,216V136a8,8,0,0,0-2.16-5.47L40,56H216Z"></path>162 </svg>163
164 {hasFilter && <div className="bg-ln-primary-50 absolute right-px top-px size-2 rounded-full" />}165 </Popover.Trigger>166 <Popover.Container>167 <Popover.Arrow />168 <Popover.Title className="sr-only">Filter {label}</Popover.Title>169 <Popover.Description className="sr-only">Filter the text in the{label}</Popover.Description>170 <FilterControl column={column} filter={model[column.id] ?? null} api={api} />171 </Popover.Container>172 </Popover>173 </div>174 );175}1import "@1771technologies/lytenyte-pro/components.css";2import { type Grid } from "@1771technologies/lytenyte-pro";3import type { GridSpec } from "./demo";4import { CheckIcon, ChevronDownIcon } from "@radix-ui/react-icons";5import { useState } from "react";6import type { GridFilter } from "./types";7import clsx from "clsx";8import { Popover, SmartSelect } from "@1771technologies/lytenyte-pro/components";9
10const textOptions = [11 { id: "equals", label: "Equals" },12 { id: "not_equals", label: "Does Not Equal" },13 { id: "contains", label: "Contains" },14 { id: "not_contains", label: "Does Not Contain" },15];16
17const numberOptions = [18 { id: "equals", label: "Equals" },19 { id: "not_equals", label: "Does Not Equal" },20 { id: "less_than", label: "Less Than" },21 { id: "less_than_or_equal", label: "Less Than Or Equal To" },22 { id: "greater_than", label: "Greater Than" },23 { id: "greater_than_or_equal", label: "Greater Than Or Equal To" },24];25
26const dateOptions = [27 { id: "before", label: "Before" },28 { id: "after", label: "After" },29];30
31type DeepPartial<T> = T extends object32 ? {33 -readonly [P in keyof T]?: DeepPartial<T[P]>;34 }35 : T;36
37export function FilterControl({38 api,39 filter: initialFilter,40 column,41}: {42 api: Grid.API<GridSpec>;43 filter: GridFilter | null;44 column: Grid.Column<GridSpec>;45}) {46 const inputType = column.type === "date" ? "date" : column.type === "number" ? "number" : "text";47 const options =48 column.type === "date" ? dateOptions : column.type === "number" ? numberOptions : textOptions;49
50 const [filter, setFilter] = useState<DeepPartial<GridFilter> | null>(initialFilter);51
52 const canSubmit = filter?.value && filter.operator;53
54 const filterValue = options.find((x) => x.id === filter?.operator);55
56 const popoverControls = Popover.useControls();57 return (58 <form59 className="grid grid-cols-1 gap-2 md:grid-cols-2"60 onSubmit={(e) => {61 if (!canSubmit) return;62 e.preventDefault();63
64 api.filterModel.set((prev) => ({ ...prev, [column.id]: filter as GridFilter }));65 popoverControls.openChange(false);66 }}67 >68 <div className="text-ln-text hidden ps-2 text-sm md:block">Operator</div>69 <div className="text-ln-text hidden ps-2 text-sm md:block">Values</div>70
71 <SmartSelect72 options={options}73 value={filter ? (textOptions.find((x) => x.id === filter.operator) ?? null) : null}74 onOptionChange={(option) => {75 if (!option) return;76
77 setFilter((prev) => {78 return { ...prev, kind: inputType, operator: option.id } as GridFilter;79 });80 }}81 kind="basic"82 trigger={83 <SmartSelect.BasicTrigger84 type="button"85 data-ln-input86 className="flex min-w-40 cursor-pointer items-center justify-between"87 >88 <div>{filterValue?.label ?? "Select..."}</div>89 <div>90 <ChevronDownIcon />91 </div>92 </SmartSelect.BasicTrigger>93 }94 >95 {(p) => {96 if (p.option.id.startsWith("separator")) {97 return <div role="separator" className="bg-ln-gray-40 my-1 h-px w-full" />;98 }99
100 return (101 <SmartSelect.Option key={p.option.id} {...p} className="flex items-center justify-between">102 {p.option.label}103 {p.selected && <CheckIcon className="text-ln-primary-50" />}104 </SmartSelect.Option>105 );106 }}107 </SmartSelect>108
109 <div>110 <label>111 <span className="sr-only">Value for the first filter</span>112 <input113 data-ln-input114 value={filter?.value ?? ""}115 className={clsx("w-full", inputType === "date" && "text-xs")}116 type={inputType}117 onChange={(e) => {118 setFilter((prev) => {119 return {120 ...prev,121 kind: inputType,122 value:123 inputType === "number"124 ? e.target.value125 ? Number.parseFloat(e.target.value)126 : null127 : e.target.value,128 } as GridFilter;129 });130 }}131 />132 </label>133 </div>134
135 <div className="flex items-center justify-between gap-4 md:col-span-2 md:grid md:grid-cols-subgrid">136 <div className="pt-2">137 <button138 data-ln-button="tertiary"139 data-ln-size="sm"140 type="button"141 className="hover:bg-ln-gray-30"142 onClick={() => popoverControls.openChange(false)}143 >144 Cancel145 </button>146 </div>147 <div className="flex justify-end gap-2 pt-2">148 <button149 data-ln-button="secondary"150 data-ln-size="sm"151 type="button"152 className="hover:bg-ln-bg-button-light"153 onClick={() => {154 api.filterModel.set((prev) => {155 const next = { ...prev };156 delete next[column.id];157
158 return next;159 });160 popoverControls.openChange(false);161 }}162 >163 Clear164 </button>165 <button data-ln-button="primary" data-ln-size="sm" disabled={!canSubmit}>166 Apply Filters167 </button>168 </div>169 </div>170 </form>171 );172}1import React, { useEffect, useState } from "react";2import "@1771technologies/lytenyte-pro/components.css";3import { useMemo } from "react";4
5const formatter = Intl.NumberFormat("en-Us", { minimumFractionDigits: 0, maximumFractionDigits: 0 });6
7export function Pager({8 page,9 pageSize,10 count,11 onPageChange,12}: {13 pageSize: number;14 page: number;15 count: number;16 onPageChange: (page: number) => void;17}) {18 const pageCount = useMemo(() => {19 return Math.ceil(count / pageSize);20 }, [count, pageSize]);21
22 return (23 <div className="flex w-full items-center justify-between">24 <div className="ms-3 text-xs tabular-nums md:text-sm">25 Rows{" "}26 <span className="font-bold">27 {formatter.format((page - 1) * pageSize + 1)}-{formatter.format(Math.min(page * pageSize, count))}28 </span>29 {count > pageSize && (30 <>31 {" "}32 of <span className="font-bold">{formatter.format(count)}</span>33 </>34 )}35 </div>36
37 <div className="flex items-center gap-1">38 <button data-ln-button="secondary" data-ln-size="md" onClick={() => onPageChange(1)}>39 <span className="sr-only">To first page</span>40 <svg41 xmlns="http://www.w3.org/2000/svg"42 width="16"43 height="16"44 fill="currentcolor"45 viewBox="0 0 256 256"46 >47 <path d="M200,48V208a8,8,0,0,1-13.66,5.66l-80-80a8,8,0,0,1,0-11.32l80-80A8,8,0,0,1,200,48ZM72,40a8,8,0,0,0-8,8V208a8,8,0,0,0,16,0V48A8,8,0,0,0,72,40Z"></path>48 </svg>49 </button>50 <button51 data-ln-button="secondary"52 data-ln-size="md"53 onClick={() => onPageChange(Math.max(1, page - 1))}54 >55 <span className="sr-only">Previous Page</span>56 <svg57 xmlns="http://www.w3.org/2000/svg"58 width="16"59 height="16"60 fill="currentcolor"61 viewBox="0 0 256 256"62 >63 <path d="M168,48V208a8,8,0,0,1-13.66,5.66l-80-80a8,8,0,0,1,0-11.32l80-80A8,8,0,0,1,168,48Z"></path>64 </svg>65 </button>66 <div className="hidden items-center gap-1 px-2 tabular-nums md:flex">67 Page <IntegerNumberInput page={page} onPageChange={onPageChange} pageCount={pageCount} />68 of <span className="font-bold">{pageCount}</span>69 </div>70
71 <button72 data-ln-button="secondary"73 data-ln-size="md"74 onClick={() => onPageChange(Math.min(page + 1, pageCount))}75 >76 <span className="sr-only">Next page</span>77 <svg78 xmlns="http://www.w3.org/2000/svg"79 width="16"80 height="16"81 fill="currentcolor"82 viewBox="0 0 256 256"83 >84 <path d="M181.66,133.66l-80,80A8,8,0,0,1,88,208V48a8,8,0,0,1,13.66-5.66l80,80A8,8,0,0,1,181.66,133.66Z"></path>85 </svg>86 </button>87 <button data-ln-button="secondary" data-ln-size="md" onClick={() => onPageChange(pageCount)}>88 <span className="sr-only">To last page</span>89 <svg90 xmlns="http://www.w3.org/2000/svg"91 width="16"92 height="16"93 fill="currentcolor"94 viewBox="0 0 256 256"95 >96 <path d="M149.66,122.34a8,8,0,0,1,0,11.32l-80,80A8,8,0,0,1,56,208V48a8,8,0,0,1,13.66-5.66ZM184,40a8,8,0,0,0-8,8V208a8,8,0,0,0,16,0V48A8,8,0,0,0,184,40Z"></path>97 </svg>98 </button>99 </div>100 </div>101 );102}103
104interface IntegerNumberInputProps {105 page: number;106 onPageChange: (value: number) => void;107 pageCount: number;108}109
110function IntegerNumberInput({ page, onPageChange, pageCount }: IntegerNumberInputProps) {111 const [inputValue, setInputValue] = useState<string>(String(page));112
113 // Keep internal state in sync if page prop changes externally114 useEffect(() => {115 setInputValue(String(page));116 }, [page]);117
118 const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {119 const value = e.target.value;120
121 if (value === "") {122 setInputValue("");123 return;124 }125
126 if (/^\d+$/.test(value)) {127 const v = Math.max(Math.min(parseInt(value, 10), pageCount), 1);128 setInputValue(String(v));129 onPageChange(v);130 }131 };132
133 const handleBlur = () => {134 if (inputValue === "") {135 setInputValue(String(page));136 onPageChange(page);137 }138 };139
140 return (141 <input142 type="text"143 style={{ width: Math.max(`${inputValue}`.length, 3) * 12 + 18 }}144 className="text-center"145 data-ln-input146 inputMode="numeric"147 value={inputValue}148 onChange={handleChange}149 onBlur={handleBlur}150 />151 );152}1import type { DataRequest, DataResponse } from "@1771technologies/lytenyte-pro";2import { data as movieData, type MovieData } from "./data.js";3import type { GridFilter } from "./types.js";4
5const sleep = () => new Promise((res) => setTimeout(res, 200));6
7export async function Server(8 reqs: DataRequest[],9 page: number,10 pageSize: number,11 filterModel: Record<string, GridFilter>,12) {13 // Simulate latency and server work.14 await sleep();15
16 const filters = Object.entries(filterModel);17
18 const data =19 filters.length === 020 ? movieData21 : movieData.filter((row) => {22 for (const [columnId, filter] of filters) {23 const value = row[columnId as keyof MovieData];24 if (!value) return false;25
26 if (columnId === "imdb_rating") {27 if (filter.kind !== "number") continue;28
29 const rating = value ? Math.round(Number.parseFloat(value.split("/")[0]) / 2) : "";30 const checkValue = rating as number;31
32 if (filter.operator === "equals" && checkValue !== filter.value) return false;33 if (filter.operator === "not_equals" && checkValue === filter.value) return false;34 if (filter.operator === "greater_than" && checkValue <= filter.value) return false;35 if (filter.operator === "greater_than_or_equal" && checkValue < filter.value) return false;36 if (filter.operator === "less_than" && checkValue >= filter.value) return false;37 if (filter.operator === "less_than_or_equal" && checkValue > filter.value) return false;38
39 continue;40 }41
42 if (columnId === "released_at") {43 const v = new Date(value);44 const filterV = new Date(filter.value as string);45
46 if (filter.operator === "before" && v >= filterV) return false;47 if (filter.operator === "after" && v <= filterV) return false;48 continue;49 }50
51 if (columnId === "genre" && filter.operator === "equals") {52 const genres = value53 .toLowerCase()54 .split(",")55 .map((x) => x.trim());56
57 if (genres.some((x) => x === String(filter.value).toLowerCase())) continue;58 return false;59 }60 if (columnId === "genre" && filter.operator === "not_equals") {61 const genres = value62 .toLowerCase()63 .split(",")64 .map((x) => x.trim());65
66 if (genres.every((x) => x !== String(filter.value).toLowerCase())) continue;67 return false;68 }69
70 if (filter.operator === "equals" && `${filter.value}`.toLowerCase() !== value.toLowerCase())71 return false;72 if (filter.operator === "not_equals" && `${filter.value}`.toLowerCase() === value.toLowerCase())73 return false;74
75 if (76 filter.operator === "contains" &&77 !value.toLowerCase().includes(`${filter.value}`.toLowerCase())78 )79 return false;80 if (81 filter.operator === "not_contains" &&82 value.toLowerCase().includes(`${filter.value}`.toLowerCase())83 ) {84 return false;85 }86 }87
88 return true;89 });90
91 let pageStart = page * pageSize;92
93 if (pageStart > data.length) {94 pageStart = data.length - pageSize;95 }96
97 const pages = reqs.map((c) => {98 const pageData = data.slice(pageStart, pageStart + pageSize);99
100 return {101 asOfTime: Date.now(),102 data: pageData.map((x) => {103 return { kind: "leaf", id: x.uniq_id, data: x };104 }),105 start: c.start,106 end: c.end,107 kind: "center",108 path: c.path,109 size: Math.min(pageSize, pageData.length),110 } satisfies DataResponse;111 });112
113 return {114 pages,115 count: data.length,116 };117}1export type FilterStringOperator = "equals" | "not_equals" | "not_contains" | "contains";2
3export interface FilterString {4 readonly operator: FilterStringOperator;5 readonly value: string;6 readonly kind: "text";7}8
9export type FilterNumberOperator =10 | "equals"11 | "not_equals"12 | "less_than"13 | "greater_than"14 | "less_than_or_equal"15 | "greater_than_or_equal";16export interface FilterNumber {17 readonly operator: FilterNumberOperator;18 readonly value: number;19 readonly kind: "number";20}21
22export type FilterDateOperator = "before" | "after";23
24export interface FilterDate {25 readonly operator: FilterDateOperator;26 readonly value: string;27 readonly kind: "date";28}29
30export type GridFilter = FilterString | FilterDate | FilterNumber;For more guidance on filtering rows on the server, see the Server Row Filtering guide.
Next Steps
- Paginated Row Sorting: Request sorted pages by sending a defined sort model to the server.
- Paginated Rows: Load large datasets by fetching rows one page at a time from the server.
- Server Row Filtering: Filter server-side rows using the server data source.
