0
# Cell Editing System
1
2
RevoGrid provides a comprehensive cell editing system with built-in editors, custom editor support, and advanced editing features including validation and range editing.
3
4
## Editing Configuration
5
6
### Basic Editing Setup
7
8
```typescript { .api }
9
interface EditingConfiguration {
10
readonly: boolean;
11
editors: Edition.Editors;
12
useClipboard: boolean;
13
}
14
```
15
16
**readonly** { .api }
17
```typescript
18
readonly: boolean = false
19
```
20
Global read-only mode. When `true`, prevents all cell editing across the grid.
21
22
**editors** { .api }
23
```typescript
24
editors: Edition.Editors = {}
25
```
26
Registry of custom cell editors. Maps editor names to editor constructors.
27
28
**useClipboard** { .api }
29
```typescript
30
useClipboard: boolean = true
31
```
32
Enable clipboard operations (copy/paste) with keyboard shortcuts.
33
34
### Column-Level Editing Control
35
36
Configure editing behavior per column:
37
38
```typescript
39
// Column with custom editor
40
{
41
prop: 'status',
42
name: 'Status',
43
editor: 'select', // Built-in select editor
44
readonly: false
45
}
46
47
// Column with conditional readonly
48
{
49
prop: 'amount',
50
name: 'Amount',
51
readonly: (params) => params.model.locked === true
52
}
53
54
// Column with custom editor instance
55
{
56
prop: 'date',
57
name: 'Date',
58
editor: CustomDateEditor
59
}
60
```
61
62
## Editor Types and Interfaces
63
64
### Base Editor Interface
65
66
```typescript { .api }
67
namespace Edition {
68
interface EditorBase {
69
element?: HTMLElement;
70
editCell?: EditCell;
71
}
72
73
interface EditorCtr {
74
new (column: RevoGrid.ColumnRegular, save?: (value: SaveData, preventFocus?: boolean) => void): EditorBase;
75
}
76
77
type Editors = {[name: string]: EditorCtr};
78
}
79
```
80
81
**EditorCtr** { .api }
82
```typescript
83
interface EditorCtr {
84
new (
85
column: RevoGrid.ColumnRegular,
86
save?: (value: SaveData, preventFocus?: boolean) => void
87
): EditorBase;
88
}
89
```
90
Constructor interface for creating editor instances.
91
92
### Edit Cell State
93
94
```typescript { .api }
95
interface EditCell {
96
x: number; // Column index
97
y: number; // Row index
98
val?: SaveData; // Current edit value
99
prop: RevoGrid.ColumnProp; // Column property
100
type: RevoGrid.DimensionRows; // Row dimension type
101
}
102
```
103
104
**SaveData** { .api }
105
```typescript
106
type SaveData = string;
107
```
108
Type for saved editor data (typically string representation).
109
110
## Built-in Editors
111
112
### Text Editor (Default)
113
114
The default text editor for simple text input:
115
116
```typescript
117
// Automatically used for columns without specific editor
118
{
119
prop: 'name',
120
name: 'Name'
121
// Uses default text editor
122
}
123
```
124
125
### Custom Editor Registration
126
127
Register custom editors in the editors registry:
128
129
```typescript
130
// Define custom editors
131
grid.editors = {
132
'select': SelectEditor,
133
'numeric': NumericEditor,
134
'date': DateEditor,
135
'checkbox': CheckboxEditor
136
};
137
```
138
139
## Creating Custom Editors
140
141
### Basic Custom Editor
142
143
```typescript
144
class CustomSelectEditor implements Edition.EditorBase {
145
public element: HTMLSelectElement;
146
public editCell: Edition.EditCell;
147
148
constructor(
149
public column: RevoGrid.ColumnRegular,
150
private save?: (value: Edition.SaveData, preventFocus?: boolean) => void
151
) {
152
this.createElement();
153
this.setupEventListeners();
154
}
155
156
private createElement() {
157
this.element = document.createElement('select');
158
this.element.className = 'revo-edit-select';
159
160
// Add options based on column configuration
161
const options = this.column.source || ['Option 1', 'Option 2', 'Option 3'];
162
options.forEach(option => {
163
const optionElement = document.createElement('option');
164
optionElement.value = option;
165
optionElement.textContent = option;
166
this.element.appendChild(optionElement);
167
});
168
}
169
170
private setupEventListeners() {
171
// Save on change
172
this.element.addEventListener('change', () => {
173
this.saveValue();
174
});
175
176
// Save on blur
177
this.element.addEventListener('blur', () => {
178
this.saveValue();
179
});
180
181
// Handle keyboard navigation
182
this.element.addEventListener('keydown', (e) => {
183
switch (e.key) {
184
case 'Escape':
185
this.cancel();
186
break;
187
case 'Enter':
188
case 'Tab':
189
this.saveValue();
190
break;
191
}
192
});
193
}
194
195
private saveValue() {
196
if (this.save) {
197
this.save(this.element.value);
198
}
199
}
200
201
private cancel() {
202
// Restore original value and exit edit mode
203
if (this.editCell && this.save) {
204
this.save(this.editCell.val || '');
205
}
206
}
207
}
208
```
209
210
### Advanced Custom Editor
211
212
```typescript
213
class NumericEditor implements Edition.EditorBase {
214
public element: HTMLInputElement;
215
public editCell: Edition.EditCell;
216
private originalValue: string = '';
217
218
constructor(
219
public column: RevoGrid.ColumnRegular,
220
private save?: (value: Edition.SaveData, preventFocus?: boolean) => void
221
) {
222
this.createElement();
223
this.setupValidation();
224
this.setupEventListeners();
225
}
226
227
private createElement() {
228
this.element = document.createElement('input');
229
this.element.type = 'number';
230
this.element.className = 'revo-edit-numeric';
231
232
// Apply column-specific configuration
233
if (this.column.min !== undefined) {
234
this.element.min = String(this.column.min);
235
}
236
if (this.column.max !== undefined) {
237
this.element.max = String(this.column.max);
238
}
239
if (this.column.step !== undefined) {
240
this.element.step = String(this.column.step);
241
}
242
}
243
244
private setupValidation() {
245
this.element.addEventListener('input', () => {
246
this.validateInput();
247
});
248
}
249
250
private validateInput() {
251
const value = this.element.value;
252
const numericValue = parseFloat(value);
253
254
// Remove invalid class first
255
this.element.classList.remove('invalid');
256
257
// Validate numeric value
258
if (value && isNaN(numericValue)) {
259
this.element.classList.add('invalid');
260
return false;
261
}
262
263
// Validate range
264
if (this.column.min !== undefined && numericValue < this.column.min) {
265
this.element.classList.add('invalid');
266
return false;
267
}
268
269
if (this.column.max !== undefined && numericValue > this.column.max) {
270
this.element.classList.add('invalid');
271
return false;
272
}
273
274
return true;
275
}
276
277
private setupEventListeners() {
278
// Store original value when editing starts
279
this.element.addEventListener('focus', () => {
280
this.originalValue = this.element.value;
281
});
282
283
// Save on Enter
284
this.element.addEventListener('keydown', (e) => {
285
switch (e.key) {
286
case 'Enter':
287
if (this.validateInput()) {
288
this.saveValue();
289
} else {
290
this.showValidationError();
291
}
292
e.preventDefault();
293
break;
294
295
case 'Escape':
296
this.cancel();
297
e.preventDefault();
298
break;
299
300
case 'Tab':
301
if (this.validateInput()) {
302
this.saveValue();
303
} else {
304
e.preventDefault();
305
this.showValidationError();
306
}
307
break;
308
}
309
});
310
311
// Save on blur if valid
312
this.element.addEventListener('blur', () => {
313
if (this.validateInput()) {
314
this.saveValue();
315
} else {
316
// Restore original value on invalid blur
317
this.element.value = this.originalValue;
318
this.element.classList.remove('invalid');
319
}
320
});
321
}
322
323
private saveValue() {
324
if (this.save && this.validateInput()) {
325
this.save(this.element.value);
326
}
327
}
328
329
private cancel() {
330
this.element.value = this.originalValue;
331
this.element.classList.remove('invalid');
332
if (this.save) {
333
this.save(this.originalValue);
334
}
335
}
336
337
private showValidationError() {
338
// Custom validation error handling
339
this.element.focus();
340
this.element.select();
341
342
// Show tooltip or other error indication
343
console.warn('Invalid numeric value');
344
}
345
}
346
```
347
348
## Editing Methods
349
350
### Programmatic Editing
351
352
**setCellEdit** { .api }
353
```typescript
354
async setCellEdit(
355
rgRow: number,
356
prop: RevoGrid.ColumnProp,
357
rowSource?: RevoGrid.DimensionRows
358
): Promise<void>
359
```
360
Enter edit mode for a specific cell programmatically.
361
362
- **rgRow**: Row index to edit
363
- **prop**: Column property to edit
364
- **rowSource**: Row dimension type (optional)
365
366
```typescript
367
// Start editing specific cell
368
await grid.setCellEdit(5, 'name');
369
370
// Edit cell in pinned row
371
await grid.setCellEdit(0, 'total', 'rowPinStart');
372
373
// Edit after focusing
374
const focused = await grid.getFocused();
375
if (focused) {
376
await grid.setCellEdit(
377
focused.cell.y,
378
focused.column.prop,
379
focused.rowType
380
);
381
}
382
```
383
384
## Editing Events
385
386
### Edit Lifecycle Events
387
388
**beforeeditstart** { .api }
389
```typescript
390
grid.addEventListener('beforeeditstart', (event) => {
391
const detail: Edition.BeforeSaveDataDetails = event.detail;
392
console.log('About to start editing:', detail);
393
394
// Prevent editing if conditions not met
395
if (!canEditCell(detail)) {
396
event.preventDefault();
397
}
398
});
399
```
400
401
**beforeedit** { .api }
402
```typescript
403
grid.addEventListener('beforeedit', (event) => {
404
const detail: Edition.BeforeSaveDataDetails = event.detail;
405
console.log('Before saving edit:', detail);
406
407
// Validate before saving
408
if (!validateEdit(detail)) {
409
event.preventDefault();
410
showValidationError(detail);
411
}
412
});
413
```
414
415
**afteredit** { .api }
416
```typescript
417
grid.addEventListener('afteredit', (event) => {
418
const detail: Edition.BeforeSaveDataDetails = event.detail;
419
console.log('Edit completed:', detail);
420
421
// Handle post-edit actions
422
onEditComplete(detail);
423
});
424
```
425
426
### Edit Event Details
427
428
```typescript { .api }
429
interface BeforeSaveDataDetails {
430
// Original model data
431
model: RevoGrid.DataType;
432
433
// Column being edited
434
prop: RevoGrid.ColumnProp;
435
436
// New value being saved
437
val: Edition.SaveData;
438
439
// Row index
440
rowIndex: number;
441
442
// Column definition
443
column: RevoGrid.ColumnRegular;
444
445
// Additional context
446
type: RevoGrid.DimensionRows;
447
}
448
```
449
450
## Range Editing
451
452
### Range Edit Events
453
454
**beforerangeedit** { .api }
455
```typescript
456
grid.addEventListener('beforerangeedit', (event) => {
457
const detail: Edition.BeforeRangeSaveDataDetails = event.detail;
458
console.log('Before range edit:', detail);
459
460
// Validate all changes in range
461
if (!validateRangeEdit(detail)) {
462
event.preventDefault();
463
}
464
});
465
```
466
467
**Range Edit Details** { .api }
468
```typescript
469
interface BeforeRangeSaveDataDetails {
470
// Array of all changes in the range
471
data: BeforeSaveDataDetails[];
472
473
// Range area being edited
474
range: Selection.RangeArea;
475
476
// Edit source (paste, autofill, etc.)
477
source: 'paste' | 'autofill' | 'edit';
478
}
479
```
480
481
### Implementing Range Operations
482
483
```typescript
484
class RangeEditManager {
485
private grid: HTMLRevoGridElement;
486
487
constructor(grid: HTMLRevoGridElement) {
488
this.grid = grid;
489
this.setupRangeEditing();
490
}
491
492
private setupRangeEditing() {
493
// Enable range selection for range editing
494
this.grid.range = true;
495
496
// Listen for range edit events
497
this.grid.addEventListener('beforerangeedit', (event) => {
498
this.handleRangeEdit(event);
499
});
500
}
501
502
private handleRangeEdit(event: CustomEvent) {
503
const detail = event.detail as Edition.BeforeRangeSaveDataDetails;
504
505
// Validate each change in the range
506
for (const change of detail.data) {
507
if (!this.validateSingleChange(change)) {
508
event.preventDefault();
509
this.showRangeEditError(change);
510
return;
511
}
512
}
513
514
// Log range edit for audit
515
this.logRangeEdit(detail);
516
}
517
518
private validateSingleChange(change: Edition.BeforeSaveDataDetails): boolean {
519
const { prop, val, column } = change;
520
521
// Check if cell is readonly
522
if (column.readonly) {
523
return false;
524
}
525
526
// Validate by data type
527
switch (column.type) {
528
case 'numeric':
529
return !isNaN(Number(val));
530
case 'email':
531
return this.isValidEmail(val);
532
case 'date':
533
return !isNaN(Date.parse(val));
534
default:
535
return true;
536
}
537
}
538
539
private isValidEmail(email: string): boolean {
540
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
541
return emailRegex.test(email);
542
}
543
544
private showRangeEditError(change: Edition.BeforeSaveDataDetails) {
545
console.error('Range edit validation failed:', change);
546
// Show user-friendly error message
547
}
548
549
private logRangeEdit(detail: Edition.BeforeRangeSaveDataDetails) {
550
console.log(`Range edit: ${detail.data.length} cells modified via ${detail.source}`);
551
}
552
553
async fillRange(value: any) {
554
const selection = await this.grid.getSelectedRange();
555
if (!selection) return;
556
557
const data = await this.grid.getSource();
558
const columns = await this.grid.getColumns();
559
560
// Create range edit data
561
const rangeData: Edition.BeforeSaveDataDetails[] = [];
562
563
for (let y = selection.y; y <= selection.y1; y++) {
564
for (let x = selection.x; x <= selection.x1; x++) {
565
const column = columns[x];
566
const rowData = data[y];
567
568
rangeData.push({
569
model: rowData,
570
prop: column.prop,
571
val: String(value),
572
rowIndex: y,
573
column: column,
574
type: 'rgRow'
575
});
576
}
577
}
578
579
// Trigger range edit event
580
const event = new CustomEvent('beforerangeedit', {
581
detail: {
582
data: rangeData,
583
range: selection,
584
source: 'api'
585
},
586
cancelable: true
587
});
588
589
this.grid.dispatchEvent(event);
590
591
if (!event.defaultPrevented) {
592
// Apply the changes
593
this.applyRangeChanges(rangeData);
594
}
595
}
596
597
private async applyRangeChanges(changes: Edition.BeforeSaveDataDetails[]) {
598
// Group changes by row for efficient updates
599
const changesByRow = new Map<number, Edition.BeforeSaveDataDetails[]>();
600
601
changes.forEach(change => {
602
if (!changesByRow.has(change.rowIndex)) {
603
changesByRow.set(change.rowIndex, []);
604
}
605
changesByRow.get(change.rowIndex)!.push(change);
606
});
607
608
// Apply changes row by row
609
const rowStore = await this.grid.getSourceStore();
610
611
changesByRow.forEach((rowChanges, rowIndex) => {
612
const currentRow = rowStore.getRow(rowIndex);
613
const updatedRow = { ...currentRow };
614
615
rowChanges.forEach(change => {
616
updatedRow[change.prop] = change.val;
617
});
618
619
rowStore.setRow(rowIndex, updatedRow);
620
});
621
622
// Refresh the grid
623
await this.grid.refresh();
624
}
625
}
626
```
627
628
## Autofill Feature
629
630
### Autofill Events
631
632
**beforeautofill** { .api }
633
```typescript
634
grid.addEventListener('beforeautofill', (event) => {
635
const rangeChange: Selection.ChangedRange = event.detail;
636
console.log('Before autofill:', rangeChange);
637
638
// Customize autofill behavior
639
if (!allowAutofill(rangeChange)) {
640
event.preventDefault();
641
}
642
});
643
```
644
645
### Custom Autofill Implementation
646
647
```typescript
648
class AutofillManager {
649
private grid: HTMLRevoGridElement;
650
651
constructor(grid: HTMLRevoGridElement) {
652
this.grid = grid;
653
this.setupAutofill();
654
}
655
656
private setupAutofill() {
657
this.grid.addEventListener('beforeautofill', (event) => {
658
this.handleAutofill(event);
659
});
660
}
661
662
private async handleAutofill(event: CustomEvent) {
663
const rangeChange = event.detail as Selection.ChangedRange;
664
665
// Get source and target ranges
666
const sourceRange = rangeChange.oldRange;
667
const targetRange = rangeChange.newRange;
668
669
if (!sourceRange || !targetRange) return;
670
671
// Implement custom autofill logic
672
const autofillData = await this.generateAutofillData(sourceRange, targetRange);
673
674
// Apply autofill data
675
if (autofillData.length > 0) {
676
this.applyAutofillData(autofillData);
677
}
678
}
679
680
private async generateAutofillData(
681
sourceRange: Selection.RangeArea,
682
targetRange: Selection.RangeArea
683
): Promise<Edition.BeforeSaveDataDetails[]> {
684
const data = await this.grid.getSource();
685
const columns = await this.grid.getColumns();
686
const autofillData: Edition.BeforeSaveDataDetails[] = [];
687
688
// Determine fill direction and pattern
689
const isHorizontal = targetRange.y === sourceRange.y && targetRange.y1 === sourceRange.y1;
690
const isVertical = targetRange.x === sourceRange.x && targetRange.x1 === sourceRange.x1;
691
692
if (isVertical) {
693
// Vertical autofill
694
for (let x = sourceRange.x; x <= sourceRange.x1; x++) {
695
const column = columns[x];
696
const sourceValues = this.getColumnValues(data, sourceRange, x);
697
const pattern = this.detectPattern(sourceValues);
698
699
let fillIndex = 0;
700
for (let y = Math.max(targetRange.y, sourceRange.y1 + 1); y <= targetRange.y1; y++) {
701
const fillValue = this.generateNextValue(pattern, fillIndex);
702
703
autofillData.push({
704
model: data[y],
705
prop: column.prop,
706
val: String(fillValue),
707
rowIndex: y,
708
column: column,
709
type: 'rgRow'
710
});
711
712
fillIndex++;
713
}
714
}
715
}
716
717
return autofillData;
718
}
719
720
private getColumnValues(
721
data: RevoGrid.DataType[],
722
range: Selection.RangeArea,
723
columnIndex: number
724
): any[] {
725
const columns = this.grid.columns;
726
const column = columns[columnIndex];
727
const values: any[] = [];
728
729
for (let y = range.y; y <= range.y1; y++) {
730
values.push(data[y][column.prop]);
731
}
732
733
return values;
734
}
735
736
private detectPattern(values: any[]): AutofillPattern {
737
// Detect numeric sequence
738
if (values.every(v => !isNaN(Number(v)))) {
739
const numbers = values.map(v => Number(v));
740
const differences = [];
741
742
for (let i = 1; i < numbers.length; i++) {
743
differences.push(numbers[i] - numbers[i - 1]);
744
}
745
746
// Check for arithmetic sequence
747
if (differences.every(d => d === differences[0])) {
748
return {
749
type: 'arithmetic',
750
increment: differences[0],
751
lastValue: numbers[numbers.length - 1]
752
};
753
}
754
}
755
756
// Detect date sequence
757
if (values.every(v => !isNaN(Date.parse(v)))) {
758
const dates = values.map(v => new Date(v));
759
// Implement date pattern detection
760
return {
761
type: 'date',
762
increment: 1, // Default: 1 day
763
lastValue: dates[dates.length - 1]
764
};
765
}
766
767
// Default: repeat pattern
768
return {
769
type: 'repeat',
770
values: values
771
};
772
}
773
774
private generateNextValue(pattern: AutofillPattern, index: number): any {
775
switch (pattern.type) {
776
case 'arithmetic':
777
return pattern.lastValue + pattern.increment * (index + 1);
778
779
case 'date':
780
const nextDate = new Date(pattern.lastValue);
781
nextDate.setDate(nextDate.getDate() + pattern.increment * (index + 1));
782
return nextDate.toISOString().split('T')[0];
783
784
case 'repeat':
785
return pattern.values[index % pattern.values.length];
786
787
default:
788
return '';
789
}
790
}
791
792
private applyAutofillData(autofillData: Edition.BeforeSaveDataDetails[]) {
793
// Similar to range edit application
794
// Implementation would be similar to applyRangeChanges in RangeEditManager
795
}
796
}
797
798
interface AutofillPattern {
799
type: 'arithmetic' | 'date' | 'repeat';
800
increment?: number;
801
lastValue?: any;
802
values?: any[];
803
}
804
```
805
806
## Usage Examples
807
808
### Complete Editing System Setup
809
810
```typescript
811
class GridEditingSystem {
812
private grid: HTMLRevoGridElement;
813
private editHistory: EditHistoryItem[] = [];
814
815
constructor(grid: HTMLRevoGridElement) {
816
this.grid = grid;
817
this.setupEditing();
818
}
819
820
private setupEditing() {
821
// Register custom editors
822
this.grid.editors = {
823
'select': SelectEditor,
824
'numeric': NumericEditor,
825
'date': DateEditor,
826
'email': EmailEditor,
827
'currency': CurrencyEditor
828
};
829
830
// Setup editing events
831
this.setupEditingEvents();
832
833
// Setup validation
834
this.setupValidation();
835
836
// Setup edit history
837
this.setupEditHistory();
838
}
839
840
private setupEditingEvents() {
841
this.grid.addEventListener('beforeeditstart', (event) => {
842
const detail = event.detail;
843
844
// Check permissions
845
if (!this.hasEditPermission(detail)) {
846
event.preventDefault();
847
this.showPermissionError();
848
return;
849
}
850
851
// Log edit start
852
console.log('Edit started:', detail);
853
});
854
855
this.grid.addEventListener('beforeedit', (event) => {
856
const detail = event.detail;
857
858
// Validate the change
859
const validation = this.validateEdit(detail);
860
if (!validation.isValid) {
861
event.preventDefault();
862
this.showValidationError(validation.error);
863
return;
864
}
865
866
// Store in history before applying
867
this.addToHistory(detail);
868
});
869
870
this.grid.addEventListener('afteredit', (event) => {
871
const detail = event.detail;
872
873
// Update dependent calculations
874
this.updateDependentCells(detail);
875
876
// Save to server
877
this.saveToServer(detail);
878
879
// Notify other systems
880
this.notifyEditComplete(detail);
881
});
882
}
883
884
private validateEdit(detail: Edition.BeforeSaveDataDetails): ValidationResult {
885
const { prop, val, column } = detail;
886
887
// Type-specific validation
888
switch (column.type) {
889
case 'numeric':
890
const numValue = Number(val);
891
if (isNaN(numValue)) {
892
return { isValid: false, error: 'Must be a valid number' };
893
}
894
if (column.min !== undefined && numValue < column.min) {
895
return { isValid: false, error: `Must be at least ${column.min}` };
896
}
897
if (column.max !== undefined && numValue > column.max) {
898
return { isValid: false, error: `Must be at most ${column.max}` };
899
}
900
break;
901
902
case 'email':
903
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(val)) {
904
return { isValid: false, error: 'Must be a valid email address' };
905
}
906
break;
907
908
case 'required':
909
if (!val || val.trim() === '') {
910
return { isValid: false, error: 'This field is required' };
911
}
912
break;
913
}
914
915
// Custom validation functions
916
if (column.validate) {
917
const customResult = column.validate(val, detail.model);
918
if (!customResult.isValid) {
919
return customResult;
920
}
921
}
922
923
return { isValid: true };
924
}
925
926
private hasEditPermission(detail: Edition.BeforeSaveDataDetails): boolean {
927
// Implement permission checking logic
928
return true; // Simplified
929
}
930
931
private addToHistory(detail: Edition.BeforeSaveDataDetails) {
932
this.editHistory.push({
933
timestamp: new Date(),
934
change: { ...detail },
935
originalValue: detail.model[detail.prop]
936
});
937
938
// Limit history size
939
if (this.editHistory.length > 100) {
940
this.editHistory.shift();
941
}
942
}
943
944
async undoLastEdit() {
945
if (this.editHistory.length === 0) return;
946
947
const lastEdit = this.editHistory.pop();
948
if (lastEdit) {
949
// Restore original value
950
const rowStore = await this.grid.getSourceStore();
951
const currentRow = rowStore.getRow(lastEdit.change.rowIndex);
952
currentRow[lastEdit.change.prop] = lastEdit.originalValue;
953
rowStore.setRow(lastEdit.change.rowIndex, currentRow);
954
955
await this.grid.refresh();
956
}
957
}
958
959
private updateDependentCells(detail: Edition.BeforeSaveDataDetails) {
960
// Example: Update total when amount changes
961
if (detail.prop === 'amount') {
962
this.recalculateTotals();
963
}
964
}
965
966
private async recalculateTotals() {
967
const data = await this.grid.getSource();
968
let total = 0;
969
970
data.forEach(row => {
971
total += Number(row.amount) || 0;
972
});
973
974
// Update summary row
975
this.grid.pinnedBottomSource = [
976
{ ...this.grid.pinnedBottomSource[0], total }
977
];
978
}
979
}
980
981
interface ValidationResult {
982
isValid: boolean;
983
error?: string;
984
}
985
986
interface EditHistoryItem {
987
timestamp: Date;
988
change: Edition.BeforeSaveDataDetails;
989
originalValue: any;
990
}
991
```
992
993
The cell editing system provides powerful capabilities for creating rich, interactive grid applications with comprehensive validation, custom editors, and advanced editing features.