JavaScript Data GridMigrate from 7.4 to 8.0

Migrate from Handsontable 7.4 to Handsontable 8.0, released on August 5, 2020.

Overview

This guide helps you upgrade your Handsontable version to 8.0.0. We strongly recommend the upgrade, as it lets you benefit from a completely new architecture of row and column management.

Context

In previous versions of Handsontable, the calculation between physical and visual indexes was based on callbacks between Handsontable's hooks. As Handsontable was getting more features, in some cases, its growing complexity led to calculation inconsistencies. To fix that, we made a major change to how indexes are mapped.

Handsontable 8.0.0 introduces IndexMapper, a new API that stores, manages, and registers indexes globally. Under the hood, it is indirectly responsible for managing both rows and columns, as a single source of truth to refer to. This modification is a major rewrite of a core feature and may result in breaking changes in your application.

General guidelines

  • Check if you use any of the features listed in the Keywords section. You need to address them first after installing Handsontable 8.0.0.
  • To keep backward compatibility, test solutions proposed in this guide. If you experience any problems, contact our Technical Support Team (opens new window).
  • You can also check the changelog for a complete list of all changes.

Keywords (alphabetically)

Installation

To update Handsontable to version 8, run:

npm install handsontable@8

Using with wrappers

When you use the wrapper you need to update it as well. Run the following command, respectively:

If you use Handsontable with React:

npm install handsontable@8 @handsontable/react@4

If you use Handsontable with Vue:

npm install handsontable @handsontable/vue@5

If you use Handsontable with Angular:

npm install handsontable@8 @handsontable/angular@6

Breaking changes of Handsontable 8.0.0

As we introduce a new architecture for index management (IndexMapper), we remove Handsontable hooks related to indexes.

To keep your app's behavior unchanged, recreate affected functionalities, using the IndexMapper's API methods.

The following examples show how to preserve previous functionality.

modifyRow, unmodifyRow, modifyCol, unmodifyCol

Handsontable's modify* and unmodify* hooks for rows and columns were removed. The actions to be taken are similar for both rows and columns.

For example, we will cover Handsontable's modifyRow hook. Prior 8.0.0 to move a row you had to use it

modifyRow(row) {
  if (row === 0) {
    return 1;
  }

  if (row === 1) {
    return 0;
  }
}

In 8.0.0 it is no longer the case. To achieve the same functionality you need to use rowIndexMapper():

hotInstance.rowIndexMapper.moveIndexes([1, 0], 0);
hotInstance.render();

Take a look on the trimming example, too. It used to work like this:

data: [
  ['A1', 'B1', 'C1'],
  ['A2', 'B2', 'C2'],
  ['A3', 'B3', 'C3'],
  ['A4', 'B4', 'C4'],
  ['A5', 'B5', 'C5'],
  ['A6', 'B6', 'C6'],
  ['A7', 'B7', 'C7'],
  ['A8', 'B8', 'C8'],
  ['A9', 'B9', 'C9'],
  ['A10', 'B10', 'C10'],
],
modifyRow(row) {
  // trimming the first row
  if (row < 9) {
    return row + 1;
  }

  return null;
}

Now, if you want to get the same results you need to use the TrimmingMap:

import { TrimmingMap } from "handsontable/es/translations";
...

const customTrimmingMap = new TrimmingMap();

hotInstance.rowIndexMapper.registerMap('customTrimmingMap', customTrimmingMap);
customTrimmingMap.setValueAtIndex(0, true); // trimming index 0
hotInstance.render();

Handsontable's hiddenRow and hiddenColumn hooks

If you use HiddenRows or HiddenColumns in your application, now you need to use corresponding IndexMapper methods:

Prior 8.0.0:

hot.hasHook('hiddenColumn') && hot.runHooks('hiddenColumn', visualColumn);

Now:

hot.columnIndexMapper.isHidden(hot.toPhysicalColumn(visualColumn));

An example for rows:

Prior 8.0.0:

hot.hasHook('hiddenRow') && hot.runHooks('hiddenRow', visualRow);

Now

hot.rowIndexMapper.isHidden(hot.toPhysicalRow(visualRow));

Removing redundant render() from Handsontable's after* hooks

The sequence of Handsontable's after... hooks changed, for example: afterLoadData, afterFilter, afterUnmergeCells are now called before the render. In the previous versions, you had to call render() to apply changes made with Handsontable's after... hooks. In some cases, depending on the number of Handsontable hooks registered, it led to rendering all the cells multiple times. Many of these operations were redundant and unnecessary, resulting only in a performance bottleneck.

From now on, you can alter the Handsontable instance in each Handsontable hook and it will re-render only once. To benefit from this change you have to review all your Handsontable hooks and remove unnecessary render calls. Example:

instance.addHook('afterFilter', function () {
  // ... your operations
  instance.render(); // <= remove this line!
});

Plugins no longer enable other plugins

From version 8.0.0 you can set plugins separately. They no longer rely on each other tightly in terms of functionality, so it is up to the developer to use them simultaneously when needed. You can avoid unwanted "extra" functionality switched on by a supporting plugin.

NestedRows and Filters no longer depend on nor enable TrimRows plugin. To keep using the TrimRows functionality, mostly use API or if your custom plugin is based on it, you need to enable the plugin explicitly:

Before 8.0.0

nestedRows: true

filters: true

After:

nestedRows: true,
trimRows: true

filters: true,
trimRows: true

ManualColumnFreeze does not rely on the ManualColumnMove plugin. To preserve the same functionality as before you need to set it explicitly:

Before 8.0.0

manualColumnFreeze: true

After:

manualColumnFreeze: true,
manualColumnMove: true

ColumnSorting will not enable ObserveChanges. To preserve the same functionality as before you need to set it explicitly:

Before 8.0.0

columnSorting: true

After:

columnSorting: true,
observeChanges: true

CollapsibleColumns plugin no longer uses HiddenColumns plugin, hence it will not be enabled. What's more, it won't enforce the inclusion of the second plugin anymore. To preserve the functionality of hiding columns separately (out of collapsing) as before you need to set it explicitly:

Before 8.0.0

collapsibleColumns: true

After:

collapsibleColumns: true,
hiddenColumns: true

Data reference and the ObserveChanges plugin

Modifying your table’s data by reference and calling render() is no longer possible. Now, all data-related operations need to be performed using the API methods such as populateFromArray() or setDataAtCell().

Also, Handsontable no longer returns a reference to the source data object. Instead, Handsontable returns a copy of the data, possibly already modified by Handsontable's modifySourceData and modifyRowData hooks. The change applies to all "getter" source methods - getSourceData(), getSourceDataAtCell(), getSourceDataAtRow(), getSourceDataAtCol() and getSourceDataArray().

Since it breaks the link to original data source reference, we introduced a new method, setSourceDataAtCell(), and a new Handsontable hook, afterSetSourceDataAtCell, to maintain similar functionality.

Before, it was possible to set source data by reference to the data variable:

data[0][0] = 'A1';

or by getting the reference to the underlying source data:

hotInstance.getSourceData()[0][0] = 'A1';

Now, you should use the API instead:

hotInstance.setSourceDataAtCell(0, 0, 'A1');

It is also worth mentioning that getSourceData() returns a clone of the entire dataset when run without arguments. However, if a row/column range is provided (getSourceData(0, 0, 10, 10)) the method filters the dataset with the columns and/or dataSchema options, and returns only the columns that are configured to be visible.

Before, it was also possible to extend the row-object by setDataAtRowProp() when your source data was an array of objects. Since 8.0.0, the only way to do that is to use setSourceDataAtCell():

const hot = new Handsontable(container, {
  data: [
    { model: 'Roadster', company: 'Tesla' },
    { model: 'i3', company: 'BMW' },
  ],
});

hot.setDataAtRowProp(0, 'available', true) // in 8.0.0, this throws an error
hot.setSourceDataAtCell(0, 'available', true) // in 8.0.0, this sets a new property

ManualRowMove and ManualColumnMove changes

We modified both the ManualRowMove plugin and the ManualColumnMove plugin, to make them work with the IndexMapper.

The following Handsontable hooks have different sets of parameters now:

Methods moveColumn(), moveColumns(), moveRow(), and moveRows() have different parameters now. Starting with Handsontable 8.0.0, the target parameter was changed to finalIndex. They work differently now and a new dragRow(), dragRows(), dragColumn and dragColumns() methods took over the old methods' functionality.

To preserve the previous functionality of moveRows() rename it to dragRows().

TIP

If the NestedRows plugin is enabled, moving rows is possible only using the UI, or using the dragRow()/dragRows() methods of the ManualRowMove plugin.

The drag* methods come with a parameter called dropIndex. It directs where to place the dragged elements. The place you intend to drag the element is managed by drop indexes. You can imagine some sort of a drop zone between actual indexes of elements:

drag_action

The move* methods come with a parameter called finalIndex. It tells where to overlap the first element from the moved ones. The place you intend to move the element is managed by visual indexes.

move_action

Please note that in case of move* methods some move actions are limited. For example, if you initiate a move of more than one element to the last position (visual index = the number of items - 1) the operation will be canceled. The first element in the collection you would like to move will try to reach the last position (finalIndex) which is feasible. However, the next ones will attempt to reach the position exceeding the number of all items.

You can find the plugin's isMovePossible() API method useful when you want to determine if the move action is possible. The movePossible parameter of Handsontable's beforeRowMove, afterRowMove, beforeColumnMove, and afterColumnMove hooks may be helpful as well.

Changes to the ManualColumnFreeze plugin

The ManualColumnFreeze plugin also works differently. Before Handsontable 8.0.0, frozen columns attempted to go back to original positions when unfreezed. Currently, the original position is not calculated. It unfreezes the column just after the "line of freeze". The functionality changed because after several actions like moving the former position was rather estimated than determined.

Using the minSpareRows option with the TrimRows plugin

Another breaking change is related to minSpareRows and minRows. The difference is visible when the data is being trimmed (i.e. by the TrimRows plugin) and the options are set. In previous versions the data which was supposed to be trimmed included minSpareRows and minRows which resulted in trimming them along with other rows. Starting with Handsontable 8.0.0, spare rows are always present and cannot be removed by trimming.

Check the following code example:

const hotInstance = new Handsontable(container, {
  data: [
    ['A1', 'B1', 'C1', 'D1', 'E1'],
    ['A2', 'B2', 'C2', 'D2', 'E2'],
    ['A3', 'B3', 'C3', 'D3', 'E3'],
    ['A4', 'B4', 'C4', 'D4', 'E4'],
    ['A5', 'B5', 'C5', 'D5', 'E5'],
  ],
  minSpareRows: 2,
  trimRows: [1, 2, 3, 4]
});

The results before:

before_8

The results after:

after_8

To ensure your application works as expected you should review it and search the use cases of minSpareRows or minRows. If your application relies on this mechanism, you may need to adapt your application's code. For example, in prior versions the following code:

const hotInstance = new Handsontable(container, {
  data: [
    ['A1', 'B1', 'C1', 'D1', 'E1'],
    ['A2', 'B2', 'C2', 'D2', 'E2'],
    ['A3', 'B3', 'C3', 'D3', 'E3'],
    ['A4', 'B4', 'C4', 'D4', 'E4'],
    ['A5', 'B5', 'C5', 'D5', 'E5'],
  ],
  trimRows: [0],
  minSpareRows: 2
}

rendered 0 spare rows. If you want to keep it that way you may need a workaround, for example:

const hotInstance = new Handsontable(container, {
  data: [
    ['A1', 'B1', 'C1', 'D1', 'E1'],
    ['A2', 'B2', 'C2', 'D2', 'E2'],
    ['A3', 'B3', 'C3', 'D3', 'E3'],
    ['A4', 'B4', 'C4', 'D4', 'E4'],
    ['A5', 'B5', 'C5', 'D5', 'E5'],
  ],
  trimRows: [0],
  beforeCreateRow(index, amount, source) {
    const rowIndexMapper = this.rowIndexMapper;
    // if any row was skipped then block a creation of row execution.
    if (source === 'auto' && rowIndexMapper.getNotSkippedIndexesLength() < rowIndexMapper.getNumberOfIndexes()) {
      return false;
    }
  },
  minSpareRows: 2
});

Custom editors

Custom editors will now use data attribute to recognize the input as an editor. Before this change, Handsontable depended on the CSS className. And it had to be handsontableInput. Now it is an attribute - data-hot-input. Focusable and editable elements must have this attribute set up to work properly. ClassNames were freed from restrictive names.

An example with data-hot-input in Custom editor, to make it work properly on a focusable element:

createElements() {
  ...
  this.TEXTAREA.className = 'handsontableInput';
  ...
}

After the changes:

createElements() {
  ...
  this.TEXTAREA.className = 'anythingYouWant';
  this.TEXTAREA.setAttribute('data-hot-input', true);
  ...
}

Indexes that exceed the data length

Also, the methods toVisualRow(), toVisualColumn() and toPhysicalRow(), toPhysicalColumn() used to return index numbers that exceeded the overall length. For example:

// Data set with just 10 rows.
const physicalRow = hotInstance.toPhysicalRow(20);
// physicalRow === 20

From now on, if you want to refer to them you will receive null:

// Data set with just 10 rows.
const physicalRow = hotInstance.toPhysicalRow(20);
// physicalRow === null

It can be a breaking change for your project if any parts of it expected the output to be a number, now it will be null. In case you still want to use it as before you need to, for example, check for null and fallback to visual row index:

const visualRow = 20;
const physicalRow = hotInstance.toPhysicalRow(visualRow) ?? visualRow;
// physicalRow === 20

RecordTranslator in plugins

The RecordTranslator object was removed, as a consequence, t property is no longer available in the plugins. This alias could be used to translate between visual and physical indexes with four methods: t.toVisualRow, t.toPhysicalRow, t.toVisualColumn, t.toPhysicalColumn. It is advised to call the following methods directly on the instance: hotInstance.toVisualRow, hotInstance.toPhysicalRow, hotInstance.toVisualColumn, hotInstance.toPhysicalColumn. The mappers can be accessed using hotInstance.rowIndexMapper and hotInstance.columnIndexMapper properties.

This example shows how to migrate plugins from using t property to calling the method directly on the instance:

Before:

const physicalColumn = this.t.toPhysicalColumn(column);

After the change:

const physicalColumn = this.hot.toPhysicalColumn(column);

Cell selection

Left mouse-button click on the corner will select all cells with headers in 8.0.0.

It used to select just one cell:

LMB_was

Now the expected behavior is to select all cells:

LMB_is

To keep the previous behavior you need to use the following workaround:

// manipulate the event that happens before the click on cells
beforeOnCellMouseDown(event, coords) {
  // apply only for coordinates that are top left corner outside the grid
  if (coords.col === -1 && coords.row === -1) {
    // stop other event listeners of the same event from being called
    event.stopImmediatePropagation();
    // use the index mapper method - getVisualFromRenderableIndex on both row and column to choose visual indexes
    // this will result in selecting the first cell in the corner
    const visualRow = this.rowIndexMapper.getVisualFromRenderableIndex(0);
    const visualColumn = this.columnIndexMapper.getVisualFromRenderableIndex(0);

    this.selectCell(visualRow, visualColumn);
  }
}

Different arguments order in Handsontable's *loadData hooks

We changed Handsontable's afterLoadData hook. Its first argument will present a data source that was set during the load data action. Now, a flag informing whether a load of data was done during initialization is the second argument. Also, we introduced a new corresponding Handsontable hook, beforeLoadData, which gets called before loading data.

Before:

afterLoadData?: (initialLoad: boolean) => void;

Now:

afterLoadData?: (sourceData: object | void, initialLoad: boolean) => void;

beforeLoadData has the same order of arguments:

beforeLoadData?: (sourceData: object | void, initialLoad: boolean) => void;

Different arguments order in Handsontable's column-resizing and row-resizing hooks

Handsontable's after* and before* hooks that are related to column/row resizing changed in terms of the order of arguments. Now the first argument is a newSize of a row or column and the second argument is column or row. The current* prefix was dropped because this Handsontable hook might not always be called on selection.

Here is the comparison:

Prior 8.0.0:

afterColumnResize?: (currentColumn: number, newSize: number, isDoubleClick: boolean) => void;
afterRowResize?: (currentRow: number, newSize: number, isDoubleClick: boolean) => void;
beforeColumnResize?: (currentColumn: number, newSize: number, isDoubleClick: boolean) => void | number;
beforeRowResize?: (currentRow: number, newSize: number, isDoubleClick: boolean) => number | void;

Now:

afterColumnResize?: (newSize: number, column: number, isDoubleClick: boolean) => void;
afterRowResize?: (newSize: number, row: number, isDoubleClick: boolean) => void;
beforeColumnResize?: (newSize: number, column: number, isDoubleClick: boolean) => void | number;
beforeRowResize?: (newSize: number, row: number, isDoubleClick: boolean) => number | void;

Selection range can have negative indexes

From v8.0.0 selecting columns or rows with headers will include headers as a part of the selection range. We see headers as positioned relatively to the dataset. If the beginning of the dataset is at position (0, 0) then headers will always have negative indexes. That makes them distinguishable from the dataset and they can be easily filtered out if not needed.

When the selection is returned as an array, just map the values and limit them to positive values:

hotInstance.getSelectedLast().map((indexIncludingHeader) => {
  return Math.max(0, indexIncludingHeader);
});

Selection Range object has a new method normalize that will do this for you:

hotInstance.getSelectedRangeLast().from.clone().normalize()

Removals

Handsontable's skipLengthCache hook was removed, as IndexMapper is now responsible for the cache and length.

Public methods colOffset() and rowOffset() were removed and their functionality is now for internal use only.

Also, an experimental feature called ganttChart was removed and is no longer supported.

If you use these features in your project and need backward compatibility, contact our Technical Support Team (opens new window).