Row Overview
LyteNyte Grid has two row types, group rows and leaf rows. These rows define the grid's primary content.
Row Node Interface
LyteNyte Grid represents rows using the RowNode interface.
This interface is a union of two types:
RowLeaf: A row with no child rows.RowGroup: A row that contains child rows and establishes a hierarchical relationship.
You can distinguish between row node types by checking the kind property. For example:
const row: RowNode = { ... };const isLeaf = row.kind === "leaf";const isGroup = row.kind === "branch";
LyteNyte Grid also provides convenience methods for identifying row node types. When using
TypeScript, these methods will narrow the type of the RowNode union:
const isLeaf = grid.api.rowIsLeaf(row);const isGroup = grid.api.rowIsGroup(row);
Row Indices
The RowNode interface does not include a row index. This omission is intentional.
A row node does not always correspond to a rendered row. The grid's data source creates row nodes,
and some data sources, such as the Server Data Source,
can return rows that are not yet visible. These rows do not have a defined row index.
Although RowNode does not store a row index, LyteNyte Grid exposes the index when it does exist.
The grid passes rowIndex to components and callbacks that operate on rendered rows.
For example, cell renderers receive a rowIndex prop, as shown in the marker column demo:
Marker Column Row Index Display
"use client";import { Grid, useClientRowDataSource } from "@1771technologies/lytenyte-pro";import "@1771technologies/lytenyte-pro/grid.css";import type { Column } from "@1771technologies/lytenyte-pro/types";import { useId } from "react";import type { OrderData } from "@1771technologies/grid-sample-data/orders";import { data } from "@1771technologies/grid-sample-data/orders";import {AvatarCell,EmailCell,IdCell,PaymentMethodCell,PriceCell,ProductCell,PurchaseDateCell,tw,} from "./components";const columns: Column<OrderData>[] = [{ id: "id", width: 60, widthMin: 60, cellRenderer: IdCell, name: "ID" },{ id: "product", cellRenderer: ProductCell, width: 200, type: "string" },{ id: "price", type: "number", cellRenderer: PriceCell, width: 100 },{ id: "customer", cellRenderer: AvatarCell, width: 180, type: "string" },{ id: "purchaseDate", cellRenderer: PurchaseDateCell, name: "Purchase Date", width: 120 },{ id: "paymentMethod", cellRenderer: PaymentMethodCell, name: "Payment Method", width: 150 },{ id: "email", cellRenderer: EmailCell, width: 220, type: "string" },];export default function ColumnBase() {const ds = useClientRowDataSource({ data: data });const grid = Grid.useLyteNyte({gridId: useId(),rowDataSource: ds,columns,columnMarkerEnabled: true,columnMarker: {cellRenderer: (p) => {return (<div className="flex h-full w-full items-center justify-center text-xs">{p.rowIndex + 1}</div>);},},rowHeight: 50,});const view = grid.view.useValue();return (<div className="lng-grid" style={{ height: 500 }}><Grid.Root grid={grid}><Grid.Viewport><Grid.Header>{view.header.layout.map((row, i) => {return (<Grid.HeaderRow key={i} headerRowIndex={i}>{row.map((c) => {if (c.kind === "group") return null;console.log(c.id);return (<Grid.HeaderCellkey={c.id}cell={c}className={tw("flex h-full w-full items-center text-nowrap text-sm capitalize",!c.id.startsWith("lytenyte") && "px-3",c.column.type === "number" && "justify-end",)}/>);})}</Grid.HeaderRow>);})}</Grid.Header><Grid.RowsContainer><Grid.RowsCenter>{view.rows.center.map((row) => {if (row.kind === "full-width") return null;return (<Grid.Row row={row} key={row.id}>{row.cells.map((c) => {return (<Grid.Cellkey={c.id}cell={c}className={tw("flex h-full w-full items-center text-sm",!c.column.id.startsWith("lytenyte") && "px-3",)}/>);})}</Grid.Row>);})}</Grid.RowsCenter></Grid.RowsContainer></Grid.Viewport></Grid.Root></div>);}
import type { CellRendererParams } from "@1771technologies/lytenyte-pro/types";import type { ClassValue } from "clsx";import clsx from "clsx";import { twMerge } from "tailwind-merge";import { format } from "date-fns";import { type JSX, type ReactNode } from "react";import type { OrderData } from "@1771technologies/grid-sample-data/orders";export function tw(...c: ClassValue[]) {return twMerge(clsx(...c));}export function ProductCell({ grid: { api }, row }: CellRendererParams<OrderData>) {if (!api.rowIsLeaf(row) || !row.data) return;const url = row.data?.productThumbnail;const title = row.data.product;const desc = row.data.productDescription;return (<div className="flex h-full w-full items-center gap-2"><img className="border-ln-gray-50 h-7 w-7 rounded-lg border" src={url} alt={title + desc} /><div className="text-ln-gray-90 flex flex-col gap-0.5"><div className="font-semibold">{title}</div><div className="text-ln-gray-70 text-xs">{desc}</div></div></div>);}export function AvatarCell({ grid: { api }, row }: CellRendererParams<OrderData>) {if (!api.rowIsLeaf(row) || !row.data) return;const url = row.data?.customerAvatar;const name = row.data.customer;return (<div className="flex h-full w-full items-center gap-2"><img className="border-ln-gray-50 h-7 w-7 rounded-full border" src={url} alt={name} /><div className="text-ln-gray-90 flex flex-col gap-0.5"><div>{name}</div></div></div>);}const formatter = new Intl.NumberFormat("en-Us", {minimumFractionDigits: 2,maximumFractionDigits: 2,});export function PriceCell({ grid: { api }, row }: CellRendererParams<OrderData>) {if (!api.rowIsLeaf(row) || !row.data) return;const price = formatter.format(row.data.price);const [dollars, cents] = price.split(".");return (<div className="flex h-full w-full items-center justify-end"><div className="flex items-baseline tabular-nums"><span className="text-ln-gray-80 font-semibold">${dollars}</span>.<span className="relative text-xs">{cents}</span></div></div>);}export function PurchaseDateCell({ grid: { api }, row }: CellRendererParams<OrderData>) {if (!api.rowIsLeaf(row) || !row.data) return;const formattedDate = format(row.data.purchaseDate, "dd MMM, yyyy");return <div className="flex h-full w-full items-center">{formattedDate}</div>;}export function IdCell({ grid: { api }, row }: CellRendererParams<OrderData>) {if (!api.rowIsLeaf(row) || !row.data) return;return <div className="text-xs tabular-nums">{row.data.id}</div>;}export function PaymentMethodCell({ grid: { api }, row }: CellRendererParams<OrderData>) {if (!api.rowIsLeaf(row) || !row.data) return;const cardNumber = row.data.cardNumber;const provider = row.data.paymentMethod;let Logo: ReactNode = null;if (provider === "Visa") Logo = <VisaLogo className="w-6" />;if (provider === "Mastercard") Logo = <MastercardLogo className="w-6" />;return (<div className="flex h-full w-full items-center gap-2"><div className="flex w-7 items-center justify-center">{Logo}</div><div className="flex items-center"><div className="bg-ln-gray-40 size-2 rounded-full"></div><div className="bg-ln-gray-40 size-2 rounded-full"></div><div className="bg-ln-gray-40 size-2 rounded-full"></div><div className="bg-ln-gray-40 size-2 rounded-full"></div></div><div className="tabular-nums">{cardNumber}</div></div>);}export function EmailCell({ grid: { api }, row }: CellRendererParams<OrderData>) {if (!api.rowIsLeaf(row) || !row.data) return;return <div className="text-ln-primary-50 flex h-full w-full items-center">{row.data.email}</div>;}const VisaLogo = (props: JSX.IntrinsicElements["svg"]) => (<svgxmlns="http://www.w3.org/2000/svg"width={2500}height={812}viewBox="0.5 0.5 999 323.684"{...props}><pathfill="#1434cb"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"/></svg>);const MastercardLogo = (props: JSX.IntrinsicElements["svg"]) => (<svgxmlns="http://www.w3.org/2000/svg"width={2500}height={1524}viewBox="55.2 38.3 464.5 287.8"{...props}><pathfill="#f79f1a"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"/><pathfill="#ea001b"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"/><pathfill="#ff5f01"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"/></svg>);
The marker column displays the row index for each rendered row.
The following cell renderer adds 1 to the index because LyteNyte Grid uses zero-based indexing:
const cellRenderer = (p) => {return <div>{p.rowIndex + 1}</div>;};
Other Row State
RowNode represents row identity and structure, not layout or view state.
LyteNyte Grid stores layout-related information separately as part of the grid's view state.
For example, row pinning state lives in the grid view, not on the row node.
The following demo shows this separation:
Row Pinning
"use client";import { Grid, useClientRowDataSource } from "@1771technologies/lytenyte-pro";import "@1771technologies/lytenyte-pro/grid.css";import type { Column } from "@1771technologies/lytenyte-pro/types";import { useId } from "react";import {ExchangeCell,makePerfHeaderCell,NetworkCell,PercentCell,PercentCellPositiveNegative,SymbolCell,tw,} from "./components";import type { DEXPerformanceData } from "@1771technologies/grid-sample-data/dex-pairs-performance";import { data } from "@1771technologies/grid-sample-data/dex-pairs-performance";const columns: Column<DEXPerformanceData>[] = [{ id: "Symbol", cellRenderer: SymbolCell, width: 220, field: "symbol" },{ id: "Network", cellRenderer: NetworkCell, width: 220, field: "network" },{ id: "Exchange", cellRenderer: ExchangeCell, width: 220, field: "exchange" },{id: "Change % 24h",cellRenderer: PercentCellPositiveNegative,headerRenderer: makePerfHeaderCell("Change", "24h"),type: "number,",field: "change24h",},{field: "perf1w",cellRenderer: PercentCellPositiveNegative,headerRenderer: makePerfHeaderCell("Perf %", "1w"),id: "Perf % 1W",type: "number,",},{field: "perf1m",cellRenderer: PercentCellPositiveNegative,headerRenderer: makePerfHeaderCell("Perf %", "1m"),id: "Perf % 1M",type: "number,",},{field: "perf3m",cellRenderer: PercentCellPositiveNegative,headerRenderer: makePerfHeaderCell("Perf %", "3m"),id: "Perf % 3M",type: "number,",},{field: "perf6m",cellRenderer: PercentCellPositiveNegative,headerRenderer: makePerfHeaderCell("Perf %", "6m"),id: "Perf % 6M",type: "number,",},{field: "perfYtd",cellRenderer: PercentCellPositiveNegative,headerRenderer: makePerfHeaderCell("Perf %", "YTD"),id: "Perf % YTD",type: "number",},{ field: "volatility", cellRenderer: PercentCell, id: "Volatility", type: "number" },{field: "volatility1m",cellRenderer: PercentCell,headerRenderer: makePerfHeaderCell("Volatility", "1m"),id: "Volatility 1M",type: "number",},];export default function ColumnBase() {const ds = useClientRowDataSource({data: data.slice(2, -2),topData: data.slice(0, 2),bottomData: data.slice(-2),});const grid = Grid.useLyteNyte({gridId: useId(),rowDataSource: ds,columns,columnBase: { width: 80 },});const view = grid.view.useValue();return (<div className="lng-grid" style={{ height: 500 }}><Grid.Root grid={grid}><Grid.Viewport><Grid.Header>{view.header.layout.map((row, i) => {return (<Grid.HeaderRow key={i} headerRowIndex={i}>{row.map((c) => {if (c.kind === "group") return null;return (<Grid.HeaderCellkey={c.id}cell={c}className={tw("text-ln-gray-60! dark:text-ln-gray-70! flex h-full w-full items-center text-nowrap px-2 text-xs capitalize",c.column.type === "number" && "justify-end",)}/>);})}</Grid.HeaderRow>);})}</Grid.Header><Grid.RowsContainer><Grid.RowsTop>{view.rows.top.map((row) => {if (row.kind === "full-width") return null;return (<Grid.Row row={row} key={row.id}>{row.cells.map((c) => {return (<Grid.Cellkey={c.id}cell={c}className="text-xs! flex h-full w-full items-center px-2"/>);})}</Grid.Row>);})}</Grid.RowsTop><Grid.RowsCenter>{view.rows.center.map((row) => {if (row.kind === "full-width") return null;return (<Grid.Row row={row} key={row.id}>{row.cells.map((c) => {return (<Grid.Cellkey={c.id}cell={c}className="text-xs! flex h-full w-full items-center px-2"/>);})}</Grid.Row>);})}</Grid.RowsCenter><Grid.RowsBottom>{view.rows.bottom.map((row) => {if (row.kind === "full-width") return null;return (<Grid.Row row={row} key={row.id}>{row.cells.map((c) => {return (<Grid.Cellkey={c.id}cell={c}className="text-xs! flex h-full w-full items-center px-2"/>);})}</Grid.Row>);})}</Grid.RowsBottom></Grid.RowsContainer></Grid.Viewport></Grid.Root></div>);}
import type {CellRendererParams,HeaderCellRendererParams,} from "@1771technologies/lytenyte-pro/types";import type { ClassValue } from "clsx";import clsx from "clsx";import { twMerge } from "tailwind-merge";import type { DEXPerformanceData } from "@1771technologies/grid-sample-data/dex-pairs-performance";import {exchanges,networks,symbols,} from "@1771technologies/grid-sample-data/dex-pairs-performance";export function tw(...c: ClassValue[]) {return twMerge(clsx(...c));}export function SymbolCell({ grid: { api }, row }: CellRendererParams<DEXPerformanceData>) {if (!api.rowIsLeaf(row) || !row.data) return null;const ticker = row.data.symbolTicker;const symbol = row.data.symbol;const image = symbols[row.data.symbolTicker];return (<div className="grid grid-cols-[20px_auto_auto] items-center gap-1.5"><div><imgsrc={image}alt={`Logo for symbol ${symbol}`}className="h-full w-full overflow-hidden rounded-full"/></div><div className="bg-ln-gray-20 text-ln-gray-100 flex h-fit items-center justify-center rounded-lg px-2 py-px text-[10px]">{ticker}</div><div className="w-full overflow-hidden text-ellipsis">{symbol.split("/")[0]}</div></div>);}export function NetworkCell({ grid: { api }, row }: CellRendererParams<DEXPerformanceData>) {if (!api.rowIsLeaf(row) || !row.data) return null;const name = row.data.network;const image = networks[name];return (<div className="grid grid-cols-[20px_1fr] items-center gap-1.5"><div><imgsrc={image}alt={`Logo for network ${name}`}className="h-full w-full overflow-hidden rounded-full"/></div><div className="w-full overflow-hidden text-ellipsis">{name}</div></div>);}export function ExchangeCell({ grid: { api }, row }: CellRendererParams<DEXPerformanceData>) {if (!api.rowIsLeaf(row) || !row.data) return null;const name = row.data.exchange;const image = exchanges[name];return (<div className="grid grid-cols-[20px_1fr] items-center gap-1.5"><div><imgsrc={image}alt={`Logo for exchange ${name}`}className="h-full w-full overflow-hidden rounded-full"/></div><div className="w-full overflow-hidden text-ellipsis">{name}</div></div>);}export function PercentCellPositiveNegative({grid: { api },column,row,}: CellRendererParams<DEXPerformanceData>) {if (!api.rowIsLeaf(row) || !row.data) return null;const field = api.columnField(column, row);if (typeof field !== "number") return "-";const value = (field > 0 ? "+" : "") + (field * 100).toFixed(2) + "%";return (<divclassName={tw("h-ful flex w-full items-center justify-end tabular-nums",field < 0 ? "text-red-600 dark:text-red-300" : "text-green-600 dark:text-green-300",)}>{value}</div>);}export function PercentCell({grid: { api },column,row,}: CellRendererParams<DEXPerformanceData>) {if (!api.rowIsLeaf(row) || !row.data) return null;const field = api.columnField(column, row);if (typeof field !== "number") return "-";const value = (field > 0 ? "+" : "") + (field * 100).toFixed(2) + "%";return <div className="h-ful flex w-full items-center justify-end tabular-nums">{value}</div>;}export const makePerfHeaderCell = (name: string, subname: string) => {return (_: HeaderCellRendererParams<DEXPerformanceData>) => {return (<div className="flex h-full w-full flex-col items-end justify-center"><div>{name}</div><div className="font-mono uppercase">{subname}</div></div>);};};
In this example, the grid reads pinned rows from the view property when rendering the top section:
<Grid.RowsTop>{view.rows.top.map((row) => {// Render logic})}</Grid.RowsTop>
In addition to pin state, RowNode does not store the following information:
- Row height
- Row detail expansion state
- Full-width row predicate
- Row selection state
The remaining guides in this section describe how LyteNyte Grid manages each of these concerns.
Next Steps
- Row Height: Change row height, support variable-height rows, and configure fill-height rows.
- Row Pinning: Freeze rows at the top or bottom of the viewport.
- Row Selection: Select single or multiple rows.
- Row Detail: Render expandable row detail content, including nested grids.