It resolves #3

This commit is contained in:
abdennour 2017-02-17 19:06:30 +03:00
commit 7918cadcd9
12 changed files with 8667 additions and 173 deletions

View file

@ -33,6 +33,12 @@ const csvData =[
npm install react-csv --save; npm install react-csv --save;
``` ```
or , for non-node developers , you can use CDN directly :
```html
<script src="" type="text/javascript"></script>
```
# Components: # Components:
This package includes actually two components: `CSVLink` and `CSVDownload`. This package includes actually two components: `CSVLink` and `CSVDownload`.
@ -91,6 +97,25 @@ data = json2csv(arrayOfLiteralObjects);
> - The meaning of headers with data of type `String` data is only prepending those headers as the first line of CSV content. > - The meaning of headers with data of type `String` data is only prepending those headers as the first line of CSV content.
### - **separator** Props:
Following a request to add [this feature](https://github.com/abdennour/react-csv/issues/3) , from `1.0.1` release, `react-csv` supports `separator` props which is equals by default a comma `,` .
```js
import {CSVLink} from 'react-csv';
<CSVLink data={array} separator={";"}>
Download me
</CSVLink>
/*
This will generate CSV with ";" as delimiter (separator)
*/
```
## 1. CSVLink Component: ## 1. CSVLink Component:
@ -136,15 +161,14 @@ import {CSVDownload} from 'react-csv';
# Contribution : # Contribution :
### Run the sample : - Unit-tests must cover at least 90 % of code .
- Write documentation of the new class, function , method , attribute ..so on.. following JSDoc syntax.
- `cd react-csv/` - Add an example for the new feature to `sample-site`.
then : - `npm start` runs the [`sample-site`](sample-site/)
- `npm install`. - `npm docgen` generates documentation in HTML output.
- `npm start` - `npm cdn` generate a bundle to be used as CDN
- browse : `http://localhost:8008`.

View file

@ -2,8 +2,8 @@ const rt = process.cwd();
const {version, name} = require(rt+'/package.json'); const {version, name} = require(rt+'/package.json');
const shell = require('child_process').execSync; const shell = require('child_process').execSync;
const cmds ={ const cmds ={
browserify:(suffix =>`node ${rt}/node_modules/browserify/bin/cmd ${rt}/index.js -o ${rt}/cdn/${name}-${suffix}.js`) , browserify:(suffix =>`node ${rt}/node_modules/browserify/bin/cmd ${rt}/cdn/window.js -o ${rt}/cdn/${name}-${suffix}.js`) ,
minify : (suffix => `node node_modules/uglify-js/bin/uglifyjs cdn/${name}-${suffix}.js -o cdn/${name}-${suffix}.min.js`) // minify : (suffix => `node node_modules/uglify-js/bin/uglifyjs ${rt}/cdn/${name}-${suffix}.js -o ${rt}/cdn/${name}-${suffix}.min.js`)
}; };

4219
cdn/react-csv-1.0.1.js Normal file

File diff suppressed because it is too large Load diff

4219
cdn/react-csv-latest.js vendored Normal file

File diff suppressed because it is too large Load diff

3
cdn/window.js Normal file
View file

@ -0,0 +1,3 @@
const {CSVLink, CSVDownload} = require('../lib/index');
window.ReactCSV = {CSVLink, CSVDownload};

View file

@ -1,6 +1,6 @@
{ {
"name": "react-csv", "name": "react-csv",
"version": "1.0.0-RC1", "version": "1.0.1",
"description": "Build CSV files on the fly basing on Array/literal object of data ", "description": "Build CSV files on the fly basing on Array/literal object of data ",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {

View file

@ -14,10 +14,17 @@ class App extends React.Component {
return ( return (
<div style={{padding: 10}}> <div style={{padding: 10}}>
<h1>react-csv</h1> <h1>react-csv</h1>
<hr />
<div> <div>
Download CSV <CSVLink data={csvData}> here </CSVLink>. Download CSV <CSVLink data={csvData}> here </CSVLink>.
</div> </div>
<hr />
<div>
Download CSV with <code>;</code> as separator : <CSVLink data={csvData} separator=";"> here </CSVLink>.
</div>
</div> </div>
); );
} }

View file

@ -16,7 +16,10 @@ class CSVDownload extends React.Component {
} }
componentDidMount(){ componentDidMount(){
this.state.page = window.open(this.buildURI(this.props.data, this.props.headers), this.props.target, this.props.specs, this.props.replace); const {data, headers, separator, target, specs, replace} = this.props;
this.state.page = window.open(
this.buildURI(data, headers, separator), target, specs, replace
);
} }
getWindow() { getWindow() {

View file

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import {buildURI} from '../core'; import {buildURI} from '../core';
import {defaultProps, PropTypes, PropsNotForwarded} from '../metaProps'; import {defaultProps, PropTypes} from '../metaProps';
class CSVLink extends React.Component { class CSVLink extends React.Component {
constructor(props) { constructor(props) {
@ -13,10 +13,10 @@ class CSVLink extends React.Component {
} }
render(){ render(){
const {data, headers, children , ...rest} = this.props; const {data, headers, separator, children , ...rest} = this.props;
return ( return (
<a {...rest} <a {...rest}
href={this.buildURI(data, headers)}> href={this.buildURI(data, headers, separator)}>
{children} {children}
</a> </a>
) )

View file

@ -18,30 +18,30 @@ export const jsons2arrays = (jsons, headers) => {
object[header] ? object[header] : ''))] object[header] ? object[header] : ''))]
}; };
export const joiner = ((data) => export const joiner = ((data,separator = ',') =>
data.map((row, index) => row.join(',')).join(`\n`) data.map((row, index) => row.join(separator)).join(`\n`)
); );
export const arrays2csv = ((data, headers) => export const arrays2csv = ((data, headers, separator) =>
joiner(headers ? [headers, ...data] : data) joiner(headers ? [headers, ...data] : data, separator)
); );
export const jsons2csv = ((data, headers) => export const jsons2csv = ((data, headers, separator) =>
joiner(jsons2arrays(data, headers)) joiner(jsons2arrays(data, headers), separator)
); );
export const string2csv = ((data, headers) => export const string2csv = ((data, headers, separator) =>
(headers) ? `${headers.join(`,`)}\n${data}`: data (headers) ? `${headers.join(separator)}\n${data}`: data
) )
export const toCSV = (data, headers) => { export const toCSV = (data, headers, separator) => {
if (isJsons(data)) return jsons2csv(data, headers); if (isJsons(data)) return jsons2csv(data, headers, separator);
if (isArrays(data)) return arrays2csv(data, headers); if (isArrays(data)) return arrays2csv(data, headers, separator);
if (typeof data ==='string') return string2csv(data, headers); if (typeof data ==='string') return string2csv(data, headers, separator);
throw new TypeError(`Data should be a "String", "Array of arrays" OR "Array of objects" `); throw new TypeError(`Data should be a "String", "Array of arrays" OR "Array of objects" `);
}; };
export const buildURI = ((data, headers) => encodeURI( export const buildURI = ((data, headers, separator) => encodeURI(
`data:text/csv;charset=utf-8,${toCSV(data, headers)}` `data:text/csv;charset=utf-8,${toCSV(data, headers, separator)}`
) )
); );

View file

@ -7,11 +7,12 @@ export const PropTypes = {
React.PropTypes.array React.PropTypes.array
]).isRequired, ]).isRequired,
headers: React.PropTypes.array, headers: React.PropTypes.array,
target: React.PropTypes.string target: React.PropTypes.string,
separator: React.PropTypes.string
}; };
export const defaultProps = { export const defaultProps = {
separator: ','
}; };
export const PropsNotForwarded = [ export const PropsNotForwarded = [

View file

@ -1,179 +1,179 @@
import expect from 'expect'; import expect from 'expect';
import sinon from 'sinon'; import sinon from 'sinon';
import { import {
isJsons, isJsons,
isArrays, isArrays,
jsonsHeaders, jsonsHeaders,
jsons2arrays, jsons2arrays,
arrays2csv, arrays2csv,
jsons2csv, jsons2csv,
string2csv, string2csv,
toCSV, toCSV,
buildURI buildURI
} from '../src/core'; } from '../src/core';
describe(`core::isJsons`, () => { describe(`core::isJsons`, () => {
it(`returns true if all items of array are literal objects`, () => { it(`returns true if all items of array are literal objects`, () => {
const target = [{}, {}, {}, {}]; const target = [{}, {}, {}, {}];
expect(isJsons(target)).toBeTruthy(); expect(isJsons(target)).toBeTruthy();
}); });
it(`returns false if one of array items is not literal object`, () => { it(`returns false if one of array items is not literal object`, () => {
let target = ["", {}, {}, {}]; let target = ["", {}, {}, {}];
expect(isJsons(target)).toBeFalsy(); expect(isJsons(target)).toBeFalsy();
target = [{}, target = [{},
[], {}, {} [], {}, {}
]; ];
expect(isJsons(target)).toBeFalsy(); expect(isJsons(target)).toBeFalsy();
}); });
}); });
describe(`core::isArrays`, () => { describe(`core::isArrays`, () => {
it(`retruns true if all array items are arrays too`, () => { it(`retruns true if all array items are arrays too`, () => {
const target = [ const target = [
[], [],
[], [],
[], [],
[] []
]; ];
expect(isArrays(target)).toBeTruthy(); expect(isArrays(target)).toBeTruthy();
}); });
it(`retruns false if one of array items is not array`, () => { it(`retruns false if one of array items is not array`, () => {
let target = [{}, let target = [{},
[], [],
[], [],
[] []
]; ];
expect(isArrays(target)).toBeFalsy(); expect(isArrays(target)).toBeFalsy();
target = [ target = [
[], new Set([]), [], [], new Set([]), [],
[] []
]; ];
expect(isArrays(target)).toBeFalsy(); expect(isArrays(target)).toBeFalsy();
target = [ target = [
[], '[]', [], [], '[]', [],
[] []
]; ];
expect(isArrays(target)).toBeFalsy(); expect(isArrays(target)).toBeFalsy();
}); });
}); });
describe(`core::jsonsHeaders`, () => { describe(`core::jsonsHeaders`, () => {
let fixtures; let fixtures;
beforeEach(() => { beforeEach(() => {
fixtures = [{ fixtures = [{
maths: 90, maths: 90,
phy: 80 phy: 80
}, { }, {
maths: 50, maths: 50,
sport: 97, sport: 97,
ch: 66 ch: 66
}, { }, {
ch: 77, ch: 77,
sport: 99 sport: 99
}] }]
}); });
it(`returns union of keys of all array items `, () => { it(`returns union of keys of all array items `, () => {
let actual = jsonsHeaders(fixtures); let actual = jsonsHeaders(fixtures);
expect(actual).toEqual([`maths`, `phy`, 'sport', 'ch']) expect(actual).toEqual([`maths`, `phy`, 'sport', 'ch'])
}), }),
it(`does not duplicate keys on the array`, () => { it(`does not duplicate keys on the array`, () => {
let actual = jsonsHeaders(fixtures); let actual = jsonsHeaders(fixtures);
let keysOfAllItems = fixtures.map((object) => Object.keys(object)) let keysOfAllItems = fixtures.map((object) => Object.keys(object))
.reduce((a, b) => [...a, ...b], []); .reduce((a, b) => [...a, ...b], []);
expect(actual.length).toBeLessThanOrEqualTo(keysOfAllItems.length); expect(actual.length).toBeLessThanOrEqualTo(keysOfAllItems.length);
expect(actual.length).toEqual(new Set(keysOfAllItems).size); expect(actual.length).toEqual(new Set(keysOfAllItems).size);
}) })
}); });
describe(`core::jsons2arrays`, () => { describe(`core::jsons2arrays`, () => {
let fixtures; let fixtures;
beforeEach(() => { beforeEach(() => {
fixtures = [{ fixtures = [{
maths: '90' maths: '90'
}, { }, {
sport: '97' sport: '97'
}, { }, {
maths: '77', maths: '77',
sport: '99' sport: '99'
}] }]
}); });
it(`converts an Array of literal objects to Array of arrays`, () => { it(`converts an Array of literal objects to Array of arrays`, () => {
const actual = jsons2arrays(fixtures); const actual = jsons2arrays(fixtures);
const expected = [ const expected = [
['maths', 'sport'], ['maths', 'sport'],
['90', ''], ['90', ''],
['', '97'], ['', '97'],
['77', '99'] ['77', '99']
]; ];
expect(actual).toEqual(expected); expect(actual).toEqual(expected);
}); });
it(`converts to Array of arrays following the order of headers`, () => { it(`converts to Array of arrays following the order of headers`, () => {
const actual = jsons2arrays(fixtures, ['sport', 'maths']); const actual = jsons2arrays(fixtures, ['sport', 'maths']);
const expected = [ const expected = [
['maths', 'sport'].reverse(), ['90', ''].reverse(), ['', '97'].reverse(), ['77', '99'].reverse() ['maths', 'sport'].reverse(), ['90', ''].reverse(), ['', '97'].reverse(), ['77', '99'].reverse()
]; ];
expect(actual).toEqual(expected); expect(actual).toEqual(expected);
}); });
it(`accepts any size of headers list`, () => { it(`accepts any size of headers list`, () => {
const headers = ['maths', 'sport', 'phy', 'ch']; const headers = ['maths', 'sport', 'phy', 'ch'];
const actual = jsons2arrays(fixtures, headers); const actual = jsons2arrays(fixtures, headers);
const expected = [ const expected = [
headers, ['90', '', '', ''], headers, ['90', '', '', ''],
['', '97', '', ''], ['', '97', '', ''],
['77', '99', '', ''] ['77', '99', '', '']
]; ];
expect(actual).toEqual(expected); expect(actual).toEqual(expected);
}); });
}); });
describe(`core::arrays2csv`, () => { describe(`core::arrays2csv`, () => {
let fixtures; let fixtures;
beforeEach(() => { beforeEach(() => {
fixtures = [ fixtures = [
[`a`, `b`], [`a`, `b`],
[`c`, `d`] [`c`, `d`]
]; ];
}); });
it(`converts Array of arrays to string in CSV format`, () => { it(`converts Array of arrays to string in CSV format`, () => {
const actual = arrays2csv(fixtures); const actual = arrays2csv(fixtures);
expect(actual).toBeA('string'); expect(actual).toBeA('string');
expect(actual.split(`\n`).join(`|`)).toEqual(`a,b|c,d`); expect(actual.split(`\n`).join(`|`)).toEqual(`a,b|c,d`);
}); });
it(`renders CSV headers whenever it was given `, () => { it(`renders CSV headers whenever it was given `, () => {
const headers = [`X`, `Y`]; const headers = [`X`, `Y`];
const firstLineOfCSV = arrays2csv(fixtures, headers).split(`\n`)[0]; const firstLineOfCSV = arrays2csv(fixtures, headers).split(`\n`)[0];
expect(firstLineOfCSV).toEqual(headers.join(`,`)); expect(firstLineOfCSV).toEqual(headers.join(`,`));
}); });
}); });
describe(`core::jsons2csv`, () => { describe(`core::jsons2csv`, () => {
let fixtures; let fixtures;
beforeEach(() => { beforeEach(() => {
fixtures = [{ fixtures = [{
X: '88', X: '88',
Y: '97' Y: '97'
}, { }, {
X: '77', X: '77',
Y: '99' Y: '99'
}] }]
}); });
it(`converts Array of literal objects to string in CSV format including headers`, () => { it(`converts Array of literal objects to string in CSV format including headers`, () => {
const actual = jsons2csv(fixtures); const actual = jsons2csv(fixtures);
const expectedHeaders = ['X', 'Y']; const expectedHeaders = ['X', 'Y'];
const expected =`${expectedHeaders.join(`,`)}|88,97|77,99`; const expected = `${expectedHeaders.join(`,`)}|88,97|77,99`;
expect(actual).toBeA('string'); expect(actual).toBeA('string');
expect(actual.split(`\n`).join(`|`)).toEqual(expected); expect(actual.split(`\n`).join(`|`)).toEqual(expected);
}); });
@ -229,7 +229,7 @@ describe(`core::toCSV`, () =>{
describe(`core::buildURI`, () =>{ describe(`core::buildURI`, () =>{
let fixtures; let fixtures;
beforeEach(() => { beforeEach(() => {
fixtures = {string:'Xy', arrays:[[],[]],jsons:[{}, {}]}; fixtures = {string:'Xy', arrays:[['a', 'b'],['c', 'd']],jsons:[{}, {}]};
}); });
it(`generates URI to download data in CSV format`,() => { it(`generates URI to download data in CSV format`,() => {
@ -239,4 +239,22 @@ describe(`core::buildURI`, () =>{
expect(buildURI(fixtures.string).startsWith(prefixCsvURI)).toBeTruthy(); expect(buildURI(fixtures.string).startsWith(prefixCsvURI)).toBeTruthy();
}); });
it(`generates CSV string according to "separator"`, () => {
const prefixCsvURI= `data:text/csv;charset=utf-8,`;
const expectedSepartorCount = fixtures.arrays.map(row => row.length -1).reduce((total, next) =>total + next, 0);
let separator = ';';
let fullURI = buildURI(fixtures.arrays,null , separator);
expect(
fullURI.slice(prefixCsvURI.length).match(/;/g).length
).toEqual(expectedSepartorCount);
separator = ',';
fullURI = buildURI(fixtures.arrays,null , separator);
expect(
fullURI.slice(prefixCsvURI.length).match(/,/g).length
).toEqual(expectedSepartorCount);
});
}); });