Handsontable legacy

This commit is contained in:
SheetJS 2026-06-10 02:51:09 -04:00
parent a881bc8924
commit 1241cd92d7
13 changed files with 612 additions and 3 deletions

@ -39,6 +39,7 @@ This demo was tested in the following configurations:
| NVIDIA RTX PRO 6000 (96 GB VRAM) + Ryzen Z2 Go (32 GB RAM) | `linux-x64` | Ollama | 2025-11-15 |
| NVIDIA RTX 5090 (32 GB VRAM) + Ryzen AI Z2 Extreme (24 GB RAM) | `win11-x64` | `llama.cpp` | 2026-05-05 |
| NVIDIA RTX 5090 (32 GB VRAM) + Ryzen Z2 Go (32 GB RAM) | `linux-x64` | Ollama | 2025-11-15 |
| NVIDIA RTX 5060 Ti (16 GB VRAM) + Ryzen Z2 Extreme (32 GB RAM) | `win11-x64` | `llama.cpp` | 2026-06-05 |
| AMD AI PRO R9700 (32 GB VRAM) + Ryzen Z1 Extreme (16 GB RAM) | `win11-x64` | Ollama | 2026-01-17 |
| AMD AI PRO R9700 (32 GB VRAM) + Ryzen Z1 Extreme (16 GB RAM) | `linux-x64` | Ollama | 2026-01-17 |
| AMD RX 9070 XT (16 GB VRAM) + Ryzen Z2 Go (32 GB RAM) | `win11-x64` | Ollama | 2026-01-17 |

@ -0,0 +1,228 @@
---
title: Handsontable
pagination_prev: demos/frontend/index
pagination_next: demos/net/index
---
<head>
<script src="https://cdn.jsdelivr.net/npm/handsontable@6.2.2/dist/handsontable.full.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/handsontable@6.2.2/dist/handsontable.full.css"/>
</head>
import current from '/version.js';
import CodeBlock from '@theme/CodeBlock';
:::danger pass
**Handsontable has relicensed away from open source!**
The original MIT license still applies to version `6.2.2`.
After adding the new `licenseKey` requirement, basic integrations still work
with version `17.1.0`.
:::
[Handsontable](https://handsontable.com/) is a robust JavaScript data grid.
The following live integrations use the standalone build:
- [Version `6.2.2` (MIT Licensed)](pathname:///handsontable/)
- [Version `17.1.0` (Proprietary)](pathname:///handsontable/non-oss.html)
:::note Tested Deployments
This demo was tested in the following environments:
| Browser | Date |
|:-------------|:-----------|
| Chromium 148 | 2026-06-09 |
| Safari 18.2 | 2026-06-09 |
:::
:::tip pass
This demo barely scratches the surface. The underlying grid component includes
many additional features that work with [SheetJS Pro](https://sheetjs.com/pro).
:::
## Integration Details
[The "Frameworks" section](/docs/getting-started/installation/frameworks) covers
installation with Yarn and other package managers.
Using the `npm` tool, the following command installs SheetJS and Handsontable:
<CodeBlock language="bash">{`\
npm i --save https://cdn.sheetjs.com/xlsx-${current}/xlsx-${current}.tgz handsontable@6.2.2`}
</CodeBlock>
Methods and components in both libraries can be loaded in pages using `import`:
```js
import { read, utils, writeFile } from 'xlsx';
import Handsontable from 'handsontable';
import 'handsontable/dist/handsontable.css'
```
:::info pass
Official framework wrapper packages (e.g. `@handsontable/react-wrapper`) should
be used in greenfield projects, as they encapsulate best practices.
Due to framework volatility, the pre-baked integrations may not work with older
versions of ReactJS.
:::
### Internal State
Handsontable uses [arrays of arrays](/docs/api/utilities/array#array-of-arrays)
under the hood by default. This is the most flexible approach for processing
arbitrary data.
If a schema is passed when the table is constructed, Handsontable will use
[arrays of objects](/docs/api/utilities/array#arrays-of-objects) instead[^1].
### Reading Data
The SheetJS [`read`](/docs/api/parse-options) function processes file data and
returns a [workbook object](/docs/csf/book). After selecting a worksheet, the
[`sheet_to_json` method](/docs/api/utilities/array#array-output) can return an
array of arrays or array of objects.
The result of `sheet_to_json` can be passed directly to the `loadData` method[^2]
of a Handsontable instance.
The following snippet fetches a file, extracts data from the first worksheet,
and passes the data to the grid:
```js
import { read, utils } from 'xlsx';
import Handsontable from 'handsontable';
/* `hot` is assumed to be the Handsontable instance */
// const hot = new Handsontable(/* ... */);
async function fetch_and_view_first_sheet() {
/* fetch and parse https://docs.sheetjs.com/pres.numbers */
const file = await (await fetch("https://docs.sheetjs.com/pres.numbers")).arrayBuffer();
const wb = read(file);
// highlight-start
/* Generate an array of arrays from the first worksheet */
const first_sheet = wb.Sheets[wb.SheetNames[0]];
const aoa = utils.sheet_to_json(first_sheet, { header: 1 });
/* load data into Handsontable instance */
hot.loadData(aoa);
// highlight-end
}
```
### Writing Data
The `getData` method[^3] of a Handsontable instance returns the current data. By
default, the method returns an array of arrays.
The SheetJS [`aoa_to_sheet`](/docs/api/utilities/array#array-of-arrays-input)
method generates a worksheet object from the data. After creating a new workbook
with [`book_new`](/docs/api/utilities/wb), [`writeFile`](/docs/api/write-options)
The following snippet pulls data from the grid and exports to `SheetJSHOT.xlsx`:
```js
import { utils, writeFile } from 'xlsx';
import Handsontable from 'handsontable';
function export_data_to_xlsx(hot) {
/* pull data from the Handsontable instance */
const aoa = hot.getData();
/* generate a SheetJS worksheet object */
const ws = utils.aoa_to_sheet(aoa);
/* generate a single-sheet workbook and export to SheetJSHOT.xlsx */
const wb = utils.book_new(ws, "Exported Data");
writeFile(wb, "SheetJSHOT.xlsx");
}
```
:::note pass
For framework-native state management, the recommended approach is to listen for
grid events and explicitly update a separate copy of the data[^4].
:::
## Demo
This sample ReactJS-powered site uses SheetJS and Handsontable to display data
from a [sample NUMBERS spreadsheet](pathname:///pres.numbers) and export data
to a new file.
1) Create a new project from the ViteJS `react-ts` template:
```bash
npm create vite@latest -- sheetjs-hot --template react-ts --no-interactive
cd sheetjs-hot
npm i
```
2) Install SheetJS and Handsontable libraries:
<CodeBlock language="bash">{`\
npm i --save https://cdn.sheetjs.com/xlsx-${current}/xlsx-${current}.tgz handsontable@6.2.2`}
</CodeBlock>
3) Start the dev server:
```bash
npm run dev
```
The terminal window will display a URL (typically `http://localhost:5173`).
Open the URL with a web browser and confirm that a page loads.
4) Download [`App.tsx`](pathname:///handsontable/App.tsx) and replace `src/App.tsx`:
```bash
curl -L -o src/App.tsx https://docs.sheetjs.com/handsontable/App.tsx
```
#### Testing
5) Refresh the browser window. A grid and two buttons should be visible:
![handsontable initial view](pathname:///handsontable/pre.png)
6) Press the "Fetch File" button. The grid will refresh with Presidential data:
![handsontable after fetch](pathname:///handsontable/intra.png)
7) Make some changes to the grid data.
:::note pass
Some statisticians believe President Grover Cleveland should be counted once.
That would imply President Clinton should be index 41 and the indices of the
other presidents should be decremented.
:::
Double-click on each cell in the Index column and decrement each value. The new
values should be 41, 42, 43, 44, and 45, as shown in the screenshot below:
![handsontable after edits](pathname:///handsontable/post.png)
8) Click on the "Download" button. The browser should attempt to download a new
spreadsheet (`SheetJSHOT.xlsx`). Save the file.
Open the generated file and verify the contents match the grid.
[^1]: See ["Array of Objects" in the "Data management"](https://handsontable.com/docs/javascript-data-grid/binding-to-data/#array-of-objects) section of the Handsontable documentation for more details.
[^2]: See [`loadData`](https://handsontable.com/docs/javascript-data-grid/api/core/#loaddata) in the Handsontable API documentation for more details.
[^3]: See [`getData`](https://handsontable.com/docs/javascript-data-grid/api/core/#getdata) in the Handsontable API documentation for more details.
[^4]: See ["Events and hooks" in the "Data management"](https://handsontable.com/docs/javascript-data-grid/events-and-hooks/) section of the Handsontable documentation for more details.

@ -54,6 +54,12 @@ through a special Export button. It handles the SheetJS operations internally.
**[The exposition has been moved to a separate page.](/docs/demos/grid/tabulator)**
#### Handsontable
[Click here for a live integration demo.](pathname:///handsontable/)
**[The exposition has been moved to a separate page.](/docs/demos/grid/handsontable)**
#### Angular UI Grid
:::danger pass

@ -21,8 +21,8 @@ MongoDB wire protocol.
:::
[FerretDB](https://www.ferretdb.com/) is an Apache 2.0-licensed document database
that implements the MongoDB wire protocol using PostgreSQL or SQLite as the backend.
[FerretDB](https://docs.ferretdb.io) is an Apache 2.0-licensed document database
that implements the MongoDB wire protocol using a PostgreSQL or SQLite backend.
[SheetJS](https://sheetjs.com) is a JavaScript library for reading and writing
data from spreadsheets.

@ -41,7 +41,7 @@ This demo was tested in the following deployments:
|:---------------|:-------------|:-----------|:-----------|
| macOS 15.3 | `darwin-x64` | 16.95.4 | 2025-04-17 |
| macOS 14.5 | `darwin-arm` | 16.106.3 | 2026-05-05 |
| Windows 11 | `win11-x64` | 365 (2506) | 2025-06-17 |
| Windows 11 | `win11-x64` | 365 (2604) | 2026-06-05 |
| Windows 11 | `win11-arm` | 365 (2503) | 2025-04-24 |
:::

@ -0,0 +1,45 @@
import { useEffect, useRef, useState } from 'react';
import { read, utils, writeFile } from 'xlsx';
import Handsontable from 'handsontable';
import 'handsontable/dist/handsontable.css'
import './App.css';
function App() {
const ref = useRef<HTMLDivElement>(null);
const [hot, setHot] = useState<Handsontable>();
useEffect(() => {
if(!ref || !ref.current) return;
const _hot = new Handsontable(ref.current, {
data: [ ["Sheet", "JS", ""], ["Hands", "on", "Table"] ],
rowHeaders: true, colHeaders: true, height: 600, width: 800
});
setHot(_hot);
return () => { if(hot && !hot.isDestroyed) hot.destroy() };
}, []);
const fetchFile = async() => {
const ab = await (await fetch("https://docs.sheetjs.com/pres.numbers")).arrayBuffer();
const wb = read(ab);
const ws = wb.Sheets[wb.SheetNames[0]];
const aoa = utils.sheet_to_json<any[]>(ws, { header: 1 });
hot.loadData(aoa);
};
const download = async() => {
if(!hot) return;
const ws = utils.aoa_to_sheet(hot.getData());
const wb = utils.book_new(ws);
writeFile(wb, "SheetJSHOT.xlsx");
};
return (
<>
<button onClick={fetchFile}>Fetch File</button>
<button onClick={download}>Download</button>
<div ref={ref}/>
</>
)
}
export default App

@ -0,0 +1,164 @@
<!DOCTYPE html>
<!-- sheetjs (C) SheetJS https://sheetjs.com -->
<!-- vim: set ts=2: -->
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta name="robots" content="noindex"/>
<title>SheetJS + Handsontable Live Demo</title>
<style>
#drop{
border:2px dashed #bbb;
-moz-border-radius:5px;
-webkit-border-radius:5px;
border-radius:5px;
padding:25px;
text-align:center;
font:20pt bold,"Vollkorn";color:#bbb
}
#b64data{
width:100%;
}
a { text-decoration: none }
</style>
<!-- Handsontable stylesheet -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/handsontable@6.2.2/dist/handsontable.full.css"/>
</head>
<body>
<pre>
<b><a href="https://sheetjs.com">SheetJS + Handsontable Demo</a></b>
<div id="drop">Drop a spreadsheet file here to see sheet data</div>
<input type="file" name="xlfile" id="xlf" /> ... or click here to select a file
<textarea id="b64data">... or paste a base64-encoding here</textarea>
</pre>
<p><input type="submit" value="Export to XLSX!" id="xport" onclick="export_xlsx();" disabled></p>
<div id="htmlout"></div>
<br />
<script src="https://cdn.jsdelivr.net/npm/handsontable@6.2.2/dist/handsontable.full.js"></script>
<script src="https://cdn.sheetjs.com/xlsx-latest/package/dist/shim.min.js"></script>
<script src="https://cdn.sheetjs.com/xlsx-latest/package/dist/xlsx.full.min.js"></script>
<script>
/*jshint browser:true */
/* eslint-env browser */
/* eslint no-use-before-define:0 */
/*global Uint8Array, Uint16Array, ArrayBuffer */
/*global XLSX, Handsontable */
var hot;
var process_wb = (function() {
var XPORT = document.getElementById('xport');
var HTMLOUT = document.getElementById('htmlout');
return function process_wb(wb) {
/* get data */
var ws = wb.Sheets[wb.SheetNames[0]];
var data = XLSX.utils.sheet_to_json(ws, {header:1});
/* destroy existing instance if any */
if(hot) hot.destroy();
/* create handsontable instance */
hot = new Handsontable(HTMLOUT, {
data: data,
rowHeaders: true,
colHeaders: true
});
HTMLOUT.style.height = (window.innerHeight - 400) + "px";
HTMLOUT.style.width = (window.innerWidth - 50) + "px";
XPORT.disabled = false;
if(typeof console !== 'undefined') console.log("output", new Date());
};
})();
var do_file = (function() {
return function do_file(files) {
var f = files[0];
var reader = new FileReader();
reader.onload = function(e) {
if(typeof console !== 'undefined') console.log("onload", new Date());
var data = e.target.result;
process_wb(XLSX.read(data));
};
reader.readAsArrayBuffer(f);
};
})();
(function() {
var drop = document.getElementById('drop');
if(!drop.addEventListener) return;
function handleDrop(e) {
e.stopPropagation();
e.preventDefault();
do_file(e.dataTransfer.files);
}
function handleDragover(e) {
e.stopPropagation();
e.preventDefault();
e.dataTransfer.dropEffect = 'copy';
}
drop.addEventListener('dragenter', handleDragover, false);
drop.addEventListener('dragover', handleDragover, false);
drop.addEventListener('drop', handleDrop, false);
})();
(function() {
var xlf = document.getElementById('xlf');
if(!xlf.addEventListener) return;
function handleFile(e) { do_file(e.target.files); }
xlf.addEventListener('change', handleFile, false);
})();
var export_xlsx = (function() {
function prep(arr) {
var out = [];
for(var i = 0; i < arr.length; ++i) {
if(!arr[i]) continue;
if(Array.isArray(arr[i])) { out[i] = arr[i]; continue };
var o = new Array();
Object.keys(arr[i]).forEach(function(k) { o[+k] = arr[i][k] });
out[i] = o;
}
return out;
}
return function export_xlsx() {
if(!hot) return;
/* convert handsontable data to worksheet */
var new_ws = XLSX.utils.aoa_to_sheet(prep(hot.getData()));
/* build workbook */
var new_wb = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(new_wb, new_ws, 'SheetJS');
/* write file and trigger a download */
XLSX.writeFile(new_wb, 'SheetJSHandsontableExport.xlsx', {bookSST:true});
};
})();
(async() => {
const ab = await(await fetch("https://docs.sheetjs.com/pres.numbers")).arrayBuffer();
process_wb(XLSX.read(ab));
})();
</script>
<script type="text/javascript">
/* eslint no-use-before-define:0 */
var _gaq = _gaq || [];
_gaq.push(['_setAccount', 'UA-36810333-1']);
_gaq.push(['_trackPageview']);
(function() {
var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
})();
</script>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

@ -0,0 +1,165 @@
<!DOCTYPE html>
<!-- sheetjs (C) SheetJS https://sheetjs.com -->
<!-- vim: set ts=2: -->
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta name="robots" content="noindex"/>
<title>SheetJS + Handsontable Live Demo</title>
<style>
#drop{
border:2px dashed #bbb;
-moz-border-radius:5px;
-webkit-border-radius:5px;
border-radius:5px;
padding:25px;
text-align:center;
font:20pt bold,"Vollkorn";color:#bbb
}
#b64data{
width:100%;
}
a { text-decoration: none }
</style>
<!-- Handsontable stylesheet -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/handsontable@17.1.0/styles/handsontable.css"/>
</head>
<body>
<pre>
<b><a href="https://sheetjs.com">SheetJS + Handsontable Demo</a></b>
<div id="drop">Drop a spreadsheet file here to see sheet data</div>
<input type="file" name="xlfile" id="xlf" /> ... or click here to select a file
<textarea id="b64data">... or paste a base64-encoding here</textarea>
</pre>
<p><input type="submit" value="Export to XLSX!" id="xport" onclick="export_xlsx();" disabled></p>
<div id="htmlout"></div>
<br />
<script src="https://cdn.jsdelivr.net/npm/handsontable@17.1.0/dist/handsontable.full.js"></script>
<script src="https://cdn.sheetjs.com/xlsx-latest/package/dist/shim.min.js"></script>
<script src="https://cdn.sheetjs.com/xlsx-latest/package/dist/xlsx.full.min.js"></script>
<script>
/*jshint browser:true */
/* eslint-env browser */
/* eslint no-use-before-define:0 */
/*global Uint8Array, Uint16Array, ArrayBuffer */
/*global XLSX, Handsontable */
var hot;
var process_wb = (function() {
var XPORT = document.getElementById('xport');
var HTMLOUT = document.getElementById('htmlout');
return function process_wb(wb) {
/* get data */
var ws = wb.Sheets[wb.SheetNames[0]];
var data = XLSX.utils.sheet_to_json(ws, {header:1});
/* destroy existing instance if any */
if(hot) hot.destroy();
/* create handsontable instance */
hot = new Handsontable(HTMLOUT, {
data: data,
rowHeaders: true,
colHeaders: true,
licenseKey: 'non-commercial-and-evaluation'
});
HTMLOUT.style.height = (window.innerHeight - 400) + "px";
HTMLOUT.style.width = (window.innerWidth - 50) + "px";
XPORT.disabled = false;
if(typeof console !== 'undefined') console.log("output", new Date());
};
})();
var do_file = (function() {
return function do_file(files) {
var f = files[0];
var reader = new FileReader();
reader.onload = function(e) {
if(typeof console !== 'undefined') console.log("onload", new Date());
var data = e.target.result;
process_wb(XLSX.read(data));
};
reader.readAsArrayBuffer(f);
};
})();
(function() {
var drop = document.getElementById('drop');
if(!drop.addEventListener) return;
function handleDrop(e) {
e.stopPropagation();
e.preventDefault();
do_file(e.dataTransfer.files);
}
function handleDragover(e) {
e.stopPropagation();
e.preventDefault();
e.dataTransfer.dropEffect = 'copy';
}
drop.addEventListener('dragenter', handleDragover, false);
drop.addEventListener('dragover', handleDragover, false);
drop.addEventListener('drop', handleDrop, false);
})();
(function() {
var xlf = document.getElementById('xlf');
if(!xlf.addEventListener) return;
function handleFile(e) { do_file(e.target.files); }
xlf.addEventListener('change', handleFile, false);
})();
var export_xlsx = (function() {
function prep(arr) {
var out = [];
for(var i = 0; i < arr.length; ++i) {
if(!arr[i]) continue;
if(Array.isArray(arr[i])) { out[i] = arr[i]; continue };
var o = new Array();
Object.keys(arr[i]).forEach(function(k) { o[+k] = arr[i][k] });
out[i] = o;
}
return out;
}
return function export_xlsx() {
if(!hot) return;
/* convert handsontable data to worksheet */
var new_ws = XLSX.utils.aoa_to_sheet(prep(hot.getData()));
/* build workbook */
var new_wb = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(new_wb, new_ws, 'SheetJS');
/* write file and trigger a download */
XLSX.writeFile(new_wb, 'SheetJSHandsontableExport.xlsx', {bookSST:true});
};
})();
(async() => {
const ab = await(await fetch("https://docs.sheetjs.com/pres.numbers")).arrayBuffer();
process_wb(XLSX.read(ab));
})();
</script>
<script type="text/javascript">
/* eslint no-use-before-define:0 */
var _gaq = _gaq || [];
_gaq.push(['_setAccount', 'UA-36810333-1']);
_gaq.push(['_trackPageview']);
(function() {
var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
})();
</script>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 301 KiB