Client Row Sorting
Sort client row data in either ascending or descending order and apply multiple sorts, where each successive sort resolves equal comparisons from the previous sorts.
LyteNyte Grid does not impose a strict row-sorting interface. Your sorting approach will vary
depending on the row source. For the client row source, use the sort property to specify
how rows should be sorted. The sort property accepts one of two types of values:
- A sort function that compares two rows. The function must return a positive number if the
first row is greater than the second, a negative number if it is less, and
0otherwise. - A sort dimension array. The array declaratively describes a list of sort comparators that
apply in order. The grid starts with the first sort dimension and proceeds to subsequent
dimensions only when the current comparator returns
0.
Sort Functions
A sort function compares two rows (of any kind). The client row data source calls the function
you pass to the sort property with two row nodes. The row nodes may be any row type (leaf, group,
or aggregation). If you are unfamiliar with row node types, see the Rows Overview guide
for more details.
The demo below shows basic sorting using a sort function. It covers only the sorting interface’s foundational capabilities. The next sections build on this foundation to implement robust sorting in LyteNyte Grid.
Basic Sort Function
78 collapsed lines
1import { Grid, useClientDataSource } from "@1771technologies/lytenyte-pro";2import "@1771technologies/lytenyte-pro/components.css";3import "@1771technologies/lytenyte-pro/light-dark.css";4import {5 ExchangeCell,6 makePerfHeaderCell,7 NetworkCell,8 PercentCell,9 PercentCellPositiveNegative,10 SymbolCell,11} from "./components.jsx";12import type { DEXPerformanceData } from "@1771technologies/grid-sample-data/dex-pairs-performance";13import { data } from "@1771technologies/grid-sample-data/dex-pairs-performance";14import { useState } from "react";15
16export interface GridSpec {17 readonly data: DEXPerformanceData;18}19
20const columns: Grid.Column<GridSpec>[] = [21 { id: "symbol", cellRenderer: SymbolCell, width: 250, name: "Symbol" },22 { id: "network", cellRenderer: NetworkCell, width: 220, name: "Network" },23 { id: "exchange", cellRenderer: ExchangeCell, width: 220, name: "Exchange" },24
25 {26 id: "change24h",27 cellRenderer: PercentCellPositiveNegative,28 headerRenderer: makePerfHeaderCell("Change", "24h"),29 name: "Change % 24h",30 type: "number,",31 },32
33 {34 id: "perf1w",35 cellRenderer: PercentCellPositiveNegative,36 headerRenderer: makePerfHeaderCell("Perf %", "1w"),37 name: "Perf % 1W",38 type: "number,",39 },40 {41 id: "perf1m",42 cellRenderer: PercentCellPositiveNegative,43 headerRenderer: makePerfHeaderCell("Perf %", "1m"),44 name: "Perf % 1M",45 type: "number,",46 },47 {48 id: "perf3m",49 cellRenderer: PercentCellPositiveNegative,50 headerRenderer: makePerfHeaderCell("Perf %", "3m"),51 name: "Perf % 3M",52 type: "number,",53 },54 {55 id: "perf6m",56 cellRenderer: PercentCellPositiveNegative,57 headerRenderer: makePerfHeaderCell("Perf %", "6m"),58 name: "Perf % 6M",59 type: "number,",60 },61 {62 id: "perfYtd",63 cellRenderer: PercentCellPositiveNegative,64 headerRenderer: makePerfHeaderCell("Perf %", "YTD"),65 name: "Perf % YTD",66 type: "number",67 },68 { id: "volatility", cellRenderer: PercentCell, name: "Volatility", type: "number" },69 {70 id: "volatility1m",71 cellRenderer: PercentCell,72 headerRenderer: makePerfHeaderCell("Volatility", "1m"),73 name: "Volatility 1M",74 type: "number",75 },76];77
78const base: Grid.ColumnBase<GridSpec> = { width: 80 };79
80const sortByChange24: Grid.T.SortFn<GridSpec["data"]> = (left, right) => {81 const leftData = left.data as DEXPerformanceData;82 const rightData = right.data as DEXPerformanceData;83
84 return leftData.change24h - rightData.change24h;85};86
87export default function ClientDemo() {88 const [sort, setSort] = useState<Grid.T.SortFn<GridSpec["data"]> | null>(null);89
90 const ds = useClientDataSource<GridSpec>({ data, sort });91
92 return (93 <>94 <div className="border-ln-border flex gap-4 border-b px-4 py-3">95 <button96 data-ln-button="website"97 data-ln-size="md"98 onClick={() => {99 setSort(() => sortByChange24);100 }}101 >102 Sort: Ch. 24H103 </button>104 <button105 data-ln-button="website"106 data-ln-size="md"107 onClick={() => {108 setSort(null);109 }}110 >111 Clear Sort112 </button>113 </div>114 <div115 className="ln-grid ln-cell:text-xs ln-header:text-xs ln-header:text-ln-text-xlight"116 style={{ height: 500 }}117 >118 <Grid columns={columns} columnBase={base} rowSource={ds} />119 </div>120 </>121 );122}1import type { ClassValue } from "clsx";2import clsx from "clsx";3import { twMerge } from "tailwind-merge";4import { exchanges, networks, symbols } from "@1771technologies/grid-sample-data/dex-pairs-performance";5
6export function tw(...c: ClassValue[]) {7 return twMerge(clsx(...c));8}9import type { Grid } from "@1771technologies/lytenyte-pro";10import type { GridSpec } from "./demo";11
12export function SymbolCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {13 if (!api.rowIsLeaf(row) || !row.data) return null;14
15 const ticker = row.data.symbolTicker;16 const symbol = row.data.symbol;17 const image = symbols[row.data.symbolTicker];18
19 return (20 <div className="grid grid-cols-[20px_auto_auto] items-center gap-1.5">21 <div>22 <img23 src={image}24 alt={`Logo for symbol ${symbol}`}25 className="h-full w-full overflow-hidden rounded-full"26 />27 </div>28 <div className="bg-ln-gray-20 text-ln-text-dark flex h-fit items-center justify-center rounded-lg px-2 py-px text-[10px]">29 {ticker}30 </div>31 <div className="w-full overflow-hidden text-ellipsis">{symbol.split("/")[0]}</div>32 </div>33 );34}35
36export function NetworkCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {37 if (!api.rowIsLeaf(row) || !row.data) return null;38
39 const name = row.data.network;40 const image = networks[name];41
42 return (43 <div className="grid grid-cols-[20px_1fr] items-center gap-1.5">44 <div>45 <img46 src={image}47 alt={`Logo for network ${name}`}48 className="h-full w-full overflow-hidden rounded-full"49 />50 </div>51 <div className="w-full overflow-hidden text-ellipsis">{name}</div>52 </div>53 );54}55
56export function ExchangeCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {57 if (!api.rowIsLeaf(row) || !row.data) return null;58
59 const name = row.data.exchange;60 const image = exchanges[name];61
62 return (63 <div className="grid grid-cols-[20px_1fr] items-center gap-1.5">64 <div>65 <img66 src={image}67 alt={`Logo for exchange ${name}`}68 className="h-full w-full overflow-hidden rounded-full"69 />70 </div>71 <div className="w-full overflow-hidden text-ellipsis">{name}</div>72 </div>73 );74}75
76export function PercentCellPositiveNegative({ api, column, row }: Grid.T.CellRendererParams<GridSpec>) {77 if (!api.rowIsLeaf(row) || !row.data) return null;78
79 const field = api.columnField(column, row);80
81 if (typeof field !== "number") return "-";82
83 const value = (field > 0 ? "+" : "") + (field * 100).toFixed(2) + "%";84
85 return (86 <div87 className={tw(88 "h-ful flex w-full items-center justify-end tabular-nums",89 field < 0 ? "text-red-600 dark:text-red-300" : "text-green-600 dark:text-green-300",90 )}91 >92 {value}93 </div>94 );95}96
97export function PercentCell({ api, column, row }: Grid.T.CellRendererParams<GridSpec>) {98 if (!api.rowIsLeaf(row) || !row.data) return null;99
100 const field = api.columnField(column, row);101
102 if (typeof field !== "number") return "-";103
104 const value = (field > 0 ? "+" : "") + (field * 100).toFixed(2) + "%";105
106 return <div className="h-ful flex w-full items-center justify-end tabular-nums">{value}</div>;107}108
109export const makePerfHeaderCell = (name: string, subname: string) => {110 return (_: Grid.T.HeaderParams<GridSpec>) => {111 return (112 <div className="flex h-full w-full flex-col items-end justify-center tabular-nums">113 <div>{name}</div>114 <div className="text-ln-text-light font-mono uppercase">{subname}</div>115 </div>116 );117 };118};The demo above covers only the basics of sorting. A more standard sorting interface would typically include additional functionalities such as:
- Sort indicator on the sorted column, such as an up or down icon.
- Sorting for any column in the grid, not just Change 24H.
- A method to switch between ascending and descending sort order.
- A scalable approach that supports arbitrary sort options, not just predefined buttons.
LyteNyte Grid lets you define your own column behavior.
Extend the column specification with a sort attribute to represent sorting.
-
Extend the
GridSpectype with your column extension:1export interface GridSpec {2readonly data: DEXPerformanceData;3readonly column: { sort?: "asc" | "desc" };4} -
Extend the
APIwith a function that updates your internal sort state:1export interface GridSpec {2readonly data: DEXPerformanceData;3readonly column: { sort?: "asc" | "desc" };4readonly api: { sortColumn: (id: string, dir: "asc" | "desc" | null) => void };5} -
Implement the sort API extension, and update your column headers to indicate the applied sort.
See the code in the demo below for the full implementation. Some important snippets are shown below.
1// Assumes the columns state is defined above23function sortColumn(id: string, dir: "asc" | "desc" | null) {4setColumns((prev) => {5const next = prev.map((x) => {6// Remove any existing sort7if (x.sort && x.id !== id) {8const next = { ...x };9delete next.sort;10return next;11}12// Apply our new sort13if (x.id === id) {14const next = { ...x };15if (dir == null) delete next.sort;16else next.sort = dir;1718return next;19}20return x;21});22return next;23});24}
In the demo below, you can click any header to sort that column. Defining a sort extension for columns enables this flexibility.
Extended Function Sort
75 collapsed lines
1import { Grid, useClientDataSource } from "@1771technologies/lytenyte-pro";2import "@1771technologies/lytenyte-pro/components.css";3import "@1771technologies/lytenyte-pro/light-dark.css";4import {5 ExchangeCell,6 Header,7 makePerfHeaderCell,8 NetworkCell,9 PercentCell,10 PercentCellPositiveNegative,11 SymbolCell,12} from "./components.jsx";13import type { DEXPerformanceData } from "@1771technologies/grid-sample-data/dex-pairs-performance";14import { data } from "@1771technologies/grid-sample-data/dex-pairs-performance";15import { useMemo, useState } from "react";16
17const initialColumns: Grid.Column<GridSpec>[] = [18 { id: "symbol", cellRenderer: SymbolCell, width: 250, name: "Symbol" },19 { id: "network", cellRenderer: NetworkCell, width: 220, name: "Network" },20 { id: "exchange", cellRenderer: ExchangeCell, width: 220, name: "Exchange" },21
22 {23 id: "change24h",24 cellRenderer: PercentCellPositiveNegative,25 headerRenderer: makePerfHeaderCell("Change", "24h"),26 name: "Change % 24h",27 type: "number,",28 },29
30 {31 id: "perf1w",32 cellRenderer: PercentCellPositiveNegative,33 headerRenderer: makePerfHeaderCell("Perf %", "1w"),34 name: "Perf % 1W",35 type: "number,",36 },37 {38 id: "perf1m",39 cellRenderer: PercentCellPositiveNegative,40 headerRenderer: makePerfHeaderCell("Perf %", "1m"),41 name: "Perf % 1M",42 type: "number,",43 },44 {45 id: "perf3m",46 cellRenderer: PercentCellPositiveNegative,47 headerRenderer: makePerfHeaderCell("Perf %", "3m"),48 name: "Perf % 3M",49 type: "number,",50 },51 {52 id: "perf6m",53 cellRenderer: PercentCellPositiveNegative,54 headerRenderer: makePerfHeaderCell("Perf %", "6m"),55 name: "Perf % 6M",56 type: "number,",57 },58 {59 id: "perfYtd",60 cellRenderer: PercentCellPositiveNegative,61 headerRenderer: makePerfHeaderCell("Perf %", "YTD"),62 name: "Perf % YTD",63 type: "number",64 },65 { id: "volatility", cellRenderer: PercentCell, name: "Volatility", type: "number", width: 110 },66 {67 id: "volatility1m",68 cellRenderer: PercentCell,69 headerRenderer: makePerfHeaderCell("Volatility", "1m"),70 name: "Volatility 1M",71 type: "number",72 },73];74
75const base: Grid.ColumnBase<GridSpec> = { width: 90, headerRenderer: Header };76
77export interface GridSpec {78 readonly data: DEXPerformanceData;79 readonly column: { sort?: "asc" | "desc" };80 readonly api: {81 sortColumn: (id: string, dir: "asc" | "desc" | null) => void;82 };83}84
85export default function ClientDemo() {86 const [columns, setColumns] = useState(initialColumns);87
88 const sortFn = useMemo(() => {89 const columnWithSort = columns.find((x) => x.sort);90 if (!columnWithSort) return null;91
92 const sort: Grid.T.SortFn<GridSpec["data"]> = (left, right) => {93 const leftData = left.data as DEXPerformanceData;94 const rightData = right.data as DEXPerformanceData;95
96 let leftValue = leftData[columnWithSort.id as keyof DEXPerformanceData];97 let rightValue = rightData[columnWithSort.id as keyof DEXPerformanceData];98
99 if (typeof leftValue === "string") leftValue = leftValue.toLowerCase();100 if (typeof rightValue === "string") rightValue = rightValue.toLowerCase();101
102 const dirChanger = columnWithSort.sort === "asc" ? 1 : -1;103
104 if (leftValue < rightValue) return -1 * dirChanger;105 if (leftValue > rightValue) return 1 * dirChanger;106 return 0;107 };108
109 return sort;110 }, [columns]);111
112 const apiExtension = useMemo<GridSpec["api"]>(() => {113 return {114 sortColumn: (id, dir) => {115 setColumns((prev) => {116 const next = prev.map((x) => {117 // Remove any existing sort118 if (x.sort && x.id !== id) {119 const next = { ...x };120 delete next.sort;121 return next;122 }123 // Apply our new sort124 if (x.id === id) {125 const next = { ...x };126 if (dir == null) delete next.sort;127 else next.sort = dir;128
129 return next;130 }131 return x;132 });133 return next;134 });135 },136 };137 }, []);138
139 const ds = useClientDataSource<GridSpec>({ data, sort: sortFn });140
141 return (142 <>143 <div144 className="ln-grid ln-cell:text-xs ln-header:text-xs ln-header:text-ln-text-xlight"145 style={{ height: 500 }}146 >147 <Grid148 apiExtension={apiExtension}149 columns={columns}150 columnBase={base}151 rowSource={ds}152 events={{153 headerCell: {154 keyDown: ({ column, event: ev }) => {155 if (ev.key === "Enter") {156 const nextSort = column.sort === "asc" ? null : column.sort === "desc" ? "asc" : "desc";157 apiExtension.sortColumn(column.id, nextSort);158 }159 },160 },161 }}162 />163 </div>164 </>165 );166}1import type { ClassValue } from "clsx";2import clsx from "clsx";3import { twMerge } from "tailwind-merge";4import { exchanges, networks, symbols } from "@1771technologies/grid-sample-data/dex-pairs-performance";5
6export function tw(...c: ClassValue[]) {7 return twMerge(clsx(...c));8}9import type { Grid } from "@1771technologies/lytenyte-pro";10import type { GridSpec } from "./demo";11import { ArrowDownIcon, ArrowUpIcon } from "@radix-ui/react-icons";12
13export function SymbolCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {14 if (!api.rowIsLeaf(row) || !row.data) return null;15
16 const ticker = row.data.symbolTicker;17 const symbol = row.data.symbol;18 const image = symbols[row.data.symbolTicker];19
20 return (21 <div className="grid grid-cols-[20px_auto_auto] items-center gap-1.5">22 <div>23 <img24 src={image}25 alt={`Logo for symbol ${symbol}`}26 className="h-full w-full overflow-hidden rounded-full"27 />28 </div>29 <div className="bg-ln-gray-20 text-ln-text-dark flex h-fit items-center justify-center rounded-lg px-2 py-px text-[10px]">30 {ticker}31 </div>32 <div className="w-full overflow-hidden text-ellipsis">{symbol.split("/")[0]}</div>33 </div>34 );35}36
37export function NetworkCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {38 if (!api.rowIsLeaf(row) || !row.data) return null;39
40 const name = row.data.network;41 const image = networks[name];42
43 return (44 <div className="grid grid-cols-[20px_1fr] items-center gap-1.5">45 <div>46 <img47 src={image}48 alt={`Logo for network ${name}`}49 className="h-full w-full overflow-hidden rounded-full"50 />51 </div>52 <div className="w-full overflow-hidden text-ellipsis">{name}</div>53 </div>54 );55}56
57export function ExchangeCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {58 if (!api.rowIsLeaf(row) || !row.data) return null;59
60 const name = row.data.exchange;61 const image = exchanges[name];62
63 return (64 <div className="grid grid-cols-[20px_1fr] items-center gap-1.5">65 <div>66 <img67 src={image}68 alt={`Logo for exchange ${name}`}69 className="h-full w-full overflow-hidden rounded-full"70 />71 </div>72 <div className="w-full overflow-hidden text-ellipsis">{name}</div>73 </div>74 );75}76
77export function PercentCellPositiveNegative({ api, column, row }: Grid.T.CellRendererParams<GridSpec>) {78 if (!api.rowIsLeaf(row) || !row.data) return null;79
80 const field = api.columnField(column, row);81
82 if (typeof field !== "number") return "-";83
84 const value = (field > 0 ? "+" : "") + (field * 100).toFixed(2) + "%";85
86 return (87 <div88 className={tw(89 "h-ful flex w-full items-center justify-end tabular-nums",90 field < 0 ? "text-red-600 dark:text-red-300" : "text-green-600 dark:text-green-300",91 )}92 >93 {value}94 </div>95 );96}97
98export function PercentCell({ api, column, row }: Grid.T.CellRendererParams<GridSpec>) {99 if (!api.rowIsLeaf(row) || !row.data) return null;100
101 const field = api.columnField(column, row);102
103 if (typeof field !== "number") return "-";104
105 const value = (field > 0 ? "+" : "") + (field * 100).toFixed(2) + "%";106
107 return <div className="h-ful flex w-full items-center justify-end tabular-nums">{value}</div>;108}109
110export const makePerfHeaderCell = (name: string, subname: string) => {111 return ({ column, api }: Grid.T.HeaderParams<GridSpec>) => {112 return (113 <div114 className="flex h-full w-full items-center justify-between"115 onClick={() => {116 const nextSort = column.sort === "asc" ? null : column.sort === "desc" ? "asc" : "desc";117 api.sortColumn(column.id, nextSort);118 }}119 >120 {column.sort === "asc" && <ArrowUpIcon className="text-ln-text-dark size-4" />}121 {column.sort === "desc" && <ArrowDownIcon className="text-ln-text-dark size-4" />}122 {!column.sort && <div />}123
124 <div className="flex flex-col items-end justify-end tabular-nums">125 <div>{name}</div>126 <div className="text-ln-text-light font-mono uppercase">{subname}</div>127 </div>128 </div>129 );130 };131};132
133export function Header({ api, column }: Grid.T.HeaderParams<GridSpec>) {134 return (135 <div136 className="text-ln-gray-60 group relative flex h-full w-full cursor-pointer items-center px-1 text-sm transition-colors"137 onClick={() => {138 const nextSort = column.sort === "asc" ? null : column.sort === "desc" ? "asc" : "desc";139 api.sortColumn(column.id, nextSort);140 }}141 >142 <div className="sort-button flex w-full items-center justify-between rounded px-1 py-1 transition-colors">143 {column.name ?? column.id}144
145 {column.sort === "asc" && <ArrowUpIcon className="text-ln-text-dark size-4" />}146 {column.sort === "desc" && <ArrowDownIcon className="text-ln-text-dark size-4" />}147 </div>148 </div>149 );150}This demo shows function-based sorting. LyteNyte Grid supports custom sort models, allowing you to implement the sorting behavior your application requires.
Function-based sorting is flexible, but it has drawbacks:
- For basic sorts, writing a custom sort function is tedious.
- Function sorting doesn’t declaratively represent multi-way sorting; it requires implementing multi-way sorts within the function.
- The sort function must handle sort direction.
LyteNyte Grid’s client row source supports dimension sorts, which are simpler than function sorts without sacrificing flexibility
Dimension Sorts
A dimension sort is an object representation of an individual sort. The interface is shown below:
1export type DimensionSort<T> = { dim: Dimension<T> | SortFn<T>; descending?: boolean };Notice that the dim property can be either a Dimension or a SortFn. This means it is possible to
represent a SortFn as a dimension sort:
1const dimSort = { dim: MySortFn };The dim field may also be a Dimension, which is any value with an id or field property, as shown
in the interface below. In particular, columns passed to the grid may also be used as dimensions. LyteNyte
Grid’s client source can compute column fields, which makes it easy to use columns as dimensions.
1export type Dimension<T> = { name?: string; field: Field<T> } | { id: string; field?: Field<T> };In the demo below, the columns have been extended with custom sort attributes. Instead of using a function to sort the row data, the demo uses the column directly as a dimension.
Column Dimension Sort
75 collapsed lines
1import { computeField, Grid, useClientDataSource } from "@1771technologies/lytenyte-pro";2import "@1771technologies/lytenyte-pro/components.css";3import "@1771technologies/lytenyte-pro/light-dark.css";4import {5 ExchangeCell,6 Header,7 makePerfHeaderCell,8 NetworkCell,9 PercentCell,10 PercentCellPositiveNegative,11 SymbolCell,12} from "./components.jsx";13import type { DEXPerformanceData } from "@1771technologies/grid-sample-data/dex-pairs-performance";14import { data } from "@1771technologies/grid-sample-data/dex-pairs-performance";15import { useMemo, useState } from "react";16
17const initialColumns: Grid.Column<GridSpec>[] = [18 { id: "symbol", cellRenderer: SymbolCell, width: 250, name: "Symbol" },19 { id: "network", cellRenderer: NetworkCell, width: 220, name: "Network" },20 { id: "exchange", cellRenderer: ExchangeCell, width: 220, name: "Exchange" },21
22 {23 id: "change24h",24 cellRenderer: PercentCellPositiveNegative,25 headerRenderer: makePerfHeaderCell("Change", "24h"),26 name: "Change % 24h",27 type: "number,",28 },29
30 {31 id: "perf1w",32 cellRenderer: PercentCellPositiveNegative,33 headerRenderer: makePerfHeaderCell("Perf %", "1w"),34 name: "Perf % 1W",35 type: "number,",36 },37 {38 id: "perf1m",39 cellRenderer: PercentCellPositiveNegative,40 headerRenderer: makePerfHeaderCell("Perf %", "1m"),41 name: "Perf % 1M",42 type: "number,",43 },44 {45 id: "perf3m",46 cellRenderer: PercentCellPositiveNegative,47 headerRenderer: makePerfHeaderCell("Perf %", "3m"),48 name: "Perf % 3M",49 type: "number,",50 },51 {52 id: "perf6m",53 cellRenderer: PercentCellPositiveNegative,54 headerRenderer: makePerfHeaderCell("Perf %", "6m"),55 name: "Perf % 6M",56 type: "number,",57 },58 {59 id: "perfYtd",60 cellRenderer: PercentCellPositiveNegative,61 headerRenderer: makePerfHeaderCell("Perf %", "YTD"),62 name: "Perf % YTD",63 type: "number",64 },65 { id: "volatility", cellRenderer: PercentCell, name: "Volatility", type: "number", width: 110 },66 {67 id: "volatility1m",68 cellRenderer: PercentCell,69 headerRenderer: makePerfHeaderCell("Volatility", "1m"),70 name: "Volatility 1M",71 type: "number",72 },73];74
75const base: Grid.ColumnBase<GridSpec> = { width: 90, headerRenderer: Header };76
77export interface GridSpec {78 readonly data: DEXPerformanceData;79 readonly column: { sort?: "asc" | "desc" };80 readonly api: {81 sortColumn: (id: string, dir: "asc" | "desc" | null) => void;82 };83}84
85export default function ClientDemo() {86 const [columns, setColumns] = useState(initialColumns);87
88 const sortDimension = useMemo(() => {89 const columnWithSort = columns.find((x) => x.sort);90 if (!columnWithSort) return null;91
92 return [93 {94 dim: {95 ...columnWithSort,96 field: (p) => {97 const value = computeField(columnWithSort.id ?? columnWithSort.field, p.row);98 if (typeof value === "string") return value.toLowerCase();99 return value;100 },101 },102 descending: columnWithSort.sort === "desc",103 },104 ] satisfies Grid.T.DimensionSort<GridSpec["data"]>[];105 }, [columns]);106
107 const apiExtension = useMemo<GridSpec["api"]>(() => {108 return {109 sortColumn: (id, dir) => {110 setColumns((prev) => {111 const next = prev.map((x) => {112 // Remove any existing sort113 if (x.sort && x.id !== id) {114 const next = { ...x };115 delete next.sort;116 return next;117 }118 // Apply our new sort119 if (x.id === id) {120 const next = { ...x };121 if (dir == null) delete next.sort;122 else next.sort = dir;123
124 return next;125 }126 return x;127 });128 return next;129 });130 },131 };132 }, []);133
134 const ds = useClientDataSource<GridSpec>({ data, sort: sortDimension });135
136 return (137 <>138 <div139 className="ln-grid ln-cell:text-xs ln-header:text-xs ln-header:text-ln-text-xlight"140 style={{ height: 500 }}141 >142 <Grid143 apiExtension={apiExtension}144 columns={columns}145 columnBase={base}146 rowSource={ds}147 events={{148 headerCell: {149 keyDown: ({ event: ev, column }) => {150 if (ev.key === "Enter") {151 const nextSort = column.sort === "asc" ? null : column.sort === "desc" ? "asc" : "desc";152 apiExtension.sortColumn(column.id, nextSort);153 }154 },155 },156 }}157 />158 </div>159 </>160 );161}1import type { ClassValue } from "clsx";2import clsx from "clsx";3import { twMerge } from "tailwind-merge";4import { exchanges, networks, symbols } from "@1771technologies/grid-sample-data/dex-pairs-performance";5
6export function tw(...c: ClassValue[]) {7 return twMerge(clsx(...c));8}9import type { Grid } from "@1771technologies/lytenyte-pro";10import type { GridSpec } from "./demo";11import { ArrowDownIcon, ArrowUpIcon } from "@radix-ui/react-icons";12
13export function SymbolCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {14 if (!api.rowIsLeaf(row) || !row.data) return null;15
16 const ticker = row.data.symbolTicker;17 const symbol = row.data.symbol;18 const image = symbols[row.data.symbolTicker];19
20 return (21 <div className="grid grid-cols-[20px_auto_auto] items-center gap-1.5">22 <div>23 <img24 src={image}25 alt={`Logo for symbol ${symbol}`}26 className="h-full w-full overflow-hidden rounded-full"27 />28 </div>29 <div className="bg-ln-gray-20 text-ln-text-dark flex h-fit items-center justify-center rounded-lg px-2 py-px text-[10px]">30 {ticker}31 </div>32 <div className="w-full overflow-hidden text-ellipsis">{symbol.split("/")[0]}</div>33 </div>34 );35}36
37export function NetworkCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {38 if (!api.rowIsLeaf(row) || !row.data) return null;39
40 const name = row.data.network;41 const image = networks[name];42
43 return (44 <div className="grid grid-cols-[20px_1fr] items-center gap-1.5">45 <div>46 <img47 src={image}48 alt={`Logo for network ${name}`}49 className="h-full w-full overflow-hidden rounded-full"50 />51 </div>52 <div className="w-full overflow-hidden text-ellipsis">{name}</div>53 </div>54 );55}56
57export function ExchangeCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {58 if (!api.rowIsLeaf(row) || !row.data) return null;59
60 const name = row.data.exchange;61 const image = exchanges[name];62
63 return (64 <div className="grid grid-cols-[20px_1fr] items-center gap-1.5">65 <div>66 <img67 src={image}68 alt={`Logo for exchange ${name}`}69 className="h-full w-full overflow-hidden rounded-full"70 />71 </div>72 <div className="w-full overflow-hidden text-ellipsis">{name}</div>73 </div>74 );75}76
77export function PercentCellPositiveNegative({ api, column, row }: Grid.T.CellRendererParams<GridSpec>) {78 if (!api.rowIsLeaf(row) || !row.data) return null;79
80 const field = api.columnField(column, row);81
82 if (typeof field !== "number") return "-";83
84 const value = (field > 0 ? "+" : "") + (field * 100).toFixed(2) + "%";85
86 return (87 <div88 className={tw(89 "h-ful flex w-full items-center justify-end tabular-nums",90 field < 0 ? "text-red-600 dark:text-red-300" : "text-green-600 dark:text-green-300",91 )}92 >93 {value}94 </div>95 );96}97
98export function PercentCell({ api, column, row }: Grid.T.CellRendererParams<GridSpec>) {99 if (!api.rowIsLeaf(row) || !row.data) return null;100
101 const field = api.columnField(column, row);102
103 if (typeof field !== "number") return "-";104
105 const value = (field > 0 ? "+" : "") + (field * 100).toFixed(2) + "%";106
107 return <div className="h-ful flex w-full items-center justify-end tabular-nums">{value}</div>;108}109
110export const makePerfHeaderCell = (name: string, subname: string) => {111 return ({ column, api }: Grid.T.HeaderParams<GridSpec>) => {112 return (113 <div114 className="flex h-full w-full items-center justify-between"115 onClick={() => {116 const nextSort = column.sort === "asc" ? null : column.sort === "desc" ? "asc" : "desc";117 api.sortColumn(column.id, nextSort);118 }}119 >120 {column.sort === "asc" && <ArrowUpIcon className="text-ln-text-dark size-4" />}121 {column.sort === "desc" && <ArrowDownIcon className="text-ln-text-dark size-4" />}122 {!column.sort && <div />}123
124 <div className="flex flex-col items-end justify-end tabular-nums">125 <div>{name}</div>126 <div className="text-ln-text-light font-mono uppercase">{subname}</div>127 </div>128 </div>129 );130 };131};132
133export function Header({ api, column }: Grid.T.HeaderParams<GridSpec>) {134 return (135 <div136 className="text-ln-gray-60 group relative flex h-full w-full cursor-pointer items-center px-1 text-sm transition-colors"137 onClick={() => {138 const nextSort = column.sort === "asc" ? null : column.sort === "desc" ? "asc" : "desc";139 api.sortColumn(column.id, nextSort);140 }}141 >142 <div className="sort-button flex w-full items-center justify-between rounded px-1 py-1 transition-colors">143 {column.name ?? column.id}144
145 {column.sort === "asc" && <ArrowUpIcon className="text-ln-text-dark size-4" />}146 {column.sort === "desc" && <ArrowDownIcon className="text-ln-text-dark size-4" />}147 </div>148 </div>149 );150}Multi-Way Sorting
Dimension sorts make multi-way sorting straightforward. A multi-way sort defines an array
of comparators. The grid applies the first comparator; if it returns 0, the grid applies the
next, continuing until a comparator returns a non-zero result or the list ends.
The demo below shows multi-way sorting using dimension sorts. To sort on more than one column, hold the Control/Command key and click a column header.
Column Multi-way Sort
75 collapsed lines
1import { computeField, Grid, useClientDataSource } from "@1771technologies/lytenyte-pro";2import "@1771technologies/lytenyte-pro/components.css";3import "@1771technologies/lytenyte-pro/light-dark.css";4import {5 ExchangeCell,6 Header,7 makePerfHeaderCell,8 NetworkCell,9 PercentCell,10 PercentCellPositiveNegative,11 SymbolCell,12} from "./components.jsx";13import type { DEXPerformanceData } from "@1771technologies/grid-sample-data/dex-pairs-performance";14import { data } from "@1771technologies/grid-sample-data/dex-pairs-performance";15import { useMemo, useState } from "react";16
17const initialColumns: Grid.Column<GridSpec>[] = [18 { id: "symbol", cellRenderer: SymbolCell, width: 250, name: "Symbol" },19 { id: "network", cellRenderer: NetworkCell, width: 220, name: "Network", sort: "desc", sortIndex: 1 },20 { id: "exchange", cellRenderer: ExchangeCell, width: 220, name: "Exchange", sort: "asc", sortIndex: 2 },21
22 {23 id: "change24h",24 cellRenderer: PercentCellPositiveNegative,25 headerRenderer: makePerfHeaderCell("Change", "24h"),26 name: "Change % 24h",27 type: "number,",28 },29
30 {31 id: "perf1w",32 cellRenderer: PercentCellPositiveNegative,33 headerRenderer: makePerfHeaderCell("Perf %", "1w"),34 name: "Perf % 1W",35 type: "number,",36 },37 {38 id: "perf1m",39 cellRenderer: PercentCellPositiveNegative,40 headerRenderer: makePerfHeaderCell("Perf %", "1m"),41 name: "Perf % 1M",42 type: "number,",43 },44 {45 id: "perf3m",46 cellRenderer: PercentCellPositiveNegative,47 headerRenderer: makePerfHeaderCell("Perf %", "3m"),48 name: "Perf % 3M",49 type: "number,",50 },51 {52 id: "perf6m",53 cellRenderer: PercentCellPositiveNegative,54 headerRenderer: makePerfHeaderCell("Perf %", "6m"),55 name: "Perf % 6M",56 type: "number,",57 },58 {59 id: "perfYtd",60 cellRenderer: PercentCellPositiveNegative,61 headerRenderer: makePerfHeaderCell("Perf %", "YTD"),62 name: "Perf % YTD",63 type: "number",64 },65 { id: "volatility", cellRenderer: PercentCell, name: "Volatility", type: "number", width: 110 },66 {67 id: "volatility1m",68 cellRenderer: PercentCell,69 headerRenderer: makePerfHeaderCell("Volatility", "1m"),70 name: "Volatility 1M",71 type: "number",72 },73];74
75const base: Grid.ColumnBase<GridSpec> = { width: 90, headerRenderer: Header };76
77export interface GridSpec {78 readonly data: DEXPerformanceData;79 readonly column: { sort?: "asc" | "desc"; sortIndex?: number };80 readonly api: {81 sortColumn: (id: string, dir: "asc" | "desc" | null, additive?: boolean) => void;82 };83}84
85export default function ClientDemo() {86 const [columns, setColumns] = useState(initialColumns);87
88 const sortDimension = useMemo(() => {89 const sorts = columns.filter((x) => x.sort).sort((l, r) => (l.sortIndex ?? 0) - (r.sortIndex ?? 0));90
91 return sorts.map((columnWithSort) => {92 return {93 dim: {94 ...columnWithSort,95 field: (p) => {96 const value = computeField(columnWithSort.id ?? columnWithSort.field, p.row);97 if (typeof value === "string") return value.toLowerCase();98 return value;99 },100 },101 descending: columnWithSort.sort === "desc",102 } satisfies Grid.T.DimensionSort<GridSpec["data"]>;103 });104 }, [columns]);105
106 const apiExtension = useMemo<GridSpec["api"]>(() => {107 return {108 sortColumn: (id, dir, additive) => {109 setColumns((prev) => {110 const nextIndex = Math.max(0, ...prev.map((x) => x.sortIndex ?? 0));111
112 const updated = prev.map((x) => {113 let next = x;114 // Remove any existing sort when we are performing a non-additive sort.115 if (x.sort && x.id !== id && !additive) {116 next = { ...x };117 delete next.sort;118 delete next.sortIndex;119 } else if (x.id === id) {120 next = { ...x };121 if (dir == null) {122 delete next.sort;123 delete next.sortIndex;124 } else {125 // We are adding a new sort126 next.sort = dir;127 if (additive && next.sortIndex == null) {128 next.sortIndex = nextIndex + 1;129 }130 }131 }132
133 return next;134 });135
136 // Ensures the sort index consistency137 const sortIndexEntries = updated138 .filter((x) => x.sort)139 .toSorted((l, r) => (l.sortIndex ?? 0) - (r.sortIndex ?? 0))140 .map((c, i) => [c.id, i + 1]);141 const newSortIndices = Object.fromEntries(sortIndexEntries);142
143 return updated.map((col) => {144 if (newSortIndices[col.id])145 return {146 ...col,147 // If we have only one sort there is no need for a sort index number.148 sortIndex: sortIndexEntries.length === 1 ? undefined : newSortIndices[col.id],149 };150 return col;151 });152 });153 },154 };155 }, []);156
157 const ds = useClientDataSource<GridSpec>({ data, sort: sortDimension });158
159 return (160 <>161 <div162 className="ln-grid ln-cell:text-xs ln-header:text-xs ln-header:text-ln-text-xlight"163 style={{ height: 500 }}164 >165 <Grid166 apiExtension={apiExtension}167 columns={columns}168 columnBase={base}169 rowSource={ds}170 events={{171 headerCell: {172 keyDown: ({ column, event: ev }) => {173 if (ev.key === "Enter") {174 const nextSort = column.sort === "asc" ? null : column.sort === "desc" ? "asc" : "desc";175 apiExtension.sortColumn(column.id, nextSort, ev.metaKey || ev.ctrlKey);176 }177 },178 },179 }}180 />181 </div>182 </>183 );184}1import type { ClassValue } from "clsx";2import clsx from "clsx";3import { twMerge } from "tailwind-merge";4import { exchanges, networks, symbols } from "@1771technologies/grid-sample-data/dex-pairs-performance";5
6export function tw(...c: ClassValue[]) {7 return twMerge(clsx(...c));8}9import type { Grid } from "@1771technologies/lytenyte-pro";10import type { GridSpec } from "./demo";11import { ArrowDownIcon, ArrowUpIcon } from "@radix-ui/react-icons";12
13export function SymbolCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {14 if (!api.rowIsLeaf(row) || !row.data) return null;15
16 const ticker = row.data.symbolTicker;17 const symbol = row.data.symbol;18 const image = symbols[row.data.symbolTicker];19
20 return (21 <div className="grid grid-cols-[20px_auto_auto] items-center gap-1.5">22 <div>23 <img24 src={image}25 alt={`Logo for symbol ${symbol}`}26 className="h-full w-full overflow-hidden rounded-full"27 />28 </div>29 <div className="bg-ln-gray-20 text-ln-text-dark flex h-fit items-center justify-center rounded-lg px-2 py-px text-[10px]">30 {ticker}31 </div>32 <div className="w-full overflow-hidden text-ellipsis">{symbol.split("/")[0]}</div>33 </div>34 );35}36
37export function NetworkCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {38 if (!api.rowIsLeaf(row) || !row.data) return null;39
40 const name = row.data.network;41 const image = networks[name];42
43 return (44 <div className="grid grid-cols-[20px_1fr] items-center gap-1.5">45 <div>46 <img47 src={image}48 alt={`Logo for network ${name}`}49 className="h-full w-full overflow-hidden rounded-full"50 />51 </div>52 <div className="w-full overflow-hidden text-ellipsis">{name}</div>53 </div>54 );55}56
57export function ExchangeCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {58 if (!api.rowIsLeaf(row) || !row.data) return null;59
60 const name = row.data.exchange;61 const image = exchanges[name];62
63 return (64 <div className="grid grid-cols-[20px_1fr] items-center gap-1.5">65 <div>66 <img67 src={image}68 alt={`Logo for exchange ${name}`}69 className="h-full w-full overflow-hidden rounded-full"70 />71 </div>72 <div className="w-full overflow-hidden text-ellipsis">{name}</div>73 </div>74 );75}76
77export function PercentCellPositiveNegative({ api, column, row }: Grid.T.CellRendererParams<GridSpec>) {78 if (!api.rowIsLeaf(row) || !row.data) return null;79
80 const field = api.columnField(column, row);81
82 if (typeof field !== "number") return "-";83
84 const value = (field > 0 ? "+" : "") + (field * 100).toFixed(2) + "%";85
86 return (87 <div88 className={tw(89 "h-ful flex w-full items-center justify-end tabular-nums",90 field < 0 ? "text-red-600 dark:text-red-300" : "text-green-600 dark:text-green-300",91 )}92 >93 {value}94 </div>95 );96}97
98export function PercentCell({ api, column, row }: Grid.T.CellRendererParams<GridSpec>) {99 if (!api.rowIsLeaf(row) || !row.data) return null;100
101 const field = api.columnField(column, row);102
103 if (typeof field !== "number") return "-";104
105 const value = (field > 0 ? "+" : "") + (field * 100).toFixed(2) + "%";106
107 return <div className="h-ful flex w-full items-center justify-end tabular-nums">{value}</div>;108}109
110export const makePerfHeaderCell = (name: string, subname: string) => {111 return ({ column, api }: Grid.T.HeaderParams<GridSpec>) => {112 return (113 <div114 className="flex h-full w-full items-center justify-between"115 onClick={(ev) => {116 const nextSort = column.sort === "asc" ? null : column.sort === "desc" ? "asc" : "desc";117 api.sortColumn(column.id, nextSort, ev.metaKey || ev.ctrlKey);118 }}119 >120 <div className="relative">121 {column.sortIndex != null && (122 <span className="text-ln-primary-50 absolute -right-1.5 -top-1">{column.sortIndex}</span>123 )}124 {column.sort === "asc" && <ArrowUpIcon className="text-ln-text-dark size-4" />}125 {column.sort === "desc" && <ArrowDownIcon className="text-ln-text-dark size-4" />}126 </div>127
128 <div className="flex flex-col items-end justify-end tabular-nums">129 <div>{name}</div>130 <div className="text-ln-text-light font-mono uppercase">{subname}</div>131 </div>132 </div>133 );134 };135};136
137export function Header({ api, column }: Grid.T.HeaderParams<GridSpec>) {138 return (139 <div140 className="text-ln-gray-60 group relative flex h-full w-full cursor-pointer items-center px-1 text-sm transition-colors"141 onClick={(ev) => {142 const nextSort = column.sort === "asc" ? null : column.sort === "desc" ? "asc" : "desc";143 api.sortColumn(column.id, nextSort, ev.metaKey || ev.ctrlKey);144 }}145 >146 <div className="sort-button flex w-full items-center justify-between rounded px-1 py-1 transition-colors">147 {column.name ?? column.id}148
149 <div className="relative">150 {column.sortIndex != null && (151 <span className="text-ln-primary-50 absolute -right-1.5 -top-1">{column.sortIndex}</span>152 )}153 {column.sort === "asc" && <ArrowUpIcon className="text-ln-text-dark size-4" />}154 {column.sort === "desc" && <ArrowDownIcon className="text-ln-text-dark size-4" />}155 </div>156 </div>157 </div>158 );159}The demo’s multi-way sort implementation replaces all existing sorts when you click a header without pressing Control/Command. This behavior is not a strict rule. Instead, it is part of the intended implementation for this demo.
LyteNyte Grid’s dimension sorts and API extensions let you define the interaction you want. Choose the interaction that fits your application.
Sorting Group Rows
You can apply the same sorts used for ordering leaf rows to order group rows. Group rows do not require special handling; simply sort by the target dimension.
The demo below illustrates group row sorting. See the Client Row Grouping guide for more on group functionality.
Sorting Group Rows
45 collapsed lines
1import "@1771technologies/lytenyte-pro/light-dark.css";2import { Grid, useClientDataSource, usePiece, type PieceWritable } from "@1771technologies/lytenyte-pro";3import { useMemo, useState } from "react";4import { ArrowDownIcon, ArrowUpIcon } from "@radix-ui/react-icons";5import { loanData, type LoanDataItem } from "@1771technologies/grid-sample-data/loan-data";6import {7 CountryCell,8 CustomerRating,9 DateCell,10 DurationCell,11 NameCell,12 NumberCell,13 OverdueCell,14 tw,15} from "./components.js";16import { RowGroupCell } from "@1771technologies/lytenyte-pro/components";17
18export interface GridSpec {19 readonly data: LoanDataItem;20 readonly api: { sort: PieceWritable<{ id: string; dir: "asc" | "desc" | null } | null> };21}22
23const columns: Grid.Column<GridSpec>[] = [24 { name: "Name", id: "name", cellRenderer: NameCell, width: 110 },25 { name: "Country", id: "country", width: 150, cellRenderer: CountryCell },26 { name: "Loan Amount", id: "loanAmount", width: 140, type: "number", cellRenderer: NumberCell },27 { name: "Balance", id: "balance", type: "number", cellRenderer: NumberCell },28 { name: "Customer Rating", id: "customerRating", type: "number", width: 150, cellRenderer: CustomerRating },29 { name: "Marital", id: "marital" },30 { name: "Education", id: "education", width: 120 },31 { name: "Job", id: "job", width: 120 },32 { name: "Overdue", id: "overdue", cellRenderer: OverdueCell },33 { name: "Duration", id: "duration", type: "number", cellRenderer: DurationCell },34 { name: "Date", id: "date", width: 110, cellRenderer: DateCell },35 { name: "Age", id: "age", width: 80, type: "number" },36 { name: "Contact", id: "contact" },37];38const base: Grid.ColumnBase<GridSpec> = { width: 100, headerRenderer: Header };39
40const group: Grid.RowGroupColumn<GridSpec> = {41 cellRenderer: RowGroupCell,42 width: 200,43 pin: "start",44};45
46export default function ClientDemo() {47 const [sort, setSort] = useState<{ id: string; dir: "asc" | "desc" | null } | null>({48 id: "__ln_group__",49 dir: "desc",50 });51 const [expansions, setExpansions] = useState<Record<string, boolean | undefined>>({52 Technician: true,53 "Technician->Tertiary": true,54 });55
56 const ds = useClientDataSource<GridSpec>({57 data: loanData,58 group: [{ id: "job" }, { id: "education" }],59 sort: useMemo(() => {60 if (!sort) return null;61
62 return [{ dim: { id: sort.id }, descending: sort.dir === "desc" }];63 }, [sort]),64 rowGroupExpansions: expansions,65 onRowGroupExpansionChange: setExpansions,66 });67
68 const sort$ = usePiece(sort, setSort);69 const apiExtension = useMemo(() => {70 return {71 sort: sort$,72 } satisfies GridSpec["api"];73 }, [sort$]);74
75 return (76 <div className="ln-grid" style={{ height: 500 }}>77 <Grid78 apiExtension={apiExtension}79 rowSource={ds}80 columns={columns}81 columnBase={base}82 rowGroupColumn={group}83 events={useMemo<Grid.Events<GridSpec>>(84 () => ({85 headerCell: {86 keyDown: ({ event: ev, column }) => {87 if (ev.key === "Enter") {88 setSort((sort) => {89 const columnSort = sort?.id === column.id ? (sort.dir ?? "desc") : null;90 const nextSort = columnSort === "asc" ? null : columnSort === "desc" ? "asc" : "desc";91 return nextSort == null ? null : { id: column.id, dir: nextSort };92 });93 }94 },95 click: ({ column }) => {96 setSort((sort) => {97 const columnSort = sort?.id === column.id ? (sort.dir ?? "desc") : null;98 const nextSort = columnSort === "asc" ? null : columnSort === "desc" ? "asc" : "desc";99 return nextSort == null ? null : { id: column.id, dir: nextSort };100 });101 },102 },103 }),104 [],105 )}106 />107 </div>108 );109}110
31 collapsed lines
111
112function Header({ api, column }: Grid.T.HeaderParams<GridSpec>) {113 const sort = api.sort.useValue();114 const columnSort = sort?.id === column.id ? (sort.dir ?? "desc") : null;115
116 return (117 <div118 className={tw(119 "relative flex h-full w-full cursor-pointer items-center text-sm transition-colors",120 column.id === "overdue" && "justify-center",121 )}122 style={{123 justifyContent: column.type === "number" ? "flex-end" : undefined,124 }}125 >126 {column.type === "number" && (127 <>128 {columnSort === "asc" && <ArrowUpIcon className="text-ln-text-dark size-4" />}129 {columnSort === "desc" && <ArrowDownIcon className="text-ln-text-dark size-4" />}130 </>131 )}132 <div>{column.name ?? column.id}</div>133 {column.type !== "number" && (134 <>135 {columnSort === "asc" && <ArrowUpIcon className="text-ln-text-dark size-4" />}136 {columnSort === "desc" && <ArrowDownIcon className="text-ln-text-dark size-4" />}137 </>138 )}139 </div>140 );141}1import { type Grid } from "@1771technologies/lytenyte-pro";2import type { GridSpec } from "./demo";3import { twMerge } from "tailwind-merge";4import clsx, { type ClassValue } from "clsx";5import { countryFlags } from "@1771technologies/grid-sample-data/loan-data";6import { useId, useMemo } from "react";7import { format, isValid, parse } from "date-fns";8
9export function tw(...c: ClassValue[]) {10 return twMerge(clsx(...c));11}12
13const formatter = new Intl.NumberFormat("en-US", {14 maximumFractionDigits: 2,15 minimumFractionDigits: 0,16});17export function NumberCell({ api, row, column }: Grid.T.CellRendererParams<GridSpec>) {18 const field = api.columnField(column, row);19
20 if (typeof field !== "number") return "-";21
22 const formatted = field < 0 ? `-$${formatter.format(Math.abs(field))}` : "$" + formatter.format(field);23
24 return (25 <div26 className={tw(27 "flex h-full w-full items-center justify-end tabular-nums",28 field < 0 && "text-red-600 dark:text-red-300",29 field > 0 && "text-green-600 dark:text-green-300",30 )}31 >32 {formatted}33 </div>34 );35}36
37export function NameCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {38 if (!api.rowIsLeaf(row) || !row.data) return "-";39
40 const url = row.data?.avatar;41
42 const name = row.data.name;43
44 return (45 <div className="flex h-full w-full items-center gap-2">46 <img className="border-ln-border-strong h-7 w-7 rounded-full border" src={url} alt={name} />47 <div className="text-ln-text-dark flex flex-col gap-0.5">48 <div>{name}</div>49 </div>50 </div>51 );52}53
54export function DurationCell({ api, row, column }: Grid.T.CellRendererParams<GridSpec>) {55 const field = api.columnField(column, row);56
57 return typeof field === "number" ? `${formatter.format(field)} days` : `${field ?? "-"}`;58}59
60export function CountryCell({ api, row, column }: Grid.T.CellRendererParams<GridSpec>) {61 const field = api.columnField(column, row);62
63 const flag = countryFlags[field as keyof typeof countryFlags];64 if (!flag) return "-";65
66 return (67 <div className="flex h-full w-full items-center gap-2">68 <img className="size-4" src={flag} alt={`country flag of ${field}`} />69 <span>{String(field ?? "-")}</span>70 </div>71 );72}73
74export function DateCell({ api, row, column }: Grid.T.CellRendererParams<GridSpec>) {75 const field = api.columnField(column, row);76
77 if (typeof field !== "string") return "-";78
79 const dateField = parse(field as string, "yyyy-MM-dd", new Date());80
81 if (!isValid(dateField)) return "-";82
83 const niceDate = format(dateField, "yyyy MMM dd");84 return <div className="flex h-full w-full items-center tabular-nums">{niceDate}</div>;85}86
87export function OverdueCell({ api, row, column }: Grid.T.CellRendererParams<GridSpec>) {88 const field = api.columnField(column, row);89 if (field !== "Yes" && field !== "No") return "-";90
91 return (92 <div93 className={tw(94 "flex w-full items-center justify-center rounded-lg py-1 font-bold",95 field === "No" && "bg-green-500/10 text-green-600",96 field === "Yes" && "bg-red-500/10 text-red-400",97 )}98 >99 {field}100 </div>101 );102}103
104export function CustomerRating({ api, row, column }: Grid.T.CellRendererParams<GridSpec>) {105 const field = api.columnField(column, row);106 if (typeof field !== "number") return String(field ?? "-");107
108 return (109 <div className="flex justify-center text-yellow-300">110 <StarRating value={field} />111 </div>112 );113}114
115export default function StarRating({ value = 0 }: { value: number }) {116 const uid = useId();117
118 const max = 5;119
120 const clamped = useMemo(() => {121 const n = Number.isFinite(value) ? value : 0;122 return Math.max(0, Math.min(max, n));123 }, [value, max]);124
125 const stars = useMemo(() => {126 return Array.from({ length: max }, (_, i) => {127 const fill = Math.max(0, Math.min(1, clamped - i)); // 0..1128 return { i, fill };129 });130 }, [clamped, max]);131
132 return (133 <div className={"inline-flex items-center"} role="img">134 {stars.map(({ i, fill }) => {135 const gradId = `${uid}-star-${i}`;136 return <StarIcon key={i} fillFraction={fill} gradientId={gradId} />;137 })}138 </div>139 );140}141
142function StarIcon({ fillFraction, gradientId }: { fillFraction: number; gradientId: string }) {143 const pct = Math.round(fillFraction * 100);144
145 return (146 <svg width="20" height="20" viewBox="0 0 24 24" aria-hidden="true">147 <defs>148 <linearGradient id={gradientId} x1="0%" y1="0%" x2="100%" y2="0%">149 <stop offset={`${pct}%`} stopColor="currentColor" />150 <stop offset={`${pct}%`} stopColor="transparent" />151 </linearGradient>152 </defs>153
154 <path155 d="M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z"156 fill={`url(#${gradientId})`}157 />158 {/* Optional outline for crisp edges */}159 <path160 d="M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z"161 fill="none"162 stroke="transparent"163 strokeWidth="1"164 opacity="0.35"165 />166 </svg>167 );168}This demo uses a different approach than the other demos in this guide to further emphasize the flexibility of defining your own sort model. Instead of extending the columns with a sort value, the demo maintains a separate sort state:
1const [sort, setSort] = useState<{ id: string; dir: "asc" | "desc" | null } | null>({2 id: "__ln_group__",3 dir: "desc",4});Tip
LyteNyte Grid automatically creates a row group column when group rows are present in the grid. Because
this column is not part of the columns you pass to the grid, the correct id for the dimension may not be
obvious.
LyteNyte Grid’s group column uses the id "__ln_group__". The client row source treats this ID in a special way
and will sort rows based on the group’s key. This means you can sort the group column with:
1{ dim: { id: "__ln_group__" }, descending: false }Next Steps
- Client Row Grouping: Create a hierarchical representation of your data by grouping rows.
- Client Row Filtering: Explore how to filter rows when using the client row source.
- Client Row Aggregations: Aggregate row data per group to display values at the group level.
