You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

排查与解决方向

  1. 修复数据访问器的双向绑定
    当前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();
        })
    }
    
  2. 修正自定义渲染器的上下文绑定
    在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);
    
  3. 触发Angular变更检测
    Angular 18的变更检测机制更严格,Handsontable的DOM更新可能未触发Angular检测。在组件中注入ChangeDetectorRef,在afterChange中手动触发:

    constructor(private cdr: ChangeDetectorRef) {}
    
    // 在afterChange事件中添加
    this.cdr.detectChanges();
    
  4. 调整渲染时机
    移除data setter中的setTimeout,改为在afterChange事件中直接调用this.hot.render(),确保数据更新后立即渲染。

  5. 检查Handsontable Angular组件的兼容性
    确认使用的Handsontable Angular组件版本与Angular 18完全兼容,避免版本不匹配导致的渲染问题。


内容的提问来源于stack exchange,提问作者Gibosky

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.15 07:48:13