Column Visibility (Hide / Show) Implementation in Vanilla-Grid
This document explains how column visibility is implemented in Vanilla-Grid, including key normalization, hide eligibility rules, refresh behavior, persistence, and interactions with ordering/freeze.
File location (May 2026): the visibility logic lives in
src/vanilla-grid/features/columns-visibility.feature.jsas a prototype extension ofVanillaGridColumnsFeature.
1. Feature Scope
Column visibility supports:
- Hide one column by key
- Show one hidden column by key
- Show all hidden columns
- Query hidden column keys
- Query whether a column is currently hideable
Public API:
hideColumn(columnKey)showColumn(columnKey)showAllColumns()getHiddenColumns()canHideColumn(columnKey)
2. Internal Data Model
Visibility uses two core collections:
_allColumns- Source-of-truth list of all columns configured by host code (including hidden columns)
_hiddenColumnKeys: Set<string>- Normalized keys of currently hidden data columns
this.columns is always the visible projection derived from _allColumns minus _hiddenColumnKeys.
3. Key Normalization and Selection Column Rules
All visibility operations normalize keys through:
_normalizeColumnKey(columnKey) => String key trimmed + lowercased
The selection checkbox column is protected by _isSelectionColumnKey(...) and is never hideable.
Consequences:
- Matching is case-insensitive and whitespace-tolerant.
- Selection column (
__vgSelection__) cannot be hidden, loaded from storage, or persisted as hidden.
4. Initialization and Column-Set Flow
When setColumns(...) runs:
- Incoming columns are stored as
_allColumns. - Declarative hidden defaults are seeded: if localStorage has no persisted hidden-column state for this grid, columns with
hidden: true(orhiddenHTML attribute on<vn-grid-column>) are added to_hiddenColumnKeys. If persisted state exists, declarative defaults are ignored — the user's previous interactions take full priority. _pruneHiddenColumnKeys()removes stale hidden keys not present anymore.- Hidden-key set is persisted.
- Visible projection is computed with
_computeVisibleColumnsFromAll(). - Selection key mapping is updated.
- Saved order is applied.
- Freeze grouping is re-applied.
- Header/body layout is refreshed.
Declarative hidden columns
The hidden attribute on <vn-grid-column> sets the initial visibility state:
<vn-grid-column field="InternalId" header="ID" type="number" hidden="true"></vn-grid-column>
Or as a bare attribute (equivalent to hidden="true"):
<vn-grid-column field="InternalId" header="ID" type="number" hidden></vn-grid-column>
The column starts hidden but the user can reveal it through the context menu "Show all columns" action. Once the user interacts with visibility (any hide/show action), the state is persisted to localStorage, and subsequent page loads use the persisted state instead of the declarative default.
This sequence ensures visibility state remains stable across schema changes.
5. Visibility Projection
_computeVisibleColumnsFromAll() builds this.columns by filtering _allColumns:
- keeps columns with missing/invalid keys (defensive path)
- always keeps selection column
- removes columns whose normalized key exists in
_hiddenColumnKeys - removes columns whose normalized key is in the group-hidden set (see below)
So hidden state is represented as exclusion, not destructive mutation.
5.1 The second exclusion source: group-hidden columns
There are two independent exclusion sources, and they must not be conflated:
| user hidden set | group-hidden set | |
|---|---|---|
| Owner | _hiddenColumnKeys (this feature) |
VanillaGridGroupingFeature |
| Origin | hideColumn() / a column's hidden: true |
derived from the applied group state |
| Persisted | yes | never |
| Public through | getHiddenColumns() |
getGroupState() |
| Cleared by | showAllColumns() / showColumn() |
ungroupColumn() / clearGrouping() only |
An applied group level's column lives in its caption row, not in a column repeating one value on every member row, so it leaves the grid while grouping is applied. That could not have been implemented by writing into the user hidden set, for three independent reasons:
hideColumn()/_canHideColumn()(§ 6) refuse a column that carries an active sort, is frozen, or would leave fewer than two visible data columns — all three would reject group removal for reasons that do not apply to it.- The hidden set is persisted (§ 9) and publicly readable, so group-driven keys written into it would outlive the grouping that caused them — a column would stay hidden after
clearGrouping(), or come back hidden on a later reload from a stale storage key. showAllColumns()clears that set wholesale, which would un-hide a grouped column and leave the two states disagreeing.
The columns feature loads before the grouping feature and holds no reference to it. It reads the group-hidden set through an injected getGroupHiddenColumnKeys closure (columnsConfig in vanilla-grid.js), the same pattern the grouping actions in the header menu already use. A null return means "grouping not loaded, or removing nothing" and leaves this function behaving exactly as it did before grouping existed — which is what keeps a partial bundle without grouping.feature.js working unchanged.
computeVisibleColumnsFromAll({ includeGroupHidden: true }) skips only the second source. It answers "which columns would be visible if nothing were grouped" — the denominator the grouping feature's own visible-column floor counts against — and must never be used to build the live column array.
See Row Grouping Implementation § 18 for the full removal design, including the sort transfer a grouped column's sort undergoes and the group bar that makes removal reversible.
6. Hide Eligibility Rules
_canHideColumn(columnKey) enforces business constraints:
- Key must normalize successfully.
- Selection column cannot be hidden.
- At least one visible data column must remain.
- computed via
_countVisibleDataColumns()
- computed via
- Column must currently exist in
this.columns.
If any condition fails, hide is rejected (false).
7. Hide / Show API Behavior
7.1 hideColumn(columnKey)
Flow:
- Normalize and validate key.
- Ensure
_allColumnsexists (rebuild fromthis.columnsif necessary). - Verify key exists in
_allColumns. - Reject if already hidden or not hideable.
- Add key to
_hiddenColumnKeys. - Remove same key from
_frozenColumnKeys(hiding a frozen column auto-unfreezes it). - Apply refresh pipeline via
_applyColumnVisibilityAndRefresh().
7.2 showColumn(columnKey)
Flow:
- Normalize key.
- Reject if selection column or key not currently hidden.
- Reject if the key is group-hidden — a grouped column is not "hidden", it is "grouped", and showing it is
ungroupColumn()'s job, not this one's. - Delete key from
_hiddenColumnKeys. - Apply refresh pipeline.
7.3 showAllColumns()
Flow:
- If hidden set is empty, return
false. - Clear
_hiddenColumnKeys. - Apply refresh pipeline.
7.4 Query APIs
getHiddenColumns()returnsArray.from(_hiddenColumnKeys.values())— user-hidden only, never a grouped columncanHideColumn(columnKey)delegates to_canHideColumn(columnKey)
8. Refresh Pipeline (_applyColumnVisibilityAndRefresh)
After any hide/show mutation, Vanilla-Grid runs a full visibility-aware refresh:
- Compute
nextColumnsfrom_allColumns. - Compare current and next key arrays; if identical, no-op (
false). - Close open header context menu.
- Replace
this.columns = nextColumns. - Reapply saved column order.
- Reapply freeze grouping.
refreshHeaderLayout().- Persist hidden keys.
- Rebuild/render body:
- if empty, show empty/loading state and update scrollbar
- else preserve
scrollTop, init/reuse virtual pool, re-render rows, update scrollbar
The scroll-preservation step pre-sets spacer heights before render to avoid browser clamping scroll position near zero during pool rebuild.
9. Persistence Model
Control options:
persistence.hiddenColumns.enabled(defaulttrue)persistence.hiddenColumns.storageKey(optional explicit key)
9.1 Storage key
resolveHiddenColumnsStorageKey() default:
vanilla-grid:hidden-columns:{pathname}:{scope}
Where scope is first available of:
gridIdoption (auto-populated from the<vanilla-grid>host elementid)bodyTable.idviewport.idheaderTable.id'default'
9.2 Stored format
JSON array of keys:
["email", "weight"]
9.3 Save path
persistHiddenColumnsToStorage():
- serializes current
_hiddenColumnKeysset values - stores under resolved key
- wrapped in
try/catch
9.4 Load path
loadHiddenColumnsFromStorage():
- resets
_hiddenColumnKeys = new Set() - parses JSON array
- normalizes each key
- skips invalid/selection keys
- inserts into hidden set
If parse fails, it logs warning and resets to empty set.
10. Interaction with Other Features
10.1 Column order
Visibility refresh calls applySavedColumnOrder() after projection. This preserves user order among currently visible columns.
10.2 Frozen columns
- Hiding a frozen column automatically removes it from
_frozenColumnKeys. - Visibility refresh then reapplies freeze grouping for remaining columns.
10.3 Virtualization and scrollbars
Visibility changes can alter column count/width layout. The refresh path reinitializes pool/layout as needed and updates scrollbar thumb state.
10.4 Row grouping
Grouping contributes the second exclusion source described in § 5.1, and reuses _applyColumnVisibilityAndRefresh() (§ 8) verbatim as its re-layout path — it already sequences width save, column set, saved order, freeze reorder, sort remap, header re-layout, and a pool rebuild that preserves scrollTop. Because it is idempotent (a no-op returning false when the resulting key list is unchanged), grouping can call it unconditionally on every group/ungroup and on every suspend/resume transition. That reuse is also what makes the group→ungroup round trip restore the column at its original position, width and freeze state for free: none of that state was ever discarded, only filtered out of a derived array.
10.5 Header context menu
Context menu labels (hideColumn, showAllColumns) are localized via messages.* and menu is closed before applying visibility mutations.
11. Failure Safety
- Methods return
falsefor invalid/no-op operations instead of throwing. - localStorage access is guarded by
windowandwindow.localStoragechecks. - Storage parse/write is wrapped in
try/catchwith warning logs. - Stale keys are pruned on
setColumns(...)to avoid accumulating invalid hidden state.
12. Example Configuration
const grid = new VanillaGrid({
persistence: {
hiddenColumns: { enabled: true, storageKey: 'my-grid:hidden:v1' }
}
});
grid.hideColumn('email');
grid.showColumn('email');
grid.showAllColumns();
For session-only behavior:
persistence: { hiddenColumns: { enabled: false } }