├── .github
└── workflows
│ ├── release.yml
│ └── test.yml
├── .gitignore
├── .prettierignore
├── .prettierrc.json
├── .releaserc
├── LICENSE
├── index.html
├── package-lock.json
├── package.json
├── readme.md
├── src
├── App.tsx
├── deps.ts
├── main.tsx
├── makeBuffer.ts
├── react-use-exceljs.ts
├── types.ts
└── vite-env.d.ts
├── test
└── makeBuffer.test.ts
├── tsconfig.json
├── tsconfig.node.json
└── vite.config.ts
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 | on:
3 | push:
4 | branches:
5 | - main
6 | jobs:
7 | release:
8 | name: Release
9 | runs-on: ubuntu-latest
10 | steps:
11 | - name: Checkout
12 | uses: actions/checkout@v3
13 | with:
14 | fetch-depth: 0
15 | - name: Setup Node.js
16 | uses: actions/setup-node@v3
17 | with:
18 | node-version: "lts/*"
19 | - name: Install dependencies
20 | run: npm install
21 | - name: Test
22 | run: npm test run
23 | - name: Build
24 | run: npm run build
25 | - name: Release
26 | env:
27 | GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
28 | GH_TOKEN: ${{ secrets.GH_TOKEN }}
29 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
30 | run: npx semantic-release
31 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Test
2 | on:
3 | pull_request:
4 | branches:
5 | - '*'
6 | jobs:
7 | test:
8 | name: Test
9 | runs-on: ubuntu-latest
10 | steps:
11 | - name: Checkout
12 | uses: actions/checkout@v3
13 | with:
14 | fetch-depth: 0
15 | - name: Setup Node.js
16 | uses: actions/setup-node@v3
17 | with:
18 | node-version: "lts/*"
19 | - name: Install dependencies
20 | run: npm install
21 | - name: Test
22 | run: npm test run
23 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | dist
2 | build
3 | package-lock.json
4 |
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "semi": false
3 | }
4 |
--------------------------------------------------------------------------------
/.releaserc:
--------------------------------------------------------------------------------
1 | {
2 | "branches": ["main"],
3 | "repositoryUrl": "https://github.com/dadamssg/react-use-exceljs"
4 | }
5 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 dadamssg
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | react-use-exceljs
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-use-exceljs",
3 | "repository": "https://github.com/dadamssg/react-use-exceljs",
4 | "version": "0.0.0",
5 | "type": "module",
6 | "main": "dist/react-use-exceljs.umd.cjs",
7 | "module": "dist/react-use-exceljs.js",
8 | "types": "dist/react-use-exceljs.d.ts",
9 | "scripts": {
10 | "dev": "vite",
11 | "build": "tsc && vite build",
12 | "preview": "vite preview",
13 | "test": "vitest"
14 | },
15 | "dependencies": {
16 | "exceljs": "^4.3.0",
17 | "file-saver": "^2.0.5"
18 | },
19 | "devDependencies": {
20 | "@types/file-saver": "^2.0.5",
21 | "@types/react": "^18.0.28",
22 | "@types/react-dom": "^18.0.11",
23 | "@vitejs/plugin-react": "^3.1.0",
24 | "prettier": "^2.8.7",
25 | "react": "^18.2.0",
26 | "react-dom": "^18.2.0",
27 | "semantic-release": "^21.0.1",
28 | "typescript": "^4.9.3",
29 | "vite": "^4.2.0",
30 | "vite-plugin-dts": "^2.2.0",
31 | "vitest": "^0.29.8"
32 | },
33 | "peerDependencies": {
34 | "react": "^16.8.0 || ^17.0.0 || ^18.0.0",
35 | "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | # react-use-exceljs
2 |
3 | A thin wrapper around the [exceljs](https://github.com/exceljs/exceljs) package that uses [file-saver](https://github.com/eligrey/FileSaver.js) for generating excel files.
4 |
5 | ## Usage
6 |
7 | ```tsx
8 | import { useExcelJS } from "react-use-exceljs"
9 |
10 | const data = [
11 | { id: 1, name: "Jane Doe", dob: new Date(1984, 6, 7) },
12 | { id: 2, name: "John Doe", dob: new Date(1965, 1, 7) },
13 | ]
14 |
15 | function App() {
16 | const excel = useExcelJS({
17 | filename: "testing.xlsx",
18 | worksheets: [
19 | {
20 | name: "Sheet 1",
21 | columns: [
22 | {
23 | header: "Id",
24 | key: "id",
25 | width: 10,
26 | },
27 | {
28 | header: "Name",
29 | key: "name",
30 | width: 32,
31 | },
32 | {
33 | header: "D.O.B.",
34 | key: "dob",
35 | width: 200,
36 | },
37 | ],
38 | },
39 | ],
40 | })
41 |
42 | const onClick = () => {
43 | excel.download(data)
44 | }
45 |
46 | return (
47 |
48 |
49 |
50 | )
51 | }
52 | ```
53 |
54 | Worksheets can use different sets of data if `download()` is passed a `Record>`.
55 |
56 | ```tsx
57 | const people = [
58 | { id: 1, name: "Jane Doe", dob: new Date(1984, 6, 7) },
59 | { id: 2, name: "John Doe", dob: new Date(1965, 1, 7) },
60 | ]
61 |
62 | const cities = [
63 | { city: 'Dallas' },
64 | { city: 'New York' },
65 | { city: 'Miami' },
66 | ]
67 |
68 | function App() {
69 | const excel = useExcelJS({
70 | filename: "testing.xlsx",
71 | worksheets: [
72 | {
73 | name: "People",
74 | columns: [
75 | {
76 | header: "Id",
77 | key: "id",
78 | width: 10,
79 | },
80 | {
81 | header: "Name",
82 | key: "name",
83 | width: 32,
84 | },
85 | {
86 | header: "D.O.B.",
87 | key: "dob",
88 | width: 200,
89 | },
90 | ],
91 | },
92 | {
93 | name: "Cities",
94 | columns: [
95 | {
96 | header: "City",
97 | key: "city",
98 | },
99 | ],
100 | },
101 | ],
102 | })
103 |
104 | const onClick = () => {
105 | excel.download({ People: people, Cities: cities })
106 | }
107 |
108 | return (
109 |
110 |
111 |
112 | )
113 | }
114 | ```
115 |
116 | You can pass an `intercept` function that will provide the generated workbook for you to modify before it is downloaded.
117 |
118 | ```tsx
119 | const excel = useExcelJS({
120 | filename: "testing.xlsx",
121 | intercept: (workbook) => {
122 | workbook.getWorksheet("Sheet 1").getColumn("id").fill = {
123 | type: "pattern",
124 | pattern: "solid",
125 | fgColor: { argb: "55e6ed" },
126 | }
127 | },
128 | worksheets: [
129 | {
130 | name: "Sheet 1",
131 | columns: [
132 | {
133 | header: "Id",
134 | key: "id",
135 | width: 10,
136 | },
137 | ],
138 | },
139 | ],
140 | })
141 | ```
142 |
143 | You can also use the non-hook function version which the `useExcelJS` uses internally.
144 |
145 | ```ts
146 | import { downloadExcelJS } from 'react-use-exceljs'
147 |
148 | const onClick = () => {
149 | downloadExcelJS({
150 | filename: "testing.xlsx",
151 | data: [
152 | {id: 1},
153 | {id: 2}
154 | ],
155 | worksheets: [
156 | {
157 | name: "Sheet 1",
158 | columns: [
159 | {
160 | header: "Id",
161 | key: "id",
162 | width: 10,
163 | },
164 | ],
165 | },
166 | ],
167 | })
168 | }
169 | ```
170 |
171 |
172 | ## Optimization
173 | The `file-saver` and the rather large `exceljs` packages are lazily loaded on initiation of an excel download.
174 |
--------------------------------------------------------------------------------
/src/App.tsx:
--------------------------------------------------------------------------------
1 | import { useExcelJS } from "./react-use-exceljs"
2 |
3 | const data = [
4 | { id: 1, name: "Jane Doe", dob: new Date(1984, 6, 7) },
5 | { id: 2, name: "John Doe", dob: new Date(1965, 1, 7) },
6 | ]
7 |
8 | function App() {
9 | const excel = useExcelJS({
10 | filename: "report.xlsx",
11 | intercept: (workbook) => {
12 | workbook.getWorksheet("Sheet 1").getColumn("id").fill = {
13 | type: "pattern",
14 | pattern: "solid",
15 | fgColor: { argb: "55e6ed" },
16 | }
17 | },
18 | worksheets: [
19 | {
20 | name: "Sheet 1",
21 | columns: [
22 | {
23 | header: "Id",
24 | key: "id",
25 | width: 10,
26 | },
27 | {
28 | header: "Name",
29 | key: "name",
30 | width: 32,
31 | },
32 | {
33 | header: "D.O.B.",
34 | key: "dob",
35 | width: 200,
36 | },
37 | ],
38 | },
39 | ],
40 | })
41 |
42 | const onClick = () => {
43 | void excel.download(data)
44 | }
45 | return (
46 |
47 |
48 |
49 | )
50 | }
51 |
52 | export default App
53 |
--------------------------------------------------------------------------------
/src/deps.ts:
--------------------------------------------------------------------------------
1 | import { saveAs } from "file-saver"
2 | import ExcelJS from 'exceljs'
3 |
4 | export default { saveAs , ExcelJS}
5 |
--------------------------------------------------------------------------------
/src/main.tsx:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import ReactDOM from "react-dom/client"
3 | import App from "./App"
4 |
5 | ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
6 |
7 |
8 |
9 | )
10 |
--------------------------------------------------------------------------------
/src/makeBuffer.ts:
--------------------------------------------------------------------------------
1 | import { InterceptFn, Sheet } from "./types"
2 |
3 | export default async function makeBuffer>({
4 | worksheets,
5 | data,
6 | intercept,
7 | }: {
8 | worksheets: T
9 | data: Array | Record>
10 | intercept?: InterceptFn
11 | }) {
12 | const { default: {ExcelJS} } = await import("./deps")
13 | let workbook = new ExcelJS.Workbook()
14 | for (const worksheet of worksheets) {
15 | const sheet = workbook.addWorksheet(worksheet.name)
16 | sheet.columns = worksheet.columns
17 | const rows = (Array.isArray(data) ? data : data[worksheet.name as T[number]["name"]]) ?? []
18 | sheet.addRows(rows)
19 | }
20 | workbook = intercept ? intercept(workbook) ?? workbook : workbook
21 | return await workbook.xlsx.writeBuffer()
22 | }
23 |
--------------------------------------------------------------------------------
/src/react-use-exceljs.ts:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import { type Data, type Filename, type InterceptFn, type Sheet } from "./types"
3 | import makeBuffer from "./makeBuffer"
4 |
5 | export function useExcelJS>({
6 | filename,
7 | worksheets,
8 | intercept,
9 | }: {
10 | worksheets: T
11 | filename?: Filename
12 | intercept?: InterceptFn
13 | }) {
14 | return {
15 | download: React.useCallback(
16 | (data: Data) =>
17 | downloadExcelJS({ filename, data, worksheets, intercept }),
18 | [filename, worksheets, intercept]
19 | ),
20 | }
21 | }
22 |
23 | export async function downloadExcelJS>({
24 | filename,
25 | data,
26 | worksheets,
27 | intercept,
28 | }: {
29 | worksheets: T
30 | data: Data
31 | filename?: Filename
32 | intercept?: InterceptFn
33 | }) {
34 | const buffer = await makeBuffer({ worksheets, data, intercept })
35 | const fileType =
36 | "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
37 | const blob = new Blob([buffer], { type: fileType })
38 | const {
39 | default: { saveAs },
40 | } = await import("./deps")
41 | saveAs(blob, filename ?? "workbook.xlsx")
42 | }
43 |
--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
1 | import { type Workbook, type Worksheet } from "exceljs"
2 |
3 | export type Sheet = { name: string; columns: Worksheet["columns"] }
4 | export type InterceptFn = (workbook: Workbook) => Workbook | void
5 | export type Filename = `${string}.xlsx`
6 | export type WorksheetName> = T[number]["name"]
7 | export type Data> =
8 | | Array
9 | | Record, Array>
10 |
--------------------------------------------------------------------------------
/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/test/makeBuffer.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, test } from "vitest"
2 | import ExcelJS from "exceljs"
3 | import makeBuffer from "../src/makeBuffer"
4 |
5 | const worksheetA = {
6 | name: "Sheet 1",
7 | columns: [
8 | {
9 | header: "Id",
10 | key: "id",
11 | width: 10,
12 | },
13 | {
14 | header: "Name",
15 | key: "name",
16 | width: 32,
17 | },
18 | {
19 | header: "D.O.B.",
20 | key: "dob",
21 | width: 200,
22 | },
23 | ],
24 | }
25 |
26 | const worksheetB = {
27 | name: "Cities",
28 | columns: [
29 | {
30 | header: "City",
31 | key: "city",
32 | },
33 | ],
34 | }
35 |
36 | test("can create one sheet", async () => {
37 | const buffer = await makeBuffer({
38 | data: [
39 | { id: 1, name: "Jane Doe", dob: new Date(1984, 6, 7) },
40 | { id: 2, name: "John Doe", dob: new Date(1965, 1, 7) },
41 | ],
42 | worksheets: [worksheetA],
43 | })
44 |
45 | const workbook = new ExcelJS.Workbook();
46 | await workbook.xlsx.load(buffer);
47 | const worksheet = workbook.getWorksheet(worksheetA.name)
48 |
49 | expect(worksheet.getCell('A1').text).toBe('Id')
50 | expect(worksheet.getCell('A2').text).toBe('1')
51 | expect(worksheet.getCell('A3').text).toBe('2')
52 | })
53 |
54 | test("can create two sheets", async () => {
55 | const buffer = await makeBuffer({
56 | data: {
57 | [worksheetA.name]: [
58 | { id: 1, name: "Jane Doe", dob: new Date(1984, 6, 7) },
59 | { id: 2, name: "John Doe", dob: new Date(1965, 1, 7) },
60 | ],
61 | [worksheetB.name]: [
62 | { city: 'Dallas' },
63 | { city: 'New York' },
64 | { city: 'Miami' },
65 | ],
66 | },
67 | worksheets: [worksheetA, worksheetB],
68 | })
69 |
70 | const workbook = new ExcelJS.Workbook();
71 | await workbook.xlsx.load(buffer);
72 | const sheetA = workbook.getWorksheet(worksheetA.name)
73 |
74 | expect(sheetA.getCell('A1').text).toBe('Id')
75 | expect(sheetA.getCell('A2').text).toBe('1')
76 | expect(sheetA.getCell('A3').text).toBe('2')
77 |
78 | const sheetB = workbook.getWorksheet(worksheetB.name)
79 |
80 | expect(sheetB.getCell('A1').text).toBe('City')
81 | expect(sheetB.getCell('A2').text).toBe('Dallas')
82 | expect(sheetB.getCell('A3').text).toBe('New York')
83 | expect(sheetB.getCell('A4').text).toBe('Miami')
84 | })
85 |
86 | test("can use intercept", async () => {
87 | const buffer = await makeBuffer({
88 | data: [
89 | { id: 1, name: "Jane Doe", dob: new Date(1984, 6, 7) },
90 | { id: 2, name: "John Doe", dob: new Date(1965, 1, 7) },
91 | ],
92 | worksheets: [worksheetA],
93 | intercept: (workbook) => {
94 | workbook.getWorksheet(worksheetA.name).getCell('A2').value = 'foobar'
95 | }
96 | })
97 |
98 | const workbook = new ExcelJS.Workbook();
99 | await workbook.xlsx.load(buffer);
100 | const worksheet = workbook.getWorksheet(worksheetA.name)
101 |
102 | expect(worksheet.getCell('A1').text).toBe('Id')
103 | expect(worksheet.getCell('A2').text).toBe('foobar')
104 | expect(worksheet.getCell('A3').text).toBe('2')
105 | })
106 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ESNext",
4 | "useDefineForClassFields": true,
5 | "lib": ["DOM", "DOM.Iterable", "ESNext"],
6 | "allowJs": false,
7 | "skipLibCheck": true,
8 | "esModuleInterop": false,
9 | "allowSyntheticDefaultImports": true,
10 | "strict": true,
11 | "forceConsistentCasingInFileNames": true,
12 | "module": "ESNext",
13 | "moduleResolution": "Node",
14 | "resolveJsonModule": true,
15 | "isolatedModules": true,
16 | "noEmit": true,
17 | "jsx": "react-jsx"
18 | },
19 | "include": ["src"],
20 | "references": [{ "path": "./tsconfig.node.json" }]
21 | }
22 |
--------------------------------------------------------------------------------
/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "module": "ESNext",
5 | "moduleResolution": "Node",
6 | "allowSyntheticDefaultImports": true
7 | },
8 | "include": ["vite.config.ts"]
9 | }
10 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { resolve } from "path"
2 | import { defineConfig } from "vite"
3 | import react from "@vitejs/plugin-react"
4 | import dts from "vite-plugin-dts"
5 |
6 | // https://vitejs.dev/config/
7 | export default defineConfig({
8 | plugins: [react(), dts()],
9 | build: {
10 | lib: {
11 | // Could also be a dictionary or array of multiple entry points
12 | entry: resolve(__dirname, "src/react-use-exceljs.ts"),
13 | name: "ReactUseExcelJS",
14 | // the proper extensions will be added
15 | fileName: "react-use-exceljs",
16 | },
17 | minify: false,
18 | rollupOptions: {
19 | // make sure to externalize deps that shouldn't be bundled
20 | // into your library
21 | external: ["react", "react-dom"],
22 | output: {
23 | // Provide global variables to use in the UMD build
24 | // for externalized deps
25 | globals: {
26 | react: "React",
27 | "react-dom": "ReactDOM",
28 | },
29 | },
30 | },
31 | },
32 | })
33 |
--------------------------------------------------------------------------------