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