This commit is contained in:
SheetJS 2023-01-14 22:36:13 -05:00
parent 89ddafaaeb
commit 3640bd8c1f
19 changed files with 1362 additions and 1039 deletions

@ -12,7 +12,7 @@ familiarity is assumed.
Other demos cover general React deployments, including:
- [Static Site Generation powered by NextJS](/docs/demos/content#nextjs)
- [Static Site Generation powered by NextJS](/docs/demos/static/nextjs)
- [iOS and Android applications powered by React Native](/docs/demos/mobile/reactnative)
- [Desktop application powered by React Native Windows + macOS](/docs/demos/desktop/reactnative)
- [React Data Grid UI component](/docs/demos/grid#react-data-grid)

@ -12,7 +12,7 @@ Components (SFC) and VueJS familiarity is assumed.
Other demos cover general VueJS deployments, including:
- [Static Site Generation powered by NuxtJS](/docs/demos/content#nuxtjs)
- [Static Site Generation powered by NuxtJS](/docs/demos/static/nuxtjs)
- [iOS and Android applications powered by Quasar](/docs/demos/mobile/quasar)
- [Desktop application powered by Tauri](/docs/demos/desktop/tauri)
- [`vue3-table-lite` UI component](/docs/demos/grid#vue3-table-lite)

@ -1066,7 +1066,7 @@ Access http://localhost:8080 in your web browser.
:::note
The [Vite section of the Content demo](/docs/demos/content#vitejs) covers asset
The [Vite section of the Content demo](/docs/demos/static/vitejs) covers asset
loaders. They are ideal for static sites pulling data from sheets at build time.
:::

File diff suppressed because it is too large Load Diff

@ -1,7 +1,7 @@
---
title: Photoshop and Creative Suite
pagination_prev: demos/server
pagination_next: demos/gsheet
pagination_next: demos/static/index
---
import Tabs from '@theme/Tabs';

@ -1,7 +1,7 @@
---
title: Chrome and Chromium
pagination_prev: demos/server
pagination_next: demos/gsheet
pagination_next: demos/static/index
---
:::warning

@ -1,7 +1,7 @@
---
title: Excel JavaScript API
pagination_prev: demos/server
pagination_next: demos/gsheet
pagination_next: demos/static/index
---
:::info

@ -1,7 +1,7 @@
---
title: Extensions
pagination_prev: demos/server
pagination_next: demos/gsheet
pagination_next: demos/static/index
---
import DocCardList from '@theme/DocCardList';

@ -0,0 +1,90 @@
---
title: Lume
pagination_prev: demos/extensions/index
pagination_next: demos/gsheet
sidebar_custom_props:
type: native
---
Lume is a lightweight, fast and flexible static site generator.
The official [Sheets plugin](https://lume.land/plugins/sheets/) uses SheetJS to
load data from spreadsheets. New users should consult the official docs.
Lume supports refreshing data during development. The generated static sites
include the raw data without referencing the underlying spreadsheet files.
## Lume Demo
:::note
This was tested against `lume v1.14.2` on 2022 December 27.
This example uses the Nunjucks template format. Lume plugins support additional
template formats, including Markdown and JSX.
:::
1) Create a stock site:
```bash
mkdir -p sheetjs-lume
cd sheetjs-lume
deno run -Ar https://deno.land/x/lume/init.ts
```
When prompted, enter the following options:
- `Use TypeScript for the configuration file`: press Enter (use default `N`)
- `Do you want to use plugins`: type `sheets` and press Enter
The project will be configured and modules will be installed.
2) Download <https://sheetjs.com/pres.numbers> and place in a `_data` folder:
```bash
mkdir -p _data
curl -L -o _data/pres.numbers https://sheetjs.com/pres.numbers
```
3) Create a `index.njk` file that references the file. Since the file is
`pres.numbers`, the parameter name is `pres`:
```liquid title="index.njk"
<h2>Presidents</h2>
<table><thead><th>Name</th><th>Index</th></thead>
<tbody>
{% for row in pres %}{% if (loop.index >= 1) %}
<tr>
<td>{{ row.Name }}</td>
<td>{{ row.Index }}</td>
</tr>
{% endif %}{% endfor %}
</tbody>
</table>
```
4) Run the development server:
```bash
deno task lume --serve
```
To verify it works, access http://localhost:3000 from your web browser.
Adding a new row and saving `pres.numbers` should refresh the data
5) Stop the server (press `CTRL+C` in the terminal window) and run
```bash
deno task lume
```
This will create a static site in the `_site` folder, which can be served with:
```bash
npx http-server _site
```
Accessing the page http://localhost:8080 will show the page contents.
This site is self-contained and ready for deployment!

@ -0,0 +1,292 @@
---
title: GatsbyJS
pagination_prev: demos/extensions/index
pagination_next: demos/gsheet
sidebar_custom_props:
type: native
---
import current from '/version.js';
Gatsby is a framework for creating websites. It uses React components for page
templates and GraphQL for loading data.
[`gatsby-transformer-excel`](https://www.gatsbyjs.com/plugins/gatsby-transformer-excel/)
is a transformer that generates GraphQL nodes for each row of each worksheet.
The plugin is officially supported by the Gatsby team. The plugin documentation
includes examples and more detailed usage instructions.
:::note
`gatsby-transformer-excel` is maintained by the Gatsby core team and all bugs
should be directed to the main Gatsby project. If it is determined to be a bug
in the parsing logic, issues should then be raised with the SheetJS project.
:::
:::caution
`gatsby-transformer-excel` uses an older version of the library. It can be
overridden through a `package.json` override in the latest versions of NodeJS:
<pre><code parentName="pre" {...{"className": "language-json"}}>{`\
{
"overrides": {
"xlsx": "https://cdn.sheetjs.com/xlsx-${current}/xlsx-${current}.tgz"
}
}`}
</code></pre>
:::
## GraphQL details
`gatsby-transformer-excel` generates nodes for each data row of each worksheet.
Under the hood, it uses [`sheet_to_json`](/docs/api/utilities#array-output)
to generate row objects using the headers in the first row as keys.
![pres.xlsx](pathname:///pres.png)
Assuming the file name is `pres.xlsx` and the data is stored in "Sheet1", the
following nodes will be created:
```js
[
{ Name: "Bill Clinton", Index: 42, type: "PresXlsxSheet1" },
{ Name: "GeorgeW Bush", Index: 43, type: "PresXlsxSheet1" },
{ Name: "Barack Obama", Index: 44, type: "PresXlsxSheet1" },
{ Name: "Donald Trump", Index: 45, type: "PresXlsxSheet1" },
{ Name: "Joseph Biden", Index: 46, type: "PresXlsxSheet1" },
]
```
The type is a proper casing of the file name concatenated with the sheet name.
The following query pulls the `Name` and `Index` fields from each row:
```graphql
{
allPresXlsxSheet1 { # "all" followed by type
edges {
node { # each line in this block should be a field in the data
Name
Index
}
}
}
}
```
## GatsbyJS Demo
:::note
This demo was tested on 2022 November 11 against `create-gatsby@3.0.0`. The
generated project used `gatsby@5.0.0` and `react@18.2.0`.
:::
### Project setup
1) Run `npm init gatsby -- -y sheetjs-gatsby` to create the template site.
2) Follow the on-screen instructions for starting the local development server:
```bash
cd sheetjs-gatsby
npm run develop
```
Open a web browser to the displayed URL (typically `http://localhost:8000/`)
3) Edit `package.json` and add the highlighted lines in the JSON object:
<pre><code parentName="pre" {...{"className": "language-json"}}>{`\
{
// highlight-start
"overrides": {
"xlsx": "https://cdn.sheetjs.com/xlsx-${current}/xlsx-${current}.tgz"
},
// highlight-end
"name": "sheetjs-gatsby",
"version": "1.0.0",
`}
</code></pre>
4) Install the library and plugins:
<pre><code parentName="pre" {...{"className": "language-bash"}}>{`\
npm i --save https://cdn.sheetjs.com/xlsx-${current}/xlsx-${current}.tgz
npm i --save gatsby-transformer-excel gatsby-source-filesystem
`}
</code></pre>
5) Edit `gatsby-config.js` and add the following lines to the `plugins` array:
```js
plugins: [
{
resolve: `gatsby-source-filesystem`,
options: {
name: `data`,
path: `${__dirname}/src/data/`,
},
},
`gatsby-transformer-excel`,
],
```
Stop and restart the development server process (`npm run develop`).
6) Make a `src/data` directory, download <https://sheetjs.com/pres.xlsx>, and
move the downloaded file into the new folder:
```bash
mkdir -p src/data
curl -L -o src/data/pres.xlsx https://sheetjs.com/pres.xlsx
```
### GraphiQL test
7) Open the GraphiQL editor at `http://localhost:8000/___graphql`.
There is an editor in the left pane. Paste the following query into the editor:
```graphql
{
allPresXlsxSheet1 {
edges {
node {
Name
Index
}
}
}
}
```
Press the Execute Query button and data should show up in the right pane:
![GraphiQL Screenshot](pathname:///gatsby/graphiql.png)
### React page
8) Create a new file `src/pages/pres.js` that uses the query and displays the result:
```jsx title="src/pages/pres.js"
import { graphql } from "gatsby"
import * as React from "react"
export const query = graphql`query {
allPresXlsxSheet1 {
edges {
node {
Name
Index
}
}
}
}`;
const PageComponent = ({data}) => {
return ( <pre>{JSON.stringify(data, 2, 2)}</pre> );
};
export default PageComponent;
```
After saving the file, access `http://localhost:8000/pres`. The displayed JSON
is the data that the component receives:
```js
{
"allPresXlsxSheet1": {
"edges": [
{
"node": {
"Name": "Bill Clinton",
"Index": 42
}
},
// ....
```
9) Change `PageComponent` to display a table based on the data:
```jsx title="src/pages/pres.js"
import { graphql } from "gatsby"
import * as React from "react"
export const query = graphql`query {
allPresXlsxSheet1 {
edges {
node {
Name
Index
}
}
}
}`;
// highlight-start
const PageComponent = ({data}) => {
const rows = data.allPresXlsxSheet1.edges.map(r => r.node);
return ( <table>
<thead><tr><th>Name</th><th>Index</th></tr></thead>
<tbody>{rows.map(row => ( <tr>
<td>{row.Name}</td>
<td>{row.Index}</td>
</tr> ))}</tbody>
</table> );
};
// highlight-end
export default PageComponent;
```
Going back to the browser, `http://localhost:8000/pres` will show a table:
![Data in Table](pathname:///gatsby/table1.png)
### Live refresh
10) Open the file `src/data/pres.xlsx` in Excel or LibreOffice or Numbers.
Add a new row at the end of the file:
![New Row in File](pathname:///gatsby/pres2.png)
Save the file and notice that the table has refreshed with the new data:
![Updated Table](pathname:///gatsby/table2.png)
### Static site
11) Stop the development server and run `npm run build`. Once the build is
finished, the display will confirm that the `/pres` route is static:
```
Pages
┌ src/pages/404.js
│ ├ /404/
│ └ /404.html
├ src/pages/index.js
│ └ /
└ src/pages/pres.js
└ /pres/
╭────────────────────────────────────────────────────────────────╮
│ │
│ (SSG) Generated at build time │
│ D (DSG) Deferred static generation - page generated at runtime │
│ ∞ (SSR) Server-side renders at runtime (uses getServerData) │
│ λ (Function) Gatsby function │
│ │
╰────────────────────────────────────────────────────────────────╯
```
The built page will be placed in `public/pres/index.html`. Open the page with a
text editor and search for "SheetJS" to verify raw HTML was generated:
```html
<tr><td>SheetJS Dev</td><td>47</td></tr>
```

@ -0,0 +1,283 @@
---
title: ViteJS
pagination_prev: demos/extensions/index
pagination_next: demos/gsheet
sidebar_custom_props:
type: bundler
---
:::note
This demo covers static asset imports. For processing files in the browser, the
["Bundlers" demo](/docs/demos/bundler#vite) includes an example.
:::
## Loaders
ViteJS supports static asset imports, but the default raw loader interprets data
as UTF-8 strings. This corrupts binary formats like XLSX and XLS, but a custom
loader can override the default behavior.
:::note Recommendation
For simple tables of data, ["Pure Data Loader"](#pure-data-loader) is strongly
recommended. The heavy work is performed at build time and the generated site
only includes the raw data.
For more complex parsing or display logic, ["Base64 Loader"](#base64-loader) is
preferable. Since the raw parsing logic is performed in the page,
:::
### Pure Data Loader
For a pure static site, a plugin can load data into an array of row objects. The
SheetJS work is performed in the plugin. The library is not loaded in the page!
```js title="vite.config.js"
import { readFileSync } from 'fs';
import { read, utils } from 'xlsx';
import { defineConfig } from 'vite';
export default defineConfig({
assetsInclude: ['**/*.xlsx'], // xlsx file should be treated as assets
plugins: [
{ // this plugin handles ?sheetjs tags
name: "vite-sheet",
transform(code, id) {
if(!id.match(/\?sheetjs$/)) return;
var wb = read(readFileSync(id.replace(/\?sheetjs$/, "")));
var data = utils.sheet_to_json(wb.Sheets[wb.SheetNames[0]]);
return `export default JSON.parse('${JSON.stringify(data)}')`;
}
}
]
});
```
This loader uses the query `sheetjs`:
```js title="main.js"
import data from './data.xlsx?sheetjs';
document.querySelector('#app').innerHTML = `<div><pre>
${data.map(row => JSON.stringify(row)).join("\n")}
</pre></div>`;
```
### Base64 Loader
This loader pulls in data as a Base64 string that can be read with `XLSX.read`.
While this approach works, it is not recommended since it loads the library in
the front-end site.
```js title="vite.config.js"
import { readFileSync } from 'fs';
import { defineConfig } from 'vite';
export default defineConfig({
assetsInclude: ['**/*.xlsx'], // mark that xlsx file should be treated as assets
plugins: [
{ // this plugin handles ?b64 tags
name: "vite-b64-plugin",
transform(code, id) {
if(!id.match(/\?b64$/)) return;
var path = id.replace(/\?b64/, "");
var data = readFileSync(path, "base64");
return `export default '${data}'`;
}
}
]
});
```
When importing using the `b64` query, the raw Base64 string will be exposed.
This can be read directly with `XLSX.read` in JS code:
```js title="main.js"
import { read, utils } from "xlsx";
/* reference workbook */
import b64 from './data.xlsx?b64';
/* parse workbook and export first sheet to CSV */
const wb = read(b64);
const wsname = wb.SheetNames[0];
const csv = utils.sheet_to_csv(wb.Sheets[wsname]);
document.querySelector('#app').innerHTML = `<div><pre>
<b>${wsname}</b>
${csv}
</pre></div>`;
```
## Complete Demo
:::note
This demo was tested on 2023 January 14 against `vite v4.0.4`.
:::
### Initial Setup
1) Create a new site using the `vue-ts` template:
```bash
npm create vite@latest sheetjs-vite -- --template vue-ts
cd sheetjs-vite
npm install
npm i --save https://cdn.sheetjs.com/xlsx-latest/xlsx-latest.tgz
```
2) Replace `vite.config.ts` with the following:
```js title="vite.config.ts"
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { readFileSync } from 'fs';
import { read, utils } from 'xlsx';
export default defineConfig({
assetsInclude: ['**/*.xlsx'], // xlsx file should be treated as assets
plugins: [
vue(),
{ // this plugin handles ?sheetjs tags
name: "vite-sheet",
transform(code, id) {
if(!id.match(/\?sheetjs$/)) return;
var wb = read(readFileSync(id.replace(/\?sheetjs$/, "")));
var data = utils.sheet_to_json(wb.Sheets[wb.SheetNames[0]]);
return `export default JSON.parse('${JSON.stringify(data)}')`;
}
},
{ // this plugin handles ?b64 tags
name: "vite-b64-plugin",
transform(code, id) {
if(!id.match(/\?b64$/)) return;
var path = id.replace(/\?b64/, "");
var data = readFileSync(path, "base64");
return `export default '${data}'`;
}
}
]
});
```
3) Make a `data` folder and download <https://sheetjs.com/pres.xlsx> :
```bash
mkdir -p data
curl -L -o data/pres.xlsx https://sheetjs.com/pres.xlsx
```
### Pure Data Test
4) Run the dev server:
```bash
npm run dev
```
Open a browser window to the displayed URL (typically http://localhost:5173 )
5) Replace the component `src/components/HelloWorld.vue` with:
```html title="src/components/HelloWorld.vue"
<script setup lang="ts">
// @ts-ignore
import data from '../../data/pres.xlsx?sheetjs';
</script>
<template>
<table>
<tr><th>Name</th><th>Index</th></tr>
<tr v-for="(row,R) in data" v-bind:key="R">
<td>{{row.Name}}</td>
<td>{{row.Index}}</td>
</tr>
</table>
</template>
```
Save and refresh the page. A data table should be displayed
6) Stop the dev server and build the site
```bash
npm run build
npx http-server dist/
```
The terminal will display a url like http://127.0.0.1:8080. Access that page
with a web browser.
7) To confirm that only the raw data is present in the page, view the page
source. The code will reference some script like `/assets/index-HASH.js`.
Open that script. Searching for `Bill Clinton` reveals the following:
```
JSON.parse('[{"Name":"Bill Clinton","Index":42}
```
Searching for `BESSELJ` should reveal no results. The SheetJS scripts are not
included in the final site!
### Base64 Test
8) Run the dev server:
```bash
npm run dev
```
Open a browser window to the displayed URL.
9) Replace the component `src/components/HelloWorld.vue` with:
```html title="src/components/HelloWorld.vue"
<script setup lang="ts">
// @ts-ignore
import b64 from '../../data/pres.xlsx?b64';
import { read, utils } from "xlsx";
/* parse workbook and convert first sheet to row array */
const wb = read(b64);
const ws = wb.Sheets[wb.SheetNames[0]];
interface IPresident { Name: string; Index: number; };
const data = utils.sheet_to_json<IPresident>(ws);
</script>
<template>
<table>
<tr><th>Name</th><th>Index</th></tr>
<tr v-for="(row,R) in data" v-bind:key="R">
<td>{{row.Name}}</td>
<td>{{row.Index}}</td>
</tr>
</table>
</template>
```
10) Stop the dev server and build the site
```bash
npm run build
npx http-server dist/
```
The terminal will display a url like http://127.0.0.1:8080. Access that page
with a web browser.
11) To confirm that only the raw data is present in the page, view the page
source. The code will reference some script like `/assets/index-HASH.js`.
Open that script. Searching for `Bill Clinton` should yield no results.
Searching for `BESSELJ` should match the code:
```
425:"BESSELJ"
```
The SheetJS library is embedded in the final site.

@ -0,0 +1,327 @@
---
title: NextJS
pagination_prev: demos/extensions/index
pagination_next: demos/gsheet
---
:::note
This was tested against `next v13.1.1` on 2023 January 14.
:::
:::info
At a high level, there are two ways to pull spreadsheet data into NextJS apps:
loading an asset module or performing the file read operations from the NextJS
lifecycle. At the time of writing, NextJS does not offer an out-of-the-box
asset module solution, so this demo focuses on raw operations. NextJS does not
watch the spreadsheets, so `next dev` hot reloading will not work!
:::
The general strategy with NextJS apps is to generate HTML snippets or data from
the lifecycle functions and reference them in the template.
HTML output can be generated using `XLSX.utils.sheet_to_html` and inserted into
the document using the `dangerouslySetInnerHTML` attribute:
```jsx
export default function Index({html, type}) { return (
// ...
// highlight-next-line
<div dangerouslySetInnerHTML={{ __html: html }} />
// ...
); }
```
:::warning Reading and writing files during the build process
`fs` cannot be statically imported from the top level in NextJS pages. The
dynamic import must happen within a lifecycle function. For example:
```js
/* it is safe to import the library from the top level */
import { readFile, utils, set_fs } from 'xlsx';
/* it is not safe to import 'fs' from the top level ! */
// import * as fs from 'fs'; // this will fail
import { join } from 'path';
import { cwd } from 'process';
export async function getServerSideProps() {
// highlight-next-line
set_fs(await import("fs")); // dynamically import 'fs' when needed
const wb = readFile(join(cwd(), "public", "sheetjs.xlsx")); // works
// ...
}
```
:::
:::caution Next 13+ and SWC
Next 13 switched to the SWC minifier. There are known issues with the minifier.
Until those issues are resolved, SWC should be disabled in `next.config.js`:
```js title="next.config.js"
module.exports = {
// highlight-next-line
swcMinify: false
};
```
:::
## NextJS Strategies
NextJS currently provides 3 strategies:
- "Static Site Generation" using `getStaticProps`
- "SSG with Dynamic Routes" using `getStaticPaths`
- "Server-Side Rendering" using `getServerSideProps`
### Static Site Generation
When using `getStaticProps`, the file will be read once during build time.
```js
import { readFile, set_fs, utils } from 'xlsx';
export async function getStaticProps() {
/* read file */
set_fs(await import("fs"));
const wb = readFile(path_to_file)
/* generate and return the html from the first worksheet */
const html = utils.sheet_to_html(wb.Sheets[wb.SheetNames[0]]);
return { props: { html } };
};
```
### Dynamic Routes
Typically a static site with dynamic routes has an endpoint `/sheets/[id]` that
implements both `getStaticPaths` and `getStaticProps`.
- `getStaticPaths` should return an array of worksheet indices:
```js
export async function getStaticPaths() {
/* read file */
set_fs(await import("fs"));
const wb = readFile(path);
/* generate an array of objects that will be used for generating pages */
const paths = wb.SheetNames.map((name, idx) => ({ params: { id: idx.toString() } }));
return { paths, fallback: false };
};
```
:::note
For a pure static site, `fallback` must be set to `false`!
:::
- `getStaticProps` will generate the actual HTML for each page:
```js
export async function getStaticProps(ctx) {
/* read file */
set_fs(await import("fs"));
const wb = readFile(path);
/* get the corresponding worksheet and generate HTML */
const ws = wb.Sheets[wb.SheetNames[ctx.params.id]]; // id from getStaticPaths
const html = utils.sheet_to_html(ws);
return { props: { html } };
};
```
### Server-Side Rendering
:::caution Do not use on a static site
These routes require a NodeJS dynamic server. Static page generation will fail!
`getStaticProps` and `getStaticPaths` support static site generation (SSG).
`getServerSideProps` is suited for NodeJS hosted deployments where the workbook
changes frequently and a static site is undesirable.
:::
When using `getServerSideProps`, the file will be read on each request.
```js
import { readFile, set_fs, utils } from 'xlsx';
export async function getServerSideProps() {
/* read file */
set_fs(await import("fs"));
const wb = readFile(path_to_file);
/* generate and return the html from the first worksheet */
const html = utils.sheet_to_html(wb.Sheets[wb.SheetNames[0]]);
return { props: { html } };
};
```
## Demo
0) Disable NextJS telemetry:
```js
npx next@13.1.1 telemetry disable
```
Confirm it is disabled by running
```js
npx next@13.1.1 telemetry status
```
1) Set up folder structure. At the end, a `pages` folder with a `sheets`
subfolder must be created. On Linux or MacOS or WSL:
```bash
mkdir -p pages/sheets/
```
2) Download the [test file](pathname:///next/sheetjs.xlsx) and place in the
project root. On Linux or MacOS or WSL:
```bash
curl -LO https://docs.sheetjs.com/next/sheetjs.xlsx
```
3) Install dependencies:
```bash
npm i --save https://cdn.sheetjs.com/xlsx-latest/xlsx-latest.tgz next@13.1.1
```
4) Download test scripts:
Download and place the following scripts in the `pages` subfolder:
- [`index.js`](pathname:///next/index.js)
- [`getServerSideProps.js`](pathname:///next/getServerSideProps.js)
- [`getStaticPaths.js`](pathname:///next/getStaticPaths.js)
- [`getStaticProps.js`](pathname:///next/getStaticProps.js)
Download [`[id].js`](pathname:///next/%5Bid%5D.js) and place in the
`pages/sheets` subfolder.
:::caution Percent-Encoding in the script name
The `[id].js` script must have the literal square brackets in the name. If your
browser saved the file to `%5Bid%5D.js`. rename the file.
:::
On Linux or MacOS or WSL:
```bash
cd pages
curl -LO https://docs.sheetjs.com/next/index.js
curl -LO https://docs.sheetjs.com/next/getServerSideProps.js
curl -LO https://docs.sheetjs.com/next/getStaticPaths.js
curl -LO https://docs.sheetjs.com/next/getStaticProps.js
cd sheets
curl -LOg 'https://docs.sheetjs.com/next/[id].js'
cd ../..
```
5) Test the deployment:
```bash
npx next@13.1.1
```
Open a web browser and access:
- http://localhost:3000 landing page
- http://localhost:3000/getStaticProps shows data from the first sheet
- http://localhost:3000/getServerSideProps shows data from the first sheet
- http://localhost:3000/getStaticPaths shows a list (3 sheets)
The individual worksheets are available at
- http://localhost:3000/sheets/0
- http://localhost:3000/sheets/1
- http://localhost:3000/sheets/2
6) Stop the server and run a production build:
```bash
npx next@13.1.1 build
```
The final output will show a list of the routes and types:
```
Route (pages) Size First Load JS
┌ ○ / 541 B 77.4 kB
├ ○ /404 181 B 73.7 kB
├ λ /getServerSideProps 594 B 77.4 kB
├ ● /getStaticPaths 2.56 kB 79.4 kB
├ ● /getStaticProps 591 B 77.4 kB
└ ● /sheets/[id] (447 ms) 569 B 77.4 kB
├ /sheets/0
├ /sheets/1
└ /sheets/2
```
As explained in the summary, the `/getStaticPaths` and `/getStaticProps` routes
are completely static. 3 `/sheets/#` pages were generated, corresponding to 3
worksheets in the file. `/getServerSideProps` is server-rendered.
7) Try to build a static site:
```bash
npx next@13.1.1 export
```
:::note The static export will fail!
A static page cannot be generated at this point because `/getServerSideProps`
is still server-rendered.
:::
8) Delete `pages/getServerSideProps.js` and rebuild:
```bash
rm -f pages/getServerSideProps.js
npx next@13.1.1 build
```
Inspecting the output, there should be no lines with the `λ` symbol:
```
Route (pages) Size First Load JS
┌ ○ / 541 B 77.4 kB
├ ○ /404 181 B 73.7 kB
├ ● /getStaticPaths 2.56 kB 79.4 kB
├ ● /getStaticProps 591 B 77.4 kB
└ ● /sheets/[id] (459 ms) 569 B 77.4 kB
├ /sheets/0
├ /sheets/1
└ /sheets/2
```
9) Generate the static site:
```bash
npx next@13.1.1 export
```
The static site will be written to the `out` subfolder, which can be hosted with
```bash
npx http-server out
```
The command will start a local HTTP server for testing the generated site. Note
that `/getServerSideProps` will 404 since the page was removed.

@ -0,0 +1,250 @@
---
title: NuxtJS
pagination_prev: demos/extensions/index
pagination_next: demos/gsheet
---
### NuxtJS
`@nuxt/content` is a file-based CMS for Nuxt, enabling static-site generation
and on-demand server rendering powered by spreadsheets.
:::note
This demo was tested on 2022 November 18 against Nuxt Content `v1.15.1`.
:::
:::warning
Nuxt Content `v2` (NuxtJS `v3`) employs a different architecture from `v1`.
There are known bugs related to corrupted binary spreadsheet files.
Greenfield projects should stick to the stable NuxtJS + Nuxt Content versions
until the issues are resolved.
:::
## Configuration
Through an override in `nuxt.config.js`, Nuxt Content will use custom parsers.
Differences from a stock `create-nuxt-app` config are shown below:
```js title="nuxt.config.js"
import { readFile, utils } from 'xlsx';
// This will be called when the files change
const parseSheet = (file, { path }) => {
// `path` is a path that can be read with `XLSX.readFile`
const wb = readFile(path);
const o = wb.SheetNames.map(name => ({ name, data: utils.sheet_to_json(wb.Sheets[name])}));
return { data: o };
}
export default {
// ...
// content.extendParser allows us to hook into the parsing step
content: {
extendParser: {
// the keys are the extensions that will be matched. The "." is required
".numbers": parseSheet,
".xlsx": parseSheet,
".xls": parseSheet,
// can add other extensions like ".fods" as desired
}
},
// ...
}
```
## Template Use
When a spreadsheet is placed in the `content` folder, Nuxt will find it. The
data can be referenced in a view with `asyncData`. The name should not include
the extension, so `"sheetjs.numbers"` would be referenced as `"sheetjs"`:
```js
async asyncData ({$content}) {
return {
// $content('sheetjs') will match files with extensions in nuxt.config.js
data: await $content('sheetjs').fetch()
};
}
```
In the template, `data.data` is an array of objects. Each object has a `name`
property for the worksheet name and a `data` array of row objects. This maps
neatly with nested `v-for`:
```xml
<!-- loop over the worksheets -->
<div v-for="item in data.data" v-bind:key="item.name">
<table>
<!-- loop over the rows of each worksheet -->
<tr v-for="row in item.data" v-bind:key="row.Index">
<!-- here `row` is a row object generated from sheet_to_json -->
<td>{{ row.Name }}</td>
<td>{{ row.Index }}</td>
</tr>
</table>
</div>
```
## Nuxt Content Demo
:::note
The project was generated using `create-nuxt-app v4.0.0`. The generated project
used Nuxt `v2.15.8` and Nuxt Content `v1.15.1`.
:::
1) Create a stock app:
```bash
npx create-nuxt-app@4.0.0 SheetJSNuxt
```
When prompted, enter the following options:
- `Project name`: press Enter (use default `SheetJSNuxt`)
- `Programming language`: press Down Arrow (`TypeScript` selected) then Enter
- `Package manager`: select `Npm` and press Enter
- `UI framework`: select `None` and press Enter
- `Nuxt.js modules`: scroll to `Content`, select with Space, then press Enter
- `Linting tools`: press Enter (do not select any Linting tools)
- `Testing framework`: select `None` and press Enter
- `Rendering mode`: select `Universal (SSR / SSG)` and press Enter
- `Deployment target`: select `Static (Static/Jamstack hosting)` and press Enter
- `Development tools`: press Enter (do not select any Development tools)
- `What is your GitHub username?`: press Enter
- `Version control system`: select `None`
The project will be configured and modules will be installed.
2) Install the SheetJS library and start the server:
```bash
cd SheetJSNuxt
npm i --save https://cdn.sheetjs.com/xlsx-latest/xlsx-latest.tgz
npm run dev
```
When the build finishes, the terminal will display a URL like:
```
Listening on: http://localhost:64688/
```
The server is listening on that URL. Open the link in a web browser.
3) Download <https://sheetjs.com/pres.xlsx> and move to the `content` folder.
```bash
curl -L -o content/pres.xlsx https://sheetjs.com/pres.xlsx
```
4) Modify `nuxt.config.js` as follows:
- Add the following to the top of the script:
```js
import { readFile, utils } from 'xlsx';
// This will be called when the files change
const parseSheet = (file, { path }) => {
// `path` is a path that can be read with `XLSX.readFile`
const wb = readFile(path);
const o = wb.SheetNames.map(name => ({ name, data: utils.sheet_to_json(wb.Sheets[name])}));
return { data: o };
}
```
- Look for the exported object. There should be a `content` property:
```js
// Content module configuration: https://go.nuxtjs.dev/config-content
content: {},
```
Replace the property with the following definition:
```js
// content.extendParser allows us to hook into the parsing step
content: {
extendParser: {
// the keys are the extensions that will be matched. The "." is required
".numbers": parseSheet,
".xlsx": parseSheet,
".xls": parseSheet,
// can add other extensions like ".fods" as desired
}
},
```
(If the property is missing, add it to the end of the exported object)
5) Replace `pages/index.vue` with the following:
```html
<!-- sheetjs (C) 2013-present SheetJS -- https://sheetjs.com -->
<template><div>
<div v-for="item in data.data" v-bind:key="item.name">
<h2>{{ item.name }}</h2>
<table><thead><tr><th>Name</th><th>Index</th></tr></thead><tbody>
<tr v-for="row in item.data" v-bind:key="row.Index">
<td>{{ row.Name }}</td>
<td>{{ row.Index }}</td>
</tr>
</tbody></table>
</div>
</div></template>
<script>
export default {
async asyncData ({$content}) {
return {
data: await $content('pres').fetch()
};
}
};
</script>
```
The browser should refresh to show the contents of the spreadsheet. If it does
not, click Refresh manually or open a new browser window.
![Nuxt Demo end of step 5](pathname:///nuxt/nuxt5.png)
6) To verify that hot loading works, open `pres.xlsx` from the `content` folder
in Excel. Add a new row to the bottom and save the file:
![Adding a new line to `pres.xlsx`](pathname:///nuxt/nuxl6.png)
The server terminal window should show a line like:
```
Updated ./content/pres.xlsx @nuxt/content 05:43:37
```
The page should automatically refresh with the new content:
![Nuxt Demo end of step 6](pathname:///nuxt/nuxt6.png)
7) Stop the server (press `CTRL+C` in the terminal window) and run
```bash
npm run generate
```
This will create a static site in the `dist` folder, which can be served with:
```bash
npx http-server dist
```
Accessing the page http://localhost:8080 will show the page contents. Verifying
the static nature is trivial: make another change in Excel and save. The page
will not change.

@ -0,0 +1,4 @@
{
"label": "Content and Static Sites",
"position": 11
}

@ -0,0 +1,86 @@
---
title: Content and Static Sites
pagination_prev: demos/extensions/index
pagination_next: demos/gsheet
---
import DocCardList from '@theme/DocCardList';
import {useCurrentSidebarCategory} from '@docusaurus/theme-common';
With the advent of server-side frameworks and content management systems, it is
possible to build sites whose source of truth is a spreadsheet! This demo
explores a number of approaches.
## Flow
At its core, the site generators provide a structure for supplying content and
templates. During a publish step, the generators will analyze the content and
generate web pages by applying the template to the content. It is spiritually
similar to "Mail Merge".
This separation of content and presentation makes it easy for analysts and other
spreadsheet users to generate files with their data and quickly update websites!
#### Live Reload
Many frameworks offer a development flow which watches files for changes. When
using a spreadsheet as the data source, this allows the content creators to see
updates in a preview environment as they make changes to the spreadsheet!
The following GIF animation shows a static site generator in development mode.
The user is editing a spreadsheet with Numbers. Every time the page is saved,
the browser refreshes to show the new content.
![Live example](pathname:///static/live.gif)
## Ecosystem
:::note Recommendation
It is strongly recommended to use a framework that provides an official plugin
for working with SheetJS. Lume is a great choice for getting started. GatsbyJS
is excellent for teams well-versed in the React JS framework.
:::
### Official
Some frameworks provide official extensions They are strongly recommended for
greenfield projects. Demos:
<ul>{useCurrentSidebarCategory().items.filter(item => item.customProps?.type == "native").map(item => {
const listyle = (item.customProps?.icon) ? {
listStyleImage: `url("${item.customProps.icon}")`
} : {};
return (<li style={listyle} {...(item.customProps?.class ? {className: item.customProps.class}: {})}>
<a href={item.href}>{item.label}</a>{item.customProps?.summary && (" - " + item.customProps.summary)}
</li>);
})}</ul>
### Bundlers
Bundlers can run JS code and process assets during development and during site
builds. Custom plugins can extract data from spreadsheets. Demos:
<ul>{useCurrentSidebarCategory().items.filter(item => item.customProps?.type == "bundler").map(item => {
const listyle = (item.customProps?.icon) ? {
listStyleImage: `url("${item.customProps.icon}")`
} : {};
return (<li style={listyle} {...(item.customProps?.class ? {className: item.customProps.class}: {})}>
<a href={item.href}>{item.label}</a>{item.customProps?.summary && (" - " + item.customProps.summary)}
</li>);
})}</ul>
### Workarounds
Other site generators require workarounds for various limitations and assumptions:
<ul>{useCurrentSidebarCategory().items.filter(item => !item?.customProps?.type).map(item => {
const listyle = (item.customProps?.icon) ? {
listStyleImage: `url("${item.customProps.icon}")`
} : {};
return (<li style={listyle} {...(item.customProps?.class ? {className: item.customProps.class}: {})}>
<a href={item.href}>{item.label}</a>{item.customProps?.summary && (" - " + item.customProps.summary)}
</li>);
})}</ul>

@ -1,6 +1,6 @@
---
title: Google Sheets
pagination_prev: demos/extensions/index
pagination_prev: demos/static/index
---
import Tabs from '@theme/Tabs';

@ -55,23 +55,35 @@ run in the web browser, demos will include interactive examples.
- [`NeutralinoJS`](/docs/demos/desktop/neutralino)
- [`React Native for Desktop`](/docs/demos/desktop/reactnative)
### Content Management and Static Sites
- [`Lume`](/docs/demos/static/lume)
- [`GatsbyJS`](/docs/demos/static/gatsbyjs)
- [`ViteJS`](/docs/demos/static/vitejs)
- [`NextJS`](/docs/demos/static/nextjs)
- [`NuxtJS`](/docs/demos/static/nuxtjs)
### App Extensions
- [`Excel JavaScript API`](/docs/demos/extensions/excelapi)
- [`ExtendScript for Adobe Apps`](/docs/demos/extensions/extendscript)
- [`Chrome and Chromium Extensions`](/docs/demos/extensions/chromium)
### Cloud Platforms
- [`Amazon Web Services`](/docs/demos/cloud/aws)
- [`Azure Functions and Storage`](/docs/demos/cloud/azure)
- [`NetSuite SuiteScript`](/docs/demos/cloud/netsuite)
- [`Salesforce Lightning Web Components`](/docs/demos/cloud/salesforce)
### Platforms and Integrations
- [`Command-Line Tools`](/docs/demos/cli)
- [`iOS and Android Mobile Applications`](/docs/demos/mobile)
- [`NodeJS Server-Side Processing`](/docs/demos/server#nodejs)
- [`Deno Server-Side Processing`](/docs/demos/server#deno)
- [`Content Management and Static Sites`](/docs/demos/content)
- [`Chrome and Chromium Extensions`](/docs/demos/chromium)
- [`Google Sheets API`](/docs/demos/gsheet)
- [`ExtendScript for Adobe Apps`](/docs/demos/extendscript)
- [`NetSuite SuiteScript`](/docs/demos/cloud/netsuite)
- [`Salesforce Lightning Web Components`](/docs/demos/cloud/salesforce)
- [`Excel JavaScript API`](/docs/demos/excelapi)
- [`Headless Automation`](/docs/demos/headless)
- [`Other JavaScript Engines`](/docs/demos/engines)
- [`Azure Functions and Storage`](/docs/demos/cloud/azure)
- [`Amazon Web Services`](/docs/demos/cloud/aws)
- [`Databases and Structured Data Stores`](/docs/demos/database)
- [`NoSQL and Unstructured Data Stores`](/docs/demos/nosql)
- [`Legacy Internet Explorer`](/docs/demos/frontend/legacy#internet-explorer)

@ -171,6 +171,7 @@ const config = {
{ from: '/docs/demos/excel', to: '/docs/demos/' },
{ from: '/docs/getting-started/demos/', to: '/docs/demos/' },
{ from: '/docs/getting-started/demos/excel', to: '/docs/demos/' },
{ from: '/docs/demos/content', to: '/docs/demos/static/' },
/* frontend */
{ from: '/docs/demos/angular', to: '/docs/demos/frontend/angular/' },
{ from: '/docs/demos/react', to: '/docs/demos/frontend/react/' },

BIN
docz/static/static/live.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 624 KiB