├── .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 | --------------------------------------------------------------------------------