577 lines
16 KiB
Markdown
577 lines
16 KiB
Markdown
|
---
|
||
|
title: Super Saiyan Sheets with Kaioken
|
||
|
sidebar_label: Kaioken
|
||
|
description: Build interactive websites with Kaioken. Seamlessly integrate spreadsheets into your app using SheetJS. Bring Excel-powered workflows and data to the modern web.
|
||
|
pagination_prev: demos/index
|
||
|
pagination_next: demos/grid/index
|
||
|
sidebar_position: 1
|
||
|
---
|
||
|
|
||
|
import current from '/version.js';
|
||
|
import Tabs from '@theme/Tabs';
|
||
|
import TabItem from '@theme/TabItem';
|
||
|
import CodeBlock from '@theme/CodeBlock';
|
||
|
|
||
|
[Kaioken](https://kaioken.dev/) is a JavaScript library for building user
|
||
|
interfaces.
|
||
|
|
||
|
[SheetJS](https://sheetjs.com) is a JavaScript library for reading and writing
|
||
|
data from spreadsheets.
|
||
|
|
||
|
This demo uses Kaioken and SheetJS to process and generate spreadsheets. We'll
|
||
|
explore how to load SheetJS in "Kaioponents" (Kaioken components) and compare
|
||
|
common state models and data flow strategies.
|
||
|
|
||
|
:::note pass
|
||
|
|
||
|
This demo focuses on Kaioken concepts. Other demos cover general deployments:
|
||
|
|
||
|
- [Desktop application powered by Tauri](/docs/demos/desktop/tauri)
|
||
|
|
||
|
:::
|
||
|
|
||
|
## Installation
|
||
|
|
||
|
[The "Frameworks" section](/docs/getting-started/installation/frameworks) covers
|
||
|
installation with Yarn and other package managers.
|
||
|
|
||
|
The library can be imported directly from JS or JSX code with:
|
||
|
|
||
|
```js
|
||
|
import { read, utils, writeFile } from 'xlsx';
|
||
|
```
|
||
|
|
||
|
|
||
|
## Internal State
|
||
|
|
||
|
The various SheetJS APIs work with various data shapes. The preferred state
|
||
|
depends on the application.
|
||
|
|
||
|
### Array of Objects
|
||
|
|
||
|
Typically, some users will create a spreadsheet with source data that should be
|
||
|
loaded into the site. This sheet will have known columns.
|
||
|
|
||
|
#### State
|
||
|
|
||
|
The example [presidents sheet](https://sheetjs.com/pres.xlsx) has one header row
|
||
|
with "Name" and "Index" columns. The natural JS representation is an object for
|
||
|
each row, using the values in the first rows as keys:
|
||
|
|
||
|
<table><thead><tr><th>Spreadsheet</th><th>State</th></tr></thead><tbody><tr><td>
|
||
|
|
||
|
![`pres.xlsx` data](pathname:///pres.png)
|
||
|
|
||
|
</td><td>
|
||
|
|
||
|
```js
|
||
|
[
|
||
|
{ Name: "Bill Clinton", Index: 42 },
|
||
|
{ Name: "GeorgeW Bush", Index: 43 },
|
||
|
{ Name: "Barack Obama", Index: 44 },
|
||
|
{ Name: "Donald Trump", Index: 45 },
|
||
|
{ Name: "Joseph Biden", Index: 46 }
|
||
|
]
|
||
|
```
|
||
|
|
||
|
</td></tr></tbody></table>
|
||
|
|
||
|
The Kaioken `useState`[^1] hook can configure the state:
|
||
|
|
||
|
<Tabs groupId="lang">
|
||
|
<TabItem name="JS" value="JavaScript">
|
||
|
|
||
|
```ts
|
||
|
import { useState } from 'kaioken';
|
||
|
|
||
|
/* the kaioponent state is an array of objects */
|
||
|
const [pres, setPres] = useState([]);
|
||
|
```
|
||
|
|
||
|
</TabItem>
|
||
|
<TabItem name="TS" value="TypeScript" default>
|
||
|
|
||
|
```ts
|
||
|
import { useState } from 'kaioken';
|
||
|
|
||
|
/* the kaioponent state is an array of objects */
|
||
|
const [pres, setPres] = useState<any[]>([]);
|
||
|
```
|
||
|
|
||
|
When the spreadsheet header row is known ahead of time, row typing is possible:
|
||
|
|
||
|
```ts
|
||
|
import { useState } from 'kaioken';
|
||
|
|
||
|
interface President {
|
||
|
Name: string;
|
||
|
Index: number;
|
||
|
}
|
||
|
|
||
|
/* the kaioponent state is an array of presidents */
|
||
|
const [pres, setPres] = useState<President[]>([]);
|
||
|
```
|
||
|
|
||
|
:::caution pass
|
||
|
|
||
|
The types are informative. They do not enforce that worksheets include the named
|
||
|
columns. A runtime data validation library should be used to verify the dataset.
|
||
|
|
||
|
When the file header is not known in advance, `any` should be used.
|
||
|
|
||
|
:::
|
||
|
|
||
|
</TabItem>
|
||
|
</Tabs>
|
||
|
|
||
|
#### Updating State
|
||
|
|
||
|
The SheetJS [`read`](/docs/api/parse-options) and [`sheet_to_json`](/docs/api/utilities/array#array-output)
|
||
|
functions simplify state updates. They are best used in the function bodies of
|
||
|
`useEffect`[^2] and `useCallback`[^3] hooks.
|
||
|
|
||
|
A `useEffect` hook can download and update state when a person loads the site:
|
||
|
|
||
|
```mermaid
|
||
|
flowchart LR
|
||
|
url[(Remote\nFile)]
|
||
|
ab[(Data\nArrayBuffer)]
|
||
|
wb(SheetJS\nWorkbook)
|
||
|
ws(SheetJS\nWorksheet)
|
||
|
aoo(array of\nobjects)
|
||
|
state((Kaioponent\nstate))
|
||
|
url --> |fetch\n\n| ab
|
||
|
ab --> |read\n\n| wb
|
||
|
wb --> |wb.Sheets\nselect sheet| ws
|
||
|
ws --> |sheet_to_json\n\n| aoo
|
||
|
aoo --> |setPres\nfrom `setState`| state
|
||
|
```
|
||
|
|
||
|
<Tabs groupId="lang">
|
||
|
<TabItem name="JS" value="JavaScript">
|
||
|
|
||
|
```js
|
||
|
import { useEffect } from 'kaioken';
|
||
|
import { read, utils } from 'xlsx';
|
||
|
|
||
|
/* Fetch and update the state once */
|
||
|
useEffect(() => { (async() => {
|
||
|
/* Download from https://sheetjs.com/pres.numbers */
|
||
|
const f = await fetch("https://sheetjs.com/pres.numbers");
|
||
|
const ab = await f.arrayBuffer();
|
||
|
|
||
|
// highlight-start
|
||
|
/* parse */
|
||
|
const wb = read(ab);
|
||
|
|
||
|
/* generate array of objects from first worksheet */
|
||
|
const ws = wb.Sheets[wb.SheetNames[0]]; // get the first worksheet
|
||
|
const data = utils.sheet_to_json(ws); // generate objects
|
||
|
|
||
|
/* update state */
|
||
|
setPres(data); // update state
|
||
|
// highlight-end
|
||
|
})(); }, []);
|
||
|
```
|
||
|
|
||
|
</TabItem>
|
||
|
<TabItem name="TS" value="TypeScript" default>
|
||
|
|
||
|
```ts
|
||
|
import { useEffect } from 'kaioken';
|
||
|
import { read, utils } from 'xlsx';
|
||
|
|
||
|
/* Fetch and update the state once */
|
||
|
useEffect(() => { (async() => {
|
||
|
/* Download from https://sheetjs.com/pres.numbers */
|
||
|
const f = await fetch("https://sheetjs.com/pres.numbers");
|
||
|
const ab = await f.arrayBuffer();
|
||
|
|
||
|
// highlight-start
|
||
|
/* parse */
|
||
|
const wb = read(ab);
|
||
|
|
||
|
/* generate array of presidents from the first worksheet */
|
||
|
const ws = wb.Sheets[wb.SheetNames[0]]; // get the first worksheet
|
||
|
const data: President[] = utils.sheet_to_json<President>(ws); // generate objects
|
||
|
|
||
|
/* update state */
|
||
|
setPres(data); // update state
|
||
|
// highlight-end
|
||
|
})(); }, []);
|
||
|
```
|
||
|
|
||
|
</TabItem>
|
||
|
</Tabs>
|
||
|
|
||
|
#### Rendering Data
|
||
|
|
||
|
Kaioponents typically render HTML tables from arrays of objects. The `TR` table
|
||
|
row elements are typically generated by mapping over the state array, as shown
|
||
|
in the example JSX code:
|
||
|
|
||
|
```jsx title="Example JSX for displaying arrays of objects"
|
||
|
<table>
|
||
|
{/* The `thead` section includes the table header row */}
|
||
|
<thead><tr><th>Name</th><th>Index</th></tr></thead>
|
||
|
{/* The `tbody` section includes the data rows */}
|
||
|
<tbody>
|
||
|
{/* generate row (TR) for each president */}
|
||
|
// highlight-start
|
||
|
{pres.map(row => (
|
||
|
<tr>
|
||
|
{/* Generate cell (TD) for name / index */}
|
||
|
<td>{row.Name}</td>
|
||
|
<td>{row.Index}</td>
|
||
|
</tr>
|
||
|
))}
|
||
|
// highlight-end
|
||
|
</tbody>
|
||
|
</table>
|
||
|
```
|
||
|
|
||
|
#### Exporting Data
|
||
|
|
||
|
The [`writeFile`](/docs/api/write-options) and [`json_to_sheet`](/docs/api/utilities/array#array-of-objects-input)
|
||
|
functions simplify exporting data. They are best used in the function bodies of
|
||
|
`useCallback`[^4] hooks attached to button or other elements.
|
||
|
|
||
|
A callback can generate a local file when a user clicks a button:
|
||
|
|
||
|
```mermaid
|
||
|
flowchart LR
|
||
|
state((Kaioponent\nstate))
|
||
|
ws(SheetJS\nWorksheet)
|
||
|
wb(SheetJS\nWorkbook)
|
||
|
file[(XLSX\nexport)]
|
||
|
state --> |json_to_sheet\n\n| ws
|
||
|
ws --> |book_new\nbook_append_sheet| wb
|
||
|
wb --> |writeFile\n\n| file
|
||
|
```
|
||
|
|
||
|
```ts
|
||
|
import { useCallback } from 'kaioken';
|
||
|
import { utils, writeFile } from 'xlsx';
|
||
|
|
||
|
/* get state data and export to XLSX */
|
||
|
const exportFile = useCallback(() => {
|
||
|
/* generate worksheet from state */
|
||
|
// highlight-next-line
|
||
|
const ws = utils.json_to_sheet(pres);
|
||
|
/* create workbook and append worksheet */
|
||
|
const wb = utils.book_new();
|
||
|
utils.book_append_sheet(wb, ws, "Data");
|
||
|
/* export to XLSX */
|
||
|
writeFile(wb, "SheetJSKaiokenAoO.xlsx");
|
||
|
}, [pres]);
|
||
|
```
|
||
|
|
||
|
#### Complete Kaioponent
|
||
|
|
||
|
This complete Kaioponent example fetches a test file and displays the data in a
|
||
|
HTML table. When the export button is clicked, a callback will export a file:
|
||
|
|
||
|
```tsx title="src/SheetJSKaiokenAoO.tsx"
|
||
|
import { useCallback, useEffect, useState } from "kaioken";
|
||
|
import { read, utils, writeFileXLSX } from 'xlsx';
|
||
|
|
||
|
interface President {
|
||
|
Name: string;
|
||
|
Index: number;
|
||
|
}
|
||
|
|
||
|
export default function SheetJSKaiokenAoO() {
|
||
|
/* the kaioponent state is an array of presidents */
|
||
|
const [pres, setPres] = useState<President[]>([]);
|
||
|
|
||
|
/* Fetch and update the state once */
|
||
|
useEffect(() => { (async() => {
|
||
|
const f = await (await fetch("https://sheetjs.com/pres.xlsx")).arrayBuffer();
|
||
|
const wb = read(f); // parse the array buffer
|
||
|
const ws = wb.Sheets[wb.SheetNames[0]]; // get the first worksheet
|
||
|
const data = utils.sheet_to_json<President>(ws); // generate objects
|
||
|
setPres(data); // update state
|
||
|
})(); }, []);
|
||
|
|
||
|
/* get state data and export to XLSX */
|
||
|
const exportFile = useCallback(() => {
|
||
|
const ws = utils.json_to_sheet(pres);
|
||
|
const wb = utils.book_new();
|
||
|
utils.book_append_sheet(wb, ws, "Data");
|
||
|
writeFileXLSX(wb, "SheetJSKaiokenAoO.xlsx");
|
||
|
}, [pres]);
|
||
|
|
||
|
return (<table><thead><tr><th>Name</th><th>Index</th></tr></thead><tbody>
|
||
|
{ /* generate row for each president */
|
||
|
pres.map(pres => (<tr>
|
||
|
<td>{pres.Name}</td>
|
||
|
<td>{pres.Index}</td>
|
||
|
</tr>))
|
||
|
}
|
||
|
</tbody><tfoot><td colSpan={2}>
|
||
|
<button onclick={exportFile}>Export XLSX</button>
|
||
|
</td></tfoot></table>);
|
||
|
}
|
||
|
```
|
||
|
|
||
|
<details open><summary><b>How to run the example</b> (click to hide)</summary>
|
||
|
|
||
|
<Tabs groupId="starter">
|
||
|
<TabItem name="vite" value="ViteJS">
|
||
|
|
||
|
:::note Tested Deployments
|
||
|
|
||
|
This demo was tested in the following environments:
|
||
|
|
||
|
| Kaioken | ViteJS | Date |
|
||
|
|:---------|:--------|:-----------|
|
||
|
| `0.11.2` | `5.2.6` | 2024-03-24 |
|
||
|
|
||
|
:::
|
||
|
|
||
|
1) Create a new site.
|
||
|
|
||
|
```bash
|
||
|
npm create vite@latest sheetjs-kaioken -- --template vanilla-ts
|
||
|
cd sheetjs-kaioken
|
||
|
pnpm add --save kaioken
|
||
|
pnpm add --save vite-plugin-kaioken -D
|
||
|
```
|
||
|
|
||
|
2) Create a new file `vite.config.ts` with the following content:
|
||
|
|
||
|
```ts title="vite.config.ts (create new file)"
|
||
|
import { defineConfig } from "vite"
|
||
|
import kaioken from "vite-plugin-kaioken"
|
||
|
|
||
|
export default defineConfig({
|
||
|
esbuild: {
|
||
|
jsxInject: `import * as kaioken from "kaioken"`,
|
||
|
jsx: "transform",
|
||
|
jsxFactory: "kaioken.createElement",
|
||
|
jsxFragment: "kaioken.fragment",
|
||
|
loader: "tsx",
|
||
|
include: ["**/*.tsx", "**/*.ts", "**/*.jsx", "**/*.js"],
|
||
|
},
|
||
|
plugins: [kaioken()],
|
||
|
})
|
||
|
```
|
||
|
|
||
|
3) Edit `tsconfig.json` and add `"jsx": "preserve"` within `compilerOptions`:
|
||
|
|
||
|
```js title="tsconfig.json (add highlighted line)"
|
||
|
{
|
||
|
"compilerOptions": {
|
||
|
// highlight-next-line
|
||
|
"jsx": "preserve",
|
||
|
```
|
||
|
|
||
|
4) Replace `src/main.ts` with the following codeblock:
|
||
|
|
||
|
```ts title="src/main.ts (replace contents)"
|
||
|
import { mount } from "kaioken";
|
||
|
import App from "./SheetJSKaiokenAoO";
|
||
|
|
||
|
const root = document.getElementById("app");
|
||
|
mount(App, root!);
|
||
|
```
|
||
|
|
||
|
5) Create a new file `src/SheetJSKaiokenAoO.tsx` using the original code example.
|
||
|
|
||
|
6) Install the SheetJS dependency and start the dev server:
|
||
|
|
||
|
<CodeBlock language="bash">{`\
|
||
|
pnpm i --save https://cdn.sheetjs.com/xlsx-${current}/xlsx-${current}.tgz
|
||
|
pnpm run dev`}
|
||
|
</CodeBlock>
|
||
|
|
||
|
7) Open a web browser and access the displayed URL (`http://localhost:5173`)
|
||
|
|
||
|
The page will refresh and show a table with an Export button. Click the button
|
||
|
and the page will attempt to download `SheetJSKaiokenAoO.xlsx`.
|
||
|
|
||
|
8) Build the site:
|
||
|
|
||
|
```bash
|
||
|
pnpm run build
|
||
|
```
|
||
|
|
||
|
The generated site will be placed in the `dist` folder.
|
||
|
|
||
|
9) Start a local web server:
|
||
|
|
||
|
```bash
|
||
|
npx http-server dist
|
||
|
```
|
||
|
|
||
|
Access the displayed URL (typically `http://localhost:8080`) with a web browser
|
||
|
and test the page.
|
||
|
|
||
|
</TabItem>
|
||
|
</Tabs>
|
||
|
|
||
|
When the page loads, the app will fetch <https://sheetjs.com/pres.xlsx> and
|
||
|
display the data from the first worksheet in a TABLE. The "Export XLSX" button
|
||
|
will generate a workbook that can be opened in a spreadsheet editor.
|
||
|
|
||
|
</details>
|
||
|
|
||
|
### HTML
|
||
|
|
||
|
The main disadvantage of the Array of Objects approach is the specific nature
|
||
|
of the columns. For more general use, passing around an Array of Arrays works.
|
||
|
However, this does not handle merge cells well!
|
||
|
|
||
|
The [`sheet_to_html`](/docs/api/utilities/html#html-table-output) function
|
||
|
generates HTML that is aware of merges and other worksheet features. To add the
|
||
|
table to the page, the current recommendation involves setting the `innerHTML`
|
||
|
attribute of a `ref`.
|
||
|
|
||
|
In this example, the kaioponent attaches a `ref` to the `DIV` container. During
|
||
|
export, the first `TABLE` child element can be parsed with [`table_to_book`](/docs/api/utilities/html#html-table-input) to
|
||
|
generate a workbook object.
|
||
|
|
||
|
```tsx title="src/SheetJSKaiokenHTML.tsx"
|
||
|
import { useCallback, useEffect, useRef } from "kaioken";
|
||
|
import { read, utils, writeFileXLSX } from 'xlsx';
|
||
|
|
||
|
export default function SheetJSKaiokenHTML() {
|
||
|
/* the ref is used in export */
|
||
|
const tbl = useRef<Element>(null);
|
||
|
|
||
|
/* Fetch and update the state once */
|
||
|
useEffect(() => { (async() => {
|
||
|
const f = await (await fetch("https://sheetjs.com/pres.xlsx")).arrayBuffer();
|
||
|
const wb = read(f); // parse the array buffer
|
||
|
const ws = wb.Sheets[wb.SheetNames[0]]; // get the first worksheet
|
||
|
// highlight-start
|
||
|
const data = utils.sheet_to_html(ws); // generate HTML
|
||
|
if(tbl.current == null) return;
|
||
|
tbl.current.innerHTML = data;
|
||
|
// highlight-end
|
||
|
})(); }, []);
|
||
|
|
||
|
/* get live table and export to XLSX */
|
||
|
const exportFile = useCallback(() => {
|
||
|
// highlight-start
|
||
|
const elt = tbl.current!.getElementsByTagName("TABLE")[0];
|
||
|
const wb = utils.table_to_book(elt);
|
||
|
// highlight-end
|
||
|
writeFileXLSX(wb, "SheetJSKaiokenHTML.xlsx");
|
||
|
}, [tbl]);
|
||
|
|
||
|
return ( <>
|
||
|
<button onclick={exportFile}>Export XLSX</button>
|
||
|
// highlight-next-line
|
||
|
<div ref={tbl}/>
|
||
|
</> );
|
||
|
}
|
||
|
```
|
||
|
|
||
|
<details open><summary><b>How to run the example</b> (click to hide)</summary>
|
||
|
|
||
|
<Tabs groupId="starter">
|
||
|
<TabItem name="vite" value="ViteJS">
|
||
|
|
||
|
:::note Tested Deployments
|
||
|
|
||
|
This demo was tested in the following environments:
|
||
|
|
||
|
| Kaioken | ViteJS | Date |
|
||
|
|:---------|:--------|:-----------|
|
||
|
| `0.11.2` | `5.2.6` | 2024-03-24 |
|
||
|
|
||
|
:::
|
||
|
|
||
|
1) Create a new site.
|
||
|
|
||
|
```bash
|
||
|
npm create vite@latest sheetjs-kaioken -- --template vanilla-ts
|
||
|
cd sheetjs-kaioken
|
||
|
pnpm add --save kaioken
|
||
|
pnpm add --save vite-plugin-kaioken -D
|
||
|
```
|
||
|
|
||
|
2) Create a new file `vite.config.ts` with the following content:
|
||
|
|
||
|
```ts title="vite.config.ts (create new file)"
|
||
|
import { defineConfig } from "vite"
|
||
|
import kaioken from "vite-plugin-kaioken"
|
||
|
|
||
|
export default defineConfig({
|
||
|
esbuild: {
|
||
|
jsxInject: `import * as kaioken from "kaioken"`,
|
||
|
jsx: "transform",
|
||
|
jsxFactory: "kaioken.createElement",
|
||
|
jsxFragment: "kaioken.fragment",
|
||
|
loader: "tsx",
|
||
|
include: ["**/*.tsx", "**/*.ts", "**/*.jsx", "**/*.js"],
|
||
|
},
|
||
|
plugins: [kaioken()],
|
||
|
})
|
||
|
```
|
||
|
|
||
|
3) Edit `tsconfig.json` and add `"jsx": "preserve"` within `compilerOptions`:
|
||
|
|
||
|
```js title="tsconfig.json (add highlighted line)"
|
||
|
{
|
||
|
"compilerOptions": {
|
||
|
// highlight-next-line
|
||
|
"jsx": "preserve",
|
||
|
```
|
||
|
|
||
|
4) Replace `src/main.ts` with the following codeblock:
|
||
|
|
||
|
```ts title="src/main.ts (replace contents)"
|
||
|
import { mount } from "kaioken";
|
||
|
import App from "./SheetJSKaiokenHTML";
|
||
|
|
||
|
const root = document.getElementById("app");
|
||
|
mount(App, root!);
|
||
|
```
|
||
|
|
||
|
5) Create a new file `src/SheetJSKaiokenHTML.tsx` using the original code example.
|
||
|
|
||
|
6) Install the SheetJS dependency and start the dev server:
|
||
|
|
||
|
<CodeBlock language="bash">{`\
|
||
|
pnpm i --save https://cdn.sheetjs.com/xlsx-${current}/xlsx-${current}.tgz
|
||
|
pnpm run dev`}
|
||
|
</CodeBlock>
|
||
|
|
||
|
7) Open a web browser and access the displayed URL (`http://localhost:5173`)
|
||
|
|
||
|
The page will refresh and show a table with an Export button. Click the button
|
||
|
and the page will attempt to download `SheetJSKaiokenHTML.xlsx`.
|
||
|
|
||
|
8) Build the site:
|
||
|
|
||
|
```bash
|
||
|
pnpm run build
|
||
|
```
|
||
|
|
||
|
The generated site will be placed in the `dist` folder.
|
||
|
|
||
|
9) Start a local web server:
|
||
|
|
||
|
```bash
|
||
|
npx http-server dist
|
||
|
```
|
||
|
|
||
|
Access the displayed URL (typically `http://localhost:8080`) with a web browser
|
||
|
and test the page.
|
||
|
|
||
|
</TabItem>
|
||
|
</Tabs>
|
||
|
|
||
|
When the page loads, the app will fetch <https://sheetjs.com/pres.xlsx> and
|
||
|
display the data from the first worksheet in a TABLE. The "Export XLSX" button
|
||
|
will generate a workbook that can be opened in a spreadsheet editor.
|
||
|
|
||
|
</details>
|
||
|
|
||
|
[^1]: See [`useState`](https://kaioken.dev/docs/hooks/usestate) in the Kaioken documentation.
|
||
|
[^2]: See [`useEffect`](https://kaioken.dev/docs/hooks/useeffect) in the Kaioken documentation.
|
||
|
[^3]: See [`useCallback`](https://kaioken.dev/docs/hooks/usecallback) in the Kaioken documentation.
|
||
|
[^4]: See [`useCallback`](https://kaioken.dev/docs/hooks/usecallback) in the Kaioken documentation.
|