Cell Editing
Cell Editing Copy Page Open additional page export options
Configure LyteNyte Grid to modify and update data directly with single-cell editing.
To enable cell editing, set the editMode property on the grid. editMode accepts one of the following values:
"cell": Edit one cell at a time.
"row": Edit a row and commit changes per row.
"readonly": Disable cell editing (default).
Set editMode to "cell" to enable single-cell editing. To make a column editable, set
these column definition properties:
editable: Boolean or predicate function that returns whether the cell
is editable. Use a predicate to control editability per row.
editRenderer: Component to render in the cell while editing.
The grid passes EditParams to
this component to handle edits.
LyteNyte Grid treats row data as immutable. When a user edits a cell, the
grid triggers the onRowDataChange callback. Use this prop to update
your data source. This guide demonstrates cell editing using
the client row source .
In the demo below, the Price and Customer columns are editable.
To edit a value, double-click any cell in either column.
Basic Cell Editing import " @1771technologies/lytenyte-pro/light-dark.css " ;
import type { OrderData } from " @1771technologies/grid-sample-data/orders " ;
import { data as initialData } from " @1771technologies/grid-sample-data/orders " ;
} from " ./components.jsx " ;
import { useClientDataSource , Grid } from " @1771technologies/lytenyte-pro " ;
import { useState } from " react " ;
import { ViewportShadows } from " @1771technologies/lytenyte-pro/components " ;
export interface GridSpec {
readonly data : OrderData ;
const columns : Grid . Column < GridSpec > [] = [
{ id : " id " , width : 60 , widthMin : 60 , cellRenderer : IdCell , name : " ID " },
{ id : " product " , cellRenderer : ProductCell , width : 200 , name : " Product " },
editMutateCommit : ( p ) => {
// You will want to validate with Zod.
const data = p . editData as any ;
const value = String ( data [ p . column . id ]) ;
// Parse the commit value to a string.
const numberValue = Number . parseFloat ( value ) ;
if ( Number . isNaN ( numberValue )) data [ p . column . id ] = null ;
else data [ p . column . id ] = numberValue ;
editRenderer : NumberEditor ,
cellRenderer : AvatarCell ,
editRenderer : TextCellEditor ,
{ id : " purchaseDate " , cellRenderer : PurchaseDateCell , name : " Purchase Date " , width : 130 , editable : true },
{ id : " paymentMethod " , cellRenderer : PaymentMethodCell , name : " Payment Method " , width : 150 },
{ id : " email " , cellRenderer : EmailCell , width : 220 , name : " Email " , editable : true },
export default function CellEditingDemo () {
const [ data , setData ] = useState ( initialData ) ;
const ds = useClientDataSource ( {
onRowDataChange : ({ center }) => {
const next = prev . map ( ( row , i ) => {
if ( center . has ( i )) return center . get ( i ) ! ;
return next as OrderData [] ;
< div className = " ln-grid " style = {{ height: 500 }}>
< Grid rowHeight = { 50 } columns = { columns } rowSource = { ds } slotShadows = { ViewportShadows } editMode = " cell " />
function TextCellEditor ({ changeValue , editValue } : Grid . T . EditParams < GridSpec >) {
className = " focus:outline-ln-primary-50 h-full w-full px-2 "
onChange = {( e ) => changeValue ( e . target . value )}
function NumberEditor ({ changeValue , editValue } : Grid . T . EditParams < GridSpec >) {
className = " focus:outline-ln-primary-50 h-full w-full px-2 "
onChange = {( e ) => changeValue ( e . target . value )}
import { format } from " date-fns " ;
import { type JSX , type ReactNode } from " react " ;
import type { Grid } from " @1771technologies/lytenyte-pro " ;
import type { GridSpec } from " ./demo.jsx " ;
export function ProductCell ({ api , row } : Grid . T . CellRendererParams < GridSpec >) {
if ( ! api . rowIsLeaf ( row ) || ! row . data ) return ;
const url = row . data ?. productThumbnail ;
const title = row . data . product ;
const desc = row . data . productDescription ;
< div className = " flex h-full w-full items-center gap-2 " >
< img className = " border-ln-border-strong h-7 w-7 rounded-lg border " src = { url } alt = { title + desc } />
< div className = " text-ln-text-dark flex flex-col gap-0.5 " >
< div className = " font-semibold " >{ title }</ div >
< div className = " text-ln-text-light text-xs " >{ desc }</ div >
export function AvatarCell ({ api , row } : Grid . T . CellRendererParams < GridSpec >) {
if ( ! api . rowIsLeaf ( row ) || ! row . data ) return ;
const url = row . data ?. customerAvatar ;
const name = row . data . customer ;
< div className = " flex h-full w-full items-center gap-2 " >
< img className = " border-ln-border-strong h-7 w-7 rounded-full border " src = { url } alt = { name } />
< div className = " text-ln-text-dark flex flex-col gap-0.5 " >
const formatter = new Intl . NumberFormat ( " en-Us " , {
minimumFractionDigits : 2 ,
maximumFractionDigits : 2 ,
export function PriceCell ({ api , row } : Grid . T . CellRendererParams < GridSpec >) {
if ( ! api . rowIsLeaf ( row ) || ! row . data ) return ;
if ( row . data . price == null ) return " - " ;
const price = formatter . format ( row . data . price ) ;
const [ dollars , cents ] = price . split ( " . " ) ;
< div className = " flex h-full w-full items-center justify-end " >
< div className = " flex items-baseline tabular-nums " >
< span className = " text-ln-text font-semibold " >${ dollars }</ span >.
< span className = " relative text-xs " >{ cents }</ span >
export function PurchaseDateCell ({ api , row } : Grid . T . CellRendererParams < GridSpec >) {
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 ({ api , row } : Grid . T . CellRendererParams < GridSpec >) {
if ( ! api . rowIsLeaf ( row ) || ! row . data ) return ;
return < div className = " text-xs tabular-nums " >{ row . data . id }</ div >;
export function PaymentMethodCell ({ api , row } : Grid . T . CellRendererParams < GridSpec >) {
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 " />;
< 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 gap-px " >
< 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 className = " tabular-nums " >{ cardNumber }</ div >
export function EmailCell ({ api , row } : Grid . T . CellRendererParams < GridSpec >) {
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 " ] ) => (
< svg xmlns = " http://www.w3.org/2000/svg " width = { 2500 } height = { 812 } viewBox = " 0.5 0.5 999 323.684 " { ... props }>
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 "
const MastercardLogo = ( props : JSX . IntrinsicElements [ " svg " ] ) => (
xmlns = " http://www.w3.org/2000/svg "
viewBox = " 55.2 38.3 464.5 287.8 "
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 "
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 "
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 "
The demo omits validation to focus on basic editing. For example, it allows negative prices.
See the Cell Edit Validation guide for best practices.
The demo defines basic edit renderers for the Price and Customer columns.
For custom renderers, see the Cell Edit Renderers guide .
The grid passes EditParams props to
each edit renderer. This demo uses:
editValue: Current value being edited. It is initialized from the cell’s value
and applied only when you commit the edit.
changeValue: Method that updates editValue during editing.
The Customer column edit renderer assigns
editValue to the <input /> value, and the input’s onChange handler calls changeValue
to update the edit value. The renderer also converts editValue to a string because
the input element expects a string value, and editValue may not initially be a string.
function TextCellEditor ({ changeValue , editValue } : Grid . T . EditParams < GridSpec >) {
className = " focus:outline-ln-primary-50 h-full w-full px-2 "
onChange = {( e ) => changeValue ( e . target . value )}
Use the editClickActivator property on the grid to change the click interaction that
starts cell editing. The property accepts one of three values:
"single-click": Begins cell editing when the user clicks an editable cell.
"double-click": Begins cell editing when the user double-clicks an editable cell. This is the default.
"none": Clicking an editable cell does not start editing.
The demo below enables single-click editing on the Price and Customer columns.
Click any cell in these columns to begin editing.
Single Click Edit import " @1771technologies/lytenyte-pro/light-dark.css " ;
import type { OrderData } from " @1771technologies/grid-sample-data/orders " ;
import { data as initialData } from " @1771technologies/grid-sample-data/orders " ;
} from " ./components.jsx " ;
import { useClientDataSource , Grid } from " @1771technologies/lytenyte-pro " ;
import { useState } from " react " ;
import { ViewportShadows } from " @1771technologies/lytenyte-pro/components " ;
export interface GridSpec {
readonly data : OrderData ;
const columns : Grid . Column < GridSpec > [] = [
{ id : " id " , width : 60 , widthMin : 60 , cellRenderer : IdCell , name : " ID " },
{ id : " product " , cellRenderer : ProductCell , width : 200 , name : " Product " },
editMutateCommit : ( p ) => {
// You will want to validate with Zod.
const data = p . editData as any ;
const value = String ( data [ p . column . id ]) ;
// Parse the commit value to a string.
const numberValue = Number . parseFloat ( value ) ;
if ( Number . isNaN ( numberValue )) data [ p . column . id ] = null ;
else data [ p . column . id ] = numberValue ;
editRenderer : NumberEditor ,
cellRenderer : AvatarCell ,
editRenderer : TextCellEditor ,
{ id : " purchaseDate " , cellRenderer : PurchaseDateCell , name : " Purchase Date " , width : 130 , editable : true },
{ id : " paymentMethod " , cellRenderer : PaymentMethodCell , name : " Payment Method " , width : 150 },
{ id : " email " , cellRenderer : EmailCell , width : 220 , name : " Email " , editable : true },
export default function CellEditingDemo () {
const [ data , setData ] = useState ( initialData ) ;
const ds = useClientDataSource ( {
onRowDataChange : ({ center }) => {
const next = prev . map ( ( row , i ) => {
if ( center . has ( i )) return center . get ( i ) ! ;
return next as OrderData [] ;
< div className = " ln-grid " style = {{ height: 500 }}>
slotShadows = { ViewportShadows }
editClickActivator = " single-click "
function TextCellEditor ({ changeValue , editValue } : Grid . T . EditParams < GridSpec >) {
className = " focus:outline-ln-primary-50 h-full w-full px-2 "
onChange = {( e ) => changeValue ( e . target . value )}
function NumberEditor ({ changeValue , editValue } : Grid . T . EditParams < GridSpec >) {
className = " focus:outline-ln-primary-50 h-full w-full px-2 "
onChange = {( e ) => changeValue ( e . target . value )}
import { format } from " date-fns " ;
import { type JSX , type ReactNode } from " react " ;
import type { Grid } from " @1771technologies/lytenyte-pro " ;
import type { GridSpec } from " ./demo.jsx " ;
export function ProductCell ({ api , row } : Grid . T . CellRendererParams < GridSpec >) {
if ( ! api . rowIsLeaf ( row ) || ! row . data ) return ;
const url = row . data ?. productThumbnail ;
const title = row . data . product ;
const desc = row . data . productDescription ;
< div className = " flex h-full w-full items-center gap-2 " >
< img className = " border-ln-border-strong h-7 w-7 rounded-lg border " src = { url } alt = { title + desc } />
< div className = " text-ln-text-dark flex flex-col gap-0.5 " >
< div className = " font-semibold " >{ title }</ div >
< div className = " text-ln-text-light text-xs " >{ desc }</ div >
export function AvatarCell ({ api , row } : Grid . T . CellRendererParams < GridSpec >) {
if ( ! api . rowIsLeaf ( row ) || ! row . data ) return ;
const url = row . data ?. customerAvatar ;
const name = row . data . customer ;
< div className = " flex h-full w-full items-center gap-2 " >
< img className = " border-ln-border-strong h-7 w-7 rounded-full border " src = { url } alt = { name } />
< div className = " text-ln-text-dark flex flex-col gap-0.5 " >
const formatter = new Intl . NumberFormat ( " en-Us " , {
minimumFractionDigits : 2 ,
maximumFractionDigits : 2 ,
export function PriceCell ({ api , row } : Grid . T . CellRendererParams < GridSpec >) {
if ( ! api . rowIsLeaf ( row ) || ! row . data ) return ;
if ( row . data . price == null ) return " - " ;
const price = formatter . format ( row . data . price ) ;
const [ dollars , cents ] = price . split ( " . " ) ;
< div className = " flex h-full w-full items-center justify-end " >
< div className = " flex items-baseline tabular-nums " >
< span className = " text-ln-text font-semibold " >${ dollars }</ span >.
< span className = " relative text-xs " >{ cents }</ span >
export function PurchaseDateCell ({ api , row } : Grid . T . CellRendererParams < GridSpec >) {
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 ({ api , row } : Grid . T . CellRendererParams < GridSpec >) {
if ( ! api . rowIsLeaf ( row ) || ! row . data ) return ;
return < div className = " text-xs tabular-nums " >{ row . data . id }</ div >;
export function PaymentMethodCell ({ api , row } : Grid . T . CellRendererParams < GridSpec >) {
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 " />;
< 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 gap-px " >
< 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 className = " tabular-nums " >{ cardNumber }</ div >
export function EmailCell ({ api , row } : Grid . T . CellRendererParams < GridSpec >) {
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 " ] ) => (
< svg xmlns = " http://www.w3.org/2000/svg " width = { 2500 } height = { 812 } viewBox = " 0.5 0.5 999 323.684 " { ... props }>
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 "
const MastercardLogo = ( props : JSX . IntrinsicElements [ " svg " ] ) => (
xmlns = " http://www.w3.org/2000/svg "
viewBox = " 55.2 38.3 464.5 287.8 "
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 "
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 "
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 "
LyteNyte Grid provides each cell edit renderer with props conforming to the
EditParams type. EditParams contains the state and
methods required to manage cell updates. The following sections detail the most
important properties of EditParams.
To manage cell editing, distinguish between the editValue and editData properties
on EditParams .
When a cell edit begins, LyteNyte Grid creates a copy of the edited row’s data and stores
it in editData. The grid then uses the column’s field property to derive the
editValue for the active cell. The pseudo-code below illustrates this process:
// Start with the column definition and row data for a given row
const column = { id : " customer " , field : " customer " };
const row = { customer : " alice " , ... otherProperties };
// When the grid begins editing, it creates a copy of the row data
const editData = structuredClone ( row ) ;
// The grid uses the column field to derive the editValue
const editValue = editData [ column . field ?? column . id ] ;
This pseudo-code does not reflect the exact implementation, but it illustrates the core
relationship between editData and editValue.
Use the changeValue method to update editValue. This method is a convenience wrapper
around changeData that applies updates to a single cell. In contrast, changeData
updates the edit data for the entire row. The following two calls are equivalent:
editParams . changeValue ( " Alice Smith " ) ;
editParams . changeData ( { ... editParams . editData , customer : " Alice Smith " } ) ;
Use the editSetter property on a column to control how the grid updates editValue
when changeValue is called.
By default, LyteNyte Grid uses the column’s field property to update the edit value
for a cell. This behavior works only when field refers to a string or numeric property.
When field is a function or a path, you must define an editSetter so the grid can
apply updates correctly.
For example, the following column definition always title-cases the edited value:
editSetter : ({ editValue , editData }) => ( {
customer : typeof editValue !== " string " ? " - " : titleCase ( editValue ) ,
The editSetter must always return the full editData object, not just the modified
value. Use editSetter to enforce update constraints and to create linked cell updates.
For more information, see the Linked Cell Edits guide .
Use the editMutateCommit property on the column to write changes to
the editData object before the grid completes the edit operation.
The editMutateCommit property provides one final opportunity to adjust the
row data before the grid fires edit events.
For example, the following definition parses the column’s value to
ensure it is a number. The code mutates the editData object directly to
change the final value of the edit.
The grid calls editMutateCommit once for each column whenever
any column value is edited.
editMutateCommit : ( p ) => {
// You will want to validate with Zod.
const data = p . editData as any ;
const value = String ( data [ p . column . id ]) ;
// Parse the commit value to a string.
const numberValue = Number . parseFloat ( value ) ;
if ( Number . isNaN ( numberValue )) data [ p . column . id ] = null ;
else data [ p . column . id ] = numberValue ;
LyteNyte Grid does not mutate the original row data during an edit.
To end an edit programmatically, EditParams provides two methods:
commit: Applies the new value via row update callbacks and ends the edit.
cancel: Discards the edited value and ends the edit.
LyteNyte Grid fires these events during the cell editing lifecycle.
For the full list, see the grid props API reference .
onEditBegin: Fires when a cell edit begins. Call preventDefault on the event params to prevent the edit.
onEditEnd: Fires when editing ends and the grid is about to commit
the edited value. Call preventDefault on the event params to prevent the commit.
onEditCancel: Fires when editing ends without applying the edited value.
onEditFail: Fires when validation fails during editing.
LyteNyte Grid’s API exposes methods for programmatically editing cells. For complete details, see the grid
API reference .
editBegin: Begins editing a specific cell.
editEnd: Ends the edit and commits the value. Pass cancel to cancel instead of commit.
editIsCellActive: Returns true if the specified cell is currently being edited.
editUpdate: Updates rows in bulk. See the Bulk Cell Editing guide for details.
Use programmatic editing APIs only for advanced scenarios.
For basic cell editing, use LyteNyte Grid’s built-in editing behavior.