← Back to list

Stop Using Basic Tables — Here’s How Handsontable Turns Your Angular App Into a Spreadsheet…

A complete step-by-step guide with real examples, code snippets, and everything you need to get started today

Santanu Chandra · 2026-05-23 06:24 · 4 claps · 10.8 min read
#angular #handsontable #web-development #javascript #frontend-development
Open on Medium ↗
Wiki topics: 🌐 · Web Development 🐾 · Pets & Animals

Stop Using Basic Tables — Here’s How Handsontable Turns Your Angular App Into a Spreadsheet Powerhouse

A complete step-by-step guide with real examples, code snippets, and everything you need to get started today

Introduction

If you have ever tried to build an editable data grid in Angular, you already know the pain. Native HTML tables are static. Angular Material tables are beautiful but limited when it comes to inline editing, sorting, column resizing, or Excel-like behavior. That is where Handsontable comes in.

Handsontable is a JavaScript/TypeScript data grid component that works like a spreadsheet right inside your web application. It supports cell editing, sorting, filtering, copy-paste, row and column resizing, validation, and dozens of other features you would expect from Microsoft Excel or Google Sheets — but inside Angular.

In this article, you will learn how to install Handsontable in an Angular project, configure it properly, work with real data, and use its most powerful features with step-by-step code examples.

What is Handsontable?

Handsontable is an open-source (with a commercial license for production use) data grid library. It renders as a spreadsheet-style table in the browser and supports:

  • Inline cell editing
  • Row and column drag-to-resize
  • Sorting and filtering
  • Copy, cut, paste (like a real spreadsheet)
  • Cell type customization (text, numeric, date, dropdown, checkbox, etc.)
  • Data validation
  • Undo and redo
  • Custom renderers and editors
  • Large dataset support with virtualization

It has official support for Angular, React, and Vue, and also works with plain JavaScript.

Prerequisites

Before you begin, make sure you have the following installed on your machine:

  • Node.js version 16 or higher
  • Angular CLI version 14 or higher
  • Basic knowledge of Angular components and TypeScript

Step 1 — Create a New Angular Project

Open your terminal and run the following command to create a fresh Angular application:

ng new handsontable-demo

During setup, Angular CLI will ask you a few questions. Choose the following options:

  • Would you like to add Angular routing? → Yes
  • Which stylesheet format would you like to use? → CSS

Once the project is created, navigate into the project folder:

cd handsontable-demo

Step 2 — Install Handsontable and the Angular Wrapper

Handsontable provides an official Angular wrapper package called @handsontable/angular. Install both the core library and the wrapper using npm:

npm install handsontable @handsontable/angular

After installation, verify that both packages appear in your package.json under dependencies:

"dependencies": {
  "@handsontable/angular": "^14.0.0",
  "handsontable": "^14.0.0"
}

The version numbers may differ depending on when you install. Always install the matching version of the core and the wrapper.

Step 3 — Import the Handsontable Module

Open your app.module.ts file and import HotTableModule from @handsontable/angular:

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { HotTableModule } from '@handsontable/angular';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    HotTableModule  // <-- Add this
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

Step 4 — Add Handsontable CSS Styles

Handsontable requires its own stylesheet to render correctly. Open your angular.json file and add the Handsontable CSS file to the styles array:

"styles": [
  "src/styles.css",
  "node_modules/handsontable/dist/handsontable.full.min.css"
]

Without this CSS, your table will appear broken and unstyled.

Step 5 — Register All Cell Types and Plugins

Handsontable version 8 and above requires you to manually register the modules you plan to use. This is important because it reduces bundle size by only including what you need.

Open your main.ts file and register everything at the top:

import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
import Handsontable from 'handsontable/base';
import { registerAllModules } from 'handsontable/registry';
// Register all modules (easiest approach for getting started)
registerAllModules();
platformBrowserDynamic()
  .bootstrapModule(AppModule)
  .catch(err => console.error(err));

If you want to minimize bundle size later, you can import only specific modules like registerCellType or registerPlugin.

Step 6 — Create Your First Basic Table

Now let us build a simple working table. Open app.component.ts and replace its content with the following:

import { Component } from '@angular/core';
import Handsontable from 'handsontable';
@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  // Sample data - array of arrays
  dataset = [
    ['Alice Johnson', 'Engineering', 85000, '2021-03-15', true],
    ['Bob Smith', 'Marketing', 72000, '2020-08-01', false],
    ['Carol White', 'Finance', 95000, '2019-11-20', true],
    ['David Lee', 'Engineering', 88000, '2022-01-10', true],
    ['Eva Brown', 'HR', 65000, '2023-05-05', false],
  ];
  // Column configuration
  columns: Handsontable.ColumnSettings[] = [
    { title: 'Full Name', type: 'text' },
    { title: 'Department', type: 'text' },
    { title: 'Salary', type: 'numeric', numericFormat: { pattern: '$0,0.00' } },
    { title: 'Join Date', type: 'date', dateFormat: 'YYYY-MM-DD' },
    { title: 'Active', type: 'checkbox' },
  ];
  // Table settings
  tableSettings: Handsontable.GridSettings = {
    rowHeaders: true,
    colHeaders: true,
    filters: true,
    dropdownMenu: true,
    columnSorting: true,
    manualColumnResize: true,
    manualRowResize: true,
    contextMenu: true,
    height: 400,
    licenseKey: 'non-commercial-and-evaluation'  // For development/non-commercial use
  };
}

Now open app.component.html and replace its content:

<div style="padding: 20px;">
  <h2>Employee Data Grid</h2>
   <hot-table
    [data]="dataset"
    [columns]="columns"
    [settings]="tableSettings">
  </hot-table>
</div>

Run your application:

ng serve

Open http://localhost:4200 in your browser. You should now see a fully interactive spreadsheet-style table with employee data.

Step 7 — Understanding the hot-table Component

The hot-table selector is the core Angular component provided by @handsontable/angular. It accepts many input properties:

Property Type Description [data] any[] The dataset to display [columns] ColumnSettings[] Column configuration [settings] GridSettings All Handsontable settings [colHeaders] boolean or string[] Enable or set column header labels [rowHeaders] boolean Show row numbers on the left [height] number Table height in pixels [width] number Table width in pixels

You can pass settings either through the [settings] binding or as individual bindings directly on the component. Both approaches work.

Step 8 — Using Object Data Instead of Arrays

The previous example used an array of arrays. In real Angular applications, you will usually work with objects. Here is how to configure Handsontable to use object data:

// In app.component.ts
interface Employee {
  name: string;
  department: string;
  salary: number;
  joinDate: string;
  active: boolean;
}

employees: Employee[] = [
  { name: 'Alice Johnson', department: 'Engineering', salary: 85000, joinDate: '2021-03-15', active: true },
  { name: 'Bob Smith', department: 'Marketing', salary: 72000, joinDate: '2020-08-01', active: false },
  { name: 'Carol White', department: 'Finance', salary: 95000, joinDate: '2019-11-20', active: true },
];
// When using objects, specify the 'data' property in each column
columns: Handsontable.ColumnSettings[] = [
  { data: 'name', title: 'Full Name', type: 'text' },
  { data: 'department', title: 'Department', type: 'dropdown', source: ['Engineering', 'Marketing', 'Finance', 'HR', 'Sales'] },
  { data: 'salary', title: 'Salary', type: 'numeric', numericFormat: { pattern: '$0,0' } },
  { data: 'joinDate', title: 'Join Date', type: 'date', dateFormat: 'YYYY-MM-DD' },
  { data: 'active', title: 'Active', type: 'checkbox' },
];

In the template, replace the data binding:

<hot-table
  [data]="employees"
  [columns]="columns"
  [settings]="tableSettings">
</hot-table>

The data property in each column configuration maps to the key in your object. This is the most common pattern used in real Angular applications.

Step 9 — Listening to Table Events (Hooks)

Handsontable provides hooks (lifecycle callbacks) that let you react to user interactions. The most useful ones for Angular apps are afterChange, afterSelection, and beforeRemoveRow.

Here is how to listen for cell changes:

// In app.component.ts
tableSettings: Handsontable.GridSettings = {
  rowHeaders: true,
  colHeaders: true,
  columnSorting: true,
  licenseKey: 'non-commercial-and-evaluation',
  // Called every time a cell value changes
  afterChange: (changes, source) => {
    if (!changes) return;

    changes.forEach(([row, col, oldValue, newValue]) => {
      console.log(`Row ${row}, Col ${col}: "${oldValue}" changed to "${newValue}"`);
      // Here you could call an API to save the change
      this.saveChange(row, col, newValue);
    });
  },
  // Called when the user selects a range of cells
  afterSelection: (row, col, row2, col2) => {
    console.log(`Selected from (${row},${col}) to (${row2},${col2})`);
  },
  // Called before a row is removed
  beforeRemoveRow: (index, amount) => {
    const confirmed = confirm(`Delete ${amount} row(s)?`);
    return confirmed; // Return false to cancel the deletion
  }
};
saveChange(row: number, col: number, value: any) {
  // Make an HTTP call here to persist the change
  console.log('Saving to server:', { row, col, value });
}

Step 10 — Getting a Reference with HotTableComponent

Sometimes you need to access the Handsontable instance directly — for example, to programmatically load data, get selected cells, or call Handsontable methods.

Use ViewChild to get a reference to the component:

import { Component, ViewChild, OnInit } from '@angular/core';
import { HotTableComponent } from '@handsontable/angular';
@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
  @ViewChild('hotTable') hotTableComponent!: HotTableComponent;
  dataset = [
    ['Alice Johnson', 'Engineering', 85000],
    ['Bob Smith', 'Marketing', 72000],
  ];
  tableSettings = {
    rowHeaders: true,
    colHeaders: ['Name', 'Department', 'Salary'],
    licenseKey: 'non-commercial-and-evaluation'
  };
  addNewRow() {
    const hotInstance = this.hotTableComponent.hotInstance;
    if (hotInstance) {
      hotInstance.alter('insert_row_below');
    }
  }
  getSelectedData() {
    const hotInstance = this.hotTableComponent.hotInstance;
    if (hotInstance) {
      const selected = hotInstance.getSelected();
      console.log('Selected ranges:', selected);
    }
  }
  loadFreshData() {
    const hotInstance = this.hotTableComponent.hotInstance;
    if (hotInstance) {
      const newData = [
        ['New Person', 'Sales', 70000],
        ['Another Person', 'HR', 60000],
      ];
      hotInstance.loadData(newData);
    }
  }
}

In the template, add the template reference variable #hotTable and some action buttons:

<div style="padding: 20px;">
  <h2>Employee Grid</h2>
  <div style="margin-bottom: 10px;">
    <button (click)="addNewRow()">Add Row</button>
    <button (click)="getSelectedData()">Log Selection</button>
    <button (click)="loadFreshData()">Load Fresh Data</button>
  </div>
  <hot-table
    #hotTable
    [data]="dataset"
    [settings]="tableSettings">
  </hot-table>
</div>

Step 11 — Real-World Example: Product Inventory Manager

Let us now build a more complete real-world example — a product inventory manager where users can add, edit, and remove products directly in the grid.

Create a new component:

ng generate component inventory

Open inventory.component.ts:

import { Component, ViewChild } from '@angular/core';
import { HotTableComponent } from '@handsontable/angular';
import Handsontable from 'handsontable';
interface Product {
  id: number;
  name: string;
  category: string;
  price: number;
  quantity: number;
  inStock: boolean;
}
@Component({
  selector: 'app-inventory',
  templateUrl: './inventory.component.html',
  styleUrls: ['./inventory.component.css']
})
export class InventoryComponent {
  @ViewChild('inventoryTable') inventoryTable!: HotTableComponent;
  products: Product[] = [
    { id: 1, name: 'Wireless Headphones', category: 'Electronics', price: 79.99, quantity: 120, inStock: true },
    { id: 2, name: 'Ergonomic Mouse', category: 'Electronics', price: 45.50, quantity: 85, inStock: true },
    { id: 3, name: 'Notebook A5', category: 'Stationery', price: 4.99, quantity: 300, inStock: true },
    { id: 4, name: 'Desk Lamp', category: 'Furniture', price: 34.00, quantity: 0, inStock: false },
    { id: 5, name: 'USB-C Hub', category: 'Electronics', price: 29.99, quantity: 55, inStock: true },
    { id: 6, name: 'Blue Pen Pack', category: 'Stationery', price: 2.50, quantity: 500, inStock: true },
  ];
  columns: Handsontable.ColumnSettings[] = [
    { data: 'id', title: 'ID', type: 'numeric', readOnly: true, width: 50 },
    { data: 'name', title: 'Product Name', type: 'text', width: 200 },
    {
      data: 'category',
      title: 'Category',
      type: 'dropdown',
      source: ['Electronics', 'Stationery', 'Furniture', 'Clothing', 'Food'],
      width: 130
    },
    {
      data: 'price',
      title: 'Price (USD)',
      type: 'numeric',
      numericFormat: { pattern: '$0,0.00', culture: 'en-US' },
      width: 120
    },
    { data: 'quantity', title: 'Qty', type: 'numeric', width: 70 },
    { data: 'inStock', title: 'In Stock', type: 'checkbox', width: 80 },
  ];
  tableSettings: Handsontable.GridSettings = {
    rowHeaders: true,
    colHeaders: true,
    filters: true,
    dropdownMenu: ['filter_by_condition', 'filter_action_bar'],
    columnSorting: true,
    manualColumnResize: true,
    contextMenu: {
      items: {
        row_above: {},
        row_below: {},
        remove_row: {},
        separator: Handsontable.plugins.ContextMenu.SEPARATOR,
        copy: {},
        cut: {}
      }
    },
    height: 450,
    stretchH: 'all',
    licenseKey: 'non-commercial-and-evaluation',
    afterChange: (changes) => {
      if (!changes) return;
      changes.forEach(([row, prop, oldVal, newVal]) => {
        if (oldVal !== newVal) {
          console.log(`Product updated - Row: ${row}, Field: ${prop}, New Value: ${newVal}`);
        }
      });
    },
    cells: (row, col) => {
      const cellProperties: Handsontable.CellMeta = {};
      const product = this.products[row];

      // Highlight out-of-stock rows in light red
      if (product && !product.inStock) {
        cellProperties.className = 'out-of-stock-row';
      }
      return cellProperties;
    }
  };
  addProduct() {
    const newProduct: Product = {
      id: this.products.length + 1,
      name: 'New Product',
      category: 'Electronics',
      price: 0,
      quantity: 0,
      inStock: true
    };
    this.products = [...this.products, newProduct];
  }
  exportToCSV() {
    const hot = this.inventoryTable.hotInstance;
    if (hot) {
      const exportPlugin = hot.getPlugin('exportFile');
      exportPlugin.downloadFile('csv', {
        bom: false,
        columnDelimiter: ',',
        columnHeaders: true,
        exportHiddenColumns: false,
        exportHiddenRows: false,
        fileExtension: 'csv',
        filename: 'inventory-[YYYY]-[MM]-[DD]',
        mimeType: 'text/csv',
        rowDelimiter: '\r\n',
        rowHeaders: false
      });
    }
  }
}

Open inventory.component.html:

<div class="inventory-wrapper">
  <div class="toolbar">
    <h2>Product Inventory</h2>
    <div class="actions">
      <button class="btn-primary" (click)="addProduct()">+ Add Product</button>
      <button class="btn-secondary" (click)="exportToCSV()">Export CSV</button>
    </div>
  </div>
 <hot-table
    #inventoryTable
    [data]="products"
    [columns]="columns"
    [settings]="tableSettings">
  </hot-table>
</div>

Open inventory.component.css:

.inventory-wrapper {
  padding: 24px;
  font-family: Arial, sans-serif;
}
.toolbar {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 16px;
}
.actions {
  display: flex;
  gap: 8px;
}
.btn-primary {
  background-color: #3b82f6;
  color: white;
  border: none;
  padding: 8px 16px;
  border-radius: 6px;
  cursor: pointer;
  font-size: 14px;
}
.btn-secondary {
  background-color: #e5e7eb;
  color: #374151;
  border: none;
  padding: 8px 16px;
  border-radius: 6px;
  cursor: pointer;
  font-size: 14px;
}
/* Custom style for out-of-stock rows */
:host ::ng-deep .out-of-stock-row {
  background-color: #fee2e2 !important;
}

Key Handsontable Cell Types

Handsontable supports many built-in cell types that control how a cell is rendered and edited:

text — Standard editable text cell. This is the default.

numeric — Numbers with optional formatting patterns like currency or decimal places.

date — Date picker integrated into the cell editor.

time — Time picker for time values.

dropdown — A select/dropdown editor populated from a source array.

checkbox — Boolean true/false rendered as a checkbox.

autocomplete — Similar to dropdown but allows typing to filter options.

password — Masks the input value like a password field.

handsontable — Embeds a nested Handsontable grid inside a cell editor (advanced use case).

Common Configuration Options

Here are the most commonly used settings in Handsontable, explained in plain language:

Here are the most commonly used settings in Handsontable, explained in plain language:

rowHeaders: true — Shows row numbers on the left side like a spreadsheet.

colHeaders: true — Shows column header labels at the top.

filters: true — Enables a small filter button in each column header for filtering data.

dropdownMenu: true — Adds a dropdown on each column header for quick actions.

columnSorting: true — Enables click-to-sort on column headers.

manualColumnResize: true — Lets users drag to resize columns.

manualRowResize: true — Lets users drag to resize rows.

contextMenu: true — Enables right-click context menu with options like insert row, delete row, copy, and cut.

fixedRowsTop: 2 — Freezes the top two rows during horizontal scroll.

fixedColumnsLeft: 1 — Freezes the leftmost column during horizontal scroll.

readOnly: true — Makes the entire table non-editable.

undo: true — Enables Ctrl+Z undo support.

stretchH: ‘all’ — Stretches columns to fill the full table width.

Handling the License Key

Handsontable uses a license-based model. During development, testing, and non-commercial projects, you can use:

licenseKey: 'non-commercial-and-evaluation'

For production applications or commercial projects, you need to purchase a license from the Handsontable website at handsontable.com/pricing and replace the key with your purchased license key. Failing to do so will show a warning banner above the table in production.

Performance Tips for Large Datasets

If your table needs to display thousands of rows, keep these tips in mind:

Use virtualization — Handsontable automatically virtualizes rows and columns, rendering only what is visible in the viewport. This is enabled by default.

Avoid triggering Angular change detection too often — use ChangeDetectionStrategy.OnPush on the component that hosts the table.

Limit re-renders — do not replace the entire dataset array unnecessarily. Instead, use hotInstance.setDataAtRowProp() to update individual cells.

Disable unnecessary plugins — if you do not need filters or sorting, leave them out of the settings to reduce overhead.

Use loadData() carefully — calling hotInstance.loadData() replaces all data and triggers a full re-render. Use it only when you need to replace the entire dataset.

Comparison: Handsontable vs Angular Material Table

Conclusion

Handsontable is one of the most complete data grid solutions available for Angular applications. When your users need to edit data in place, sort and filter large datasets, or work with spreadsheet-like interfaces, Handsontable delivers all of that with minimal configuration.

In this article, you walked through the complete setup process from installing the package to building a real-world product inventory manager. You learned how to use object data, listen to cell changes, access the Handsontable instance with ViewChild, and customize cell types and styling.

The official documentation at handsontable.com/docs is comprehensive and includes examples for every feature. If you are building a data-intensive Angular application, Handsontable is absolutely worth adding to your toolkit.

Follow for more Angular tips, deep-dives, and real-world component guides.


메타데이터
post_id
d35b620ceb4a
slug
stop-using-basic-tables-heres-how-handsontable-turns-your-angular-app-into-a-spreadsheet-d35b620ceb4a
url
https://medium.com/@Rajdip27/stop-using-basic-tables-heres-how-handsontable-turns-your-angular-app-into-a-spreadsheet-d35b620ceb4a
canonical_url
https://medium.com/@Rajdip27/stop-using-basic-tables-heres-how-handsontable-turns-your-angular-app-into-a-spreadsheet-d35b620ceb4a
author_url
https://medium.com/@Rajdip27
status
ok
fetched_at
2026-06-26 03:39:16