1183 lines
33 KiB
Plaintext
1183 lines
33 KiB
Plaintext
---
|
|
title: Export Tutorial
|
|
pagination_prev: getting-started/installation/index
|
|
pagination_next: getting-started/roadmap
|
|
sidebar_position: 2
|
|
---
|
|
|
|
import current from '/version.js';
|
|
import Tabs from '@theme/Tabs';
|
|
import TabItem from '@theme/TabItem';
|
|
import CodeBlock from '@theme/CodeBlock';
|
|
|
|
Many modern data sources provide an API to download data in JSON format. Many
|
|
users prefer to work in spreadsheet software. SheetJS libraries help bridge the
|
|
gap by translating programmer-friendly JSON to user-friendly workbooks.
|
|
|
|
The goal of this example is to generate a XLSX workbook of US President names
|
|
and birthdates. We will download and wrangle a JSON dataset using standard
|
|
JavaScript functions. Once we have a simple list of names and birthdates, we
|
|
will use SheetJS API functions to build a workbook object and export to XLSX.
|
|
|
|
The ["Live Demo"](#live-demo) section includes a working demo in this page!
|
|
["Run the Demo Locally"](#run-the-demo-locally) shows how to run the workflow in
|
|
iOS / Android apps, desktop apps, NodeJS scripts and other environments.
|
|
|
|
The following sequence diagram shows the process:
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
actor U as User
|
|
participant P as Page
|
|
participant A as API
|
|
U->>P: click button
|
|
P->>A: fetch data
|
|
A->>P: raw data
|
|
Note over P: process data
|
|
Note over P: make workbook
|
|
Note over P: export file
|
|
P->>U: download workbook
|
|
```
|
|
|
|
|
|
## Acquire Data
|
|
|
|
The raw data is available in JSON form[^1]. It has been mirrored at
|
|
https://sheetjs.com/data/executive.json
|
|
|
|
### Raw Data
|
|
|
|
Acquiring the data is straightforward with `fetch`:
|
|
|
|
```js
|
|
const url = "https://sheetjs.com/data/executive.json";
|
|
const raw_data = await (await fetch(url)).json();
|
|
```
|
|
|
|
<details>
|
|
<summary><b>Code Explanation</b> (click to show)</summary>
|
|
|
|
`fetch` is a low-level API for downloading data from an endpoint. It separates
|
|
the network step from the response parsing step.
|
|
|
|
**Network Step**
|
|
|
|
`fetch(url)` returns a `Promise` representing the network request. The browser
|
|
will attempt to download data from the URL. If the network request succeeded,
|
|
the `Promise` will "return" with a `Response` object.
|
|
|
|
Using modern syntax, inside an `async` function, code should `await` the fetch:
|
|
|
|
```js
|
|
const response = await fetch(url);
|
|
```
|
|
|
|
**Checking Status Code**
|
|
|
|
If the file is not available, the `fetch` will still succeed.
|
|
|
|
The status code, stored in the `status` property of the `Response` object, is a
|
|
standard HTTP status code number. Code should check the result.
|
|
|
|
Typically servers will return status `404` "File not Found" if the file is not
|
|
available. A successful request should have status `200` "OK".
|
|
|
|
**Extracting Data**
|
|
|
|
`Response#json` will try to parse the data using `JSON.parse`. Like `fetch`, the
|
|
`json` method returns a `Promise` that must be `await`-ed:
|
|
|
|
```js
|
|
const raw_data = await response.json();
|
|
```
|
|
|
|
:::note pass
|
|
|
|
The `Response` object has other useful methods. `Response#arrayBuffer` will
|
|
return the raw data as an `ArrayBuffer`, suitable for parsing workbook files.
|
|
|
|
:::
|
|
|
|
**Production Use**
|
|
|
|
Functions can test each part independently and report different errors:
|
|
|
|
```js
|
|
async function get_data_from_endpoint(url) {
|
|
/* perform network request */
|
|
let response;
|
|
try {
|
|
response = await fetch(url);
|
|
} catch(e) {
|
|
/* network error */
|
|
throw new Error(`Network Error: ${e.message}`);
|
|
}
|
|
|
|
/* check status code */
|
|
if(response.status == 404) {
|
|
/* server 404 error -- file not found */
|
|
throw new Error("File not found");
|
|
}
|
|
if(response.status != 200) {
|
|
/* for most servers, a successful response will have status 200 */
|
|
throw new Error(`Server status ${response.status}: ${response.statusText}`);
|
|
}
|
|
|
|
/* parse JSON */
|
|
let data;
|
|
try {
|
|
data = await response.json();
|
|
} catch(e) {
|
|
/* parsing error */
|
|
throw new Error(`Parsing Error: ${e.message}`);
|
|
}
|
|
|
|
return data;
|
|
}
|
|
```
|
|
|
|
</details>
|
|
|
|
The raw data is an Array of objects[^2]. For this discussion, the relevant data
|
|
for John Adams is shown below:
|
|
|
|
```js
|
|
{
|
|
"name": {
|
|
"first": "John", // <-- first name
|
|
"last": "Adams" // <-- last name
|
|
},
|
|
"bio": {
|
|
"birthday": "1735-10-19", // <-- birthday
|
|
},
|
|
"terms": [ // <-- array of presidential terms
|
|
{ "type": "viceprez", "start": "1789-04-21", },
|
|
{ "type": "viceprez", "start": "1793-03-04", },
|
|
{ "type": "prez", "start": "1797-03-04", } // <-- presidential term
|
|
]
|
|
}
|
|
```
|
|
|
|
### Filtering for Presidents
|
|
|
|
The dataset includes Aaron Burr, a Vice President who was never President!
|
|
|
|
The `terms` field of each object is an array of terms. A term is a Presidential
|
|
term if the `type` property is `"prez"`. We are interested in Presidents that
|
|
served at least one term. The following line creates an array of Presidents:
|
|
|
|
```js
|
|
const prez = raw_data.filter(row => row.terms.some(term => term.type === "prez"));
|
|
```
|
|
|
|
:::caution pass
|
|
|
|
JavaScript code can be extremely concise. The "Code Explanation" blocks explain
|
|
the code in more detail.
|
|
|
|
:::
|
|
|
|
<details>
|
|
<summary><b>Code Explanation</b> (click to show)</summary>
|
|
|
|
**Verifying if a person was a US President**
|
|
|
|
`Array#some` takes a function and calls it on each element of an array in order.
|
|
If the function ever returns `true`, `Array#some` returns `true`. If each call
|
|
returns `false`, `Array#some` returns `false`.
|
|
|
|
The following function tests if a term is presidential:
|
|
|
|
```js
|
|
const term_is_presidential = term => term.type == "prez";
|
|
```
|
|
|
|
To test if a person was a President, that function should be tested against
|
|
every term in the `terms` array:
|
|
|
|
```js
|
|
const person_was_president = person => person.terms.some(term => term.type == "prez");
|
|
```
|
|
|
|
**Creating a list of US Presidents**
|
|
|
|
`Array#filter` takes a function and returns an array. The function is called on
|
|
each element in order. If the function returns `true`, the element is added to
|
|
the final array. If the function returns false, the element is not added.
|
|
|
|
Using the previous function, this line filters the dataset for Presidents:
|
|
|
|
```js
|
|
const prez = raw_data.filter(row => person_was_president(row));
|
|
```
|
|
|
|
Placing the `person_was_president` function in-line, the final code is:
|
|
|
|
```js
|
|
const prez = raw_data.filter(row => row.terms.some(term => term.type == "prez"));
|
|
```
|
|
|
|
</details>
|
|
|
|
### Sorting by First Term
|
|
|
|
The dataset is sorted in chronological order by the first presidential or vice
|
|
presidential term. The Vice President and President in a given term are sorted
|
|
alphabetically. Joe Biden and Barack Obama were Vice President and President
|
|
respectively in 2009. Since "Biden" is alphabetically before "Obama", Biden's
|
|
data point appears first. The goal is to sort the presidents in order of their
|
|
presidential term.
|
|
|
|
The first step is adding the first presidential term start date to the dataset.
|
|
The following code looks at each president and creates a `start` property that
|
|
represents the start of the first presidential term.
|
|
|
|
```js
|
|
prez.forEach(row => row.start = row.terms.find(term => term.type === "prez").start);
|
|
```
|
|
|
|
<details>
|
|
<summary><b>Code Explanation</b> (click to show)</summary>
|
|
|
|
**Finding the first presidential term**
|
|
|
|
`Array#find` will find the first value in an array that matches a criterion.
|
|
The first presidential term can be found with the following function:
|
|
|
|
```js
|
|
const first_prez_term = prez => prez.terms.find(term => term.type === "prez");
|
|
```
|
|
|
|
:::note pass
|
|
|
|
If no element in the array matches the criterion, `Array#find` does not return
|
|
a value. In this case, since `prez` was created by filtering for people that
|
|
served at least one presidential term, the code assumes a term exists.
|
|
|
|
:::
|
|
|
|
The start of a President's first Presidential term is therefore
|
|
|
|
```js
|
|
const first_prez_term_start = prez => first_prez_term(prez).start;
|
|
```
|
|
|
|
**Adding the first start date to one row**
|
|
|
|
The following function creates the desired `start` property:
|
|
|
|
```js
|
|
const prez_add_start = prez => prez.start = first_prez_term_start(prez);
|
|
```
|
|
|
|
**Adding the first start date to each row**
|
|
|
|
`Array#forEach` takes a function and calls it for every element in the array.
|
|
Any modifications to objects affect the objects in the original array.
|
|
|
|
The previous function can be used directly:
|
|
|
|
```js
|
|
prez.forEach(row => prez_add_start(row));
|
|
```
|
|
|
|
Working in reverse, each partial function can be inserted in place. These lines
|
|
of code are equivalent:
|
|
|
|
```js
|
|
/* start */
|
|
prez.forEach(row => prez_add_start(row));
|
|
|
|
/* put `prez_add_start` definition into the line */
|
|
prez.forEach(row => row.start = first_prez_term_start(row));
|
|
|
|
/* put `first_prez_term_start` definition into the line */
|
|
prez.forEach(row => row.start = first_prez_term(row).start);
|
|
|
|
/* put `first_prez_term` definition into the line */
|
|
prez.forEach(row => row.start = row.terms.find(term => term.type === "prez").start);
|
|
```
|
|
|
|
</details>
|
|
|
|
At this point, each row in the `prez` array has a `start` property. Since the
|
|
`start` properties are strings, the following line sorts the array:
|
|
|
|
```js
|
|
prez.sort((l,r) => l.start.localeCompare(r.start));
|
|
```
|
|
|
|
<details>
|
|
<summary><b>Code Explanation</b> (click to show)</summary>
|
|
|
|
**Comparator Functions and Relative Ordering in JavaScript**
|
|
|
|
A comparator takes two arguments and returns a number that represents the
|
|
relative ordering. `comparator(a,b)` should return a negative number if `a`
|
|
should be placed before `b`. If `b` should be placed before `a`, the comparator
|
|
should return a positive number.
|
|
|
|
If the `start` properties were numbers, the following comparator would suffice:
|
|
|
|
```js
|
|
const comparator_numbers = (a,b) => a - b;
|
|
```
|
|
|
|
For strings, JavaScript comparison operators can work:
|
|
|
|
```js
|
|
const comparator_string_simple = (a,b) => a == b ? 0 : a < b ? -1 : 1;
|
|
```
|
|
|
|
However, that comparator does not handle diacritics. For example, `"z" < "é"`.
|
|
It is strongly recommended to use `String#localeCompare` to compare strings:
|
|
|
|
```js
|
|
const comparator_string = (a,b) => a.localeCompare(b);
|
|
```
|
|
|
|
**Comparing two Presidents**
|
|
|
|
The `start` properties of the Presidents should be compared:
|
|
|
|
```js
|
|
const compare_prez = (a,b) => (a.start).localeCompare(b.start);
|
|
```
|
|
|
|
**Sorting the Array**
|
|
|
|
`Array#sort` takes a comparator function and sorts the array in place. Using
|
|
the Presidential comparator:
|
|
|
|
```js
|
|
prez.sort((l,r) => compare_prez(l,r));
|
|
```
|
|
|
|
Placing the `compare_prez` function in the body:
|
|
|
|
```js
|
|
prez.sort((l,r) => l.start.localeCompare(r.start));
|
|
```
|
|
|
|
</details>
|
|
|
|
### Reshaping the Array
|
|
|
|
For this example, the name will be the first name combined with the last name
|
|
(`row.name.first + " " + row.name.last`) and the birthday will be available at
|
|
`row.bio.birthday`. Using `Array#map`, the dataset can be massaged in one call:
|
|
|
|
```js
|
|
const rows = prez.map(row => ({
|
|
name: row.name.first + " " + row.name.last,
|
|
birthday: row.bio.birthday
|
|
}));
|
|
```
|
|
|
|
<details>
|
|
<summary><b>Code Explanation</b> (click to show)</summary>
|
|
|
|
**Wrangling One Data Row**
|
|
|
|
The key fields for John Adams are shown below:
|
|
|
|
```js
|
|
{
|
|
"name": {
|
|
"first": "John", // <-- first name
|
|
"last": "Adams" // <-- last name
|
|
},
|
|
"bio": {
|
|
"birthday": "1735-10-19", // <-- birthday
|
|
}
|
|
}
|
|
```
|
|
|
|
If `row` is the object, then
|
|
- `row.name.first` is the first name ("John")
|
|
- `row.name.last` is the last name ("Adams")
|
|
- `row.bio.birthday` is the birthday ("1735-10-19")
|
|
|
|
The desired object has a `name` and `birthday` field:
|
|
|
|
```js
|
|
function get_data(row) {
|
|
var name = row.name.first + " " + row.name.last;
|
|
var birthday = row.bio.birthday;
|
|
return ({
|
|
name: name,
|
|
birthday: birthday
|
|
});
|
|
}
|
|
```
|
|
|
|
This can be shortened by adding the fields to the object directly:
|
|
|
|
```js
|
|
function get_data(row) {
|
|
return ({
|
|
name: row.name.first + " " + row.name.last,
|
|
birthday: row.bio.birthday
|
|
});
|
|
}
|
|
```
|
|
|
|
When writing an arrow function that returns an object, parentheses are required:
|
|
|
|
```js
|
|
// open paren required --V
|
|
const get_data = row => ({
|
|
name: row.name.first + " " + row.name.last,
|
|
birthday: row.bio.birthday
|
|
});
|
|
// ^-- close paren required
|
|
```
|
|
|
|
**Wrangling the entire dataset**
|
|
|
|
`Array#map` calls a function on each element of an array and returns a new array
|
|
with the return values of each function.
|
|
|
|
Using the previous method:
|
|
|
|
```js
|
|
const rows = prez.map(row => get_data(row));
|
|
```
|
|
|
|
The `get_data` function can be added in place:
|
|
|
|
```js
|
|
const rows = prez.map(row => ({
|
|
name: row.name.first + " " + row.name.last,
|
|
birthday: row.bio.birthday
|
|
}));
|
|
```
|
|
|
|
</details>
|
|
|
|
The result is an array of "simple" objects with no nesting:
|
|
|
|
```js
|
|
[
|
|
{ name: "George Washington", birthday: "1732-02-22" },
|
|
{ name: "John Adams", birthday: "1735-10-19" },
|
|
// ... one row per President
|
|
]
|
|
```
|
|
|
|
## Create a Workbook
|
|
|
|
With the cleaned dataset, `XLSX.utils.json_to_sheet`[^3] generates a worksheet:
|
|
|
|
```js
|
|
const worksheet = XLSX.utils.json_to_sheet(rows);
|
|
```
|
|
|
|
`XLSX.utils.book_new`[^4] creates a new workbook and `XLSX.utils.book_append_sheet`[^5]
|
|
appends a worksheet to the workbook. The new worksheet will be called "Dates":
|
|
|
|
```js
|
|
const workbook = XLSX.utils.book_new();
|
|
XLSX.utils.book_append_sheet(workbook, worksheet, "Dates");
|
|
```
|
|
|
|
## Clean up Workbook
|
|
|
|
The data is in the workbook and can be exported.
|
|
|
|
![Rough export](pathname:///example/rough.png)
|
|
|
|
There are multiple opportunities for improvement: the headers can be renamed and
|
|
the column widths can be adjusted.
|
|
|
|
:::tip pass
|
|
|
|
[SheetJS Pro](https://sheetjs.com/pro) offers additional styling options like
|
|
cell styling and frozen rows.
|
|
|
|
:::
|
|
|
|
<details>
|
|
<summary><b>Changing Header Names</b> (click to show)</summary>
|
|
|
|
By default, `json_to_sheet` creates a worksheet with a header row. In this case,
|
|
the headers come from the JS object keys: "name" and "birthday".
|
|
|
|
The headers are in cells `A1` and `B1`. `XLSX.utils.sheet_add_aoa`[^6] can write
|
|
text values to the existing worksheet starting at cell `A1`:
|
|
|
|
```js
|
|
XLSX.utils.sheet_add_aoa(worksheet, [["Name", "Birthday"]], { origin: "A1" });
|
|
```
|
|
|
|
</details>
|
|
|
|
<details>
|
|
<summary><b>Changing Column Widths</b> (click to show)</summary>
|
|
|
|
Some of the names are longer than the default column width. Column widths are
|
|
set by setting the `"!cols"` worksheet property.[^7]
|
|
|
|
The following line sets the width of column A to approximately 10 characters:
|
|
|
|
```js
|
|
worksheet["!cols"] = [ { wch: 10 } ]; // set column A width to 10 characters
|
|
```
|
|
|
|
One `Array#reduce` call over `rows` can calculate the maximum width:
|
|
|
|
```js
|
|
const max_width = rows.reduce((w, r) => Math.max(w, r.name.length), 10);
|
|
worksheet["!cols"] = [ { wch: max_width } ];
|
|
```
|
|
|
|
</details>
|
|
|
|
After cleanup, the generated workbook looks like the screenshot below:
|
|
|
|
![Final export](pathname:///example/final.png)
|
|
|
|
## Export a File
|
|
|
|
`XLSX.writeFile`[^8] creates a spreadsheet file and tries to write it to the
|
|
system. In the browser, it will try to prompt the user to download the file. In
|
|
NodeJS, it will write to the local directory.
|
|
|
|
```js
|
|
XLSX.writeFile(workbook, "Presidents.xlsx", { compression: true });
|
|
```
|
|
|
|
## Live Demo
|
|
|
|
This demo runs in the web browser! Click "Click to Generate File!" and the
|
|
browser should try to create `Presidents.xlsx`
|
|
|
|
```jsx live
|
|
function Presidents() { return ( <button onClick={async () => {
|
|
/* fetch JSON data and parse */
|
|
const url = "https://sheetjs.com/data/executive.json";
|
|
const raw_data = await (await fetch(url)).json();
|
|
|
|
/* filter for the Presidents */
|
|
const prez = raw_data.filter(row => row.terms.some(term => term.type === "prez"));
|
|
|
|
/* sort by first presidential term */
|
|
prez.forEach(row => row.start = row.terms.find(term => term.type === "prez").start);
|
|
prez.sort((l,r) => l.start.localeCompare(r.start));
|
|
|
|
/* flatten objects */
|
|
const rows = prez.map(row => ({
|
|
name: row.name.first + " " + row.name.last,
|
|
birthday: row.bio.birthday
|
|
}));
|
|
|
|
/* generate worksheet and workbook */
|
|
const worksheet = XLSX.utils.json_to_sheet(rows);
|
|
const workbook = XLSX.utils.book_new();
|
|
XLSX.utils.book_append_sheet(workbook, worksheet, "Dates");
|
|
|
|
/* fix headers */
|
|
XLSX.utils.sheet_add_aoa(worksheet, [["Name", "Birthday"]], { origin: "A1" });
|
|
|
|
/* calculate column width */
|
|
const max_width = rows.reduce((w, r) => Math.max(w, r.name.length), 10);
|
|
worksheet["!cols"] = [ { wch: max_width } ];
|
|
|
|
/* create an XLSX file and try to save to Presidents.xlsx */
|
|
XLSX.writeFile(workbook, "Presidents.xlsx", { compression: true });
|
|
}}><b>Click to Generate file!</b></button> ); }
|
|
```
|
|
|
|
https://sheetjs.com/pres.html is a hosted version of this demo.
|
|
|
|
## Run the Demo Locally
|
|
|
|
<Tabs>
|
|
<TabItem value="browser" label="Web Browser">
|
|
|
|
Save the following script to `SheetJSStandaloneDemo.html`:
|
|
|
|
<CodeBlock language="html" title="SheetJSStandaloneDemo.html">{`\
|
|
<body>
|
|
<script src="https://cdn.sheetjs.com/xlsx-${current}/package/dist/xlsx.full.min.js"></script>
|
|
<script>
|
|
(async() => {
|
|
/* fetch JSON data and parse */
|
|
const url = "https://sheetjs.com/data/executive.json";
|
|
const raw_data = await (await fetch(url)).json();
|
|
\n\
|
|
/* filter for the Presidents */
|
|
const prez = raw_data.filter(row => row.terms.some(term => term.type === "prez"));
|
|
\n\
|
|
/* sort by first presidential term */
|
|
prez.forEach(row => row.start = row.terms.find(term => term.type === "prez").start);
|
|
prez.sort((l,r) => l.start.localeCompare(r.start));
|
|
\n\
|
|
/* flatten objects */
|
|
const rows = prez.map(row => ({
|
|
name: row.name.first + " " + row.name.last,
|
|
birthday: row.bio.birthday
|
|
}));
|
|
\n\
|
|
/* generate worksheet and workbook */
|
|
const worksheet = XLSX.utils.json_to_sheet(rows);
|
|
const workbook = XLSX.utils.book_new();
|
|
XLSX.utils.book_append_sheet(workbook, worksheet, "Dates");
|
|
\n\
|
|
/* fix headers */
|
|
XLSX.utils.sheet_add_aoa(worksheet, [["Name", "Birthday"]], { origin: "A1" });
|
|
\n\
|
|
/* calculate column width */
|
|
const max_width = rows.reduce((w, r) => Math.max(w, r.name.length), 10);
|
|
worksheet["!cols"] = [ { wch: max_width } ];
|
|
\n\
|
|
/* create an XLSX file and try to save to Presidents.xlsx */
|
|
XLSX.writeFile(workbook, "Presidents.xlsx", { compression: true });
|
|
})();
|
|
</script>
|
|
</body>`}
|
|
</CodeBlock>
|
|
|
|
After saving the file, run a local web server in the folder with the HTML file.
|
|
For example, if NodeJS is installed:
|
|
|
|
```bash
|
|
npx http-server .
|
|
```
|
|
|
|
The server process will display a URL (typically `http://127.0.0.1:8080`). Open
|
|
`http://127.0.0.1:8080/SheetJSStandaloneDemo.html` in your browser.
|
|
|
|
</TabItem>
|
|
<TabItem value="nodejs" label="Command-Line (NodeJS)">
|
|
|
|
Install the dependencies:
|
|
|
|
<Tabs groupId="ssplat">
|
|
<TabItem value="nodejs" label="NodeJS">
|
|
|
|
<CodeBlock language="bash">{`\
|
|
npm i --save https://cdn.sheetjs.com/xlsx-${current}/xlsx-${current}.tgz`}
|
|
</CodeBlock>
|
|
|
|
</TabItem>
|
|
<TabItem value="bun" label="Bun">
|
|
|
|
<CodeBlock language="bash">{`\
|
|
bun install https://cdn.sheetjs.com/xlsx-${current}/xlsx-${current}.tgz`}
|
|
</CodeBlock>
|
|
|
|
</TabItem>
|
|
</Tabs>
|
|
|
|
Save the following script to `SheetJSNodeJS.js`:
|
|
|
|
```js title="SheetJSNodeJS.js"
|
|
const XLSX = require("xlsx");
|
|
|
|
(async() => {
|
|
/* fetch JSON data and parse */
|
|
const url = "https://sheetjs.com/data/executive.json";
|
|
const raw_data = await (await fetch(url)).json();
|
|
|
|
/* filter for the Presidents */
|
|
const prez = raw_data.filter(row => row.terms.some(term => term.type === "prez"));
|
|
|
|
/* sort by first presidential term */
|
|
prez.forEach(row => row.start = row.terms.find(term => term.type === "prez").start);
|
|
prez.sort((l,r) => l.start.localeCompare(r.start));
|
|
|
|
/* flatten objects */
|
|
const rows = prez.map(row => ({
|
|
name: row.name.first + " " + row.name.last,
|
|
birthday: row.bio.birthday
|
|
}));
|
|
|
|
/* generate worksheet and workbook */
|
|
const worksheet = XLSX.utils.json_to_sheet(rows);
|
|
const workbook = XLSX.utils.book_new();
|
|
XLSX.utils.book_append_sheet(workbook, worksheet, "Dates");
|
|
|
|
/* fix headers */
|
|
XLSX.utils.sheet_add_aoa(worksheet, [["Name", "Birthday"]], { origin: "A1" });
|
|
|
|
/* calculate column width */
|
|
const max_width = rows.reduce((w, r) => Math.max(w, r.name.length), 10);
|
|
worksheet["!cols"] = [ { wch: max_width } ];
|
|
|
|
/* create an XLSX file and try to save to Presidents.xlsx */
|
|
XLSX.writeFile(workbook, "Presidents.xlsx", { compression: true });
|
|
})();
|
|
```
|
|
|
|
After saving the script, run the script:
|
|
|
|
<Tabs groupId="ssplat">
|
|
<TabItem value="nodejs" label="NodeJS">
|
|
|
|
```bash
|
|
node SheetJSNodeJS.js
|
|
```
|
|
|
|
</TabItem>
|
|
<TabItem value="bun" label="Bun">
|
|
|
|
```bash
|
|
bun run SheetJSNodeJS.js
|
|
```
|
|
|
|
</TabItem>
|
|
</Tabs>
|
|
|
|
This script will write a new file `Presidents.xlsx` in the same folder.
|
|
|
|
:::caution pass
|
|
|
|
Native `fetch` support was added in NodeJS 18. For older versions of NodeJS,
|
|
the script will throw an error `fetch is not defined`. A third-party library
|
|
like `axios` presents a similar API for fetching data:
|
|
|
|
<details>
|
|
<summary><b>Example using axios</b> (click to show)</summary>
|
|
|
|
Install the dependencies:
|
|
|
|
<CodeBlock language="bash">{`\
|
|
npm i --save https://cdn.sheetjs.com/xlsx-${current}/xlsx-${current}.tgz axios`}
|
|
</CodeBlock>
|
|
|
|
Save the following script to `SheetJSAxios.js` (differences are highlighted):
|
|
|
|
```js title="SheetJSAxios.js"
|
|
const XLSX = require("xlsx");
|
|
// highlight-next-line
|
|
const axios = require("axios");
|
|
|
|
(async() => {
|
|
/* fetch JSON data and parse */
|
|
const url = "https://sheetjs.com/data/executive.json";
|
|
// highlight-next-line
|
|
const raw_data = (await axios(url, {responseType: "json"})).data;
|
|
|
|
/* filter for the Presidents */
|
|
const prez = raw_data.filter(row => row.terms.some(term => term.type === "prez"));
|
|
|
|
/* sort by first presidential term */
|
|
prez.forEach(row => row.start = row.terms.find(term => term.type === "prez").start);
|
|
prez.sort((l,r) => l.start.localeCompare(r.start));
|
|
|
|
/* flatten objects */
|
|
const rows = prez.map(row => ({
|
|
name: row.name.first + " " + row.name.last,
|
|
birthday: row.bio.birthday
|
|
}));
|
|
|
|
/* generate worksheet and workbook */
|
|
const worksheet = XLSX.utils.json_to_sheet(rows);
|
|
const workbook = XLSX.utils.book_new();
|
|
XLSX.utils.book_append_sheet(workbook, worksheet, "Dates");
|
|
|
|
/* fix headers */
|
|
XLSX.utils.sheet_add_aoa(worksheet, [["Name", "Birthday"]], { origin: "A1" });
|
|
|
|
/* calculate column width */
|
|
const max_width = rows.reduce((w, r) => Math.max(w, r.name.length), 10);
|
|
worksheet["!cols"] = [ { wch: max_width } ];
|
|
|
|
/* create an XLSX file and try to save to Presidents.xlsx */
|
|
XLSX.writeFile(workbook, "Presidents.xlsx", { compression: true });
|
|
})();
|
|
```
|
|
|
|
After saving the script, run the script:
|
|
|
|
```bash
|
|
node SheetJSAxios.js
|
|
```
|
|
|
|
This script will write a new file `Presidents.xlsx` in the same folder.
|
|
|
|
</details>
|
|
|
|
:::
|
|
|
|
<details>
|
|
<summary><b>Other Server-Side Platforms</b> (click to show)</summary>
|
|
|
|
<Tabs>
|
|
<TabItem value="deno" label="Deno">
|
|
|
|
Save the following script to `SheetJSDeno.ts`:
|
|
|
|
<CodeBlock language="ts" title="SheetJSDeno.ts">{`\
|
|
// @deno-types="https://cdn.sheetjs.com/xlsx-${current}/package/types/index.d.ts"
|
|
import * as XLSX from 'https://cdn.sheetjs.com/xlsx-${current}/package/xlsx.mjs';
|
|
\n\
|
|
/* fetch JSON data and parse */
|
|
const url = "https://sheetjs.com/data/executive.json";
|
|
const raw_data = await (await fetch(url)).json();
|
|
\n\
|
|
/* filter for the Presidents */
|
|
const prez = raw_data.filter((row: any) => row.terms.some((term: any) => term.type === "prez"));
|
|
\n\
|
|
/* sort by first presidential term */
|
|
prez.forEach(row => row.start = row.terms.find(term => term.type === "prez").start);
|
|
prez.sort((l,r) => l.start.localeCompare(r.start));
|
|
\n\
|
|
/* flatten objects */
|
|
const rows = prez.map((row: any) => ({
|
|
name: row.name.first + " " + row.name.last,
|
|
birthday: row.bio.birthday
|
|
}));
|
|
\n\
|
|
/* generate worksheet and workbook */
|
|
const worksheet = XLSX.utils.json_to_sheet(rows);
|
|
const workbook = XLSX.utils.book_new();
|
|
XLSX.utils.book_append_sheet(workbook, worksheet, "Dates");
|
|
\n\
|
|
/* fix headers */
|
|
XLSX.utils.sheet_add_aoa(worksheet, [["Name", "Birthday"]], { origin: "A1" });
|
|
\n\
|
|
/* calculate column width */
|
|
const max_width = rows.reduce((w: number, r: any) => Math.max(w, r.name.length), 10);
|
|
worksheet["!cols"] = [ { wch: max_width } ];
|
|
\n\
|
|
/* create an XLSX file and try to save to Presidents.xlsx */
|
|
XLSX.writeFile(workbook, "Presidents.xlsx", { compression: true });`}
|
|
</CodeBlock>
|
|
|
|
After saving the script, run the script:
|
|
|
|
```bash
|
|
deno run -A SheetJSDeno.ts
|
|
```
|
|
|
|
This script will write a new file `Presidents.xlsx` in the same folder.
|
|
|
|
</TabItem>
|
|
</Tabs>
|
|
|
|
</details>
|
|
|
|
</TabItem>
|
|
|
|
<TabItem value="desktop" label="Desktop App">
|
|
|
|
Save the following script to `SheetJSNW.html`:
|
|
|
|
<CodeBlock language="html" title="SheetJSNW.html">{`\
|
|
<body>
|
|
<script src="https://cdn.sheetjs.com/xlsx-${current}/package/dist/xlsx.full.min.js"></script>
|
|
<script>
|
|
(async() => {
|
|
/* fetch JSON data and parse */
|
|
const url = "https://sheetjs.com/data/executive.json";
|
|
const raw_data = await (await fetch(url)).json();
|
|
\n\
|
|
/* filter for the Presidents */
|
|
const prez = raw_data.filter(row => row.terms.some(term => term.type === "prez"));
|
|
\n\
|
|
/* sort by first presidential term */
|
|
prez.forEach(row => row.start = row.terms.find(term => term.type === "prez").start);
|
|
prez.sort((l,r) => l.start.localeCompare(r.start));
|
|
\n\
|
|
/* flatten objects */
|
|
const rows = prez.map(row => ({
|
|
name: row.name.first + " " + row.name.last,
|
|
birthday: row.bio.birthday
|
|
}));
|
|
\n\
|
|
/* generate worksheet and workbook */
|
|
const worksheet = XLSX.utils.json_to_sheet(rows);
|
|
const workbook = XLSX.utils.book_new();
|
|
XLSX.utils.book_append_sheet(workbook, worksheet, "Dates");
|
|
\n\
|
|
/* fix headers */
|
|
XLSX.utils.sheet_add_aoa(worksheet, [["Name", "Birthday"]], { origin: "A1" });
|
|
\n\
|
|
/* calculate column width */
|
|
const max_width = rows.reduce((w, r) => Math.max(w, r.name.length), 10);
|
|
worksheet["!cols"] = [ { wch: max_width } ];
|
|
\n\
|
|
/* create an XLSX file and try to save to Presidents.xlsx */
|
|
XLSX.writeFile(workbook, "Presidents.xlsx", { compression: true });
|
|
})();
|
|
</script>
|
|
</body>`}
|
|
</CodeBlock>
|
|
|
|
Save the following to `package.json`:
|
|
|
|
<CodeBlock language="json" title="package.json">{`\
|
|
{
|
|
"name": "sheetjs-nwjs",
|
|
"author": "sheetjs",
|
|
"version": "0.0.0",
|
|
"main": "SheetJSNW.html",
|
|
"dependencies": {
|
|
"nw": "0.77.0",
|
|
"xlsx": "https://cdn.sheetjs.com/xlsx-${current}/xlsx-${current}.tgz"
|
|
}
|
|
}`}
|
|
</CodeBlock>
|
|
|
|
Install dependencies and run:
|
|
|
|
```bash
|
|
npm i
|
|
npx nw .
|
|
```
|
|
|
|
The app will show a save dialog. After selecting a path, it will write the file.
|
|
|
|
</TabItem>
|
|
<TabItem value="mobile" label="Mobile App">
|
|
|
|
:::note Initial Setup
|
|
|
|
Follow the [Environment Setup](https://reactnative.dev/docs/environment-setup)
|
|
of the React Native documentation before testing the demo.
|
|
|
|
:::
|
|
|
|
:::caution pass
|
|
|
|
For Android testing, React Native requires Java 11. It will not work with
|
|
current Java releases.
|
|
|
|
:::
|
|
|
|
:::danger pass
|
|
|
|
There are a number of potential pitfalls.
|
|
|
|
The [React Native demo](/docs/demos/mobile/reactnative) lists some issues
|
|
encountered in previous test runs and potential resolutions.
|
|
|
|
**Please reach out to [the SheetJS chat](https://sheetjs.com/chat) if there are
|
|
any issues not mentioned in the demo page.**
|
|
|
|
:::
|
|
|
|
Create a new project by running the following commands in the Terminal:
|
|
|
|
<CodeBlock language="bash">{`\
|
|
npx -y react-native@0.73.6 init SheetJSPres --version="0.73.6"
|
|
cd SheetJSPres
|
|
\n\
|
|
npm i -S https://cdn.sheetjs.com/xlsx-${current}/xlsx-${current}.tgz react-native-blob-util@0.19.8`}
|
|
</CodeBlock>
|
|
|
|
Save the following to `App.tsx` in the project:
|
|
|
|
```js title="App.tsx"
|
|
import React from 'react';
|
|
import { Alert, Button, SafeAreaView, Text, View } from 'react-native';
|
|
import { utils, version, write } from 'xlsx';
|
|
import RNBU from 'react-native-blob-util';
|
|
|
|
const make_workbook = async() => {
|
|
/* fetch JSON data and parse */
|
|
const url = "https://sheetjs.com/data/executive.json";
|
|
const raw_data = await (await fetch(url)).json();
|
|
|
|
/* filter for the Presidents */
|
|
const prez = raw_data.filter(row => row.terms.some(term => term.type === "prez"));
|
|
|
|
/* sort by first presidential term */
|
|
prez.forEach(row => row.start = row.terms.find(term => term.type === "prez").start);
|
|
prez.sort((l,r) => l.start.localeCompare(r.start));
|
|
|
|
/* flatten objects */
|
|
const rows = prez.map(row => ({
|
|
name: row.name.first + " " + row.name.last,
|
|
birthday: row.bio.birthday
|
|
}));
|
|
|
|
/* generate worksheet and workbook */
|
|
const worksheet = utils.json_to_sheet(rows);
|
|
const workbook = utils.book_new();
|
|
utils.book_append_sheet(workbook, worksheet, "Dates");
|
|
|
|
/* fix headers */
|
|
utils.sheet_add_aoa(worksheet, [["Name", "Birthday"]], { origin: "A1" });
|
|
|
|
/* calculate column width */
|
|
const max_width = rows.reduce((w, r) => Math.max(w, r.name.length), 10);
|
|
worksheet["!cols"] = [ { wch: max_width } ];
|
|
|
|
/* React Native does not support `writeFile`. This is a low-level write ! */
|
|
|
|
/* write workbook to buffer */
|
|
const buf = write(workbook, {type:'buffer', bookType:"xlsx"});
|
|
|
|
/* write buffer to file */
|
|
const filename = RNBU.fs.dirs.DocumentDir + "/Presidents.xlsx";
|
|
await RNBU.fs.writeFile(filename, Array.from(buf), 'ascii');
|
|
|
|
/* Copy to downloads directory (android) */
|
|
try { await RNBU.MediaCollection.copyToMediaStore({
|
|
parentFolder: "",
|
|
mimeType: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
name: "Presidents.xlsx"
|
|
}, "Download", filename); } catch(e) {}
|
|
|
|
return filename;
|
|
};
|
|
|
|
const App = () => ( <SafeAreaView><View style={{ marginTop: 32, padding: 24 }}>
|
|
<Text style={{ fontSize: 24, fontWeight: 'bold' }}>SheetJS {version} Export Demo</Text>
|
|
<Button title='Press to Export' onPress={async() => {
|
|
try {
|
|
const filename = await make_workbook();
|
|
Alert.alert("Export Finished", `Exported to ${filename}`);
|
|
} catch(err) {
|
|
Alert.alert("Export Error", `Error ${err.message||err}`);
|
|
}
|
|
}}/>
|
|
</View></SafeAreaView> );
|
|
|
|
export default App;
|
|
```
|
|
|
|
<Tabs>
|
|
<TabItem value="asim" label="Android">
|
|
|
|
:::note pass
|
|
|
|
The Android demo has been tested in Windows, Arch Linux (Steam Deck) and macOS.
|
|
|
|
:::
|
|
|
|
Test the app in the Android simulator:
|
|
|
|
```bash
|
|
npx react-native start
|
|
```
|
|
|
|
Once Metro is ready, it will display the commands:
|
|
|
|
```
|
|
r - reload the app
|
|
d - open developer menu
|
|
i - run on iOS
|
|
a - run on Android
|
|
```
|
|
|
|
Press `a` to run on Android. The app will launch in the emulator.
|
|
|
|
After clicking "Press to Export", the app will show an alert with the location
|
|
to the generated file (`/data/user/0/com.sheetjspres/files/Presidents.xlsx`)
|
|
|
|
In the Android simulator, pulling the file requires additional steps:
|
|
|
|
```bash
|
|
adb root
|
|
adb pull /data/user/0/com.sheetjspres/files/Presidents.xlsx Presidents.xlsx
|
|
```
|
|
|
|
This command generates `Presidents.xlsx` which can be opened.
|
|
|
|
:::info Device Testing
|
|
|
|
["Running on Device"](https://reactnative.dev/docs/running-on-device) in the
|
|
React Native docs covers device configuration. To summarize:
|
|
|
|
1) Enable USB debugging on the Android device.
|
|
|
|
2) Connect the Android device to the computer with a USB cable.
|
|
|
|
3) Close any running Android and iOS emulators.
|
|
|
|
4) Run `npx react-native run-android`
|
|
|
|
`Presidents.xlsx` will be copied to the `Downloads` folder. The file is visible
|
|
in the Files app and can be opened with the Google Sheets app.
|
|
|
|
:::
|
|
|
|
:::caution pass
|
|
|
|
**This demo worked on multiple local Android devices in local tests.** It is not
|
|
guaranteed to run on every Android device or Android version.
|
|
|
|
The [React Native demo](/docs/demos/mobile/reactnative) lists some issues
|
|
encountered in previous test runs and potential resolutions.
|
|
|
|
Please reach out to [the SheetJS chat](https://sheetjs.com/chat) if there are
|
|
any issues not mentioned in the demo page.
|
|
|
|
:::
|
|
|
|
</TabItem>
|
|
<TabItem value="ios" label="iOS">
|
|
|
|
:::caution pass
|
|
|
|
This demo runs in iOS and requires a Macintosh computer with Xcode installed.
|
|
|
|
:::
|
|
|
|
The native component must be linked:
|
|
|
|
```bash
|
|
cd ios; pod install; cd ..
|
|
```
|
|
|
|
Test the app in the iOS simulator:
|
|
|
|
```bash
|
|
npm run ios
|
|
```
|
|
|
|
After clicking "Press to Export", the app will show an alert with the location
|
|
to the generated file.
|
|
|
|
:::info Device Testing
|
|
|
|
["Running on Device"](https://reactnative.dev/docs/running-on-device) in the
|
|
React Native docs covers device configuration.
|
|
|
|
The `UIFileSharingEnabled` and `LSSupportsOpeningDocumentsInPlace` entitlements
|
|
are required for iOS to show the generated files in the "Files" app.
|
|
|
|
The highlighted lines should be added to the iOS project `Info.plist` just
|
|
before the last `</dict>` tag:
|
|
|
|
```xml title="ios/SheetJSPres/Info.plist"
|
|
<key>UIViewControllerBasedStatusBarAppearance</key>
|
|
<false/>
|
|
<!-- highlight-start -->
|
|
<key>UIFileSharingEnabled</key>
|
|
<true/>
|
|
<key>LSSupportsOpeningDocumentsInPlace</key>
|
|
<true/>
|
|
<!-- highlight-end -->
|
|
</dict>
|
|
</plist>
|
|
```
|
|
|
|
After adding the settings and rebuilding the app, the file will be visible in
|
|
the "Files" app. Under "On My iPhone", there will be a folder `SheetJSPres`.
|
|
Within the folder there will be a file named `Presidents`. Touch the file to
|
|
see a preview of the data. The Numbers app can open the file.
|
|
|
|
:::
|
|
|
|
</TabItem>
|
|
</Tabs>
|
|
|
|
</TabItem>
|
|
</Tabs>
|
|
|
|
[^1]: https://theunitedstates.io/congress-legislators/executive.json is the
|
|
original location of the example dataset. The contributors to the dataset
|
|
dedicated the content to the public domain.
|
|
[^2]: See ["The Executive Branch"](https://github.com/unitedstates/congress-legislators#the-executive-branch)
|
|
in the dataset documentation.
|
|
[^3]: See [`json_to_sheet` in "Utilities"](/docs/api/utilities/array#array-of-objects-input)
|
|
[^4]: See [`book_new` in "Utilities"](/docs/api/utilities/wb)
|
|
[^5]: See [`book_append_sheet` in "Utilities"](/docs/api/utilities/wb)
|
|
[^6]: See [`sheet_add_aoa` in "Utilities"](/docs/api/utilities/array#array-of-arrays-input)
|
|
[^7]: See ["Column Properties"](/docs/csf/features/colprops)
|
|
[^8]: See [`writeFile` in "Writing Files"](/docs/api/write-options) |