Skip to content

Build a dynamic column visibility toggle

In this tutorial, you will build a checkbox list outside the grid that shows or hides columns on demand. You will learn how to use updateSettings to update the columns array while preserving each column’s type, renderer, and validator configuration.

TypeScript
/* file: app.component.ts */
import { Component, ViewChild } from '@angular/core';
import { GridSettings, HotTableComponent, HotTableModule } from '@handsontable/angular-wrapper';
type ColumnConfig = {
data: string;
title: string;
type: string;
width: number;
numericFormat?: { pattern: string; culture: string };
dateFormat?: string;
source?: string[];
};
const allColumns: ColumnConfig[] = [
{ data: 'name', title: 'Name', type: 'text', width: 140 },
{ data: 'department', title: 'Department', type: 'text', width: 120 },
{ data: 'role', title: 'Role', type: 'text', width: 150 },
{
data: 'salary',
title: 'Salary',
type: 'numeric',
numericFormat: { pattern: '$0,0', culture: 'en-US' },
width: 110,
},
{ data: 'startDate', title: 'Start Date', type: 'date', dateFormat: 'YYYY-MM-DD', width: 110 },
{ data: 'location', title: 'Location', type: 'text', width: 110 },
{
data: 'status',
title: 'Status',
type: 'dropdown',
source: ['Active', 'On Leave', 'Inactive'],
width: 100,
},
];
const data = [
{ name: 'Alice Johnson', department: 'Engineering', role: 'Senior Engineer', salary: 95000, startDate: '2019-03-12', location: 'New York', status: 'Active' },
{ name: 'Bob Martinez', department: 'Marketing', role: 'Marketing Manager', salary: 78000, startDate: '2020-07-01', location: 'Chicago', status: 'Active' },
{ name: 'Carol Lee', department: 'Engineering', role: 'Tech Lead', salary: 115000, startDate: '2017-11-15', location: 'San Francisco', status: 'Active' },
{ name: 'David Kim', department: 'HR', role: 'HR Specialist', salary: 65000, startDate: '2021-02-28', location: 'Austin', status: 'On Leave' },
{ name: 'Eva Novak', department: 'Finance', role: 'Financial Analyst', salary: 82000, startDate: '2018-09-03', location: 'New York', status: 'Active' },
{ name: 'Frank Chen', department: 'Engineering', role: 'Junior Engineer', salary: 72000, startDate: '2022-05-16', location: 'Seattle', status: 'Active' },
{ name: 'Grace Okafor', department: 'Sales', role: 'Sales Executive', salary: 70000, startDate: '2020-01-20', location: 'Dallas', status: 'Active' },
{ name: 'Henry Walsh', department: 'Finance', role: 'Finance Director', salary: 130000, startDate: '2015-06-10', location: 'Chicago', status: 'Active' },
];
@Component({
standalone: true,
imports: [HotTableModule],
selector: 'example1-column-visibility',
template: `
<div style="margin-bottom: 10px;">
@for (col of allColumns; track col.data; let i = $index) {
<label style="margin-right: 12px; display: inline-flex; align-items: center; gap: 4px;">
<input
type="checkbox"
[checked]="visibleIndices.has(i)"
[disabled]="visibleIndices.size === 1 && visibleIndices.has(i)"
(change)="toggleColumn(i, $event)"
/>
{{ col.title }}
</label>
}
</div>
<hot-table [data]="data" [settings]="gridSettings"></hot-table>
`,
})
export class AppComponent {
@ViewChild(HotTableComponent, { static: false }) readonly hotTable!: HotTableComponent;
readonly data = data;
readonly allColumns = allColumns;
// Track which column indices are currently visible. Start with all visible.
visibleIndices = new Set(allColumns.map((_, i) => i));
gridSettings: GridSettings = {
columns: this.getVisibleColumns(),
colHeaders: this.getVisibleHeaders(),
rowHeaders: true,
height: 'auto',
width: '100%',
autoWrapRow: true,
};
toggleColumn(index: number, event: Event): void {
const checked = (event.target as HTMLInputElement).checked;
if (!checked) {
// Prevent hiding the last visible column.
if (this.visibleIndices.size === 1) {
(event.target as HTMLInputElement).checked = true;
return;
}
this.visibleIndices.delete(index);
} else {
this.visibleIndices.add(index);
}
// Reassign to trigger Angular change detection on the template bindings.
this.visibleIndices = new Set(this.visibleIndices);
this.hotTable?.hotInstance?.updateSettings({
columns: this.getVisibleColumns(),
colHeaders: this.getVisibleHeaders(),
});
}
private getVisibleColumns(): ColumnConfig[] {
return allColumns.filter((_, i) => this.visibleIndices.has(i));
}
private getVisibleHeaders(): string[] {
return allColumns.filter((_, i) => this.visibleIndices.has(i)).map(col => col.title);
}
}
/* end-file */
/* file: app.config.ts */
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { registerAllModules } from 'handsontable/registry';
import { HOT_GLOBAL_CONFIG, HotGlobalConfig, NON_COMMERCIAL_LICENSE } from '@handsontable/angular-wrapper';
registerAllModules();
export const appConfig: ApplicationConfig = {
providers: [
provideZoneChangeDetection({ eventCoalescing: true }),
{
provide: HOT_GLOBAL_CONFIG,
useValue: { license: NON_COMMERCIAL_LICENSE } as HotGlobalConfig,
},
],
};
/* end-file */
HTML
<div><example1-column-visibility></example1-column-visibility></div>

Overview

Difficulty: Beginner Time: ~15 minutes

This recipe shows how to build a checkbox list outside the grid that toggles column visibility. Each checkbox maps to a column. Checking or unchecking it calls hot.updateSettings() with a filtered subset of your full columns config. The column’s type, renderer, and validator are always preserved because the source config is never mutated.

What You’ll Build

  • A <div id="column-toggles"> container above the grid, populated with one labeled checkbox per column
  • An allColumns array that acts as the single source of truth for all column configurations
  • Toggle logic that adds or removes a column index from a visibleIndices Set on each checkbox change
  • A guard that prevents the user from hiding every column (at least one must remain visible)

Before you begin

This recipe uses only the built-in Handsontable API. No extra dependencies are required.

The example uses HR/workforce data with seven columns: Name, Department, Role, Salary, Start Date, Location, and Status. The Salary column uses the numeric type with formatting. The Status column uses the dropdown type. This variety demonstrates that hot.updateSettings() restores each column’s full configuration — not just its header text.

Step 1 — Define the full columns config

const allColumns = [
{ data: 'name', title: 'Name', type: 'text', width: 140 },
{ data: 'department', title: 'Department', type: 'text', width: 120 },
{ data: 'role', title: 'Role', type: 'text', width: 150 },
{
data: 'salary',
title: 'Salary',
type: 'numeric',
numericFormat: { pattern: '$0,0', culture: 'en-US' },
width: 110,
},
{ data: 'startDate', title: 'Start Date', type: 'date', dateFormat: 'YYYY-MM-DD', width: 110 },
{ data: 'location', title: 'Location', type: 'text', width: 110 },
{
data: 'status',
title: 'Status',
type: 'dropdown',
source: ['Active', 'On Leave', 'Inactive'],
width: 100,
},
];

What’s happening: allColumns is declared once and never modified. Every toggle operation reads from it to produce a filtered subset. Keeping this array immutable means you can always reconstruct any combination of visible columns without storing redundant state.

Why not mutate? If you splice or delete entries from allColumns, you lose the config for hidden columns and cannot restore them. An immutable source lets you re-derive the visible set at any time.

Step 2 — Track visible column indices

const visibleIndices = new Set(allColumns.map((_, i) => i));

What’s happening: visibleIndices is a Set of integer indices into allColumns. It starts with every index (all columns visible). A Set is used rather than an array because membership checks (has) and removals (delete) are O(1), and duplicates are automatically prevented.

Deriving the visible subset:

function getVisibleColumns() {
return allColumns.filter((_, i) => visibleIndices.has(i));
}
function getVisibleHeaders() {
return allColumns.filter((_, i) => visibleIndices.has(i)).map(col => col.title);
}

These two helpers produce the arguments that hot.updateSettings() needs on every toggle. They are pure functions with no side effects.

Step 3 — Initialize Handsontable

const hot = new Handsontable(container, {
data,
columns: getVisibleColumns(),
colHeaders: getVisibleHeaders(),
rowHeaders: true,
height: 'auto',
width: '100%',
autoWrapRow: true,
licenseKey: 'non-commercial-and-evaluation',
});

What’s happening: The grid starts with all columns visible, so getVisibleColumns() and getVisibleHeaders() return full arrays at this point. The initial state of the grid and the initial checkbox state both derive from visibleIndices, so they are always in sync.

Step 4 — Generate the checkbox list

const togglesContainer = document.querySelector('#column-toggles');
allColumns.forEach((col, index) => {
const label = document.createElement('label');
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.checked = true;
checkbox.dataset.colIndex = String(index);
label.appendChild(checkbox);
label.appendChild(document.createTextNode(col.title));
togglesContainer.appendChild(label);
});

What’s happening: The checkboxes are generated programmatically from allColumns, so the list never goes out of sync with the columns config. Each checkbox stores its column index in dataset.colIndex. Setting checkbox.checked = true on creation matches the initial state of visibleIndices.

Why generate checkboxes in JS rather than hardcode them in HTML? Generating them from the same allColumns array means a single source of truth. If you add or rename a column config entry, the checkbox list updates automatically.

Step 5 — Implement the toggle handler

checkbox.addEventListener('change', () => {
if (!checkbox.checked) {
if (visibleIndices.size === 1) {
checkbox.checked = true; // revert -- cannot hide last column
return;
}
visibleIndices.delete(index);
} else {
visibleIndices.add(index);
}
hot.updateSettings({
columns: getVisibleColumns(),
colHeaders: getVisibleHeaders(),
});
togglesContainer.querySelectorAll('input[type="checkbox"]').forEach(cb => {
const idx = Number(cb.dataset.colIndex);
cb.disabled = visibleIndices.size === 1 && visibleIndices.has(idx);
});
});

What’s happening, step by step:

  1. When the user unchecks a box, the handler checks if only one column is currently visible. If so, it reverts the checkbox to checked and returns — the grid is not changed.
  2. Otherwise it removes the index from visibleIndices (hide) or adds it (show).
  3. It calls hot.updateSettings() with the new columns and colHeaders arrays. Handsontable re-renders immediately.
  4. It scans all checkboxes and disables the one checkbox whose column is the sole remaining visible column. A disabled checkbox shows the user that this column cannot be hidden right now.

Why hot.updateSettings() instead of DOM manipulation? Handsontable owns the grid’s DOM. Modifying column elements directly would bypass Handsontable’s internal state and cause rendering inconsistencies. updateSettings() is the documented way to change column configuration at runtime. It triggers a full re-render and keeps all internal state consistent.

Why does the column type survive toggling? When you re-show a column, getVisibleColumns() reads the original config object from allColumns. That object still has its type, numericFormat, source, or any other property you set. Nothing was lost because nothing was mutated.

How It Works - Complete Flow

  1. Page load: allColumns is declared. visibleIndices contains all indices. The grid initializes with all columns. Checkboxes are generated with checked = true.
  2. User unchecks “Salary”: The change handler removes index 3 from visibleIndices. hot.updateSettings() is called with a four-column columns array and a four-item colHeaders array. The grid re-renders without the Salary column.
  3. User checks “Salary” again: Index 3 is added back to visibleIndices. hot.updateSettings() restores the full numeric column config — including numericFormat — and the grid re-renders with Salary visible.
  4. User hides columns until only one remains: The handler disables the last remaining checkbox. The user cannot produce an empty grid.

What you learned

  • Declare an immutable allColumns array as the single source of truth for all column configurations.
  • Use a Set of indices to track visibility state and derive the active subset with a filter.
  • Call hot.updateSettings({ columns, colHeaders }) to apply column changes at runtime.
  • Generate checkbox controls programmatically from the same config array to keep the UI in sync.
  • Guard against an empty grid by checking visibleIndices.size before hiding a column.

Next steps

  • Add a Show all / Hide all button that sets visibleIndices to a full or minimal set and calls hot.updateSettings() once.
  • Persist the visible set to localStorage so the user’s column preferences survive page refreshes.
  • Combine this pattern with manualColumnResize or manualColumnMove for a full column-management toolbar.
  • Replace the checkbox list with a drag-and-drop column chooser panel for more advanced UI.