Column ID & Name
Every column in LyteNyte Grid must have a unique ID. LyteNyte Grid uses this ID to manage column related state. When the ID is not human readable, you can provide a display name.
Unique Column IDs
LyteNyte Grid uses the id of a column to uniquely identify and track the column.
The id is used for retrieval, detecting movement, and computing cell values.
Column IDs are immutable. If you change a column's id value, LyteNyte Grid
treats the column as a new column, even if the rest of the object is unchanged.
A column id can be any string that fits your data model.
Many developers set the id to a key on the row data. This is convenient
since LyteNyte Grid also uses the id as a fallback when a column
does not define a field. See the Column Field guide for details.
Column Display Name
By default, LyteNyte Grid uses the column's id as its display name.
The display name appears in the column header and in components that manage
columns, such as the Column Manager.
If the id is not suitable for display, set the name property to a
human-readable string. In the example below, several columns specify
a name to improve their header text:
const columns: Column<OrderData>[] = [{ id: "id", name: "ID" },{ id: "product" },{ id: "price" },{ id: "customer" },{ id: "purchaseDate", name: "Purchase Date" },{ id: "paymentMethod", name: "Payment Method" },{ id: "email" },];
Human Readable Names
"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 },{ id: "price", type: "number", cellRenderer: PriceCell, width: 100 },{ id: "customer", cellRenderer: AvatarCell, width: 180 },{ id: "purchaseDate", cellRenderer: PurchaseDateCell, name: "Purchase Date", width: 120 },{ id: "paymentMethod", cellRenderer: PaymentMethodCell, name: "Payment Method", width: 150 },{ id: "email", cellRenderer: EmailCell, width: 220 },];export default function ColumnBase() {const ds = useClientRowDataSource({ data: data });const grid = Grid.useLyteNyte({gridId: useId(),rowDataSource: ds,columns,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;return (<Grid.HeaderCellkey={c.id}cell={c}className={tw("flex h-full w-full items-center px-3 text-sm text-nowrap capitalize",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="flex h-full w-full items-center px-3 text-sm"/>);})}</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, 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>);
In this example, purchaseDate, paymentMethod, and id use name to
provide readable labels, while the remaining IDs are already clear. Developers often
set name when the id is written in snake case or camel case.
Updating the Display Name
A column's id is immutable, but its name can be updated. To change the display
name, update the column specification. LyteNyte Grid provides the columnUpdate API
for updating individual columns.
For example, you may want users to rename columns. In the demo below, users can double-click a header to edit the display name. Pressing Enter or clicking away saves the new value.
Renaming Column Headers
"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,HeaderCell,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, name: "Product" },{ id: "price", type: "number", cellRenderer: PriceCell, width: 100, name: "Price" },{ id: "customer", cellRenderer: AvatarCell, width: 180, name: "Customer" },{ id: "purchaseDate", cellRenderer: PurchaseDateCell, name: "Purchase Date", width: 120 },{ id: "paymentMethod", cellRenderer: PaymentMethodCell, name: "Payment Method", width: 150 },{ id: "email", cellRenderer: EmailCell, width: 220, name: "Email" },];export default function ColumnBase() {const ds = useClientRowDataSource({ data: data });const grid = Grid.useLyteNyte({gridId: useId(),rowDataSource: ds,columns,columnBase: {headerRenderer: HeaderCell,},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;return (<Grid.HeaderCellkey={c.id}cell={c}onKeyDown={(ev) => {// Fire event to the child renderer. In future LyteNyte will support direct// event delegation.if (ev.key === "Enter") {ev.currentTarget.firstElementChild?.dispatchEvent(new Event("begin-edit", { bubbles: false }),);}}}className={tw("focus-visible:outline-ln-primary-50 flex h-full w-full items-center text-nowrap text-sm capitalize focus-visible:outline-1 focus-visible:outline-offset-[-1px]",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="flex h-full w-full items-center px-3 text-sm"/>);})}</Grid.Row>);})}</Grid.RowsCenter></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 { format } from "date-fns";import { useEffect, useState, 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 HeaderCell({ column, grid }: HeaderCellRendererParams<OrderData>) {const [isUpdating, setIsUpdating] = useState(false);const onUpdate = (v: string) => {setIsUpdating(false);grid.api.columnUpdate({ [column.id]: { name: v } });};const [el, setEl] = useState<HTMLElement | null>(null);useEffect(() => {if (!el) return;const controller = new AbortController();el.addEventListener("begin-edit",() => {setIsUpdating(true);},{ signal: controller.signal },);return () => controller.abort();}, [el]);return (<divref={setEl}className={tw("flex h-full w-full items-center", !isUpdating && "px-3")}onDoubleClick={() => {if (isUpdating) return;setIsUpdating(true);}}>{isUpdating && <HeaderCellInput name={column.name ?? column.id} onUpdate={onUpdate} />}{!isUpdating && <>{column.name ?? column.id}</>}</div>);}function HeaderCellInput({name,onUpdate,...props}: JSX.IntrinsicElements["input"] & { name: string; onUpdate: (newName: string) => void }) {const [value, setValue] = useState(name);return (<inputclassName="bg-ln-gray-10 h-full w-full px-3 outline-none"value={value}onChange={(e) => setValue(e.target.value)}autoFocusonKeyDown={(e) => {if (e.key === "Escape") {// Try focus the header cell. This assumes only two levels of nesting.e.currentTarget.parentElement?.parentElement?.focus();onUpdate(name);}if (e.key === "Enter") {e.stopPropagation();e.currentTarget.parentElement?.parentElement?.focus();onUpdate(value);}}}onBlur={() => onUpdate(value)}{...props}/>);}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>);
A custom header cell powers this behavior. After editing, the
header cell calls columnUpdate:
// In the HeaderCell component of components.tsxconst onUpdate = (v: string) => {setIsUpdating(false);grid.api.columnUpdate({ [column.id]: { name: v } });};
Next Steps
- Column Base: See the default column values and how to override them with the
columnBaseproperty. - Column Resizing: Change column widths programmatically or via user interaction.
- Column Moving: Reorder columns programmatically or through drag-and-drop.
- Column Field: Control how a column retrieves its value for each cell.