From 197e05254439515efd4bfabebf1a4f9c4ebf230b Mon Sep 17 00:00:00 2001 From: SheetJS Date: Mon, 30 May 2022 22:53:56 -0400 Subject: [PATCH] excel javascript api --- .gitattributes | 1 + Makefile | 2 +- .../04-getting-started/03-demos/03-excel.md | 248 ++++++++++++++++++ .../docs/04-getting-started/03-demos/index.md | 1 + docz/docs/07-csf/07-features/01-formulae.md | 6 +- docz/static/files/xlcfextern1.png | Bin 0 -> 11668 bytes docz/static/files/xlcfversion.png | Bin 0 -> 3104 bytes 7 files changed, 256 insertions(+), 2 deletions(-) create mode 100644 .gitattributes create mode 100644 docz/docs/04-getting-started/03-demos/03-excel.md create mode 100644 docz/static/files/xlcfextern1.png create mode 100644 docz/static/files/xlcfversion.png diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..9070d83 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +docs/** linguist-generated \ No newline at end of file diff --git a/Makefile b/Makefile index 2b444dd..40fac21 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,7 @@ build: mv docz/build/ docs cp CNAME docs -.PHONY: server +.PHONY: serve serve: cd docs; python -mSimpleHTTPServer; cd - diff --git a/docz/docs/04-getting-started/03-demos/03-excel.md b/docz/docs/04-getting-started/03-demos/03-excel.md new file mode 100644 index 0000000..a4b0687 --- /dev/null +++ b/docz/docs/04-getting-started/03-demos/03-excel.md @@ -0,0 +1,248 @@ +--- +sidebar_position: 4 +--- + +# Excel JavaScript API + +Office 2016 introduced a JavaScript API for interacting with the application. +It offers solutions for custom functions as well as task panes. + +Excel currently does not provide support for working with Apple Numbers files +and some legacy file formats. SheetJS fills the gap. + +This demo creates a new custom function to add much-needed functionality: + +- `SHEETJS.EXTERN()` tries to fetch an external spreadsheet and insert the data +into the worksheet. + +This demo focuses on the basic mechanics. Advanced topics like Excel Custom +Function parameters are covered in the official Office JavaScript API docs. +SheetJS worksheet metadata and other properties are covered in this doc site. + +## Creating a new Add-in + +
Initial Platform Setup (click to show) + +The tool for generating Office Add-ins depends on NodeJS and various libraries. +[Install NodeJS](https://nodejs.org/) and the required dependencies: + +```powershell +npm install -g yo bower generator-office +``` + +
+ +
Creating a new Project (click to show) + +Run `yo office` from the command line. It will ask a few questions. + +- "Choose a project type": "Excel Custom Functions Add-in project" + +- "Choose a script type": "JavaScript", + +- "What do you want to name your add-in?": "SheetJSImport" + +You will see a screen like + +``` +? Choose a project type: Excel Custom Functions Add-in project +? Choose a script type: JavaScript +? What do you want to name your add-in? SheetJSImport + +---------------------------------------------------------------------------------- + + Creating SheetJSImport add-in for Excel using JavaScript and Excel-functions +at C:\Users\SheetJS\Documents\SheetJSImport + +---------------------------------------------------------------------------------- +``` + +It helpfully prints out the next steps: + +```powershell +cd SheetJSImport +npm run build +npm start +``` + +If you have [VSCodium](https://vscodium.com/) installed, the folder can be opened with + +```powershell +codium . +``` + +
+ +Running `npm start` will open up a terminal window and a new Excel window with +the loaded add-in. Keep the terminal window open (it can be minimized). When +you make a change, close both the Excel window and the terminal window before +running `npm start` again. + +## Integrating the SheetJS Library + +The library can be installed like any other NodeJS module: + +```powershell +npm i --save https://cdn.sheetjs.com/xlsx-latest/xlsx-latest.tgz +``` + +To be sure the library is loaded, remove all of the existing functions from +`src\functions\functions.js`. The new contents should be + +```js src\functions\functions.js +var XLSX = require("xlsx"); + +/** + * Print SheetJS Library Version + * @customfunction + * @returns {string[][]} The SheetJS Library Version. + */ +function version() { + return [[XLSX.version]]; +} +``` + +The `manifest.xml` should also be updated to reflect the function namespace: + +```xml + +``` + +After making the change, save the files. Close the terminal window and the +Excel window (do not save the Excel file). Re-run `npm start`. + +In the new Excel window, enter the formula `=SHEETJS.VERSION()` in cell E1. You +should see something similar to the following screenshot: + +![`SHEETJS.VERSION` output](pathname:///files/xlcfversion.png) + +This indicates that the SheetJS library has been loaded. + +## Dynamic Arrays and SheetJS Array of Arrays + +The [`sheet_to_json`](../../api/utilities#json) helper function can generate +arrays of arrays of values based on the worksheet data. Excel custom functions +transparently treat these as Dynamic Arrays. + +## Fetching Files from the Internet + +For the next step, we will try to fetch data from an external resource. + is an Apple Numbers file. Excel does not +understand Numbers files and it will not open them. + +
Excel bug related to `fetch` (click to show) + +`fetch` is available to custom functions: + +```js +async function extern() { + try { + const url = "https://sheetjs.com/pres.numbers"; // URL to download + const res = await fetch(url); // fetch data + const ab = await res.arrayBuffer(); // get data as an array buffer + + // DO SOMETHING WITH THE DATA HERE + + } catch(e) { return e; } // pass error back to Excel +} +``` + +When fetching data, functions typically receive an `ArrayBuffer` which stores +the file data. This is readily parsed with `read`: + +```js +var wb = XLSX.read(ab); // parse workbook +``` + +**This is how it should work**. + +[There are outstanding bugs](https://github.com/OfficeDev/office-js/issues/2186) + +For the purposes of this demo, a Base64-encoded file will be used. The +workaround involves fetching that Base64 file, getting the text, and parsing +with the [`base64` type:](../../api/parse-options#input-type) + +```js +async function extern() { + try { + const url = "https://sheetjs.com/pres.numbers.b64"; // URL to download + const res = await fetch(url); // fetch data + const text = await res.text(); // get data as an array buffer + + var wb = XLSX.read(text, { type: "base64" }); + // DO SOMETHING WITH THE DATA HERE + + } catch(e) { return e; } // pass error back to Excel +} +``` + +Base64-encoded files can be generated with PowerShell: + +```powershell +[convert]::ToBase64String([System.IO.File]::ReadAllBytes((Resolve-Path "path\to\file"))) > file.b64 +``` + +
+ + +The `.Sheets` property of the workbook object holds all of the worksheets and +the `.SheetNames` property is an array of worksheet names. Picking the first +worksheet is fairly straightforward: + +```js +var ws = wb.Sheets[wb.SheetNames[0]]; // get first worksheet +``` + +This data can be converted to an Array of Arrays in one line: + +```js +var aoa = XLSX.utils.sheet_to_json(ws, {header: 1}); // get data as array of arrays +``` + +To demonstrate the parsing ability, a Base64-encoded version of the file will +be used. This file contains no binary characters and should "just work". Once +the aforementioned Excel bug is fixed, the non-Base64 version can be used. + +This new function should be added to `src\functions\functions.js`: + +```js src\functions\functions.js +/** + * Download file and write data + * @customfunction + * @returns {any[][]} Worksheet data + */ +async function extern() { + try { + /* URL */ + // const url = "https://sheetjs.com/pres.numbers"; // Once Excel bug is fixed + const url = "https://sheetjs.com/pres.numbers.b64"; // workaround + + /* Fetch Data */ + const res = await fetch(url); + + /* Get Data */ + // const ab = await res.arrayBuffer(); // Once Excel bug is fixed + const b64 = await res.text(); // workaround + + /* Parse Data */ + // var wb = XLSX.read(ab); // Once Excel bug is fixed + var wb = XLSX.read(b64, { type: "base64" }); // workaround + + /* get and return data */ + var ws = wb.Sheets[wb.SheetNames[0]]; // get first worksheet + var aoa = XLSX.utils.sheet_to_json(ws, { header: 1 }); // get data as array of arrays + return [[url]]; + } catch(e) { return [[e]]; } // pass error back to Excel +} +``` + +After making the change, save the files. Close the terminal window and the +Excel window (do not save the Excel file). Re-run `npm start`. + +Enter the formula `=SHEETJS.EXTERN()` in cell D1 and hit Enter. Excel should +pull in the data and generate a dynamic array: + +![`SHEETJS.VERSION` output](pathname:///files/xlcfextern1.png) + +[SheetJS Pro](https://sheetjs.com/pro) offers additional features that can be +used in Excel Custom Functions and Add-ins \ No newline at end of file diff --git a/docz/docs/04-getting-started/03-demos/index.md b/docz/docs/04-getting-started/03-demos/index.md index dd63162..40ac92f 100644 --- a/docz/docs/04-getting-started/03-demos/index.md +++ b/docz/docs/04-getting-started/03-demos/index.md @@ -34,6 +34,7 @@ The demo projects include small runnable examples and short explainers. - [`Chrome / Chromium Extension`](https://github.com/SheetJS/SheetJS/tree/master/demos/chrome/) - [`Google Sheets API`](./gsheet) - [`ExtendScript for Adobe Apps`](./extendscript) +- [`Excel JavaScript API`](./excel) - [`Headless Browsers`](https://github.com/SheetJS/SheetJS/tree/master/demos/headless/) - [`Other JavaScript Engines`](https://github.com/SheetJS/SheetJS/tree/master/demos/altjs/) - [`"serverless" functions`](https://github.com/SheetJS/SheetJS/tree/master/demos/function/) diff --git a/docz/docs/07-csf/07-features/01-formulae.md b/docz/docs/07-csf/07-features/01-formulae.md index c0ddfef..89d826e 100644 --- a/docz/docs/07-csf/07-features/01-formulae.md +++ b/docz/docs/07-csf/07-features/01-formulae.md @@ -325,9 +325,13 @@ For example, when the computer language and region is set to Spanish, Excel interprets `=CONTAR(A1:C3)` as if `CONTAR` is the `COUNT` function. However, in the actual file, Excel stores `COUNT(A1:C3)`. +Function arguments are separated with commas. For example, the Spanish Excel +formula `=CONTAR(A1:C3;B4:D6)` is equivalent to the SheetJS formula string +`COUNT(A1:A3,B4:D6)` + [JSON Translation table](https://oss.sheetjs.com/notes/fmla/table.json). -
Interactive Translator (click to show) +
Function Name Translator (click to show) ```jsx live /* The live editor requires this function wrapper */ diff --git a/docz/static/files/xlcfextern1.png b/docz/static/files/xlcfextern1.png new file mode 100644 index 0000000000000000000000000000000000000000..15de71e266313b2a26c3d5940de4d1bc60d2df36 GIT binary patch literal 11668 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 BD@6bR literal 0 HcmV?d00001