Skip to content

updateColumnDefinition on a frozen column throws in ResizeColumns and permanently corrupts frozen column offsets #4959

Description

@Bartheyrman22

Describe the bug

https://jsfiddle.net/bnj37v5e/

Calling updateColumnDefinition() on a column that is currently frozen rejects with TypeError: column.element.after is not a function, and leaves FrozenColumns.leftColumns holding the deleted column permanently. Every frozen column positioned after that stale entry is then offset by one extra column width, so it renders on top of the first scrolling column and stays there while scrolling horizontally.

It does not recover on its own. Every later re-render that touches a frozen column throws again on the same dead entry, so subsequent column deletions never dispatch column-deleted either. In an app that relabels a set of columns in one pass, the first failure takes the rest of the pass with it.

The cause is an ordering issue in Column.delete() (src/js/core/column/Column.js), which does:

this.element = false;
this.table.columnManager.deregisterColumn(this);
this.table.columnManager.rerenderColumns(true);
this.dispatch("column-deleted", this);
Step 3 triggers a width recalculation that reaches ResizeColumns.columnWidthUpdated(), which for a frozen column iterates frozenColumns.leftColumns:

columnWidthUpdated(column){
if(column.modules.frozen){
if(this.table.modules.frozenColumns.leftColumns.includes(column)){
this.table.modules.frozenColumns.leftColumns.forEach((col) => {
this.reinitializeColumn(col);
});

FrozenColumns has not been told about the deletion yet — that is step 4 — so the list still contains the column whose element was nulled in step 1. reinitializeColumn() ends with column.element.after(column.modules.resize.handleEl);, which throws on false. The throw propagates out of the Promise executor in Column.delete(), so step 4 never runs and FrozenColumns never rebuilds. Because updateDefinition() adds the replacement column before deleting the original, the list that survives contains both copies — hence the extra entry and the one-column offset.

Two preconditions, both confirmed by toggling them:

Columns must be resizable. With columnDefaults: {resizable: false} the same page runs clean.
At least one frozen column must precede the one being updated. With a single frozen column at margin 0px it resolves cleanly.
Suggested fix — a column with no element has nothing to position, so this is safe in general:

// src/js/modules/ResizeColumns/ResizeColumns.js
reinitializeColumn(column){
if(!column.element){
return;
}
var frozenOffset = this.frozenColumnOffset(column);

Alternatively, move the column-deleted dispatch in Column.delete() above rerenderColumns(true) so FrozenColumns rebuilds before anything re-renders against its lists. That is the more direct fix, but it changes when every column-deleted subscriber is notified.

Tabulator Info

Which version of Tabulator are you using? 6.5.2, and also reproduced on 6.3.1. The relevant functions (ResizeColumns.reinitializeColumn, ResizeColumns.columnWidthUpdated, ResizeColumns.frozenColumnOffset) are byte-identical between those two releases, and Column.delete() has the same dispatch order in both, so everything in that range looks affected.
Working Example

To Reproduce

Open the fiddle. The table has a frozen rowHeader gutter, a frozen field-less "Actions" column, a frozen column A, and unfrozen columns B/C/D.
On tableBuilt it logs the frozen-column offsets, calls table.updateColumnDefinition("a", {title:"A renamed", frozen:true}), and logs the offsets again.
Read the

 under the table.
Observed:

BEFORE: (no field)@0px | (no field)@55px | a@175px
promise: REJECTED - column.element.after is not a function
AFTER: (no field)@0px | (no field)@55px | (no field)@175px | a@295px
columnsByIndex: 6
The third AFTER entry is the deleted column — its field was set to false by updateDefinition(), which is why it prints as (no field). columnsByIndex is still correct at 6; only FrozenColumns is corrupt.

Look at the table: column A is drawn on top of column B. Scroll sideways — it stays there.
Expected behavior

The column is relabelled, the returned promise resolves, and leftColumns still holds three entries at 0px / 55px / 175px, with column A sitting immediately left of column B.

Screenshots

N/A — the logged offsets in the fiddle show it more precisely than an image.

Desktop (please complete the following information):

OS: Linux (x86_64)
Browser: Chrome
Version: 138
Smartphone (please complete the following information):

Not tested — nothing about this looks device-specific.
Additional context

Found in an app that relabels the grid's columns per client, where one of those columns is frozen. We are shipping the if(!column.element) return; guard above as a local patch, which fixes it with no other observable change.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Possible BugA possible bug that needs investigation

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions