Bulk Cell Editing
Use the LyteNyte Grid API to perform bulk cell updates. Provide updated data for targeted rows, and the grid will update the corresponding cells as if they were edited directly.
Note
Bulk updates require familiarity with cell editing. See the Cell Editing guide for more details.
Edit Update Method
You can edit cells in bulk using the editUpdateRows method on the grid’s API. The
editUpdateRows method requires a map of rows to update. The update map can use
two key types:
number: Represents the row index of the row that should be updated.string: Represents the row ID of the row that should be updated.
When you provide a number key, LyteNyte Grid resolves the row index to its
row ID. Avoid setting both the row index and row ID for the same row in the map.
The demo below uses the editUpdateRows method to update the Price column in
bulk.
Bulk Cell Editing
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 { useRef, useState } from "react";16import { 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 { id: "product", cellRenderer: ProductCell, width: 200, name: "Product" },25 {26 id: "price",27 type: "number",28 cellRenderer: PriceCell,29 width: 100,30 name: "Price",31 editMutateCommit: (p) => {32 // You will want to validate with Zod.33 const data = p.editData as any;34 const value = String(data[p.column.id]);35
36 // Parse the commit value to a string.37 const numberValue = Number.parseFloat(value);38 if (Number.isNaN(numberValue)) data[p.column.id] = null;39 else data[p.column.id] = numberValue;40 },41 editable: true,42 editRenderer: NumberEditor,43 },44 {45 id: "customer",46 cellRenderer: AvatarCell,47 width: 180,48 name: "Customer",49 },50 { id: "purchaseDate", cellRenderer: PurchaseDateCell, name: "Purchase Date", width: 130, editable: true },51 { id: "paymentMethod", cellRenderer: PaymentMethodCell, name: "Payment Method", width: 150 },52 { id: "email", cellRenderer: EmailCell, width: 220, name: "Email", editable: true },53];54
55export default function CellEditingDemo() {56 const [data, setData] = useState(initialData);57 const ds = useClientDataSource({58 data,59 onRowDataChange: ({ center }) => {60 setData((prev) => {61 const next = prev.map((row, i) => {62 if (center.has(i)) return center.get(i)!;63 return row;64 });65
66 return next as OrderData[];67 });68 },69 });70 const apiRef = useRef<Grid.API<GridSpec>>(null);71
72 return (73 <>74 <div className="border-ln-border flex items-center gap-4 border-b px-2 py-2">75 <button76 data-ln-button="tertiary"77 data-ln-size="md"78 className="flex items-center gap-2"79 onClick={() => {80 const update = new Map(81 data.map((x, i) => {82 return [i, { ...x, price: x.price + 10 }];83 }),84 );85
86 apiRef.current?.editUpdateRows(update);87 }}88 >89 Inc. Price +$10.0090 </button>91 <button92 data-ln-button="tertiary"93 data-ln-size="md"94 className="flex items-center gap-2"95 onClick={() => {96 const update = new Map(97 data.map((x, i) => {98 const next = Math.max(x.price - 10, 1);99 return [i, { ...x, price: next }];100 }),101 );102
103 apiRef.current?.editUpdateRows(update);104 }}105 >106 Dec. Price -$10.00107 </button>108 </div>109
110 <div className="ln-grid" style={{ height: 500 }}>111 <Grid112 ref={apiRef}113 rowHeight={50}114 columns={columns}115 rowSource={ds}116 slotShadows={ViewportShadows}117 editMode="cell"118 />119 </div>120 </>121 );122}123
124function NumberEditor({ changeValue, editValue }: Grid.T.EditParams<GridSpec>) {125 return (126 <input127 className="focus:outline-ln-primary-50 h-full w-full px-2"128 value={`${editValue}`}129 type="number"130 onChange={(e) => changeValue(e.target.value)}131 />132 );133}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 }: Grid.T.CellRendererParams<GridSpec>) {46 if (!api.rowIsLeaf(row) || !row.data) return;47
48 if (row.data.price == null) return "-";49 const price = formatter.format(row.data.price);50 const [dollars, cents] = price.split(".");51
52 return (53 <div className="flex h-full w-full items-center justify-end">54 <div className="flex items-baseline tabular-nums">55 <span className="text-ln-text font-semibold">${dollars}</span>.56 <span className="relative text-xs">{cents}</span>57 </div>58 </div>59 );60}61
62export function PurchaseDateCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {63 if (!api.rowIsLeaf(row) || !row.data) return;64
65 const formattedDate = format(row.data.purchaseDate, "dd MMM, yyyy");66
67 return <div className="flex h-full w-full items-center">{formattedDate}</div>;68}69export function IdCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {70 if (!api.rowIsLeaf(row) || !row.data) return;71
72 return <div className="text-xs tabular-nums">{row.data.id}</div>;73}74
75export function PaymentMethodCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {76 if (!api.rowIsLeaf(row) || !row.data) return;77
78 const cardNumber = row.data.cardNumber;79 const provider = row.data.paymentMethod;80
81 let Logo: ReactNode = null;82 if (provider === "Visa") Logo = <VisaLogo className="w-6" />;83 if (provider === "Mastercard") Logo = <MastercardLogo className="w-6" />;84
85 return (86 <div className="flex h-full w-full items-center gap-2">87 <div className="flex w-7 items-center justify-center">{Logo}</div>88 <div className="flex items-center gap-px">89 <div className="bg-ln-gray-40 size-2 rounded-full"></div>90 <div className="bg-ln-gray-40 size-2 rounded-full"></div>91 <div className="bg-ln-gray-40 size-2 rounded-full"></div>92 <div className="bg-ln-gray-40 size-2 rounded-full"></div>93 </div>94 <div className="tabular-nums">{cardNumber}</div>95 </div>96 );97}98
99export function EmailCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {100 if (!api.rowIsLeaf(row) || !row.data) return;101
102 return <div className="text-ln-primary-50 flex h-full w-full items-center">{row.data.email}</div>;103}104
105const VisaLogo = (props: JSX.IntrinsicElements["svg"]) => (106 <svg xmlns="http://www.w3.org/2000/svg" width={2500} height={812} viewBox="0.5 0.5 999 323.684" {...props}>107 <path108 fill="#1434cb"109 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"110 />111 </svg>112);113
114const MastercardLogo = (props: JSX.IntrinsicElements["svg"]) => (115 <svg116 xmlns="http://www.w3.org/2000/svg"117 width={2500}118 height={1524}119 viewBox="55.2 38.3 464.5 287.8"120 {...props}121 >122 <path123 fill="#f79f1a"124 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"125 />126 <path127 fill="#ea001b"128 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"129 />130 <path131 fill="#ff5f01"132 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"133 />134 </svg>135);The code for increasing the price is shown below. Notice that the code creates a new update map,
then calls editUpdateRows to update the rows through LyteNyte Grid’s edit functionality.
1<button2 onClick={() => {3 const update = new Map(4 data.map((x, i) => {5 return [i, { ...x, price: x.price + 10 }];6 }),7 );8
9 apiRef.current?.editUpdate(update);10 }}11>12 Increase Price (+10)13 <ArrowUpIcon />14</button>Bulk Update vs. Direct Row Update
At first glance, the editUpdateRows method appears to do the same thing as updating
the rows in the grid directly. However, there is a distinction. When you update rows through
editUpdateRows, LyteNyte Grid runs the same validations as if the user edited the cell directly.
All rows must pass validation for LyteNyte Grid to apply the bulk update. The editUpdateRows
method returns the row validation result, which you can use to provide feedback
if the update fails.
Bulk Editing Cells
You can use the editUpdateCells method to bulk update a set of cells in
the grid, rather than updating entire rows. The editUpdateCells method is a convenience
wrapper that uses editUpdateRows internally. LyteNyte Grid converts the provided cell
updates into row updates and then calls editUpdateRows.
The demo below demonstrates editUpdateCells by implementing standard clipboard
functionality, including copy, cut, and paste. See the
Clipboard guide for more details.
Click and drag to select a range of cells. Press Ctrl C to copy, Ctrl X to cut,
and Ctrl V to paste. The demo does not validate pasted values.
For validation, see the Cell Editing Validation guide.
Clipboard Bulk Editing
1"use client";2
3import "./demo.css";4import "@1771technologies/lytenyte-pro/components.css";5import "@1771technologies/lytenyte-pro/light-dark.css";6import { Grid, useClientDataSource } from "@1771technologies/lytenyte-pro";7import {8 CountryCell,9 CustomerRating,10 DateCell,11 DurationCell,12 NameCell,13 NumberCell,14 OverdueCell,15} from "./components.js";16import { useCallback, useMemo, useState } from "react";17import { loanData, type LoanDataItem } from "@1771technologies/grid-sample-data/loan-data";18
19export interface GridSpec {20 readonly data: LoanDataItem;21}22
23const columns: Grid.Column<GridSpec>[] = [24 { name: "Name", id: "name", cellRenderer: NameCell, width: 130 },25 { name: "Country", id: "country", width: 150, cellRenderer: CountryCell },26 { name: "Loan Amount", id: "loanAmount", width: 120, type: "number", cellRenderer: NumberCell },27 { name: "Balance", id: "balance", type: "number", cellRenderer: NumberCell },28 { name: "Customer Rating", id: "customerRating", type: "number", width: 125, cellRenderer: CustomerRating },29 { name: "Marital", id: "marital" },30 { name: "Education", id: "education", hide: true },31 { name: "Job", id: "job", width: 120, hide: true },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];38
39const base: Grid.ColumnBase<GridSpec> = {40 width: 100,41 cellRenderer: (p) => {42 const field = p.api.columnField(p.column, p.row);43
44 if (field == null) return "";45
46 return String(field);47 },48};49
50export default function ExportDemo() {51 const [data, setData] = useState(loanData);52 const ds = useClientDataSource({53 data: data,54 onRowDataChange: (p) => {55 setData((prev) => {56 const next = [...prev];57 p.center.forEach((v, i) => {58 next[i] = v as LoanDataItem;59 });60
61 return next;62 });63 },64 });65 const [selections, setSelections] = useState<Grid.T.DataRect[]>([66 { rowStart: 2, rowEnd: 6, columnStart: 1, columnEnd: 3 },67 ]);68
69 const [api, setApi] = useState<Grid.API<GridSpec> | null>(null);70
71 const getFirstSelection = useCallback(() => {72 return selections.at(0) ?? null;73 }, [selections]);74
75 const handleCopy = useCallback(async () => {76 if (!api) return;77
78 const selectionToCopy = getFirstSelection();79 if (!selectionToCopy) return;80
81 const data = await api.exportData({82 rect: selectionToCopy,83 });84
85 const stringToCopy = data.data86 .map((row) => {87 return row.map((cell) => `${cell ?? ""}`).join("\t");88 })89 .join("\n");90
91 await navigator.clipboard.writeText(stringToCopy);92 }, [api, getFirstSelection]);93
94 const handleGridUpdate = useCallback(95 async (updates: unknown[][]) => {96 if (!api) return;97 if (!document.activeElement) return null;98
99 const position = api.positionFromElement(document.activeElement as HTMLElement);100 if (position?.kind !== "cell") return;101
102 // Repeat paste for excel like behavior103 const selection = api.cellSelections().at(-1);104 let repeat: Repeat | null = null;105 if (selection) {106 const xDiff = selection.columnEnd - selection.columnStart;107 const yDiff = selection.rowEnd - selection.rowStart;108
109 const columnLengths = updates.map((x) => x.length);110
111 const allTheSameLength = columnLengths.every((x) => x === columnLengths[0]);112
113 if (allTheSameLength)114 repeat = rectRepeat({ cols: columnLengths[0], rows: updates.length }, { cols: xDiff, rows: yDiff });115 }116 if (repeat) {117 for (let r = 0; r < updates.length; r++) {118 const row = updates[r];119 const newRow = [];120 for (let i = 0; i < repeat.x; i++) newRow.push(...row);121
122 updates[r] = newRow;123 }124
125 const final = [];126 for (let i = 0; i < repeat.y; i++) final.push(...updates);127
128 updates = final;129 }130
131 const map = new Map<number, { column: number; value: any }[]>();132
133 for (let i = 0; i < updates.length; i++) {134 const rowI = i + position.rowIndex;135 const row = api.rowByIndex(rowI).get();136
137 if (!row?.data || api.rowIsGroup(row)) continue;138
139 const data = updates[i];140
141 const columnUpdates: { column: number; value: any }[] = [];142
143 for (let j = 0; j < data.length; j++) {144 const colI = j + position.colIndex;145
146 console.log(position.colIndex);147 const column = api.columnByIndex(colI);148 if (!column) continue;149
150 const rawValue = data[j];151
152 let value = column.type === "number" ? Number.parseFloat(rawValue as string) : rawValue;153 if (column.type === "number" && Number.isNaN(value)) value = rawValue;154
155 columnUpdates.push({ column: colI, value: value });156 }157 map.set(rowI, columnUpdates);158 }159
160 if (!api.editUpdateCells(map)) alert("Copy operation failed");161 },162 [api],163 );164
165 return (166 <div className="ln-grid" style={{ height: 500 }}>167 <Grid168 events={useMemo<Grid.Events<GridSpec>>(() => {169 return {170 viewport: {171 keyDown: async (p) => {172 if (!p.event.ctrlKey && !p.event.metaKey) return;173
174 if (p.event.key === "x") {175 const rect = getFirstSelection();176 if (!rect) return;177
178 await handleCopy();179
180 const updates: any[][] = [];181 for (let i = rect.rowStart; i < rect.rowEnd; i++) {182 const row: any[] = [];183
184 for (let j = rect.columnStart; j < rect.columnEnd; j++) row.push(null);185 updates.push(row);186 }187
188 p.viewport.classList.add("copy-flash");189
190 handleGridUpdate(updates);191
192 setTimeout(() => p.viewport.classList.remove("copy-flash"), 500);193 } else if (p.event.key === "v") {194 const content = await navigator.clipboard.readText();195
196 // If your data needs some parsing handle it here on via the editSetter on the column.197 const updates = content.split("\n").map((c) => {198 return c199 .trim()200 .split("\t")201 .map((x) => x.trim());202 });203
204 handleGridUpdate(updates);205 } else if (p.event.key === "c") {206 p.viewport.classList.add("copy-flash");207 await handleCopy();208 setTimeout(() => p.viewport.classList.remove("copy-flash"), 500);209 }210 },211 },212 };213 }, [getFirstSelection, handleCopy, handleGridUpdate])}214 ref={setApi}215 columnBase={base}216 columns={columns}217 editMode="cell"218 rowSource={ds}219 cellSelections={selections}220 onCellSelectionChange={setSelections}221 cellSelectionMode="range"222 />223 </div>224 );225}226type Rect = { rows: number; cols: number };227type Repeat = { x: number; y: number };228
229function rectRepeat(small: Rect, big: Rect): Repeat | null {230 const sr = small.rows;231 const sc = small.cols;232 const br = big.rows;233 const bc = big.cols;234
235 // Basic validation (optional but handy)236 if (![sr, sc, br, bc].every(Number.isInteger)) return null;237 if (sr <= 0 || sc <= 0 || br <= 0 || bc <= 0) return null;238
239 // Must divide evenly for a perfect tiling240 if (br % sr !== 0) return null;241 if (bc % sc !== 0) return null;242
243 return { x: bc / sc, y: br / sr };244}1@keyframes flash {2 0% {3 background-color: var(--color-ln-primary-10);4 }5 50% {6 background-color: var(--color-ln-primary-30);7 }8 100% {9 background-color: var(--color-ln-primary-10);10 }11}12.copy-flash {13 & [data-ln-cell-selection-rect="true"]:not([data-ln-cell-selection-is-pivot="true"]) {14 animation: flash 0.5s ease-out forwards;15 }16}1import { type Grid } from "@1771technologies/lytenyte-pro";2import type { GridSpec } from "./demo.js";3import { twMerge } from "tailwind-merge";4import clsx, { type ClassValue } from "clsx";5import { countryFlags, nameToAvatar } 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")21 return <div className="flex h-full w-full items-center">{String(field ?? "")}</div>;22
23 const formatted = field < 0 ? `-$${formatter.format(Math.abs(field))}` : "$" + formatter.format(field);24
25 return (26 <div27 className={tw(28 "flex h-full w-full items-center justify-end tabular-nums",29 field < 0 && "text-red-600 dark:text-red-300",30 field > 0 && "text-green-600 dark:text-green-300",31 )}32 >33 {formatted}34 </div>35 );36}37
38export function NameCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {39 if (!api.rowIsLeaf(row) || !row.data) return;40
41 const name = row.data.name;42 const url = nameToAvatar[name];43
44 return (45 <div className="flex h-full w-full items-center gap-2">46 {url && <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 if (typeof field === "number")58 return <div className="flex h-full w-full items-center justify-end">{formatter.format(field)} days</div>;59
60 return <div className="flex h-full w-full items-center justify-start">{String(field ?? "")}</div>;61}62
63export function CountryCell({ api, row, column }: Grid.T.CellRendererParams<GridSpec>) {64 const field = api.columnField(column, row);65
66 const flag = countryFlags[field as keyof typeof countryFlags];67
68 return (69 <div className="flex h-full w-full items-center gap-2">70 {flag && <img className="size-4" src={flag} alt={`country flag of ${field}`} />}71 <span>{String(field ?? "")}</span>72 </div>73 );74}75
76export function DateCell({ api, row, column }: Grid.T.CellRendererParams<GridSpec>) {77 const field = api.columnField(column, row);78
79 if (typeof field !== "string")80 return <div className="flex h-full w-full items-center">{String(field ?? "")}</div>;81
82 const dateField = parse(field as string, "yyyy-MM-dd", new Date());83
84 if (!isValid(dateField))85 return <div className="flex h-full w-full items-center">{String(field ?? "")}</div>;86
87 const niceDate = format(dateField, "yyyy MMM dd");88 return <div className="flex h-full w-full items-center tabular-nums">{niceDate}</div>;89}90
91export function OverdueCell({ api, row, column }: Grid.T.CellRendererParams<GridSpec>) {92 const field = api.columnField(column, row);93 if (field !== "Yes" && field !== "No")94 return <div className="flex h-full w-full items-center">{String(field ?? "")}</div>;95
96 return (97 <div98 className={tw(99 "flex w-full items-center justify-center rounded-lg py-1 font-bold",100 field === "No" && "bg-green-500/10 text-green-600",101 field === "Yes" && "bg-red-500/10 text-red-400",102 )}103 >104 {field}105 </div>106 );107}108
109export function CustomerRating({ api, row, column }: Grid.T.CellRendererParams<GridSpec>) {110 const field = api.columnField(column, row);111 if (typeof field !== "number" || field > 5 || field < 0)112 return <div className="flex h-full w-full items-center">{String(field ?? "")}</div>;113
114 return (115 <div className="flex justify-center text-yellow-300">116 <StarRating value={field} />117 </div>118 );119}120
121export default function StarRating({ value = 0 }: { value: number }) {122 const uid = useId();123
124 const max = 5;125
126 const clamped = useMemo(() => {127 const n = Number.isFinite(value) ? value : 0;128 return Math.max(0, Math.min(max, n));129 }, [value, max]);130
131 const stars = useMemo(() => {132 return Array.from({ length: max }, (_, i) => {133 const fill = Math.max(0, Math.min(1, clamped - i)); // 0..1134 return { i, fill };135 });136 }, [clamped, max]);137
138 return (139 <div className={"inline-flex items-center"} role="img">140 {stars.map(({ i, fill }) => {141 const gradId = `${uid}-star-${i}`;142 return <StarIcon key={i} fillFraction={fill} gradientId={gradId} />;143 })}144 </div>145 );146}147
148function StarIcon({ fillFraction, gradientId }: { fillFraction: number; gradientId: string }) {149 const pct = Math.round(fillFraction * 100);150
151 return (152 <svg width="20" height="20" viewBox="0 0 24 24" aria-hidden="true">153 <defs>154 <linearGradient id={gradientId} x1="0%" y1="0%" x2="100%" y2="0%">155 <stop offset={`${pct}%`} stopColor="currentColor" />156 <stop offset={`${pct}%`} stopColor="transparent" />157 </linearGradient>158 </defs>159
160 <path161 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"162 fill={`url(#${gradientId})`}163 />164 {/* Optional outline for crisp edges */}165 <path166 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"167 fill="none"168 stroke="transparent"169 strokeWidth="1"170 opacity="0.35"171 />172 </svg>173 );174}Note
The clipboard demo uses cell range selection, which is a
feature. However,
the editUpdateCells method is available in the Core edition of LyteNyte Grid.
Next Steps
- Cell Edit Validation: Validate cell edits to ensure updates are correct.
- Cell Editing: Edit cell values and commit updates in the grid.
- Cell Edit Renderers: Define custom renderers to update a cell in the grid.