Angular 18中HandsOnTable数值单元格编辑后不显示值问题
问题:Angular 18中HandsOnTable数值单元格编辑后值不显示
从Angular 4迁移至Angular 18,并改用HandsOnTable Angular组件后,出现以下问题:编辑数值单元格后新值无法在表格中显示,但验证逻辑、BeforeChange/AfterChange事件均正常触发,且底层数据源已实际更新。
组件配置代码
@Component({ selector: 'dlb-handsontable', templateUrl: './handsontable.component.html' }) export class HandsontableComponent implements AfterViewInit, OnChanges, OnDestroy { public hot!: Handsontable; @Input() set data(data: any) { this._data = data; if (this.hot) { this.updateData(data); this.sort(); setTimeout(() => { this.sort(this.sorting.column, this.sorting.order); this.render(); }, 200); } } // Key configuration for numeric cells public setupHandsontable(dataSchema: any = null) { this.container = this.el.nativeElement.querySelector('#hot-demo'); this.dataSchema = dataSchema; const hotSettings: Handsontable.GridSettings = { data: this._data, licenseKey: 'non-commercial-and-evaluation', colHeaders: this._colHeaders, columns: this._columns, columnSorting: {indicator: true}, minSpareRows: 0, fixedColumnsLeft: 2, manualColumnResize: true, afterChange: (changes: CellChange[] | null, source: ChangeSource) => { changes!.forEach(change => { if (change![2] === change![3]) return; const row = change![0]; const physicalIndex = this.hot.toPhysicalRow(row); const data: RowObject = this.hot.getSourceDataAtRow(physicalIndex); if (data) { console.log(data); } }) }, beforeChange: (changes: (CellChange | null)[], source: ChangeSource) => { if (source === 'loadData' || !changes) { return; //don't do anything as this is called when table is loaded } changes.forEach(change => { if (change![2] === change![3]) return; const row = change![0]; const physicalIndex = this.hot.toPhysicalRow(row); const data: RowObject = this.hot.getSourceDataAtRow(physicalIndex); if (data) { data['setDirty'] = true; this._isChanged = true; } }); }, afterRender: () => { requestAnimationFrame(() => { this.processing.emit({isProcessing: false}); }); } }; this.hot = new Handsontable(this.container!, hotSettings); if (dataSchema) { this.updateSettings({ dataSchema: this.dataSchema }); } } }
列定义代码
const numericColumns: NumericColumn[] = months.map((x) => ({ data: this.dateproperty(x.toDBDateString()), allowEmpty: true, format: '0,0.0', type: 'numeric', colWidths: 90, validator: this.allocationValidator, columnSorting: true, renderer: this.allocationRenderer, }));
父组件(AllocationComponent)代码
@Component({ selector: 'allocations', templateUrl: './allocations.component.html', }) export class AllocationsComponent implements OnInit, AfterViewInit, OnDestroy { @ViewChild(HandsontableComponent, { static: false }) handsontable?: HandsontableComponent; private generateColumns(): ColumnDefinition[] { // ... other columns const numericColumns: NumericColumn[] = months.map((x) => ({ data: this.dateproperty(x.toDBDateString()), allowEmpty: true, format: '0,0.0', type: 'numeric', colWidths: 90, validator: this.allocationValidator, columnSorting: true, renderer: this.allocationRenderer, })); return [...dynamicColumns, ...numericColumns]; } private customRenderer = ( instance: Handsontable.Core, TD: HTMLTableCellElement, row: number, column: number, prop: string | number, value: any, cellProperties: Handsontable.CellProperties, renderer: any ) => { Handsontable.renderers.NumericRenderer.call( this, instance, TD, row, column, prop, value, cellProperties ); const physicalIndex = instance.toPhysicalRow(row); const data: Handsontable.RowObject = instance.getSourceDataAtRow(physicalIndex); if (data) { TD.classList.remove('modified-cell', 'red-cell'); if (data["isDirty"] && cellProperties["isModified"]) { TD.classList.add('modified-cell'); } else if (data["isDirty"] && !data["isValid"]) { TD.classList.add('red-cell'); } } return TD; }; public allocationRenderer = (instance: any, td: any, row: any, col: any, prop: any, value: any, cellProperties: any) => { let renderer = Handsontable.renderers.NumericRenderer; this.customRenderer(instance, td, row, col, prop, value, cellProperties, renderer); }; private dateproperty(date: string) { return (row: any) => { if (!row?.allocations) return; const allocation = row.allocations.find( (alloc: any) => alloc.month === date ); return allocation ? allocation.alloc_percent : null; }; } }
已尝试操作
- 控制台日志确认模型数据已更新
- 确认renderer已被调用
- 验证逻辑正常工作
- 更新后手动调用
render()方法 - 确认
dateproperty访问器返回正确值 - 检查
customRenderer实现
期望与实际行为
- 期望:编辑数值单元格后,新值立即在表格中显示
- 实际:编辑后单元格显示为空或保留旧值,但底层数据已变更
环境信息
- Angular: 18
- HandsOnTable版本: 最新版
- 浏览器: Chrome 120
排查与解决方向
修复数据访问器的双向绑定
当前dateproperty返回的是只读函数,仅能读取alloc_percent,无法将编辑后的新值写回row.allocations对应的对象。Handsontable需要列的data配置支持读写,或者在afterChange中手动更新数据源:afterChange: (changes: CellChange[] | null, source: ChangeSource) => { if (source !== 'edit') return; changes!.forEach(change => { const [rowIdx, prop, oldVal, newVal] = change!; if (oldVal === newVal) return; const physicalRow = this.hot.toPhysicalRow(rowIdx); const rowData = this.hot.getSourceDataAtRow(physicalRow); // 根据prop找到对应的月份,更新alloc_percent const targetMonth = extractMonthFromProp(prop); // 自行实现提取月份的逻辑 const allocation = rowData.allocations.find(alloc => alloc.month === targetMonth); if (allocation) { allocation.alloc_percent = newVal; } this.hot.render(); }) }修正自定义渲染器的上下文绑定
在customRenderer中调用NumericRenderer.call时,错误绑定了组件的this,应改为绑定Handsontable实例的上下文:// 原代码 Handsontable.renderers.NumericRenderer.call(this, instance, TD, row, column, prop, value, cellProperties); // 修改为 Handsontable.renderers.NumericRenderer.call(instance, instance, TD, row, column, prop, value, cellProperties); // 或者直接调用renderer函数 renderer(instance, TD, row, column, prop, value, cellProperties);触发Angular变更检测
Angular 18的变更检测机制更严格,Handsontable的DOM更新可能未触发Angular检测。在组件中注入ChangeDetectorRef,在afterChange中手动触发:constructor(private cdr: ChangeDetectorRef) {} // 在afterChange事件中添加 this.cdr.detectChanges();调整渲染时机
移除datasetter中的setTimeout,改为在afterChange事件中直接调用this.hot.render(),确保数据更新后立即渲染。检查Handsontable Angular组件的兼容性
确认使用的Handsontable Angular组件版本与Angular 18完全兼容,避免版本不匹配导致的渲染问题。
内容的提问来源于stack exchange,提问作者Gibosky
相关产品推荐
相关产品推荐

