React Data GridMigrate from 7.4 to 8.0
Migrate from Handsontable 7.4 to Handsontable 8.0, released on August 5, 2020.
- Overview
- Context
- General guidelines
- Keywords (alphabetically)
- Installation
- Breaking changes of Handsontable 8.0.0
- Index-related Handsontable hooks were removed
- modifyRow, unmodifyRow, modifyCol, unmodifyCol
- Handsontable's hiddenRow and hiddenColumn hooks
- Removing redundant render() from Handsontable's after* hooks
- Plugins no longer enable other plugins
- Data reference and the ObserveChanges plugin
- ManualRowMove and ManualColumnMove changes
- Changes to the ManualColumnFreeze plugin
- Using the minSpareRows option with the TrimRows plugin
- Custom editors
- Indexes that exceed the data length
- RecordTranslator in plugins
- Cell selection
- Different arguments order in Handsontable's *loadData hooks
- Different arguments order in Handsontable's column-resizing and row-resizing hooks
- Selection range can have negative indexes
- Removals
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)
afterFilter
afterLoadData
afterRowMove
afterUnmergeCells
batch()
beforeRowMove
CollapsibleColumns
ColumnSorting
- Data binding
dragColumns()
dragRows
Filters
finalIndex
GanttChart
HiddenColumns
insert
isMovePossible
ManualColumnFreeze
ManualColumnMove
ManualRowMove
minRows
minSpareRows
modifyCol
modifyRow
moveColumns
moveRows
NestedRows
ObserveChanges
populateFromArray()
RecordTranslator
setDataAtCell()
setDataAtRowProp()
skipLengthCache
toPhysicalColumn()
toPhysicalRow()
toVisualColumn()
toVisualRow()
TrimRows
unmodifyCol
unmodifyRow
Installation
To update Handsontable to version 8, run:
npm install handsontable@8 @handsontable/react@4
Breaking changes of Handsontable 8.0.0
Index-related Handsontable hooks were removed
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()
:
import { useRef } from 'react';
const hotTableComponentRef = useRef(null);
hotTableComponentRef.current.hotInstance.rowIndexMapper.moveIndexes([1, 0], 0);
hotTableComponentRef.current.hotInstance.render();
Take a look on the trimming example, too. It used to work like this:
<HotTable
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";
import { useRef } from 'react';
const hotTableComponentRef = useRef(null);
...
const customTrimmingMap = new TrimmingMap();
hotTableComponentRef.current.hotInstance.rowIndexMapper.registerMap('customTrimmingMap', customTrimmingMap);
customTrimmingMap.setValueAtIndex(0, true); // trimming index 0
hotTableComponentRef.current.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:
hotTableComponentRef.current.hotInstance.getSourceData()[0][0] = 'A1';
Now, you should use the API instead:
hotTableComponentRef.current.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()
:
<HotTable>
data={[
{ model: 'Roadster', company: 'Tesla' },
{ model: 'i3', company: 'BMW' },
]}
/>
hotTableComponentRef.current.hotInstance.setDataAtRowProp(0, 'available', true) // in 8.0.0, this throws an error
hotTableComponentRef.current.hotInstance.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:
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.
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:
<HotTable
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:
The results after:
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:
<HotTable
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:
<HotTable
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:
Now the expected behavior is to select all cells:
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:
import { useRef } from 'react';
const hotTableComponentRef = useRef(null);
hotTableComponentRef.current.hotInstance.getSelectedLast().map((indexIncludingHeader) => {
return Math.max(0, indexIncludingHeader);
});
Selection Range object has a new method normalize
that will do this for you:
import { useRef } from 'react';
const hotTableComponentRef = useRef(null);
hotTableComponentRef.current.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).