Linked Cell Edits
LyteNyte Grid handles edits on a cell-by-cell basis but applies changes to the entire row. This allows you to link cells so that updating one value automatically updates others within the same row.
Note
This guide builds on the concepts covered in the Cell Editing and Cell Edit Renderers documentation. We recommend reviewing those topics before proceeding.
Editing Linked Cells
A standard edit renderer calls the changeValue method on the EditParams passed to the
renderer. The changeValue method updates only the edit value for the cell currently being edited. To create
a linked cell edit, use the changeData method instead, and provide new edit values for all cells that should
update when the edited cell changes.
In the demo below, the Product column is linked to the Price column. Selecting a new product automatically updates the Price to that product’s default value.
Linked edits don’t have to be bidirectional. Editing Product updates Price, but editing Price does not change the selected product.
Updating Linked Cells
21 collapsed lines
1import "@1771technologies/lytenyte-pro/components.css";2import "@1771technologies/lytenyte-pro/light-dark.css";3import type { OrderData } from "@1771technologies/grid-sample-data/orders";4import { data as initialData } from "@1771technologies/grid-sample-data/orders";5import {6 AvatarCell,7 EmailCell,8 IdCell,9 PaymentMethodCell,10 PriceCell,11 ProductCell,12 PurchaseDateCell,13} from "./components.jsx";14import { useClientDataSource, Grid } from "@1771technologies/lytenyte-pro";15import { useState } from "react";16import { SmartSelect, 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 {25 id: "product",26 cellRenderer: ProductCell,27 width: 200,28 name: "Product",29 editable: true,30 editOnPrintable: false,31 editRenderer: ProductSelect,32 },33 {34 id: "price",35 type: "number",36 cellRenderer: PriceCell,37 width: 100,38 name: "Price",39 editMutateCommit: (p) => {40 // You will want to validate with Zod.41 const data = p.editData as any;42 const value = String(data[p.column.id]);43
44 // Parse the commit value to a string.45 const numberValue = Number.parseFloat(value);46 if (Number.isNaN(numberValue)) data[p.column.id] = null;47 else data[p.column.id] = numberValue;48 },49 editable: true,50 editRenderer: NumberEditor,51 },52 {53 id: "customer",54 cellRenderer: AvatarCell,55 width: 180,56 name: "Customer",57 },58 { id: "purchaseDate", cellRenderer: PurchaseDateCell, name: "Purchase Date", width: 130 },59 { id: "paymentMethod", cellRenderer: PaymentMethodCell, name: "Payment Method", width: 150 },60 { id: "email", cellRenderer: EmailCell, width: 220, name: "Email", editable: true },61];62
63export default function CellEditingDemo() {64 const [data, setData] = useState(initialData);65 const ds = useClientDataSource({66 data,67 onRowDataChange: ({ center }) => {68 setData((prev) => {69 const next = prev.map((row, i) => {70 if (center.has(i)) return center.get(i)!;71 return row;72 });73
74 return next as OrderData[];75 });76 },77 });78
79 return (80 <div className="ln-grid" style={{ height: 500 }}>81 <Grid rowHeight={50} columns={columns} rowSource={ds} slotShadows={ViewportShadows} editMode="cell" />82 </div>83 );84}85
86const options = initialData.map((x) => ({87 id: x.product,88 product: x.product,89 productThumbnail: x.productThumbnail,90 price: x.price,91 productDescription: x.productDescription,92}));93
94function ProductSelect({ changeData, editValue, editData, commit }: Grid.T.EditParams<GridSpec>) {95 const value = options.find((x) => x.id === editValue) ?? null;96
97 const [open, setOpen] = useState(false);98
99 return (100 <SmartSelect101 kind="basic"102 options={options}103 open={open}104 onOpenChange={setOpen}105 onOpenChangeComplete={(b) => {106 if (!b) commit();107 }}108 openKeys={[" ", "ArrowDown"]}109 container={<SmartSelect.Container className="max-h-50 overflow-auto" />}110 trigger={111 <SmartSelect.BasicTrigger112 className="focus:outline-ln-primary-50 flex h-full w-full items-center gap-2 focus:outline focus:-outline-offset-1"113 autoFocus114 onFocus={() => setOpen(true)}115 >116 {value?.productThumbnail && (117 <img118 className="border-ln-border-strong h-7 w-7 rounded-lg border"119 src={value.productThumbnail}120 alt={value.id}121 />122 )}123 {editValue as string}124 </SmartSelect.BasicTrigger>125 }126 value={value}127 onOptionChange={(p) => {128 if (p) {129 changeData({130 ...(editData as Record<string, unknown>),131 product: p.product,132 price: p.price,133 productThumbnail: p.productThumbnail,134 productDescription: p.productDescription,135 });136 setOpen(false);137 }138 }}139 >140 {(p) => {141 return (142 <SmartSelect.Option key={p.option.id} {...p} className="flex items-center gap-2">143 <img144 className="border-ln-border-strong flex h-7 w-7 rounded-lg border"145 src={p.option.productThumbnail}146 alt={p.option.id}147 />148 <div>{p.option.product}</div>149 </SmartSelect.Option>150 );151 }}152 </SmartSelect>153 );154}155
156function NumberEditor({ changeValue, editValue }: Grid.T.EditParams<GridSpec>) {157 return (158 <input159 className="focus:outline-ln-primary-50 h-full w-full px-2"160 value={`${editValue}`}161 type="number"162 onChange={(e) => changeValue(e.target.value)}163 />164 );165}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, editData }: Grid.T.CellRendererParams<GridSpec>) {46 if (!api.rowIsLeaf(row) || !row.data) return;47
48 const data = (editData as Record<string, number>) ?? row.data;49
50 if (data.price == null) return "-";51
52 const price = formatter.format(data.price);53 const [dollars, cents] = price.split(".");54
55 return (56 <div className={"flex h-full w-full items-center justify-end"}>57 <div className="flex items-baseline tabular-nums">58 <span className="text-ln-text font-semibold">${dollars}</span>.59 <span className="relative text-xs">{cents}</span>60 </div>61 </div>62 );63}64
65export function PurchaseDateCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {66 if (!api.rowIsLeaf(row) || !row.data) return;67
68 const formattedDate = format(row.data.purchaseDate, "dd MMM, yyyy");69
70 return <div className="flex h-full w-full items-center">{formattedDate}</div>;71}72export function IdCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {73 if (!api.rowIsLeaf(row) || !row.data) return;74
75 return <div className="text-xs tabular-nums">{row.data.id}</div>;76}77
78export function PaymentMethodCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {79 if (!api.rowIsLeaf(row) || !row.data) return;80
81 const cardNumber = row.data.cardNumber;82 const provider = row.data.paymentMethod;83
84 let Logo: ReactNode = null;85 if (provider === "Visa") Logo = <VisaLogo className="w-6" />;86 if (provider === "Mastercard") Logo = <MastercardLogo className="w-6" />;87
88 return (89 <div className="flex h-full w-full items-center gap-2">90 <div className="flex w-7 items-center justify-center">{Logo}</div>91 <div className="flex items-center gap-px">92 <div className="bg-ln-gray-40 size-2 rounded-full"></div>93 <div className="bg-ln-gray-40 size-2 rounded-full"></div>94 <div className="bg-ln-gray-40 size-2 rounded-full"></div>95 <div className="bg-ln-gray-40 size-2 rounded-full"></div>96 </div>97 <div className="tabular-nums">{cardNumber}</div>98 </div>99 );100}101
102export function EmailCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {103 if (!api.rowIsLeaf(row) || !row.data) return;104
105 return <div className="text-ln-primary-50 flex h-full w-full items-center">{row.data.email}</div>;106}107
108const VisaLogo = (props: JSX.IntrinsicElements["svg"]) => (109 <svg xmlns="http://www.w3.org/2000/svg" width={2500} height={812} viewBox="0.5 0.5 999 323.684" {...props}>110 <path111 fill="#1434cb"112 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"113 />114 </svg>115);116
117const MastercardLogo = (props: JSX.IntrinsicElements["svg"]) => (118 <svg119 xmlns="http://www.w3.org/2000/svg"120 width={2500}121 height={1524}122 viewBox="55.2 38.3 464.5 287.8"123 {...props}124 >125 <path126 fill="#f79f1a"127 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"128 />129 <path130 fill="#ea001b"131 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"132 />133 <path134 fill="#ff5f01"135 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"136 />137 </svg>138);Tip
A cell edit does not have to update another editable cell. You can create linked edits that update cells which are not themselves editable.
Next Steps
- Cell Editing: Edit cell values and commit updates in the grid.
- Full Row Cell Editing: Simultaneously edit cells across an entire row.
- Cell Edit Validation: Validate cell edits to ensure updates are correct.
