Column Resizing Implementation in Vanilla-Grid
This document explains how column resizing is implemented in Vanilla-Grid, including pointer lifecycle, live width updates, resize-guide rendering, frozen-column interaction, and width persistence.
File location (May 2026): the resize logic lives in
src/vanilla-grid/features/columns-resize.feature.jsas a prototype extension ofVanillaGridColumnsFeature.
1. Feature Scope
Column resizing supports:
- Drag resize from header handles (
.vn-grid-col-resizer) - Real-time visual updates in header and body tables
- Last-column independent resizing
- Cascading resize for non-last columns: when adjacent column cannot shrink further (at its min-width), space is borrowed from the next column over, and so on
- Optional visual resize guide line (
.vn-grid-resize-guide) - Persistent widths in
localStorage - Configurable per-column minimum width via
column.minWidth - Configurable per-column maximum width via
column.maxWidth - Per-column
resizableflag to disable resizing on individual columns
Related options:
persistence.columnWidths.enabled(defaulttrue)persistence.columnWidths.storageKey(optional explicit key)
2. Structural Primitives
Resizing is built on three structural elements:
- Header
<colgroup>(headerColGroup) — source-of-truth for column widths - Body
<colgroup>(bodyColGroup) — mirrored from header widths - Header resize handles (
.vn-grid-col-resizer) inside eachth(except selection checkbox column)
Base CSS geometry:
.vn-grid-col-resizeris absolute at right edge, width12px, full height.vn-grid-col-resizer::afterrenders the visible center line.vn-grid-header-tableand.vn-grid-body-tableboth usetable-layout: fixed
This combination gives predictable width math and avoids content-driven reflow surprises.
3. Initialization and Listener Wiring
In renderHeader():
- every non-selection header cell gets a
.vn-grid-col-resizerelement — the handle is always rendered so it acts as a visual column separator - resizer events are delegated: a single per-grid
pointerdown/dblclick/mousemove/mouseleavelistener set onthis.header(see_initHeaderDelegatedListenersin rendering.feature.js) resolves the handle viae.target.closest('.vn-grid-col-resizer')and its stampeddataset.colIndex— no per-resizer closures.pointerdownon a resizer callsstartResize(e);dblclickauto-fits the column
When column.resizable is false:
- The handle is rendered (as a visual separator) but with
cursor: defaultand no tooltip. startResizeexits early, preventing direct drag-resize.autoFitColumnreturns early, preventing content-driven resize.- The header context-menu "Auto-fit" button is disabled.
- After layout finishes,
_lockNonResizableColumns()records the current pixel width in an internal per-grid lock map on the columns feature (setNonResizableWidthLocks, keyed by normalized column key)._getMinColumnWidth/_getMaxColumnWidthconsult the lock first, so the cascade math sees zero slack and cannot push or pull the column. The host's publiccolumn.minWidth/maxWidthfields are never written — the same column array can be reused across grids without carrying baked pixel constraints.
The HTML attribute resizable="false" on <vn-grid-column> maps to column.resizable = false.
For resizable columns, the delegated hover-tooltip mousemove handler caches each resizer's bounding rect in a WeakMap keyed by the resizer element. Entries are invalidated by an instance-level generation counter (_resizerRectGeneration) that the grid's single window-resize handler (_handleResize) bumps, and dropped on hover exit. No per-resizer listeners or window listeners are registered, so header rebuilds cannot leak anything.
startResize attaches document-level listeners via:
attachResizeListeners()pointermove→handleResizeMovepointerup/pointercancel→stopResize
Listeners are detached in detachResizeListeners() and again during destroy() for safety.
4. Resize State Machine
resizeState stores active interaction state:
activepointerIdstartXstartWidths(startWidth,nextStartWidth) — legacy convenience pairallStartWidths— array of starting widths for every column (enables cascading resize)indexisLastColumndraggingpendingDxrafIdresizerEl
4.1 Start (startResize)
startResize(e) does:
- Validate target column index.
- Freeze current measured widths into colgroups:
freezeAllColumnWidths(false)syncBodyColWidths()
- Capture starting widths for all columns into
allStartWidthsarray (enables cascading). - Populate
resizeState. - Set cursor to
col-resize. - Attempt
setPointerCaptureon resizer. - Show resize guide for eligible columns.
- Attach move/up listeners.
4.2 Move (handleResizeMove)
handleResizeMove(e) behavior:
- Calculates
dx = e.clientX - startX - Starts drag after a small threshold (
abs(dx) >= 3) - Queues visual updates through
requestAnimationFrame(one in-flight frame max)
Inside the rAF callback:
- Reads current + adjacent min widths via
_getMinColumnWidth - Applies clamped
dx - Writes inline
style.widthon relevant header<col>nodes - Mirrors to body with
syncBodyColWidths() - Updates resize guide position (
_updateResizeGuide) - Updates tooltips
- Recomputes frozen offsets when needed (
_applyFrozenColumnStyles)
4.3 Stop (stopResize)
stopResize():
- Releases pointer capture if held.
- Cancels pending rAF.
- Detaches listeners.
- Persists final widths (
saveColumnWidths→persistColumnWidthsToStorage). - Recalculates table width policy (
adjustTableWidthAfterResize). - Hides guide and resets state (
_resetResizeState). - Restores cursor/user-select.
- Reapplies frozen styles if active.
5. Width Math and Constraints
5.1 Minimum widths
_getMinColumnWidth(column):
- selection column (
__vgSelection__) minimum:28 - if
column.minWidthis a finite positive number:round(column.minWidth)(no floor — explicit config is honoured) - a column that renders BOTH the sort indicator and the filter funnel (sorting and filtering both enabled at grid AND column level —
_columnHasSortAndFilterChrome(column)) minimum:96 - all other columns minimum:
60
The 96 floor exists because below it the header's own text/sort-indicator content no longer fits inside the filter funnel's reserved zone (th:has(.vn-grid-filter-icon) .vn-grid-header-label { padding-right: 27px } in vanilla-grid.css) and visually collides with the funnel. It is a worst-case constant across shipped themes (th padding + ellipsis-floor text box + text/indicator gap + sort-indicator box + funnel reserve), not a live DOM measurement — resize is a per-frame path, so it deliberately avoids the getComputedStyle reads autoFitColumn's width math performs once per gesture (§14).
Consumers can set minWidth on any column definition. The HTML attribute min-width on <vn-grid-column> is mapped to column.minWidth by the web component.
getColWidth(...) always returns at least this minimum.
5.2 Non-last column behavior (cascading resize)
For a non-last column, the grid uses cascading resize. Instead of only affecting the immediate neighbor, space can be borrowed from multiple columns in the drag direction.
Dragging right (growing the active column):
- Calculate total available slack from columns to the right: for each column
ifromidx+1to the last, slack =startWidth[i] - minWidth[i]. - The actual grow amount is clamped to the total available slack.
- Shrinkage is distributed across right columns sequentially (nearest neighbor first).
Dragging left (shrinking the active column, growing right-side columns):
- Calculate total available grow capacity from columns to the right: for each column
ifromidx+1to the last, grow =maxWidth[i] - startWidth[i]. Locked (non-resizable) columns havemaxWidth = currentWidth, contributing 0. - Calculate total available shrink capacity from columns to the left: for each column
ifromidxdown to0, slack =startWidth[i] - minWidth[i]. - The actual movement is clamped to the minimum of both capacities.
- Growth is distributed across right columns sequentially (nearest neighbor first); locked columns are naturally skipped because their grow capacity is 0.
- Shrinkage is distributed across left columns sequentially (nearest neighbor first).
This ensures that a column resize is never blocked just because the immediate neighbor is at its minimum width, as long as there is space available further along.
All width calculations use the allStartWidths array captured at drag start, so each animation frame recalculates from the same baseline (no cumulative drift).
5.2b Maximum widths
_getMaxColumnWidth(column):
- if
column.maxWidthis a finite positive number:max(minWidth, round(column.maxWidth)) - otherwise:
Infinity(no upper bound)
Consumers can set maxWidth on any column definition. The HTML attribute max-width on <vn-grid-column> is mapped to column.maxWidth by the web component.
When set, both live drag resize and auto-fit will cap the column width at this value. Cascading resize clamps growth against the active column's maxWidth before distributing shrinkage to neighbors.
5.3 Last column behavior (independent)
For the last column:
- width changes independently:
startWidth + clampedDx - shrinking is bounded by min width
- growing is bounded by max width (if configured)
maxShrink = startWidth - currentMinCol
maxGrow = currentMaxCol - startWidth
clampedDx = min(maxGrow, max(-maxShrink, dx))
6. Header/Body Synchronization
syncBodyColWidths() copies header col widths into body col widths and computes total width sum.
If total width > 0:
- assigns identical explicit width to both tables
This enforces deterministic alignment under table-layout: fixed and keeps body/header columns pixel-synchronized during drag.
7. Table Width Reconciliation After Drag
adjustTableWidthAfterResize() ensures correct horizontal-scroll behavior:
- Sums explicit column widths.
- Compares to
viewport.clientWidth. - If sum <= viewport width:
- sets both tables to viewport width (no horizontal overflow)
- Else:
- sets both tables to exact sum width (horizontal scroll enabled)
- Updates scrollbar layout/thumb.
This prevents stale overflow after resizing the final columns.
7.1 stretchToFit and the smart distribution pass
The stretch-to-fit logic lives in its own feature file —
src/vanilla-grid/features/stretch-to-fit.feature.js —
which extends VanillaGridColumnsFeature.prototype with two new methods:
applyStretchToFit({ source, persist })— the single entry point._distributeStretchAllocation(caps, amount)— pure water-filling math (unit-tested).
It is invoked from three places: the grid-core post-render hook
(_absorbViewportSurplusToFlexColumn now just delegates), the viewport
resize debouncer, and adjustTableWidthAfterResize (when stretch is on).
It is also invoked directly (with persist: true) at the end of
stopResize and autoFitColumn so that the persistence invariant below
is maintained.
Persistence invariant (stretch-to-fit ON). After every applyStretchToFit({ persist: true })
call — including the delta < 1 "nothing to do" early return — the
in-memory savedColumnWidths map and the on-storage snapshot are synced
from the live inline widths via _persistStretchFlexWidths(flex, widths).
This guarantees savedColumnWidths.get(key) === inlineWidth for every
flex column once a stretch pass has run. The invariant is what allows
freezeAllColumnWidths(false) (called at startResize) to remain a
visual no-op: without it, the column would jump to the stale saved value
the instant the user grabs the resize handle.
freezeAllColumnWidths(false) itself is also stretch-aware: when stretch
is active and the column has a meaningful live measured width
(measured > minWidth), the live value takes priority over savedColumnWidths.
This is a defensive second line of defence — even if some future code
path leaves a stale entry in the map, the user's resize-start will still
not bump the column away from the cursor.
Behaviour depends on the stretchToFit option:
stretchToFit: false(default) —applyStretchToFitis a no-op. The user's declarative widths and any persisted resizes are authoritative: leftover viewport space stays empty (table is naturally narrower than the viewport), and a column sum that exceeds the viewport produces a normal horizontal scrollbar. This is critical for persistence correctness: silently shrinking the last column to absorb a vertical- scrollbar gutter would desync the rendered width fromsavedColumnWidthsand lose the user's resize on the next persist.stretchToFit: true(opt-in) — runs the constraint-aware distribution pass with the following invariants:- Per-column
maxWidthis respected. A column withmax-width="185"is never grown beyond 185 px, no matter how much viewport surplus remains. - Per-column
minWidthis respected on shrink. When the column sum overflows the viewport (e.g. afterautoFitAll), eligible columns are shrunk proportionally down to theirminWidthfloor; if that is still not enough, the natural horizontal scrollbar takes over. resizable="false"columns are not touched.- The selection column is not touched.
- Frozen columns are not touched. (They are positioned by
applyFrozenColumnStylesand must keep their measured width.) - The table never grows beyond the sum of eligible columns'
maxWidth. If every resizable column saturates before the viewport is filled, the table stays narrower than the viewport and a gap appears on the right — this is intentional, per the constraint that user-declared max-widths are absolute caps.
- Per-column
Algorithm. Eligible (flex) columns get a proportional share of the
delta (viewport - lockedTotal - flexTotal - safetyMargin) via classic
water-filling: every round, each still-active slot receives a share
proportional to its remaining capacity; columns that hit their cap drop
out and the leftover is redistributed among the survivors. Columns with no
maxWidth (unbounded) share any leftover that bounded columns cannot
absorb. Convergence is bounded by the number of flex columns.
autoFitAllColumns coordination. When stretch is on, the per-column
inflate-the-flex-column trick inside autoFitColumn is suppressed (via
the _suppressStretchAbsorb flag set by autoFitAllColumns). After the
N per-column autofits complete, applyStretchToFit({ persist: true })
runs exactly once to compute and persist the final layout. This avoids
both the O(N) layout cost and the per-iteration overshoot of maxWidth
that the legacy single-column absorb produced.
8. Resize Guide Overlay
Guide element lifecycle:
_ensureResizeGuide()creates.vn-grid-resize-guideonce inside.vn-grid-table-container_showResizeGuide(columnIndex)displays + positions_updateResizeGuide(columnIndex)tracks current handle X_hideResizeGuide()hides_destroyResizeGuide()removes on teardown
Positioning model:
- X: center of active
.vn-grid-col-resizerrelative to container - Y start: below header (
headerSpacer.offsetHeight) - Height: full body area (
top: headerHeight; bottom: 0)
Special-case:
- Guide is not shown for the last frozen column, because it would visually collide with the freeze boundary guide.
9. Frozen Column Interaction
When frozen columns exist:
- live resize calls
_applyFrozenColumnStyles()during drag - stop phase calls
_applyFrozenColumnStyles()again for final alignment
This recalculates sticky offsets so frozen cells remain pinned correctly while widths change.
Additionally, base CSS sets:
.vn-grid-header-table th.vn-grid-frozen-col-last .vn-grid-col-resizer {
background: transparent !important;
}
and paints its pseudo-line with freeze boundary color to avoid a white-square artifact and keep visual continuity.
10. Persistence Model
10.1 Storage key
resolveColumnWidthsStorageKey() produces:
vanilla-grid:column-widths:{pathname}:{scope}
Where scope is first available of:
gridIdoption (auto-populated from the<vanilla-grid>host elementid)bodyTable.idviewport.idheaderTable.id'default'
Can be overridden with persistence.columnWidths.storageKey.
10.2 Save path
saveColumnWidths() reads header <col> inline widths into savedColumnWidths map, then calls persistColumnWidthsToStorage().
Persisted format:
{ "name": 220, "city": 180, "weight": 120 }
10.3 Load/apply path
- Constructor calls
loadColumnWidthsFromStorage(). refreshHeaderLayout()attemptsapplySavedColumnWidths()to pre-set widths from the persisted map.freezeAllColumnWidths(...)always runs afterwards; it respects saved widths for columns that have them and computes deterministic widths from config/measurement for columns that don't (e.g. a previously-hidden column revealed via "Show all columns").
This guarantees stable initial layout even when persisted data is absent, invalid, or when newly-visible columns have no saved width.
11. Failure Safety and Cleanup
- All localStorage operations are wrapped in
try/catch; failures only log warnings. - Pointer capture calls are guarded (
try/catch) for browser compatibility. - Active resize listeners and guides are always removed during
destroy(). _resetResizeState()zeroes mutable state and hides guide to prevent stuck UI artifacts.
12. Visual/Theming Hooks
Themes can customize resize visuals without changing behavior:
.vn-grid-col-resizerbackground.vn-grid-col-resizer::afterline color.vn-grid-resize-guideappearance (base uses white +mix-blend-mode: difference)
Behavioral geometry (handle width, absolute positioning, guide stacking) should stay in base CSS.
13. Example Configuration
const grid = new VanillaGrid({
persistence: {
columnWidths: { enabled: true, storageKey: 'my-grid:column-widths' }
}
});
Per-column constraints:
<vn-grid-column field="Name" header="Name" min-width="100" max-width="400"></vn-grid-column>
<vn-grid-column field="Id" header="ID" type="number" resizable="false"></vn-grid-column>
For session-only sizing:
persistence: { columnWidths: { enabled: false } }
14. Auto-Fit API
14.1 Single-column auto-fit
autoFitColumn(columnKey, options?) sizes a column to fit its content:
- Measures header text width using a hidden in-DOM
<span>so the full CSS cascade is inherited naturally. - Measurement is batched into three phases, because appending a probe span dirties layout and reading its rect immediately afterwards forces a full synchronous re-layout of the table.
_measureColumnFitWidththerefore (1) appends a probe for every measured string in the column — header text,.vn-grid-header-secondary-text, and each non-empty cell of every non-caption pool row — reading no geometry; (2) reads every probe rect plus every other geometry/style value the column needs (the.vn-grid-sort-indicatoroffsetWidth, cell widget rects, allgetComputedStylepadding/gap/margin reads) in one pass with no intervening write; (3) detaches the probes and computes the result arithmetically. Detaching is itself a write, so anything read after it would force another reflow — that is why phase 2 pulls the indicator and widget reads forward. This collapses roughly one forced reflow per cell into one per column; see21-performance-analysis.md§2.4 for the measured before/after. It is the same read/write-split shapefreezeAllColumnWidthsuses (§2.1 there). - Batching cannot change the measured widths. The probes are
position: absolute— out of flow, so a probe alters neither its own cell's box nor a sibling's — andwhite-space: nowrap, so the width read is the string's intrinsic width, independent of the column's current width. N live probes measure identically to one probe reused N times. The spans come from a small module-level pool reused across columns and across repeated auto-fits. - The header row is measured as the SUM of its parts.
.vn-grid-header-labelis a flex row: the text wrapper (label plus sort indicator), then the secondary header text, then the aggregate badge,column-gapapart. The header term is thereforelabel + (gap + secondary) + (gap + badge). It used to bemax(label, secondary), as if the secondary text sat on its own line, which fitted every column with secondary text too narrow ("Temperature [°C]" came out as "Temp…"). Three details keep the sum exact, all read in phase 2: the label counts at least itsmin-width: 2emellipsis floor (a short label such as "Wt" takes more room than its text); the secondary-text probe carries the.vn-grid-header-secondary-textclass, so it measures at the theme's smaller font size rather than theth's; and the badge, which never shrinks, is read from its liveoffsetWidth. - Header chrome is measured, not assumed. Everything between the ellipsizing
.vn-grid-header-textand the column edge is read from the live DOM, in the same spirit as thethpadding: the.vn-grid-header-labelpadding-right(the filter-funnel reserve applied byth:has(.vn-grid-filter-icon)) plus the.vn-grid-sort-indicator'soffsetWidthand the header-text wrapper's flexgap. Both are theme-overridable —vn-grid-material.cssships its own indicator box — so a fixed constant desynchronises from what the header actually reserves. It previously used a flat22pxagainst ~45px of real chrome, which left every filterable column fitted ~23px too narrow and its header still ellipsized right after an explicit auto-fit. When no indicator element exists yet (pre-render), it falls back to the old22pxfor sortable columns. - Rounded up with a 1px cushion. The text measurement is fractional (38.125px for "Active"), and a column that settles a fraction of a pixel short still ellipsizes — a sub-pixel band that integer
scrollWidth/clientWidthchecks cannot see (see21-performance-analysis.mdon the tooltip-detection fix). The header total is thereforeMath.ceil(text + padding + chrome) + 1. - Iterates all visible pool rows and measures each body cell text width, skipping group caption rows and group footer rows.
- A caption row is never measured. When grouping is on, some pool rows are captions, and a caption puts its entire label —
Country: Australia — 85 items— into the toggle cell (the first visible non-internal column) and deliberately lets it overflow across the rest of the row; see § 7 of 22-grouping-implementation.md. That label is the row's content, not the column's, so measuring it sized the toggle column to hold a country name — 210px instead of 90px for anIDcolumn insamples/grid-minimal-js/— and, because only the materialized captions are visible to the measurement, sized it differently depending on where the grid was scrolled and what was collapsed. The loop therefore skips any pool row carryingvn-grid-row-caption, the same markermeasureActualRowHeight()tests to keep captions out of the row-height measurement. Consequences worth knowing: a caption can still be wider than the column that holds it (that overflow is the intended rendering), and a fully collapsed grid, whose pool holds only captions, fits that column to its header alone — the existing "measure what is materialized" contract, not a new rule. - A group footer row is never measured either, for the other of those two reasons.
Σ 1,204under Projects genuinely is that column's content, positionally aligned — so the "it is the row's content" argument does not apply to it. What does apply, and harder than it does to data rows, is materialization-dependence: a total is routinely wider than any single value in its column (85 rows averaging14sum to1,204), so the widest thing the column must hold is a value that is only sometimes in the pool. Fitting with the viewport at the top of a large group would size the column to its data and clip the total further down, with no second signal that the same fit would now produce a different width. The loop therefore skips any pool row carryingvn-grid-row-footer(see § 7.1 of 22-grouping-implementation.md). - An aggregated column is measured from its computed totals instead. The projection scan already finalizes one value per group per level, and it tracks the minimum and maximum of them per aggregated column as it goes. Auto-fit asks the grid for those two numbers formatted the way a footer cell renders them, prepends the function marker, and measures two probes plus a marker — O(1) per column rather than O(pool), and the same answer whatever is scrolled into view, including a viewport with no footer row materialized at all. Both extremes rather than just the maximum, because formatted width does not track magnitude:
-9,999is wider than10,000, and an accounting or currency format widens a negative further.- A number extreme is padded before formatting — its own integer digits plus
0.4444444444, so the probe carries as many fraction digits as the column's formatter shows — because with varying fraction digits the widest total is often neither extreme (12.333between9.5and13). A count or a temporal value is probed as it is. See § 7.1 of 22-grouping-implementation.md. - The probes are hosted in any materialized cell of the column and carry the footer's own classes (
vn-grid-group-footer-value/-marker), so the footer's typography — its--vn-grid-group-footer-font-weightin particular — applies to the measurement through the ordinary cascade, exactly as every other probe inherits the cascade of the cell it is measured in. The marker's themeable gap is read from the probe's own computedmargin-inline-endin the same batched read phase as everything else. - A built-in function's marker is an icon, not text: its probe is an empty span that also carries
vn-grid-aggregate-icon, so its width is the CSS box (--vn-grid-aggregate-icon-size,1emof the footer's font) rather than a string's advance. A host text marker (messages.aggregateFunctionMarkers) is probed as text, as before._getAggregateFitStrings()hands back the marker as the same{ icon }/{ text }descriptor the footer renders from, so the two cannot disagree. - A string result, which only a registered reducer returns, is folded into the extremes by length, the longest kept: otherwise the column would fit whichever group's value was seen first (22-grouping-implementation.md, auto-fit).
- When the pool holds no ordinary data cell at all (a fully collapsed grid, whose rows are all captions and footers), the cell padding falls back to the header's — the same fallback the cell loop already uses when a cell reports none.
- A host that never calls auto-fit and sets a narrow fixed width still clips its totals. That is the ordinary behaviour of a fixed width on any column and needs no aggregate-specific rule;
vn-grid-truncate-ellipsisgoverns how it looks.
- A number extreme is padded before formatting — its own integer digits plus
- Picks the maximum of header and cell measurements, floors at
minWidth, caps atmaxWidth. - Residual truncation is still possible in two cases, both by design and neither a measurement error: a column pinned at the dynamic
maxWidthcap below cannot grow past it, and under stretch-to-fit the post-fit redistribution pass (§applyStretchToFit) can settle a column below its fit width when the fitted total would overflow the viewport. - Dynamic
maxWidthcap (when no explicitoptions.maxWidth/ per-columnmaxWidthapplies):3 × (viewport.clientWidth / visibleColumnCount). The denominator is the count of columns currently visible inside the viewport (_getVisibleColumnCount()— header cells overlapping the viewport rect), not the total column count. On a wide, many-column grid most columns overflow horizontally and are reached by scrolling; dividing the visible viewport width by the total count would collapse the cap to a tiny value and stop legitimately wide content from ever fitting. It falls back to the total column count when the layout hasn't rendered yet, and to480when the viewport width is unknown. - Applies the resulting width to both
<colgroup>and callsadjustTableWidthAfterResize(). persistoption (defaulttrue) saves the new width to localStorage.modeoption:'missingOnly'(default, skips if a persisted width already exists) or'overwriteAll'(always resizes).
Trigger points:
- Double-click on the column's resize handle.
- Header context-menu → "Auto-fit column" button.
- Programmatic call:
element.autoFitColumn(columnKey, options).
Restrictions:
- Skipped if
column.resizable === false. - Skipped for the selection column (
__vgSelection__).
// Programmatic single-column auto-fit
document.querySelector('vn-grid').autoFitColumn('Name');
// Force overwrite ignoring persisted width
document.querySelector('vn-grid').autoFitColumn('Name', { mode: 'overwriteAll' });
14.2 Auto-fit all visible columns
autoFitAllColumns(options?) iterates all columns left-to-right and calls autoFitColumn for each eligible column:
- Skips the selection column.
- Skips hidden columns (those in the hidden-columns set).
- Skips columns with
resizable === false. - Calls
autoFitColumnwithpersist: falsefor every eligible column to avoid repeated localStorage writes per column; on completion it persists all widths in a single call. - The
optionsobject is forwarded to eachautoFitColumncall (e.g.minWidth,maxWidth). - Returns
trueif at least one column was resized,falseotherwise.
Trigger points:
- Header context-menu "Auto-fit column" already covers individual columns; this API is intended for bulk programmatic use.
- Frontend "Auto-fit all columns" toolbar button (
#autoFitAllBtn) added to the grid status bar.
// Auto-fit every visible, resizable column
document.querySelector('vn-grid').autoFitAllColumns();
// With custom min-width override
document.querySelector('vn-grid').autoFitAllColumns({ minWidth: 80 });
// Session-only — do not persist widths
document.querySelector('vn-grid').autoFitAllColumns({ persist: false });