Handsontable : The Best JavaScript Spreadsheet for Web Apps

Spreadsheets are an important and powerful business tool, which are used to store valuable information.

For example,

  • In the financial sector, they are used for managing account information, budgets, transactions, billing, invoices, and receipts.
  • In schools, teachers can use spreadsheets to track students progress, calculate grades, and identify relevant data, such as high and low scores, missing tests, and students who are struggling.
  • Managing lists like telephone directories, to-do lists, grocery lists, etc.

Spreadsheets help us to manage a large amount of data in a grid type structure, with lots of other features like conditional formatting, comparison, filtering and so on.

Nowadays, the scope of spreadsheets includes web applications as well. We already have Online Spreadsheet apps like ZOHO Sheet, Google Sheets, Microsoft Office 365, Quip, Smartsheet, Airtable, etc. These applications are only used for managing spreadsheets.

But what if we want to add spreadsheet features to our web applications, for example, to display employees in an employee management system, a grade list in an online examination system or inventories in an inventory management system.

We can do a lot more by providing a spreadsheet experience in our web applications. For this purpose, we can use grids or data-tables, but these tools do not provide the same spreadsheet experience in a web application.

The best tool to provide the same spreadsheet experience in a web application is Handsontable.

Handsontable the bes

Handsontable is a JavaScript Spreadsheet component for Web applications. It provides a familiar spreadsheet experience in web apps.

It is easy to use, and as customizable as possible. It can handle a very large volume of data without any performance issue.

In this article, we will take a look at the topics below:

It comes with all basic excel features

Some of the most popular features include: 

handsontable community edition
handsontable pro

Handsontable For Popular Frameworks

Handsontable uses wrappers and directives to support the most popular front-end frameworks

Handsontable for Angular is the official Angular wrapper for Handsontable.

We will look at a step by step usage of Handsontable in Angular 2+.

We will create a separate Angular 6 project named handsontable-demo.

Note: For demo purposes, I have created a separate project. You can directly
include it in your existing application

Create a new project using

ng new handsontable-demo

Once the Angular project is created, install Handsontable.

Install Handsontable

Use the npm command below to add Handsontable to the Angular application.

npm install handsontable @handsontable/angular

Include CSS

Once the package is installed, add the CSS import statement below to styles.css

@import '~handsontable/dist/handsontable.full.css';

Import HotTableModule

Import HotTableModule from @handsontable/angular into AppModule, and add it to the @NgModule.imports array as shown below:

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';
import { HotTableModule } from '@handsontable/angular';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    HotTableModule.forRoot()
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

Now we are ready to use Handsontable in our application, but before that let’s setup our dataset.

Setup Dataset

Currently, we are not getting data from the server. So here we will create a mock data file and get data from that file.

Here I have created a separate cardata.ts file which contains 2.5K + car sales records in JSON format as shown below:

export const CARS = [
  {
    "Year": 2007,
    "Month": 1,
    "Make": "Volkswagen",
    "Model": "Volkswagen Passat",
    "Quantity": 1267,
    "Pct": 10
  },
  {
    "Year": 2007,
    "Month": 1,
    "Make": "Toyota",
    "Model": "Toyota Rav4",
    "Quantity": 819,
    "Pct": 6.5
  },
  ...
  ...
];

Note : This is just a sample snippet. In cardata.ts, the CARS array contains 2.5K+ records. For the original file refer to source code at

https://github.com/ngdevelop-tech/angular-handsontable/…/cardata.ts

Display Handsontable in the template

We will get data from cardata.ts and save it in the dataset variable of app.component.ts.

Now to display this dataset as a Handsontable spreadsheet, you just need to add a <hot-table> tag to the template as shown below:

<hot-table [colHeaders]="true" [rowHeaders]="true" [data]="dataset"></hot-table>

Here,

The [colHeaders] and [rowHeaders] properties are used to show column headers and row headers, respectively.

The [data] property is bound with the dataset property.

Our final app.component.html will look like this:  

<div style="text-align:center">
<h1>
    Handsontable Demo
</h1>
</div>
<div>
   <hot-table [colHeaders]="true" [rowHeaders]="true" [data]="dataset"
              [height]="600" [width]="600" >
   </hot-table>
</div>

Now run the application using ng serve.

You will get a spreadsheet on the screen as shown below, with default features like Column selection, Row selection, Editing, Copy/Paste, and Drag.

Now let’s customize this spreadsheet.

Scrolling : To make the grid scrollable, set constant width and height using the [width] and [height] properties of <hot-table> as shown below :

<hot-table [colHeaders]="true" [rowHeaders]="true" [data]="dataset" [height]="600" [width]="600" ></hot-table>

Then, if the table contains enough rows or columns, you can scroll through it.

Fixing : You can fix rows on the top or columns on the left by using the [fixedRowsTop] and [fixedColumnsLeft] properties. For example, to fix the top two columns we need to set [fixedRowsTop]="2"

Context Menu: You can control the visibility of the context menu through the [contextMenu] Boolean value. By default, it is false. If you enable it, you get the following list of options:

  • Insert row above/below
  • Insert column left/right
  • Remove row/column
  • Read only
  • Alignment
  • Cut
  • Copy
  • Freeze / Unfreeze column

Manual Freezing : Manual Freezing allows us to freeze a column using the context menu option. You can enable it by setting [manualColumnFreeze] as true.

Sorting : You can enable sorting at the table level by setting the [columnSorting] property as true or passing an object containing a column index and the sortOrder property.

We can show the sorting indicator by enabling the [sortIndicator] property.

Refer the https://docs.handsontable.com/4.0.0/demo-sorting.html for more information.

Searching : We can enable Search Plugin in Handsontable by enabling the [search] property. When enabled, Search Plugin exposes a new method query(queryStr), where queryStr is a string to be found within the table. By default, the search is case-insensitive.

We need to handle this method inside the component. To get a hot-table instance inside the component, we need to inject the HotTableRegisterer dependency into the constructor as shown in the second window below. We will get this hot-table using the getInstance(‘hot-Id’) method of HotTableRegisterer.

hot-Id is the property provided in <hot-table> as shown below:

<hot-table hotId="car_table" [colHeaders]="options.columnHeaders"
           [rowHeaders]="options.rowHeaders" [data]="dataset" [search]="true">
</hot-table>

We will get this instance in the searchValue method of the component as shown below:

import {CARS } from './cardata';
import { HotTableRegisterer } from '@handsontable/angular';
…
export class AppComponent {
  dataset = CARS;
  …
  constructor(private _hotRegisterer: HotTableRegisterer) { }
  
  searchValue($event){
    const instance = this._hotRegisterer.getInstance('car_table');
    instance.getPlugin('Search').query($event.target.value);
    instance.render();
  }
}
In the template we will call this method in the search string input.
<input class="form-control" name="searchString" (input)="searchValue($event)" >

Same way, we can implement other features of Handsontable in Angular. Refer the Handsontable Docs for more information.

Final Code Review

Note : I have used Bootstrap for the user interface. Refer the Bootstrap installation in Angular.

import { Component } from '@angular/core';
import {CARS } from './cardata';
import { HotTableRegisterer } from '@handsontable/angular';
@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  title = 'app';
  dataset = CARS;

  options = {
    rowHeaders : true,
    columnHeaders : true,
    fixedRowsTop : 0,
    fixedColumnsLeft : 0,
    contextMenu : false,
    manualColumnFreeze : false,
    columnSorting : false
  }

  constructor(private _hotRegisterer: HotTableRegisterer) { }
  
  searchValue($event){
    const instance = this._hotRegisterer.getInstance('car_table');
    instance.getPlugin('Search').query($event.target.value);
    instance.render();
  }
  
}
<nav class="navbar navbar-expand-md navbar-light bg-primary">
  <a class="navbar-brand">Handsontable Demo</a>
</nav>

<div class="container-fluid">


  <div class="row">
    <div class="col-md-6">
      <div class="card border-dark">
        <div class="card-body">
          <h4 class="card-title">Headers</h4>
          <button type="button" class="btn btn-info" (click)="options.columnHeaders = !options.columnHeaders">Enable / Disable Column Headers</button>
          <button type="button" class="btn btn-info" (click)="options.rowHeaders = !options.rowHeaders">Enable / Disable Row Headers</button>
        </div>
      </div>
      <div class="card border-dark">
        <div class="card-body">
          <h4 class="card-title">Fixing</h4>
          <form class=" form-inline">
            <div class="form-group">
              <label for="">Row </label>
              <input class="form-control" name="fixedRows" [(ngModel)]="options.fixedRowsTop" type="number">
            </div>
            <div class="form-group">
              <label for="">Column Fixing</label>
              <input type="number" class="form-control" name="fixedColumns" [(ngModel)]="options.fixedColumnsLeft">
            </div>
          </form>
          
        </div>
      </div>
      <div class="card border-dark">
        <div class="card-body">
          <h4 class="card-title">Context Menu is  <span *ngIf="!options.contextMenu">Disabled</span> <span *ngIf="options.contextMenu">Enabled</span>  </h4>
          <button type="button" class="btn btn-info" (click)="options.contextMenu = !options.contextMenu">Enable / Disable Context Menu</button>
          
          <div *ngIf="options.contextMenu">
              <hr>
          Manual Freezing is  <span *ngIf="!options.manualColumnFreeze">Disabled</span> <span *ngIf="options.manualColumnFreeze">Enabled</span>  
          <br>
          <button type="button" class="btn btn-info" (click)="options.manualColumnFreeze = !options.manualColumnFreeze">Enable / Disable </button>
          </div>
        </div>
      </div>
      <div class="card border-dark">
        <div class="card-body">
          <h4 class="card-title">Column Sorting is <span *ngIf="!options.columnSorting">Disabled</span> <span *ngIf="options.columnSorting">Enabled</span>  </h4>
          <button type="button" class="btn btn-info" (click)="options.columnSorting = !options.columnSorting">Enable / Disable</button>
        </div>
      </div>
      <div class="card border-dark">
          <div class="card-body">
            <h4 class="card-title">Searching</h4>
            <form class=" form-inline">
              <div class="form-group">
                <label for="">Enter Search String </label>
                <input class="form-control" name="searchString" (input)="searchValue($event)" >
              </div>
            </form>
            
          </div>
        </div>
  

    </div>



    <div class="col-md-6">
      <hot-table hotId="car_table" [colHeaders]="options.columnHeaders" 
                  [fixedRowsTop]="options.fixedRowsTop"
                  [fixedColumnsLeft]="options.fixedColumnsLeft"
                  [rowHeaders]="options.rowHeaders"
                  [data]="dataset" 
                  [height]="700" [width]="650"
                  [contextMenu]="options.contextMenu"
                  [manualColumnFreeze]="options.manualColumnFreeze" 
                  [columnSorting]="options.columnSorting"
                  [sortIndicator]="true"
                  [search]="true"> </hot-table>
      

    </div>

  </div>

</div>
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppComponent } from './app.component';
import { HotTableModule } from '@handsontable/angular';
import { FormsModule } from '@angular/forms';
@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    FormsModule,
    HotTableModule.forRoot()
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

Final Output