├── .circleci
└── config.yml
├── .editorconfig
├── .github
└── workflows
│ └── codeql-analysis.yml
├── .gitignore
├── .npmignore
├── .npmrc
├── .prettierrc.json
├── CHANGELOG.md
├── LICENSE.md
├── README.md
├── eslint.config.mjs
├── example
├── README.md
├── package.json
├── public
│ └── index.html
└── src
│ ├── App.css
│ ├── App.js
│ ├── index.css
│ ├── index.js
│ └── logo.svg
├── mocha.bootstrap.js
├── package-lock.json
├── package.json
├── renovate.json
├── src
├── index.tsx
└── lib
│ ├── csv.spec.ts
│ └── csv.ts
├── tsconfig-cjs.json
└── tsconfig.json
/.circleci/config.yml:
--------------------------------------------------------------------------------
1 | version: 2.1
2 |
3 | orbs:
4 | node: circleci/node@7.1.0
5 |
6 | save: &save
7 | save_cache:
8 | key: code-{{ .Revision }}
9 | paths:
10 | - .
11 | - '~/.npm-global'
12 |
13 | restore: &restore
14 | restore_cache:
15 | key: code-{{ .Revision }}
16 |
17 | jobs:
18 | lint:
19 | docker:
20 | - image: cimg/base:stable
21 | steps:
22 | - checkout
23 | - node/install:
24 | node-version: '22'
25 | - run: npm i
26 | - run: cd example && npm i
27 | - run: npm run lint
28 | test:
29 | docker:
30 | - image: cimg/base:stable
31 | steps:
32 | - checkout
33 | - node/install:
34 | node-version: '22'
35 | - run: npm i
36 | - run: npm test
37 |
38 | workflows:
39 | version: 2
40 | main_workflow:
41 | jobs:
42 | - lint
43 | - test
44 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # http://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | # change these settings to your own preference
6 | indent_style = space
7 | indent_size = 2
8 |
9 | # it's recommend to keep these unchanged
10 | end_of_line = lf
11 | charset = utf-8
12 | trim_trailing_whitespace = true
13 | insert_final_newline = true
14 |
15 | [*.md]
16 | trim_trailing_whitespace = false
17 |
18 | [{package,bower}.json]
19 | indent_style = space
20 | indent_size = 2
21 |
--------------------------------------------------------------------------------
/.github/workflows/codeql-analysis.yml:
--------------------------------------------------------------------------------
1 | # For most projects, this workflow file will not need changing; you simply need
2 | # to commit it to your repository.
3 | #
4 | # You may wish to alter this file to override the set of languages analyzed,
5 | # or to provide custom queries or build logic.
6 | name: "CodeQL"
7 |
8 | on:
9 | push:
10 | branches: [master]
11 | pull_request:
12 | # The branches below must be a subset of the branches above
13 | branches: [master]
14 | schedule:
15 | - cron: '0 7 * * 0'
16 |
17 | jobs:
18 | analyze:
19 | name: Analyze
20 | runs-on: ubuntu-latest
21 |
22 | strategy:
23 | fail-fast: false
24 | matrix:
25 | # Override automatic language detection by changing the below list
26 | # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python']
27 | language: ['javascript']
28 | # Learn more...
29 | # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection
30 |
31 | steps:
32 | - name: Checkout repository
33 | uses: actions/checkout@v4
34 | with:
35 | # We must fetch at least the immediate parents so that if this is
36 | # a pull request then we can checkout the head.
37 | fetch-depth: 2
38 |
39 | # If this run was triggered by a pull request event, then checkout
40 | # the head of the pull request instead of the merge commit.
41 | - run: git checkout HEAD^2
42 | if: ${{ github.event_name == 'pull_request' }}
43 |
44 | # Initializes the CodeQL tools for scanning.
45 | - name: Initialize CodeQL
46 | uses: github/codeql-action/init@v3
47 | with:
48 | languages: ${{ matrix.language }}
49 | # If you wish to specify custom queries, you can do so here or in a config file.
50 | # By default, queries listed here will override any specified in a config file.
51 | # Prefix the list here with "+" to use these queries and those in the config file.
52 | # queries: ./path/to/local/query, your-org/your-repo/queries@main
53 |
54 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
55 | # If this step fails, then you should remove it and run the build manually (see below)
56 | - name: Autobuild
57 | uses: github/codeql-action/autobuild@v3
58 |
59 | # ℹ️ Command-line programs to run using the OS shell.
60 | # 📚 https://git.io/JvXDl
61 |
62 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
63 | # and modify them (or add more) to build your code if your project
64 | # uses a compiled language
65 |
66 | #- run: |
67 | # make bootstrap
68 | # make release
69 |
70 | - name: Perform CodeQL Analysis
71 | uses: github/codeql-action/analyze@v3
72 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | .vscode
3 | node_modules/
4 | dist/
5 | example/package-lock.json
6 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | .*
2 | mocha*
3 | node_modules/
4 | example/
5 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | save=true
2 | save-exact=true
3 |
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "trailingComma": "es5",
3 | "tabWidth": 2,
4 | "useTabs": false,
5 | "semi": false,
6 | "singleQuote": true,
7 | "jsxSingleQuote": false,
8 | "jsxBracketSameLine": false,
9 | "bracketSpacing": true,
10 | "printWidth": 120,
11 | "quoteProps": "consistent",
12 | "endOfLine": "lf",
13 | "overrides": [
14 | {
15 | "files": "*.md",
16 | "options": {
17 | "semi": true
18 | }
19 | }
20 | ]
21 | }
22 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Change Log
2 |
3 | ## [3.3.0] - 2024-12-30
4 |
5 | ### Added
6 |
7 | - React 19 support [#460](https://github.com/dolezel/react-csv-downloader/pull/460)
8 |
9 | ## [3.2.0] - 2024-12-18
10 |
11 | ### Added
12 |
13 | - Allowing number type [#450](https://github.com/dolezel/react-csv-downloader/pull/450)
14 |
15 | ## [3.1.1] - 2024-09-17
16 |
17 | ### Fixed
18 |
19 | - Fix duplicate columns [#438](https://github.com/dolezel/react-csv-downloader/pull/438)
20 |
21 | ## [3.1.0] - 2024-01-06
22 |
23 | ### Added
24 |
25 | - Ability to add title at top of the csv [#386](https://github.com/dolezel/react-csv-downloader/pull/386)
26 |
27 | ## [3.0.0] - 2023-10-29
28 |
29 | ### Fixed
30 |
31 | - Ability to specify column order for arrays of data [#375](https://github.com/dolezel/react-csv-downloader/pull/375)
32 |
33 | ## [2.9.1] - 2023-04-07
34 |
35 | ### Fixed
36 |
37 | - Fix passing props [#347](https://github.com/dolezel/react-csv-downloader/pull/347)
38 |
39 | ## [2.9.0] - 2022-11-18
40 |
41 | ### Changed
42 |
43 | - Fix sourcemaps [#321](https://github.com/dolezel/react-csv-downloader/pull/321)
44 | - Handle error and empty data [#322](https://github.com/dolezel/react-csv-downloader/pull/322)
45 |
46 | ## [2.8.0] - 2022-04-22
47 |
48 | ### Changed
49 |
50 | - Removed tslint, added eslint [#313](https://github.com/dolezel/react-csv-downloader/pull/313)
51 | - Installs without error with React 18
52 | - Also fixes error when data is null or undefined
53 |
54 | ## [2.7.1] - 2021-12-15
55 |
56 | ### Added
57 |
58 | - Loosen supported npm version [#301](https://github.com/dolezel/react-csv-downloader/pull/301)
59 |
60 | ## [2.7.0] - 2021-07-09
61 |
62 | ### Added
63 |
64 | - adds support for sep meta instruction [#265](https://github.com/dolezel/react-csv-downloader/pull/265)
65 | - Making engine less restrictive [#267](https://github.com/dolezel/react-csv-downloader/pull/267)
66 |
67 | ## [2.6.0] - 2021-07-03
68 |
69 | ### Added
70 |
71 | - add support to async datas resolution [#259](https://github.com/dolezel/react-csv-downloader/pull/259)
72 | - Prettier config [#260](https://github.com/dolezel/react-csv-downloader/pull/260)
73 | - Exporting toCsv function [#261](https://github.com/dolezel/react-csv-downloader/pull/261)
74 | - Nulls and undefineds should be converted to empty strings [#262](https://github.com/dolezel/react-csv-downloader/pull/262)
75 |
76 | ## [2.5.0] - 2021-06-19
77 |
78 | ### Added
79 |
80 | - Add disabled option [#252](https://github.com/dolezel/react-csv-downloader/pull/252)
81 |
82 | ## [2.4.0] - 2021-05-16
83 |
84 | ### Changed
85 |
86 | - Proper main/module resolution, using es2015 target [#245](https://github.com/dolezel/react-csv-downloader/pull/245)
87 |
88 | ## [2.3.0] - 2021-05-16
89 |
90 | ### Added
91 |
92 | - Optional extension [#244](https://github.com/dolezel/react-csv-downloader/pull/244)
93 |
94 | ## [2.2.0] - 2020-10-26
95 |
96 | ### Added
97 |
98 | - Support for React v17 [#215](https://github.com/dolezel/react-csv-downloader/pull/215)
99 |
100 | ## [2.1.0] - 2020-09-16
101 |
102 | ### Added
103 |
104 | - Passing props to children [#206](https://github.com/dolezel/react-csv-downloader/pull/206)
105 |
106 | ## [2.0.2] - 2020-08-03
107 |
108 | ### Fixed
109 |
110 | - Fix processing empty data [#195](https://github.com/dolezel/react-csv-downloader/pull/195)
111 |
112 | ## [2.0.1] - 2020-08-03
113 |
114 | ### Fixed
115 |
116 | - Fix processing chunks [#194](https://github.com/dolezel/react-csv-downloader/pull/194)
117 |
118 | ## [2.0.0] - 2020-07-16
119 |
120 | ### Breaking changes
121 |
122 | - Async processing with splitting to chunks [#187](https://github.com/dolezel/react-csv-downloader/pull/187)
123 |
124 | `csv` function is now async and contents of CSV file are generated asynchronously
125 |
126 | ## [1.9.0] - 2020-07-16
127 |
128 | ### Added
129 |
130 | - Option for new line at end of file [#185](https://github.com/dolezel/react-csv-downloader/pull/185)
131 |
132 | ## [1.8.0] - 2020-04-23
133 |
134 | ### Changed
135 |
136 | - Compiling as commonjs [#165](https://github.com/dolezel/react-csv-downloader/pull/165)
137 |
138 | ## [1.7.0] - 2020-04-23
139 |
140 | ### Changed
141 |
142 | - Compute data on click [#163](https://github.com/dolezel/react-csv-downloader/pull/163)
143 |
144 | ## [1.6.0] - 2020-03-27
145 |
146 | ### Added
147 |
148 | - Rewritten in Typescript [#123](https://github.com/dolezel/react-csv-downloader/pull/123)
149 |
150 | ## [1.5.0] - 2019-12-17
151 |
152 | ### Added
153 |
154 | - Add functionality to wrap column value with any character [#138](https://github.com/dolezel/react-csv-downloader/pull/138)
155 |
156 | ## [1.4.0] - 2019-10-04
157 |
158 | ### Changed
159 |
160 | - Refactoring code, removing deprecated componentWillReceiveProps and computing csv when needed [#121](https://github.com/dolezel/react-csv-downloader/pull/121)
161 |
162 | ## [1.3.0] - 2019-10-03
163 |
164 | ### Changed
165 |
166 | - Using file-saver for cross-browser support of saving files [#120](https://github.com/dolezel/react-csv-downloader/pull/120)
167 |
168 | ## [1.2.0] - 2019-10-03
169 |
170 | ### Changed
171 |
172 | - dependencies and dev dependencies
173 | - create-react-app for example [#119](https://github.com/dolezel/react-csv-downloader/pull/119)
174 |
175 | ## [1.1.0] - 2019-01-23
176 |
177 | ### Fixed
178 |
179 | - Changing BOM to \ufeff [#72](https://github.com/dolezel/react-csv-downloader/pull/72)
180 |
181 | ## [1.0.0] - 2019-01-11
182 |
183 | ### Changed
184 |
185 | - Fixed up click event for mozilla (Using MouseEvent) [#66](https://github.com/dolezel/react-csv-downloader/pull/66)
186 | - Upgraded dependencies, example fixes, peerDependencies [#67](https://github.com/dolezel/react-csv-downloader/pull/67)
187 |
188 | ## [0.2.0] - 2018-09-04
189 |
190 | ### Changed
191 |
192 | - Use Blob instead of Data URI to support large file downloads [#1](https://github.com/dolezel/react-csv-downloader/pull/1)
193 | - Updated dependencies and rewritten tooling
194 |
195 | ## [0.1.1] - 2016-05-03
196 |
197 | ### Fixed
198 |
199 | - license name in package.json
200 | - dependencies and dev dependencies
201 |
202 | ## [0.1.0] - 2016-02-06
203 |
204 | ### Added
205 |
206 | - Initial commit
207 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | Copyright (c) 2016 Castellant Guillaume
2 |
3 | This software is released under the MIT license:
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
13 | all 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
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # React CSV Downloader
2 |
3 | [![Renovate badge][renovate-badge]][renovate]
4 | [![CircleCI Status][build-badge]][build]
5 | [![Dependency Status][deps-badge]][deps]
6 | [![devDependency Status][dev-deps-badge]][dev-deps]
7 |
8 | A simple react component to allow download CSV file from js object
9 |
10 | ## Installation
11 |
12 | ```sh
13 | npm install --save react-csv-downloader
14 | ```
15 |
16 | ## Usage
17 |
18 | Use with children component
19 |
20 | ```jsx
21 | import CsvDownloader from 'react-csv-downloader';
22 |
23 |
24 | ;
25 | ```
26 |
27 | Use without children component
28 |
29 | ```jsx
30 |
31 | ```
32 |
33 | ### Datas
34 |
35 | pass the downloaded datas as a component prop
36 |
37 | ```jsx
38 | const datas = [
39 | {
40 | cell1: 'row 1 - cell 1',
41 | cell2: 'row 1 - cell 2',
42 | },
43 | {
44 | cell1: 'row 2 - cell 1',
45 | cell2: 'row 2 - cell 2',
46 | },
47 | ];
48 |
49 | ;
50 | ```
51 |
52 | ### Datas (on demand with async function resolver)
53 |
54 | pass a function to compute datas to be downloaded
55 |
56 | ```jsx
57 | const asyncFnComputeDate = () => {
58 | // do whatever you need async
59 | return Promise.resolve([
60 | {
61 | cell1: 'row 1 - cell 1',
62 | cell2: 'row 1 - cell 2',
63 | },
64 | {
65 | cell1: 'row 2 - cell 1',
66 | cell2: 'row 2 - cell 2',
67 | },
68 | ]);
69 | };
70 |
71 | ;
72 | ```
73 |
74 | ### Column
75 |
76 | pass the columns definition as a component prop to change the cell display name. If column isn't passed the cell display name is automatically defined with datas keys
77 |
78 | ```jsx
79 | const columns = [
80 | {
81 | id: 'cell1',
82 | displayName: 'Cell 1',
83 | },
84 | {
85 | id: 'cell2',
86 | displayName: 'Cell 2',
87 | },
88 | ];
89 |
90 | ;
91 | ```
92 |
93 | You can also use the columns definition to set the columns display order
94 |
95 | ## Props
96 |
97 | | Name | Type | Default | Required | Description |
98 | | -------------- | ---------------------------- | --------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
99 | | columns | array of object | null | false | Columns definition |
100 | | datas | array of object/Func/Promise | null | true | Downloaded datas or a Promise or a function that can resolve data on demand (async) |
101 | | filename | string | null | true | You can pass the filename without extension. The extension is automatically added |
102 | | extension | string | '.csv' | false | You can pass the file extension, note that it will affect filename |
103 | | separator | string | ',' | false | Columns separator |
104 | | noHeader | boolean | false | false | If `true` the header isn't added to the csv file |
105 | | prefix | string or boolean | false | false | Filename prefix. If `true` prefix becomes a timestamp |
106 | | suffix | string or boolean | false | false | Filename suffix/postfix. If `true` suffix becomes a timestamp |
107 | | text | string | null | false | Download button text. Used if no children component. |
108 | | wrapColumnChar | string | '' | false | Character to wrap every data and header value with. |
109 | | bom | boolean | true | false | Activate or deactivate bom mode |
110 | | newLineAtEnd | boolean | false | false | Insert new line at end of file. |
111 | | disabled | boolean | false | false | If `true` the download process is blocked. |
112 | | meta | boolean | false | false | If `true` the downloaded file will contain meta instruction sep to help microsoft excel and open office to recognize the sepator character. |
113 | | handleError | function | undefined | false | Function to be invoked on error data |
114 | | handleEmpty | function | undefined | false | Function to be invoked on empty result data
115 | | title | string | undefined | false | You can pass a string to be added as a title at the top of the sheet
116 |
117 | All other props are passed to button or wrapping component.
118 |
119 | ## Full example
120 |
121 | pass the downloaded datas as a component prop
122 |
123 | ```jsx
124 | render() {
125 | const columns = [{
126 | id: 'first',
127 | displayName: 'First column'
128 | }, {
129 | id: 'second',
130 | displayName: 'Second column'
131 | }];
132 |
133 | const datas = [{
134 | first: 'foo',
135 | second: 'bar'
136 | }, {
137 | first: 'foobar',
138 | second: 'foobar'
139 | }];
140 |
141 | return (
142 |
143 |
151 |
152 | );
153 | }
154 |
155 | // content of myfile.csv
156 | // 'First column';'Second column'
157 | // 'foo';'bar'
158 | // 'foobar';'foobar'
159 | ```
160 |
161 | ## Get CSV contents
162 |
163 | If you just need to get CSV contents, use `import { toCsv } from 'react-csv-downloader';` to import toCsv function and use it directly.
164 |
165 | ## License
166 |
167 | [MIT License](http://opensource.org/licenses/MIT)
168 |
169 | [renovate-badge]: https://img.shields.io/badge/renovate-enabled-brightgreen.svg
170 | [renovate]: https://renovatebot.com/
171 | [build-badge]: https://circleci.com/gh/dolezel/react-csv-downloader.svg?style=svg
172 | [build]: https://circleci.com/gh/dolezel/workflows/react-csv-downloader
173 | [deps-badge]: https://david-dm.org/dolezel/react-csv-downloader.svg
174 | [deps]: https://david-dm.org/dolezel/react-csv-downloader
175 | [dev-deps-badge]: https://david-dm.org/dolezel/react-csv-downloader/dev-status.svg
176 | [dev-deps]: https://david-dm.org/dolezel/react-csv-downloader#info=devDependencies
177 |
--------------------------------------------------------------------------------
/eslint.config.mjs:
--------------------------------------------------------------------------------
1 | import importPlugin from 'eslint-plugin-import'
2 | import prettierPluginRecomended from 'eslint-plugin-prettier/recommended'
3 | import reactPlugin from 'eslint-plugin-react'
4 | import reactHooksPlugin from 'eslint-plugin-react-hooks'
5 | import sonarjsPlugin from 'eslint-plugin-sonarjs'
6 | import globals from 'globals'
7 | import eslintJs from '@eslint/js'
8 | import eslintTs from 'typescript-eslint' // eslint-disable-line import/no-unresolved
9 |
10 | const config = eslintTs.config(
11 | eslintJs.configs.recommended,
12 | ...eslintTs.configs.recommended,
13 | importPlugin.flatConfigs.recommended,
14 | importPlugin.flatConfigs.typescript,
15 | reactPlugin.configs.flat.recommended,
16 | {
17 | plugins: {
18 | 'react-hooks': reactHooksPlugin,
19 | },
20 | rules: {
21 | 'react/react-in-jsx-scope': 'off',
22 | ...reactHooksPlugin.configs.recommended.rules,
23 | },
24 | },
25 | sonarjsPlugin.configs.recommended,
26 | prettierPluginRecomended,
27 | {
28 | ignores: ['node_modules/', 'dist/'],
29 | },
30 | {
31 | languageOptions: {
32 | globals: {
33 | ...globals.node,
34 | ...globals.browser,
35 | ...globals.mocha,
36 | ...globals.es2021,
37 | },
38 | ecmaVersion: 'latest',
39 | sourceType: 'module',
40 | },
41 | },
42 | {
43 | files: ['**/*.js'],
44 | rules: {
45 | '@typescript-eslint/no-require-imports': 'off',
46 | },
47 | }
48 | )
49 |
50 | export default config
51 |
--------------------------------------------------------------------------------
/example/README.md:
--------------------------------------------------------------------------------
1 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
2 |
3 | ## Available Scripts
4 |
5 | In the project directory, you can run:
6 |
7 | ### `npm start`
8 |
9 | Runs the app in the development mode.
10 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
11 |
12 | The page will reload if you make edits.
13 | You will also see any lint errors in the console.
14 |
15 | ### `npm test`
16 |
17 | Launches the test runner in the interactive watch mode.
18 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
19 |
20 | ### `npm run build`
21 |
22 | Builds the app for production to the `build` folder.
23 | It correctly bundles React in production mode and optimizes the build for the best performance.
24 |
25 | The build is minified and the filenames include the hashes.
26 | Your app is ready to be deployed!
27 |
28 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
29 |
30 | ### `npm run eject`
31 |
32 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!**
33 |
34 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
35 |
36 | Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
37 |
38 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
39 |
40 | ## Learn More
41 |
42 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
43 |
44 | To learn React, check out the [React documentation](https://reactjs.org/).
45 |
46 | ### Code Splitting
47 |
48 | This section has moved here: https://facebook.github.io/create-react-app/docs/code-splitting
49 |
50 | ### Analyzing the Bundle Size
51 |
52 | This section has moved here: https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size
53 |
54 | ### Making a Progressive Web App
55 |
56 | This section has moved here: https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app
57 |
58 | ### Advanced Configuration
59 |
60 | This section has moved here: https://facebook.github.io/create-react-app/docs/advanced-configuration
61 |
62 | ### Deployment
63 |
64 | This section has moved here: https://facebook.github.io/create-react-app/docs/deployment
65 |
66 | ### `npm run build` fails to minify
67 |
68 | This section has moved here: https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify
69 |
--------------------------------------------------------------------------------
/example/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-csv-downloader-example",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "react": "19.1.0",
7 | "react-csv-downloader": "file:../",
8 | "react-dom": "19.1.0",
9 | "react-scripts": "5.0.1"
10 | },
11 | "scripts": {
12 | "start": "react-scripts start"
13 | },
14 | "eslintConfig": {
15 | "extends": "react-app"
16 | },
17 | "browserslist": {
18 | "production": [
19 | ">0.2%",
20 | "not dead",
21 | "not op_mini all"
22 | ],
23 | "development": [
24 | "last 1 chrome version",
25 | "last 1 firefox version",
26 | "last 1 safari version",
27 | "last 1 ie version"
28 | ]
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/example/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | react-csv-downloader example
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/example/src/App.css:
--------------------------------------------------------------------------------
1 | .App {
2 | text-align: center;
3 | }
4 |
5 | .App-logo {
6 | height: 40vmin;
7 | }
8 |
9 | .App-header {
10 | background-color: #282c34;
11 | min-height: 100vh;
12 | display: flex;
13 | flex-direction: column;
14 | align-items: center;
15 | justify-content: center;
16 | font-size: calc(10px + 2vmin);
17 | color: white;
18 | }
19 |
--------------------------------------------------------------------------------
/example/src/App.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import logo from './logo.svg'
3 | import './App.css'
4 |
5 | import CsvDownloader from 'react-csv-downloader'
6 |
7 | const head = [
8 | {
9 | id: 'first',
10 | displayName: 'First column',
11 | },
12 | {
13 | id: 'second',
14 | displayName: 'Second column',
15 | },
16 | ]
17 |
18 | const datas = [
19 | {
20 | first: 'foo',
21 | second: 'bar',
22 | },
23 | {
24 | first: 'foobar',
25 | second: 'foobar',
26 | },
27 | ]
28 |
29 | const asyncComputeDatas = async () => {
30 | return Promise.resolve(datas)
31 | }
32 |
33 | const asyncUndefined = async () => {
34 | return Promise.resolve(undefined)
35 | }
36 |
37 | function App() {
38 | return (
39 |
40 |
41 |
42 |
43 |
44 |
51 |
52 |
60 |
61 |
68 |
69 |
70 | )
71 | }
72 |
73 | export default App
74 |
--------------------------------------------------------------------------------
/example/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans',
4 | 'Droid Sans', 'Helvetica Neue', sans-serif;
5 | -webkit-font-smoothing: antialiased;
6 | -moz-osx-font-smoothing: grayscale;
7 | }
8 |
9 | code {
10 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace;
11 | }
12 |
--------------------------------------------------------------------------------
/example/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import * as ReactDOMClient from 'react-dom/client'
3 | import './index.css'
4 | import App from './App'
5 |
6 | const container = document.getElementById('root')
7 | const root = ReactDOMClient.createRoot(container)
8 | root.render()
9 |
--------------------------------------------------------------------------------
/example/src/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/mocha.bootstrap.js:
--------------------------------------------------------------------------------
1 | const config = require('./tsconfig.json')
2 |
3 | config.compilerOptions.module = 'commonjs'
4 | config.transpileOnly = true
5 |
6 | require('ts-node').register(config)
7 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-csv-downloader",
3 | "version": "3.3.0",
4 | "description": "React csv downloader",
5 | "main": "dist/cjs/index.js",
6 | "module": "dist/esm/index.js",
7 | "types": "dist/esm/index.d.ts",
8 | "scripts": {
9 | "test": "mocha --require ./mocha.bootstrap.js --require chai/register-expect.js \"src/**/*.spec.ts\"",
10 | "lint": "eslint",
11 | "format": "prettier --ignore-path .gitignore --write \"**/*.+(js|jsx|ts|tsx|json)\"",
12 | "start": "npm start --prefix example",
13 | "prebuild": "rimraf dist",
14 | "build": "tsc -p tsconfig.json && tsc -p tsconfig-cjs.json",
15 | "prepare": "npm run build"
16 | },
17 | "repository": {
18 | "type": "git",
19 | "url": "https://github.com/dolezel/react-csv-downloader.git"
20 | },
21 | "keywords": [
22 | "React",
23 | "CSV",
24 | "Export",
25 | "Download"
26 | ],
27 | "author": "Castellant Guillaume ",
28 | "contributors": [
29 | "Jan Dolezel ",
30 | "Herbert Pimentel "
31 | ],
32 | "license": "MIT",
33 | "bugs": {
34 | "url": "https://github.com/dolezel/react-csv-downloader/issues"
35 | },
36 | "homepage": "https://github.com/dolezel/react-csv-downloader",
37 | "engines": {
38 | "npm": ">=7.0.0"
39 | },
40 | "dependencies": {
41 | "file-saver": "^2.0.2"
42 | },
43 | "devDependencies": {
44 | "@eslint/js": "9.30.1",
45 | "@types/chai": "5.2.2",
46 | "@types/file-saver": "2.0.7",
47 | "@types/mocha": "10.0.10",
48 | "@types/node": "22.16.0",
49 | "@types/react": "19.1.8",
50 | "chai": "5.2.0",
51 | "eslint": "9.30.1",
52 | "eslint-config-prettier": "10.1.5",
53 | "eslint-plugin-import": "2.32.0",
54 | "eslint-plugin-prettier": "5.5.1",
55 | "eslint-plugin-react": "7.37.5",
56 | "eslint-plugin-react-hooks": "5.2.0",
57 | "eslint-plugin-sonarjs": "3.0.4",
58 | "globals": "16.3.0",
59 | "mocha": "11.7.1",
60 | "prettier": "3.6.2",
61 | "react": "19.1.0",
62 | "rimraf": "6.0.1",
63 | "ts-node": "10.9.2",
64 | "typescript": "5.8.3",
65 | "typescript-eslint": "8.35.1"
66 | },
67 | "peerDependencies": {
68 | "react": "^16.6.3 || ^17.0.0 || ^18.0.0 || ^19.0.0"
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "config:base",
4 | ":automergeMinor",
5 | ":automergeLinters",
6 | "schedule:weekly",
7 | "group:allNonMajor",
8 | ":pinOnlyDevDependencies"
9 | ]
10 | }
11 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import * as FileSaver from 'file-saver'
2 | import * as React from 'react'
3 |
4 | import toCsv, { ICsvProps } from './lib/csv'
5 |
6 | export { toCsv, ICsvProps }
7 |
8 | export type PrefixSuffix = boolean | string | number
9 |
10 | export interface ICsvDownloadProps
11 | extends ICsvProps,
12 | Omit, 'prefix'> {
13 | bom?: boolean
14 | filename: string
15 | extension?: string
16 | prefix?: PrefixSuffix
17 | suffix?: PrefixSuffix
18 | text?: string
19 | disabled?: boolean
20 | meta?: boolean
21 | handleError?: (err: unknown) => void
22 | handleEmpty?: () => void
23 | title?: string
24 | }
25 |
26 | export default class CsvDownload extends React.Component {
27 | public handleClick = async () => {
28 | const { suffix, prefix, bom, extension, disabled, meta, separator, handleError, handleEmpty } = this.props
29 |
30 | if (disabled) {
31 | return
32 | }
33 |
34 | let { filename } = this.props
35 | let csv: string | void
36 | try {
37 | csv = await toCsv(this.props)
38 | } catch (err) {
39 | return handleError?.(err)
40 | }
41 | if (!csv) {
42 | if (handleEmpty) {
43 | return handleEmpty()
44 | } else {
45 | csv = ''
46 | }
47 | }
48 | const bomCode = bom !== false ? '\ufeff' : ''
49 | const metaContent = meta ? `sep=${separator}\r\n` : ''
50 |
51 | const resolvedExtension = extension || '.csv'
52 | if (filename.indexOf(resolvedExtension) === -1) {
53 | filename += resolvedExtension
54 | }
55 |
56 | if (suffix) {
57 | filename =
58 | typeof suffix === 'string' || typeof suffix === 'number'
59 | ? filename.replace(resolvedExtension, `_${suffix}${resolvedExtension}`)
60 | : filename.replace(resolvedExtension, `_${new Date().getTime()}${resolvedExtension}`)
61 | }
62 |
63 | if (prefix) {
64 | filename =
65 | typeof prefix === 'string' || typeof prefix === 'number'
66 | ? `${prefix}_${filename}`
67 | : `${new Date().getTime()}_${filename}`
68 | }
69 |
70 | const blob = new Blob([`${bomCode}${metaContent}${csv}`], {
71 | type: 'text/csv;charset=utf-8',
72 | })
73 | FileSaver.saveAs(blob, filename)
74 | }
75 |
76 | public render() {
77 | const {
78 | children,
79 | text,
80 | disabled,
81 | /* eslint-disable @typescript-eslint/no-unused-vars */
82 | bom,
83 | filename,
84 | extension,
85 | prefix,
86 | suffix,
87 | meta,
88 | handleError,
89 | handleEmpty,
90 | columns,
91 | datas,
92 | separator,
93 | noHeader,
94 | wrapColumnChar,
95 | newLineAtEnd,
96 | chunkSize,
97 | /* eslint-enable @typescript-eslint/no-unused-vars */
98 | ...props
99 | } = this.props
100 |
101 | if (typeof children === 'undefined') {
102 | return (
103 |
106 | )
107 | }
108 |
109 | return (
110 |
111 | {children}
112 |
113 | )
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/src/lib/csv.spec.ts:
--------------------------------------------------------------------------------
1 | import csv from './csv'
2 |
3 | const expect = (globalThis as { expect?: Chai.ExpectStatic }).expect!
4 |
5 | const newLine = '\r\n'
6 |
7 | const columnSet1 = [{ id: 'cell1' }]
8 | const columnSet2 = [{ id: 'cell1' }, { id: 'cell2' }]
9 | const columnSet3 = [{ id: 'cell1', displayName: 'Cell name' }]
10 | const columnSet4 = [{ id: 'cell2' }, { id: 'cell1' }]
11 | const columnSet5 = [
12 | { displayName: 'FirstC', id: '2' },
13 | { displayName: 'SecondC', id: '1' },
14 | { displayName: 'ThirdC', id: '0' },
15 | ]
16 |
17 | const dataSet1 = [{ cell1: 'row1' }]
18 | const dataSet2 = [{ cell1: 'row1', cell2: 'row1' }]
19 | const dataSet3 = [['cell1', 'cell2']]
20 | const dataSet4 = [
21 | ['cell1', 'cell2'],
22 | ['cell1', 'cell2'],
23 | ]
24 | const dataSet5 = [{ cell1: 'row1' }, { cell1: 'row2' }]
25 | const dataSet6 = [{ cell1: 'row1' }, { cell1: null }, { cell1: 'row3' }]
26 | const dataSet7 = [{ cell1: 'row1' }, { cell1: undefined }, { cell1: 'row3' }]
27 | const dataSet8 = [['ThirdD', 'SecondD', 'FirstD']]
28 |
29 | describe('CSV Creator', () => {
30 | it('Should work with empty data', async () => {
31 | const result = await csv({ columns: [], datas: [] })
32 | expect(result).to.equal(``)
33 | })
34 |
35 | describe('Default separator', () => {
36 | const separator = ','
37 |
38 | it('Single cell', async () => {
39 | const result = await csv({ columns: columnSet1, datas: dataSet1 })
40 | expect(result).to.equal(`cell1${newLine}row1`)
41 | })
42 |
43 | it('Multiple cell', async () => {
44 | const result = await csv({ columns: columnSet2, datas: dataSet2 })
45 | expect(result).to.equal(`cell1${separator}cell2${newLine}row1${separator}row1`)
46 | })
47 |
48 | it('Header display name', async () => {
49 | const result = await csv({ columns: columnSet3, datas: dataSet1 })
50 | expect(result).to.equal(`Cell name${newLine}row1`)
51 | })
52 |
53 | it('Ordered cell', async () => {
54 | const result = await csv({ columns: columnSet4, datas: dataSet2 })
55 | expect(result).to.equal(`cell2${separator}cell1${newLine}row1${separator}row1`)
56 | })
57 |
58 | it('Ordered cell 2', async () => {
59 | const result = await csv({ columns: columnSet5, datas: dataSet8 })
60 | expect(result).to.equal(
61 | `FirstC${separator}SecondC${separator}ThirdC${newLine}FirstD${separator}SecondD${separator}ThirdD`
62 | )
63 | })
64 |
65 | it('No header', async () => {
66 | const result = await csv({
67 | columns: columnSet1,
68 | datas: dataSet1,
69 | separator,
70 | noHeader: true,
71 | })
72 | expect(result).to.equal('row1')
73 | })
74 |
75 | it('Auto header', async () => {
76 | const result = await csv({ columns: false, datas: dataSet2 })
77 | expect(result).to.equal(`cell1${separator}cell2${newLine}row1${separator}row1`)
78 | })
79 |
80 | it('array of array datas - single row', async () => {
81 | const result = await csv({ columns: false, datas: dataSet3 })
82 | expect(result).to.equal(`cell1${separator}cell2`)
83 | })
84 |
85 | it('array of array datas - multiple row', async () => {
86 | const result = await csv({ columns: false, datas: dataSet4 })
87 | expect(result).to.equal(`cell1${separator}cell2${newLine}cell1${separator}cell2`)
88 | })
89 |
90 | it('array of array datas - with header', async () => {
91 | const result = await csv({ columns: columnSet4, datas: dataSet4 })
92 | expect(result).to.equal(`cell2${separator}cell1${newLine}cell1${separator}cell2${newLine}cell1${separator}cell2`)
93 | })
94 | })
95 |
96 | describe('Column Wrap', () => {
97 | const separator = ','
98 | const wrapColumnChar = '"'
99 |
100 | it('Single cell', async () => {
101 | const result = await csv({
102 | columns: columnSet1,
103 | datas: dataSet1,
104 | wrapColumnChar,
105 | })
106 | expect(result).to.equal(`${wrapColumnChar}cell1${wrapColumnChar}${newLine}${wrapColumnChar}row1${wrapColumnChar}`)
107 | })
108 |
109 | it('Multiple cell', async () => {
110 | const result = await csv({
111 | columns: columnSet2,
112 | datas: dataSet2,
113 | wrapColumnChar,
114 | })
115 | expect(result).to.equal(
116 | `${wrapColumnChar}cell1${wrapColumnChar}${separator}${wrapColumnChar}cell2${wrapColumnChar}${newLine}${wrapColumnChar}row1${wrapColumnChar}${separator}${wrapColumnChar}row1${wrapColumnChar}`
117 | )
118 | })
119 |
120 | it('Header display name', async () => {
121 | const result = await csv({
122 | columns: columnSet3,
123 | datas: dataSet1,
124 | wrapColumnChar,
125 | })
126 | expect(result).to.equal(
127 | `${wrapColumnChar}Cell name${wrapColumnChar}${newLine}${wrapColumnChar}row1${wrapColumnChar}`
128 | )
129 | })
130 |
131 | it('Ordered cell', async () => {
132 | const result = await csv({
133 | columns: columnSet4,
134 | datas: dataSet2,
135 | wrapColumnChar,
136 | })
137 | expect(result).to.equal(
138 | `${wrapColumnChar}cell2${wrapColumnChar}${separator}${wrapColumnChar}cell1${wrapColumnChar}${newLine}${wrapColumnChar}row1${wrapColumnChar}${separator}${wrapColumnChar}row1${wrapColumnChar}`
139 | )
140 | })
141 |
142 | it('No header', async () => {
143 | const result = await csv({
144 | columns: columnSet1,
145 | datas: dataSet1,
146 | separator,
147 | noHeader: true,
148 | wrapColumnChar,
149 | })
150 | expect(result).to.equal(`${wrapColumnChar}row1${wrapColumnChar}`)
151 | })
152 |
153 | it('Auto header', async () => {
154 | const result = await csv({
155 | columns: false,
156 | datas: dataSet2,
157 | wrapColumnChar,
158 | })
159 | expect(result).to.equal(
160 | `${wrapColumnChar}cell1${wrapColumnChar}${separator}${wrapColumnChar}cell2${wrapColumnChar}${newLine}${wrapColumnChar}row1${wrapColumnChar}${separator}${wrapColumnChar}row1${wrapColumnChar}`
161 | )
162 | })
163 |
164 | it('array of array datas - single row', async () => {
165 | const result = await csv({
166 | columns: false,
167 | datas: dataSet3,
168 | wrapColumnChar,
169 | })
170 | expect(result).to.equal(
171 | `${wrapColumnChar}cell1${wrapColumnChar}${separator}${wrapColumnChar}cell2${wrapColumnChar}`
172 | )
173 | })
174 |
175 | it('array of array datas - multiple row', async () => {
176 | const result = await csv({
177 | columns: false,
178 | datas: dataSet4,
179 | wrapColumnChar,
180 | })
181 | expect(result).to.equal(
182 | `${wrapColumnChar}cell1${wrapColumnChar}${separator}${wrapColumnChar}cell2${wrapColumnChar}${newLine}${wrapColumnChar}cell1${wrapColumnChar}${separator}${wrapColumnChar}cell2${wrapColumnChar}`
183 | )
184 | })
185 |
186 | it('array of array datas - with header', async () => {
187 | const result = await csv({
188 | columns: columnSet4,
189 | datas: dataSet4,
190 | wrapColumnChar,
191 | })
192 | expect(result).to.equal(
193 | `${wrapColumnChar}cell2${wrapColumnChar}${separator}${wrapColumnChar}cell1${wrapColumnChar}${newLine}${wrapColumnChar}cell1${wrapColumnChar}${separator}${wrapColumnChar}cell2${wrapColumnChar}${newLine}${wrapColumnChar}cell1${wrapColumnChar}${separator}${wrapColumnChar}cell2${wrapColumnChar}`
194 | )
195 | })
196 | })
197 |
198 | describe('Semicolon separator', () => {
199 | const separator = ';'
200 |
201 | it('Single cell', async () => {
202 | const result = await csv({
203 | columns: columnSet1,
204 | datas: dataSet1,
205 | separator,
206 | })
207 | expect(result).to.equal(`cell1${newLine}row1`)
208 | })
209 |
210 | it('Multiple cell', async () => {
211 | const result = await csv({
212 | columns: columnSet2,
213 | datas: dataSet2,
214 | separator,
215 | })
216 | expect(result).to.equal(`cell1${separator}cell2${newLine}row1${separator}row1`)
217 | })
218 |
219 | it('Header display name', async () => {
220 | const result = await csv({
221 | columns: columnSet3,
222 | datas: dataSet1,
223 | separator,
224 | })
225 | expect(result).to.equal(`Cell name${newLine}row1`)
226 | })
227 |
228 | it('Ordered cell', async () => {
229 | const result = await csv({
230 | columns: columnSet4,
231 | datas: dataSet2,
232 | separator,
233 | })
234 | expect(result).to.equal(`cell2${separator}cell1${newLine}row1${separator}row1`)
235 | })
236 |
237 | it('No header', async () => {
238 | const result = await csv({
239 | columns: columnSet1,
240 | datas: dataSet1,
241 | separator,
242 | noHeader: true,
243 | })
244 | expect(result).to.equal('row1')
245 | })
246 |
247 | it('Auto header', async () => {
248 | const result = await csv({ columns: false, datas: dataSet2, separator })
249 | expect(result).to.equal(`cell1${separator}cell2${newLine}row1${separator}row1`)
250 | })
251 |
252 | it('array of array datas - single row', async () => {
253 | const result = await csv({ columns: false, datas: dataSet3, separator })
254 | expect(result).to.equal(`cell1${separator}cell2`)
255 | })
256 |
257 | it('array of array datas - multiple row', async () => {
258 | const result = await csv({ columns: false, datas: dataSet4, separator })
259 | expect(result).to.equal(`cell1${separator}cell2${newLine}cell1${separator}cell2`)
260 | })
261 |
262 | it('array of array datas - with header', async () => {
263 | const result = await csv({
264 | columns: columnSet4,
265 | datas: dataSet4,
266 | separator,
267 | })
268 | expect(result).to.equal(`cell2${separator}cell1${newLine}cell1${separator}cell2${newLine}cell1${separator}cell2`)
269 | })
270 | })
271 |
272 | describe('New line at end', () => {
273 | it('should not insert new line at end', async () => {
274 | const result = await csv({
275 | columns: columnSet1,
276 | datas: dataSet5,
277 | newLineAtEnd: false,
278 | })
279 | expect(result).to.equal(`cell1${newLine}row1${newLine}row2`)
280 | })
281 | it('should insert new line at end', async () => {
282 | const result = await csv({
283 | columns: columnSet1,
284 | datas: dataSet5,
285 | newLineAtEnd: true,
286 | })
287 | expect(result).to.equal(`cell1${newLine}row1${newLine}row2${newLine}`)
288 | })
289 | })
290 |
291 | describe('Should process chunks', () => {
292 | it('should process each line as a chunk', async () => {
293 | const result = await csv({
294 | columns: columnSet1,
295 | datas: dataSet5,
296 | chunkSize: 1,
297 | })
298 | expect(result).to.equal(`cell1${newLine}row1${newLine}row2`)
299 | })
300 | })
301 |
302 | describe('Nulls and undefineds', () => {
303 | it('should convert null to empty field', async () => {
304 | const result = await csv({ columns: columnSet1, datas: dataSet6 })
305 | expect(result).to.equal(`cell1${newLine}row1${newLine}${newLine}row3`)
306 | })
307 |
308 | it('should convert null to empty field', async () => {
309 | const result = await csv({ columns: columnSet1, datas: dataSet7 })
310 | expect(result).to.equal(`cell1${newLine}row1${newLine}${newLine}row3`)
311 | })
312 | })
313 |
314 | describe('Issue #411', () => {
315 | it('should not duplicate columns', async () => {
316 | const data = [
317 | { k1: 'v1', k2: 'v2' },
318 | { k1: 'v3', k2: 'v4' },
319 | { k1: 'v5', k2: 'v6' },
320 | ]
321 | const result = await csv({ datas: data })
322 | expect(result).to.equal(`k1,k2${newLine}v1,v2${newLine}v3,v4${newLine}v5,v6`)
323 | })
324 | })
325 |
326 | describe('Numbers', () => {
327 | it('should be ok', async () => {
328 | const people = [
329 | { name: 'Alice', age: 25 },
330 | { name: 'Bob', age: 27 },
331 | { name: 'Charlie', age: 40 },
332 | ]
333 | const result = await csv({ datas: people })
334 | expect(result).to.equal(`name,age${newLine}Alice,25${newLine}Bob,27${newLine}Charlie,40`)
335 | })
336 | })
337 | })
338 |
--------------------------------------------------------------------------------
/src/lib/csv.ts:
--------------------------------------------------------------------------------
1 | export interface IColumn {
2 | displayName?: string
3 | id: string
4 | }
5 |
6 | export type ColumnsDefinition = (string | IColumn)[]
7 | export type Columns = ColumnsDefinition | undefined | false
8 | export type Datas = (string[] | { [key: string]: string | number | null | undefined })[]
9 |
10 | interface Header {
11 | order: string[]
12 | map: Record
13 | }
14 |
15 | const newLine = '\r\n'
16 | const raf = typeof requestAnimationFrame === 'function' ? requestAnimationFrame : process.nextTick
17 |
18 | const makeWrapper = (wrapChar: string) => (str: string) => `${wrapChar}${str}${wrapChar}`
19 | const makeResolver = (resolve: (result: string) => unknown, newLineAtEnd: boolean) => (content: string[]) => {
20 | if (newLineAtEnd) {
21 | content.push('')
22 | }
23 |
24 | resolve(content.join(newLine))
25 | }
26 |
27 | const identityMapping = (arr: string[], initialMapping: Header): Header =>
28 | arr.reduce((acc, k) => {
29 | if (!acc.map[k]) {
30 | acc.map[k] = k
31 | acc.order.push(k)
32 | }
33 | return acc
34 | }, initialMapping)
35 |
36 | const extractHeaderFromData = (datas: Datas): Header =>
37 | datas.reduce(
38 | (acc: Header, v) => {
39 | return Array.isArray(v) ? acc : identityMapping(Object.keys(v), acc)
40 | },
41 | {
42 | order: [],
43 | map: {},
44 | }
45 | )
46 |
47 | const extractHeaderFromColumns = (columns: ColumnsDefinition): Header =>
48 | columns.reduce(
49 | (acc: Header, v) => {
50 | let id, value
51 | if (typeof v === 'string') {
52 | id = v
53 | value = v
54 | } else {
55 | id = v.id
56 | value = v.displayName ?? v.id
57 | }
58 | acc.map[id] = value
59 | acc.order.push(id)
60 | return acc
61 | },
62 | { order: [], map: {} }
63 | )
64 |
65 | function toChunks(arr: T[], chunkSize: number): T[][] {
66 | return [...Array(Math.ceil(arr.length / chunkSize))].reduce((acc, _, i) => {
67 | const begin = i * chunkSize
68 | return acc.concat([arr.slice(begin, begin + chunkSize)])
69 | }, [])
70 | }
71 |
72 | const createChunkProcessor = (
73 | resolve: ReturnType,
74 | wrap: ReturnType,
75 | content: string[],
76 | datas: Datas,
77 | columnOrder: string[],
78 | separator: string,
79 | chunkSize: number
80 | ) => {
81 | const chunks = toChunks(datas, chunkSize)
82 | let i = 0
83 | return function processChunk() {
84 | if (i >= chunks.length) {
85 | resolve(content)
86 | return
87 | }
88 |
89 | const chunk = chunks[i]
90 | // @ts-expect-error 7053
91 | const asArray = Array.isArray(chunk[0]) && !columnOrder.some((k) => typeof chunk[0][k] !== 'undefined')
92 | i += 1
93 | chunk
94 | // @ts-expect-error 7053
95 | .map((v) => (asArray ? v : columnOrder.map((k) => v[k] ?? '')) as string[])
96 | .forEach((v) => {
97 | content.push(v.map(wrap).join(separator))
98 | })
99 |
100 | raf(processChunk)
101 | }
102 | }
103 |
104 | export interface ICsvProps {
105 | columns?: Columns
106 | datas: Datas | (() => Datas) | (() => Promise) | Promise
107 | separator?: string
108 | noHeader?: boolean
109 | wrapColumnChar?: string
110 | newLineAtEnd?: boolean
111 | chunkSize?: number
112 | title?: string
113 | }
114 |
115 | export default async function csv({
116 | columns,
117 | datas,
118 | separator = ',',
119 | noHeader = false,
120 | wrapColumnChar = '',
121 | newLineAtEnd = false,
122 | chunkSize = 1000,
123 | title = '',
124 | }: ICsvProps) {
125 | // eslint-disable-next-line no-async-promise-executor
126 | return new Promise(async (_resolve, reject) => {
127 | const resolve = makeResolver(_resolve, newLineAtEnd)
128 | const wrap = makeWrapper(wrapColumnChar)
129 |
130 | try {
131 | datas = typeof datas === 'function' ? await datas() : await datas
132 | if (!Array.isArray(datas)) {
133 | return _resolve()
134 | }
135 |
136 | const { map, order }: Header = columns ? extractHeaderFromColumns(columns) : extractHeaderFromData(datas)
137 |
138 | const content: string[] = []
139 |
140 | if (!noHeader) {
141 | const headerNames = order.map((id) => map[id])
142 | if (headerNames.length > 0) {
143 | if (title !== '') {
144 | content.push(title)
145 | }
146 | content.push(headerNames.map(wrap).join(separator))
147 | }
148 | }
149 |
150 | const processChunk = createChunkProcessor(resolve, wrap, content, datas, order, separator, chunkSize)
151 |
152 | raf(processChunk)
153 | } catch (err) {
154 | return reject(err)
155 | }
156 | })
157 | }
158 |
--------------------------------------------------------------------------------
/tsconfig-cjs.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "module": "commonjs",
5 | "outDir": "dist/cjs"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "outDir": "dist/esm",
4 | "module": "es2020",
5 | "target": "es2015",
6 | "lib": ["es2017", "dom"],
7 | "moduleResolution": "node",
8 | "allowJs": false,
9 | "sourceMap": true,
10 | "jsx": "react",
11 | "declaration": true,
12 | "forceConsistentCasingInFileNames": true,
13 | "strict": true,
14 | "noUnusedLocals": true,
15 | "noUnusedParameters": true,
16 | "noImplicitReturns": true,
17 | "noImplicitThis": true,
18 | "noImplicitAny": true
19 | },
20 | "include": ["src"],
21 | "exclude": ["node_modules", "dist"]
22 | }
23 |
--------------------------------------------------------------------------------