Filtering Numbers
Create custom number filters in LyteNyte Grid. This guide covers common filters for numeric cell values.
Note
Applying a number filter varies depending on the row source. Refer to the guides below for each supported row data source:
While this guide focuses on using client row filtering, these filter concepts apply to all row sources.
Basic Number Filtering
LyteNyte Grid provides flexible filter capabilities that let you define any type of number filter. You can check whether a value is equal to, less than, greater than, or evaluate any other numeric comparison.
To create a number filter, define a function that receives a row node and
returns true to keep the row or false to remove it.
For example, the following function filters a list of products and keeps only rows where the product price is greater than $50:
1const filterPrice: Grid.T.FilterFn<GridSpec["data"]> = (row) => {2 return row.data.price > 50;3};Click the Price Greater Than $300 switch to toggle the filter state.
Number Filtering
30 collapsed lines
1import "@1771technologies/lytenyte-pro/light-dark.css";2import type { OrderData } from "@1771technologies/grid-sample-data/orders";3import { data } from "@1771technologies/grid-sample-data/orders";4import {5 AvatarCell,6 EmailCell,7 IdCell,8 PaymentMethodCell,9 PriceCell,10 ProductCell,11 PurchaseDateCell,12 SwitchToggle,13} from "./components.jsx";14import { useClientDataSource, Grid } from "@1771technologies/lytenyte-pro";15import { useState } from "react";16import { ViewportShadows } from "@1771technologies/lytenyte-pro/components";17
18export interface GridSpec {19 readonly data: OrderData;20}21
22const columns: Grid.Column<GridSpec>[] = [23 { id: "id", width: 60, widthMin: 60, cellRenderer: IdCell, name: "ID" },24 { id: "product", cellRenderer: ProductCell, width: 200, name: "Product" },25 { id: "price", type: "number", cellRenderer: PriceCell, width: 100, name: "Price" },26 { id: "customer", cellRenderer: AvatarCell, width: 180, name: "Customer" },27 { id: "purchaseDate", cellRenderer: PurchaseDateCell, name: "Purchase Date", width: 130 },28 { id: "paymentMethod", cellRenderer: PaymentMethodCell, name: "Payment Method", width: 150 },29 { id: "email", cellRenderer: EmailCell, width: 220, name: "Email" },30];31
32const filterPrice: Grid.T.FilterFn<GridSpec["data"]> = (row) => row.data.price > 300;33
34export default function FilterDemo() {35 const [filterValues, setFilterValues] = useState(true);36 const ds = useClientDataSource<GridSpec>({37 data,38 filter: filterValues ? filterPrice : null,39 });40
41 return (42 <>43 <div className="border-ln-border flex w-full border-b px-2 py-2">44 <SwitchToggle45 label="Price Greater Than $300"46 checked={filterValues}47 onChange={() => {48 setFilterValues((prev) => !prev);49 }}50 />51 </div>52
53 <div className="ln-grid" style={{ height: 500 }}>54 <Grid rowHeight={50} columns={columns} rowSource={ds} slotShadows={ViewportShadows} editMode="cell" />55 </div>56 </>57 );58}1import { format } from "date-fns";2import { useId, type CSSProperties, type JSX, type ReactNode } from "react";3import type { Grid } from "@1771technologies/lytenyte-pro";4import type { GridSpec } from "./demo.jsx";5import { Switch } from "radix-ui";6
7export function ProductCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {8 if (!api.rowIsLeaf(row) || !row.data) return;9
10 const url = row.data?.productThumbnail;11 const title = row.data.product;12 const desc = row.data.productDescription;13
14 return (15 <div className="flex h-full w-full items-center gap-2">16 <img className="border-ln-border-strong h-7 w-7 rounded-lg border" src={url} alt={title + desc} />17 <div className="text-ln-text-dark flex flex-col gap-0.5">18 <div className="font-semibold">{title}</div>19 <div className="text-ln-text-light text-xs">{desc}</div>20 </div>21 </div>22 );23}24
25export function AvatarCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {26 if (!api.rowIsLeaf(row) || !row.data) return;27
28 const url = row.data?.customerAvatar;29
30 const name = row.data.customer;31
32 return (33 <div className="flex h-full w-full items-center gap-2">34 <img className="border-ln-border-strong h-7 w-7 rounded-full border" src={url} alt={name} />35 <div className="text-ln-text-dark flex flex-col gap-0.5">36 <div>{name}</div>37 </div>38 </div>39 );40}41
42const formatter = new Intl.NumberFormat("en-Us", {43 minimumFractionDigits: 2,44 maximumFractionDigits: 2,45});46export function PriceCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {47 if (!api.rowIsLeaf(row) || !row.data) return;48
49 const price = formatter.format(row.data.price);50 const [dollars, cents] = price.split(".");51
52 return (53 <div className="flex h-full w-full items-center justify-end">54 <div className="flex items-baseline tabular-nums">55 <span className="text-ln-text font-semibold">${dollars}</span>.56 <span className="relative text-xs">{cents}</span>57 </div>58 </div>59 );60}61
62export function PurchaseDateCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {63 if (!api.rowIsLeaf(row) || !row.data) return;64
65 const formattedDate = format(row.data.purchaseDate, "dd MMM, yyyy");66
67 return <div className="flex h-full w-full items-center">{formattedDate}</div>;68}69export function IdCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {70 if (!api.rowIsLeaf(row) || !row.data) return;71
72 return <div className="text-xs tabular-nums">{row.data.id}</div>;73}74
75export function PaymentMethodCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {76 if (!api.rowIsLeaf(row) || !row.data) return;77
78 const cardNumber = row.data.cardNumber;79 const provider = row.data.paymentMethod;80
81 let Logo: ReactNode = null;82 if (provider === "Visa") Logo = <VisaLogo className="w-6" />;83 if (provider === "Mastercard") Logo = <MastercardLogo className="w-6" />;84
85 return (86 <div className="flex h-full w-full items-center gap-2">87 <div className="flex w-7 items-center justify-center">{Logo}</div>88 <div className="flex items-center gap-px">89 <div className="bg-ln-gray-40 size-2 rounded-full"></div>90 <div className="bg-ln-gray-40 size-2 rounded-full"></div>91 <div className="bg-ln-gray-40 size-2 rounded-full"></div>92 <div className="bg-ln-gray-40 size-2 rounded-full"></div>93 </div>94 <div className="tabular-nums">{cardNumber}</div>95 </div>96 );97}98
99export function EmailCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {100 if (!api.rowIsLeaf(row) || !row.data) return;101
102 return <div className="text-ln-primary-50 flex h-full w-full items-center">{row.data.email}</div>;103}104
105const VisaLogo = (props: JSX.IntrinsicElements["svg"]) => (106 <svg xmlns="http://www.w3.org/2000/svg" width={2500} height={812} viewBox="0.5 0.5 999 323.684" {...props}>107 <path108 fill="#1434cb"109 d="M651.185.5c-70.933 0-134.322 36.766-134.322 104.694 0 77.9 112.423 83.28 112.423 122.415 0 16.478-18.884 31.229-51.137 31.229-45.773 0-79.984-20.611-79.984-20.611l-14.638 68.547s39.41 17.41 91.734 17.41c77.552 0 138.576-38.572 138.576-107.66 0-82.316-112.89-87.537-112.89-123.86 0-12.91 15.501-27.053 47.662-27.053 36.286 0 65.892 14.99 65.892 14.99l14.326-66.204S696.614.5 651.185.5zM2.218 5.497.5 15.49s29.842 5.461 56.719 16.356c34.606 12.492 37.072 19.765 42.9 42.353l63.51 244.832h85.138L379.927 5.497h-84.942L210.707 218.67l-34.39-180.696c-3.154-20.68-19.13-32.477-38.685-32.477H2.218zm411.865 0L347.449 319.03h80.999l66.4-313.534h-80.765zm451.759 0c-19.532 0-29.88 10.457-37.474 28.73L709.699 319.03h84.942l16.434-47.468h103.483l9.994 47.468H999.5L934.115 5.497h-68.273zm11.047 84.707 25.178 117.653h-67.454z"110 />111 </svg>112);113
114const MastercardLogo = (props: JSX.IntrinsicElements["svg"]) => (115 <svg116 xmlns="http://www.w3.org/2000/svg"117 width={2500}118 height={1524}119 viewBox="55.2 38.3 464.5 287.8"120 {...props}121 >122 <path123 fill="#f79f1a"124 d="M519.7 182.2c0 79.5-64.3 143.9-143.6 143.9s-143.6-64.4-143.6-143.9S296.7 38.3 376 38.3s143.7 64.4 143.7 143.9z"125 />126 <path127 fill="#ea001b"128 d="M342.4 182.2c0 79.5-64.3 143.9-143.6 143.9S55.2 261.7 55.2 182.2 119.5 38.3 198.8 38.3s143.6 64.4 143.6 143.9z"129 />130 <path131 fill="#ff5f01"132 d="M287.4 68.9c-33.5 26.3-55 67.3-55 113.3s21.5 87 55 113.3c33.5-26.3 55-67.3 55-113.3s-21.5-86.9-55-113.3z"133 />134 </svg>135);136
137export function SwitchToggle(props: { label: string; checked: boolean; onChange: (b: boolean) => void }) {138 const id = useId();139 return (140 <div className="flex items-center gap-2">141 <label className="text-ln-text-dark text-sm leading-none" htmlFor={id}>142 {props.label}143 </label>144 <Switch.Root145 className="bg-ln-gray-10 data-[state=checked]:bg-ln-primary-50 h-5.5 w-9.5 border-ln-border-strong relative cursor-pointer rounded-full border outline-none"146 id={id}147 checked={props.checked}148 onCheckedChange={(c) => props.onChange(c)}149 style={{ "-webkit-tap-highlight-color": "rgba(0, 0, 0, 0)" } as CSSProperties}150 >151 <Switch.Thumb className="size-4.5 block translate-x-0.5 rounded-full bg-white/95 shadow transition-transform duration-100 will-change-transform data-[state=checked]:translate-x-4 data-[state=checked]:bg-white" />152 </Switch.Root>153 </div>154 );155}Number Filter Model
Define number filters dynamically rather than relying on predefined logic. LyteNyte Grid lets you define a filter model and a set of operations to build custom filters. This section presents one approach. Design the filter model that best fits your application’s requirements.
Begin by defining the type representation for your number filter. The code below defines a basic filter model you can use to create a filter function for the client row data source:
1export type FilterNumberOperator =2 | "greater_than"3 | "greater_than_or_equals"4 | "less_than"5 | "less_than_or_equals"6 | "equals"7 | "not_equals";8
9export interface FilterNumber {10 readonly operator: FilterNumberOperator;11 readonly value: number;12}13
14export interface GridFilter {15 readonly left: FilterNumber;16 readonly right: FilterNumber | null;17 readonly operator: "AND" | "OR";18}19
20export interface GridSpec {21 readonly data: OrderData;22 readonly api: {23 readonly filterModel: PieceWritable<Record<string, GridFilter>>;24 };25}Open the filter popover by clicking the funnel icon on the Price column.
Number Filter User Interface
33 collapsed lines
1import "@1771technologies/lytenyte-pro/light-dark.css";2import type { OrderData } from "@1771technologies/grid-sample-data/orders";3import { data } from "@1771technologies/grid-sample-data/orders";4import {5 AvatarCell,6 EmailCell,7 IdCell,8 PaymentMethodCell,9 PriceCell,10 ProductCell,11 PurchaseDateCell,12} from "./components.jsx";13import { useClientDataSource, Grid, type PieceWritable, usePiece } from "@1771technologies/lytenyte-pro";14import { Header } from "./filter.jsx";15import { useMemo, useState } from "react";16import { ViewportShadows } from "@1771technologies/lytenyte-pro/components";17
18export interface GridSpec {19 readonly data: OrderData;20 readonly api: {21 readonly filterModel: PieceWritable<Record<string, GridFilter>>;22 };23}24
25const columns: Grid.Column<GridSpec>[] = [26 { id: "id", width: 60, widthMin: 60, cellRenderer: IdCell, name: "ID" },27 { id: "product", cellRenderer: ProductCell, width: 200, name: "Product" },28 { id: "price", type: "number", cellRenderer: PriceCell, width: 100, name: "Price", headerRenderer: Header },29 { id: "customer", cellRenderer: AvatarCell, width: 180, name: "Customer" },30 { id: "purchaseDate", cellRenderer: PurchaseDateCell, name: "Purchase Date", width: 130 },31 { id: "paymentMethod", cellRenderer: PaymentMethodCell, name: "Payment Method", width: 150 },32 { id: "email", cellRenderer: EmailCell, width: 220, name: "Email" },33];34
35export type FilterNumberOperator =36 | "greater_than"37 | "greater_than_or_equals"38 | "less_than"39 | "less_than_or_equals"40 | "equals"41 | "not_equals";42
43export interface FilterNumber {44 readonly operator: FilterNumberOperator;45 readonly value: number;46}47
48export interface GridFilter {49 readonly left: FilterNumber;50 readonly right: FilterNumber | null;51 readonly operator: "AND" | "OR";52}53
54export default function FilterDemo() {55 const [filter, setFilter] = useState<Record<string, GridFilter>>({});56 const filterModel = usePiece(filter, setFilter);57
58 const filterFn = useMemo(() => {59 const entries = Object.entries(filter);60
61 const evaluateNumberFilter = (operator: FilterNumberOperator, compare: number, value: number) => {62 if (operator === "equals") return value === compare;63 if (operator === "greater_than") return compare > value;64 if (operator === "greater_than_or_equals") return compare >= value;65 if (operator === "less_than") return compare < value;66 if (operator === "less_than_or_equals") return compare <= value;67 if (operator === "not_equals") return value !== compare;68
69 return false;70 };71
72 return entries.map<Grid.T.FilterFn<GridSpec["data"]>>(([column, filter]) => {73 return (row) => {74 const value = row.data[column as keyof GridSpec["data"]];75
76 // We are only working with number filters, so lets filter out none number77 if (typeof value !== "number") return false;78
79 const compareValue = value;80
81 const leftResult = evaluateNumberFilter(filter.left.operator, compareValue, filter.left.value);82 if (!filter.right) return leftResult;83
84 if (filter.operator === "OR")85 return leftResult || evaluateNumberFilter(filter.right.operator, compareValue, filter.right.value);86
87 return leftResult && evaluateNumberFilter(filter.right.operator, compareValue, filter.right.value);88 };89 });90 }, [filter]);91
92 const ds = useClientDataSource<GridSpec>({93 data,94 filter: filterFn,95 });96
97 return (98 <>99 <div className="ln-grid" style={{ height: 500 }}>100 <Grid101 apiExtension={useMemo(() => ({ filterModel }), [filterModel])}102 rowHeight={50}103 columns={columns}104 rowSource={ds}105 slotShadows={ViewportShadows}106 />107 </div>108 </>109 );110}1import { format } from "date-fns";2import { type JSX, type ReactNode } from "react";3import type { Grid } from "@1771technologies/lytenyte-pro";4import type { GridSpec } from "./demo.jsx";5
6export function ProductCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {7 if (!api.rowIsLeaf(row) || !row.data) return;8
9 const url = row.data?.productThumbnail;10 const title = row.data.product;11 const desc = row.data.productDescription;12
13 return (14 <div className="flex h-full w-full items-center gap-2">15 <img className="border-ln-border-strong h-7 w-7 rounded-lg border" src={url} alt={title + desc} />16 <div className="text-ln-text-dark flex flex-col gap-0.5">17 <div className="font-semibold">{title}</div>18 <div className="text-ln-text-light text-xs">{desc}</div>19 </div>20 </div>21 );22}23
24export function AvatarCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {25 if (!api.rowIsLeaf(row) || !row.data) return;26
27 const url = row.data?.customerAvatar;28
29 const name = row.data.customer;30
31 return (32 <div className="flex h-full w-full items-center gap-2">33 <img className="border-ln-border-strong h-7 w-7 rounded-full border" src={url} alt={name} />34 <div className="text-ln-text-dark flex flex-col gap-0.5">35 <div>{name}</div>36 </div>37 </div>38 );39}40
41const formatter = new Intl.NumberFormat("en-Us", {42 minimumFractionDigits: 2,43 maximumFractionDigits: 2,44});45export function PriceCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {46 if (!api.rowIsLeaf(row) || !row.data) return;47
48 const price = formatter.format(row.data.price);49 const [dollars, cents] = price.split(".");50
51 return (52 <div className="flex h-full w-full items-center justify-end">53 <div className="flex items-baseline tabular-nums">54 <span className="text-ln-text font-semibold">${dollars}</span>.55 <span className="relative text-xs">{cents}</span>56 </div>57 </div>58 );59}60
61export function PurchaseDateCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {62 if (!api.rowIsLeaf(row) || !row.data) return;63
64 const formattedDate = format(row.data.purchaseDate, "dd MMM, yyyy");65
66 return <div className="flex h-full w-full items-center">{formattedDate}</div>;67}68export function IdCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {69 if (!api.rowIsLeaf(row) || !row.data) return;70
71 return <div className="text-xs tabular-nums">{row.data.id}</div>;72}73
74export function PaymentMethodCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {75 if (!api.rowIsLeaf(row) || !row.data) return;76
77 const cardNumber = row.data.cardNumber;78 const provider = row.data.paymentMethod;79
80 let Logo: ReactNode = null;81 if (provider === "Visa") Logo = <VisaLogo className="w-6" />;82 if (provider === "Mastercard") Logo = <MastercardLogo className="w-6" />;83
84 return (85 <div className="flex h-full w-full items-center gap-2">86 <div className="flex w-7 items-center justify-center">{Logo}</div>87 <div className="flex items-center gap-px">88 <div className="bg-ln-gray-40 size-2 rounded-full"></div>89 <div className="bg-ln-gray-40 size-2 rounded-full"></div>90 <div className="bg-ln-gray-40 size-2 rounded-full"></div>91 <div className="bg-ln-gray-40 size-2 rounded-full"></div>92 </div>93 <div className="tabular-nums">{cardNumber}</div>94 </div>95 );96}97
98export function EmailCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {99 if (!api.rowIsLeaf(row) || !row.data) return;100
101 return <div className="text-ln-primary-50 flex h-full w-full items-center">{row.data.email}</div>;102}103
104const VisaLogo = (props: JSX.IntrinsicElements["svg"]) => (105 <svg xmlns="http://www.w3.org/2000/svg" width={2500} height={812} viewBox="0.5 0.5 999 323.684" {...props}>106 <path107 fill="#1434cb"108 d="M651.185.5c-70.933 0-134.322 36.766-134.322 104.694 0 77.9 112.423 83.28 112.423 122.415 0 16.478-18.884 31.229-51.137 31.229-45.773 0-79.984-20.611-79.984-20.611l-14.638 68.547s39.41 17.41 91.734 17.41c77.552 0 138.576-38.572 138.576-107.66 0-82.316-112.89-87.537-112.89-123.86 0-12.91 15.501-27.053 47.662-27.053 36.286 0 65.892 14.99 65.892 14.99l14.326-66.204S696.614.5 651.185.5zM2.218 5.497.5 15.49s29.842 5.461 56.719 16.356c34.606 12.492 37.072 19.765 42.9 42.353l63.51 244.832h85.138L379.927 5.497h-84.942L210.707 218.67l-34.39-180.696c-3.154-20.68-19.13-32.477-38.685-32.477H2.218zm411.865 0L347.449 319.03h80.999l66.4-313.534h-80.765zm451.759 0c-19.532 0-29.88 10.457-37.474 28.73L709.699 319.03h84.942l16.434-47.468h103.483l9.994 47.468H999.5L934.115 5.497h-68.273zm11.047 84.707 25.178 117.653h-67.454z"109 />110 </svg>111);112
113const MastercardLogo = (props: JSX.IntrinsicElements["svg"]) => (114 <svg115 xmlns="http://www.w3.org/2000/svg"116 width={2500}117 height={1524}118 viewBox="55.2 38.3 464.5 287.8"119 {...props}120 >121 <path122 fill="#f79f1a"123 d="M519.7 182.2c0 79.5-64.3 143.9-143.6 143.9s-143.6-64.4-143.6-143.9S296.7 38.3 376 38.3s143.7 64.4 143.7 143.9z"124 />125 <path126 fill="#ea001b"127 d="M342.4 182.2c0 79.5-64.3 143.9-143.6 143.9S55.2 261.7 55.2 182.2 119.5 38.3 198.8 38.3s143.6 64.4 143.6 143.9z"128 />129 <path130 fill="#ff5f01"131 d="M287.4 68.9c-33.5 26.3-55 67.3-55 113.3s21.5 87 55 113.3c33.5-26.3 55-67.3 55-113.3s-21.5-86.9-55-113.3z"132 />133 </svg>134);1import "@1771technologies/lytenyte-pro/components.css";2import { type Grid } from "@1771technologies/lytenyte-pro";3import type { FilterNumberOperator, GridFilter, GridSpec } from "./demo";4import { CheckIcon, ChevronDownIcon } from "@radix-ui/react-icons";5import { useState } from "react";6import { twMerge } from "tailwind-merge";7import clsx, { type ClassValue } from "clsx";8import { Popover, SmartSelect } from "@1771technologies/lytenyte-pro/components";9
10function tw(...c: ClassValue[]) {11 return twMerge(clsx(...c));12}13
14export function Header({ api, column }: Grid.T.HeaderParams<GridSpec>) {15 const label = column.name ?? column.id;16
17 const model = api.filterModel.useValue();18 const hasFilter = !!model[column.id];19 return (20 <div className="flex h-full w-full items-center justify-between">21 <div>{label}</div>22
23 <Popover>24 <Popover.Trigger data-ln-button="secondary" data-ln-icon data-ln-size="sm" className="relative">25 <div className="sr-only">Filter the {label}</div>26 <svg27 xmlns="http://www.w3.org/2000/svg"28 width="16"29 height="16"30 fill="currentcolor"31 viewBox="0 0 256 256"32 >33 <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>34 </svg>35 {hasFilter && <div className="bg-ln-primary-50 absolute right-px top-px size-2 rounded-full" />}36 </Popover.Trigger>37 <Popover.Container>38 <Popover.Arrow />39 <Popover.Title className="sr-only">Filter {label}</Popover.Title>40 <Popover.Description className="sr-only">Filter the numbers in the{label}</Popover.Description>41 <TextFilterControl column={column} filter={model[column.id] ?? null} api={api}></TextFilterControl>42 </Popover.Container>43 </Popover>44 </div>45 );46}47
48const selectOptions = [49 { id: "equals", label: "Equals" },50 { id: "not_equals", label: "Does Not Equal" },51 { id: "separator", selectable: false },52 { id: "greater_than", label: "Greater Than" },53 { id: "greater_than_or_equals", label: "Greater Than or Equal To" },54 { id: "less_than", label: "Less Than" },55 { id: "less_than_or_equals", label: "Less Than or Equal To" },56];57
58type DeepPartial<T> = T extends object59 ? {60 -readonly [P in keyof T]?: DeepPartial<T[P]>;61 }62 : T;63
64function TextFilterControl({65 api,66 filter: initialFilter,67 column,68}: {69 api: Grid.API<GridSpec>;70 filter: GridFilter | null;71 column: Grid.Column<GridSpec>;72}) {73 const [filter, setFilter] = useState<DeepPartial<GridFilter> | null>(initialFilter);74
75 const canShowSecond = filter?.right || (filter?.left?.operator && filter.left.value != null);76
77 const leftValue = selectOptions.find((x) => x.id === filter?.left?.operator) ?? null;78 const rightValue = selectOptions.find((x) => x.id === filter?.right?.operator) ?? null;79
80 const combineOperator = filter?.operator ?? "AND";81
82 const canSubmit =83 (filter?.left?.value != null && filter.left?.operator) ||84 (filter?.right?.value != null && filter.right.operator);85
86 const popoverControls = Popover.useControls();87 return (88 <form89 className="grid grid-cols-1 gap-2 md:grid-cols-2"90 onSubmit={(e) => {91 if (!canSubmit) return;92 e.preventDefault();93
94 const finalFilter: DeepPartial<GridFilter> = {};95 if (filter?.left?.value != null && filter?.left.operator) finalFilter.left = filter.left;96 if (filter?.right?.value != null && filter?.right.operator) {97 // If the left filter is incomplete then the we use the right filter value as the left filter.98 if (!finalFilter.left) {99 finalFilter.left = filter.right;100 finalFilter.right = null;101 } else {102 finalFilter.right = filter.right;103 }104 }105
106 finalFilter.operator = combineOperator;107
108 api.filterModel.set((prev) => ({ ...prev, [column.id]: finalFilter as GridFilter }));109 popoverControls.openChange(false);110 }}111 >112 <div className="text-ln-text hidden ps-2 text-sm md:block">Operator</div>113 <div className="text-ln-text hidden ps-2 text-sm md:block">Values</div>114
115 <SmartSelect116 options={selectOptions}117 value={filter ? (selectOptions.find((x) => x.id === filter?.left?.operator) ?? null) : null}118 onOptionChange={(option) => {119 if (!option) return;120
121 setFilter((prev) => {122 if (!prev) return { left: { operator: option.id as FilterNumberOperator } };123 return { ...prev, left: { ...prev.left, operator: option.id as FilterNumberOperator } };124 });125 }}126 kind="basic"127 trigger={128 <SmartSelect.BasicTrigger129 type="button"130 data-ln-input131 className="flex min-w-40 cursor-pointer items-center justify-between"132 >133 <div>{leftValue?.label ?? "Select..."}</div>134 <div>135 <ChevronDownIcon />136 </div>137 </SmartSelect.BasicTrigger>138 }139 >140 {(p) => {141 if (p.option.id.startsWith("separator")) {142 return <div role="separator" className="bg-ln-gray-40 my-1 h-px w-full" />;143 }144
145 return (146 <SmartSelect.Option key={p.option.id} {...p} className="flex items-center justify-between">147 {p.option.label}148 {p.selected && <CheckIcon className="text-ln-primary-50" />}149 </SmartSelect.Option>150 );151 }}152 </SmartSelect>153
154 <div>155 <label>156 <span className="sr-only">Value for the first filter</span>157 <input158 data-ln-input159 value={filter?.left?.value ?? ""}160 className="w-full"161 type="number"162 onChange={(e) => {163 setFilter((prev) => {164 if (!prev) return { left: { value: Number.parseFloat(e.target.value) } };165
166 return { ...prev, left: { ...prev.left, value: Number.parseFloat(e.target.value) } };167 });168 }}169 />170 </label>171 </div>172
173 {canShowSecond && (174 <>175 <div className="grid grid-cols-2 py-1 md:col-span-2 md:grid-cols-subgrid">176 <label className="flex justify-end gap-2 pe-2">177 <input178 type="radio"179 value="AND"180 name="operator"181 className={tw(182 "border-ln-gray-40 checked:border-ln-primary-50 h-4 w-4 cursor-pointer select-none appearance-none rounded-full border checked:border-[5px]",183 "focus-visible:outline-ln-primary-50 focus-visible:outline focus-visible:outline-offset-1",184 )}185 checked={combineOperator === "AND"}186 onChange={() => {187 setFilter((prev) => {188 if (!prev) return { operator: "AND" };189 return { ...prev, operator: "AND" };190 });191 }}192 />193 <span>And</span>194 </label>195 <label className="flex gap-2 ps-2">196 <input197 type="radio"198 value="OR"199 name="operator"200 className={tw(201 "border-ln-gray-40 checked:border-ln-primary-50 h-4 w-4 cursor-pointer select-none appearance-none rounded-full border checked:border-[5px]",202 "focus-visible:outline-ln-primary-50 focus-visible:outline focus-visible:outline-offset-1",203 )}204 checked={combineOperator === "OR"}205 onChange={() => {206 setFilter((prev) => {207 if (!prev) return { operator: "OR" };208 return { ...prev, operator: "OR" };209 });210 }}211 />212 <span>Or</span>213 </label>214 </div>215
216 <SmartSelect217 options={selectOptions}218 value={filter ? (selectOptions.find((x) => x.id === filter?.right?.operator) ?? null) : null}219 onOptionChange={(option) => {220 if (!option) return;221
222 setFilter((prev) => {223 if (!prev) return { right: { operator: option.id as FilterNumberOperator } };224 return { ...prev, right: { ...prev.right, operator: option.id as FilterNumberOperator } };225 });226 }}227 kind="basic"228 trigger={229 <SmartSelect.BasicTrigger230 type="button"231 data-ln-input232 className="flex min-w-40 cursor-pointer items-center justify-between"233 >234 <div>{rightValue?.label ?? "Select..."}</div>235 <div>236 <ChevronDownIcon />237 </div>238 </SmartSelect.BasicTrigger>239 }240 >241 {(p) => {242 if (p.option.id.startsWith("separator")) {243 return <div role="separator" className="bg-ln-gray-40 my-1 h-px w-full" />;244 }245
246 return (247 <SmartSelect.Option key={p.option.id} {...p} className="flex items-center justify-between">248 {p.option.label}249 </SmartSelect.Option>250 );251 }}252 </SmartSelect>253
254 <div>255 <label>256 <span className="sr-only">Value for the second filter</span>257 <input258 data-ln-input259 className="w-full"260 value={filter?.right?.value ?? ""}261 type="number"262 onChange={(e) => {263 setFilter((prev) => {264 if (!prev) return { right: { value: Number.parseFloat(e.target.value) } };265
266 return { ...prev, right: { ...prev.right, value: Number.parseFloat(e.target.value) } };267 });268 }}269 />270 </label>271 </div>272 </>273 )}274
275 <div className="flex items-center justify-between gap-4 md:col-span-2 md:grid md:grid-cols-subgrid">276 <div className="pt-2">277 <button278 data-ln-button="tertiary"279 data-ln-size="sm"280 type="button"281 className="hover:bg-ln-gray-30"282 onClick={() => popoverControls.openChange(false)}283 >284 Cancel285 </button>286 </div>287 <div className="flex justify-end gap-2 pt-2">288 <button289 data-ln-button="secondary"290 data-ln-size="sm"291 type="button"292 className="hover:bg-ln-bg-button-light"293 onClick={() => {294 api.filterModel.set((prev) => {295 const next = { ...prev };296 delete next[column.id];297
298 return next;299 });300 popoverControls.openChange(false);301 }}302 >303 Clear304 </button>305 <button data-ln-button="primary" data-ln-size="sm" disabled={!canSubmit}>306 Apply Filters307 </button>308 </div>309 </div>310 </form>311 );312}The code below defines the filter model. It creates filter model state and passes it to the grid API as an extension. The demo also includes a filter UI for applying filters interactively.
When a user applies a filter, useMemo creates a new filterFn, which you then pass to the
client data source. See the filter.tsx file in the demo’s expanded code for the logic that
builds the filter UI.
1const [filter, setFilter] = useState<Record<string, GridFilter>>({});2const filterModel = usePiece(filter, setFilter);3
4const filterFn = useMemo(() => {5 const entries = Object.entries(filter);6
7 const evaluateNumberFilter = (operator: FilterNumberOperator, compare: number, value: number) => {8 if (operator === "equals") return value === compare;9 if (operator === "greater_than") return compare > value;10 if (operator === "greater_than_or_equals") return compare >= value;11 if (operator === "less_than") return compare < value;12 if (operator === "less_than_or_equals") return compare <= value;13 if (operator === "not_equals") return value !== compare;14
15 return false;16 };17
18 return entries.map<Grid.T.FilterFn<GridSpec["data"]>>(([column, filter]) => {19 return (row) => {16 collapsed lines
20 const value = row.data[column as keyof GridSpec["data"]];21
22 // This guide only covers number filters, so return false for non-number values.23 if (typeof value !== "number") return false;24
25 const compareValue = value;26
27 const leftResult = evaluateNumberFilter(filter.left.operator, compareValue, filter.left.value);28
29 if (!filter.right) return leftResult;30
31 if (filter.operator === "OR") {32 return leftResult || evaluateNumberFilter(filter.right.operator, compareValue, filter.right.value);33 }34
35 return leftResult && evaluateNumberFilter(filter.right.operator, compareValue, filter.right.value);36 };37 });38}, [filter]);Next Steps
- Set Filtering: Learn how to create custom set filters and filter rows.
- Filtering Best Practices: General guidelines for creating usable filter interactions.
- Filtering Text: Create and apply text filters to client-side data.
