├── .gitignore
├── .npmignore
├── README.md
├── examples
├── app.js
└── index.html
├── gulpfile.js
├── index.js
├── media
└── react-table-select.gif
├── package.json
├── preprocessor.js
└── src
├── __tests__
├── table-body-test.js
├── table-head-test.js
└── table-test.js
├── table-body.js
├── table-head.js
└── table.js
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | examples/public/
3 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | examples/
2 | media/
3 | src/__tests__/
4 | preprocessor.js
5 | gulpfile.js
6 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | #React Selectable Table
2 |
3 | Table component with selectable rows for Facebook's [React](https://github.com/facebook/react)
4 |
5 | An example styled w/ [Bootstrap](https://github.com/twbs/bootstrap):
6 |
7 | 
8 |
9 | ## Installation
10 | This has been primarily developed for use with Browserify and as such is shared
11 | as a CommonJS module via npm.
12 |
13 | ```sh
14 | npm install --save react-table-select
15 | ```
16 |
17 | ## Usage
18 | Assuming you're using JSX:
19 | ```js
20 | var React = require('react');
21 | var TableSelect = require('react-table-select');
22 |
23 | React.render(
24 | , document.body);
28 | ```
29 |
30 | To access the selected rows from outside of the component save the component as a ref:
31 | ```js
32 |
33 |
34 | // See the onChange method for more info on selectedRows
35 | this.refs.table.state.selectedRows
36 | ```
37 |
38 | ## Props
39 |
40 | #### `{string} className`
41 |
42 | Optional, set the class on this component's child `table` element. Use this to apply styles or anything else that you need.
43 |
44 | #### `{array} data`
45 |
46 | An array of Objects to render as a selectable table.
47 |
48 | #### `{array} columns`
49 |
50 | Optional, use this if you'd like to specify custom fields. By default the table will create a column for every unique key it finds in the array, for example:
51 | ```js
52 | this.props.data = [{one: 'fish'}, {two: 'fish'}, {red: 'fish'}, {blue: 'fish'}]
53 | fields = [one, two, red, blue]
54 | ```
55 |
56 | #### `{function} onChange`
57 |
58 | Optional, a callback to work with the selectedRows when they change:
59 | ```js
60 | callback = function(selectedRows) {}
61 | ```
62 | `selectedRows` will be an `Array` of `Int`, the indices of the currently selected rows (empty if no rows are selected). Some concrete examples:
63 | - No rows selected, `selectedRows` should be `[]`
64 | - 1st row selected, `selectedRows` should be `[0]`
65 | - 1st, 2nd, & 4th rows selected, `selectedRows` should be `[0, 1, 3]`
66 |
67 | With the indices of the selected rows the selected data can be easily found using a `.map()` operation or similar.
68 |
69 | ##Example
70 | A small example is included, to see it in action follow these steps:
71 | ```sh
72 | git clone https://github.com/AllenSH12/react-table-select.git
73 | cd react-table-select
74 |
75 | npm install
76 | gulp example
77 |
78 | cd examples/
79 | python -m SimpleHTTPServer # or an HTTP server of your choice
80 |
81 | # visit localhost:8000 in your browser
82 | ```
83 |
84 | ##Testing
85 | Tests are implemented with Facebook's [Jest](https://github.com/facebook/jest) and can be run via npm:
86 | ```sh
87 | npm install # Only needs to be run the 1st time
88 | npm test
89 | ```
90 |
--------------------------------------------------------------------------------
/examples/app.js:
--------------------------------------------------------------------------------
1 | var React = require('react');
2 | var Table = require('../index');
3 |
4 | var data = [
5 | {
6 | "age": "<5",
7 | "population": "2704659",
8 | "minor": "True"
9 | },
10 | {
11 | "age": "5-13",
12 | "population": "4499890",
13 | "minor": "True"
14 | },
15 | {
16 | "age": "14-17",
17 | "population": "2159981",
18 | "minor": "True"
19 | },
20 | {
21 | "age": "18-24",
22 | "population": "3853788"
23 | },
24 | {
25 | "age": "25-44",
26 | "population": "14106543"
27 | },
28 | {
29 | "age": "45-64",
30 | "population": "8819342"
31 | },
32 | {
33 | "age": "≥65",
34 | "population": "612463"
35 | }
36 | ];
37 |
38 | var App = React.createClass({
39 | getInitialState: function() {
40 | return {
41 | data: data,
42 | selected: []
43 | };
44 | },
45 |
46 | unselect: function(e) {
47 | e.preventDefault();
48 |
49 | this.handleChange([]);
50 |
51 | this.refs.table.setState({ selectedRows: [] });
52 | },
53 |
54 | log: function(e) {
55 | e.preventDefault();
56 |
57 | var selectedRows = this.refs.table.state.selectedRows;
58 | console.log(selectedRows);
59 | },
60 |
61 | fill: function(e) {
62 | e.preventDefault();
63 |
64 | var rows = [];
65 |
66 | for (var i = 0; i < this.state.data.length; i++) {
67 | rows.push(i);
68 | }
69 |
70 | this.handleChange(rows);
71 |
72 | this.refs.table.setState({
73 | selectedRows: rows
74 | });
75 | },
76 |
77 | handleChange: function(rows) {
78 | this.setState({
79 | selected: rows
80 | });
81 | },
82 |
83 | render: function() {
84 | return (
85 |
86 |
87 |
88 |
External Controls:
89 |
90 | Select All
93 | Unselect All
96 |
97 |
Selected rows: {this.state.selected.join(', ')}
98 |
99 |
100 |
110 |
111 | );
112 | }
113 | });
114 |
115 | React.render( , document.body);
116 |
--------------------------------------------------------------------------------
/examples/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | React Selectable Table
6 |
7 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/gulpfile.js:
--------------------------------------------------------------------------------
1 | var gulp = require('gulp');
2 | var browserify = require('browserify');
3 | var reactify = require('reactify');
4 | var source = require('vinyl-source-stream');
5 |
6 | gulp.task('example', function() {
7 | browserify('./examples/app.js')
8 | .transform(reactify)
9 | .bundle()
10 | .pipe(source('app.js'))
11 | .pipe(gulp.dest('./examples/public'));
12 | });
13 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | module.exports = require('./src/table');
2 |
--------------------------------------------------------------------------------
/media/react-table-select.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AllenSH12/react-table-select/5d36984cf5f88bd3d01110610510e69d0c31c3e4/media/react-table-select.gif
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-table-select",
3 | "version": "0.2.7",
4 | "repository": "https://github.com/AllenSH12/react-table-select.git",
5 | "description": "React table with selectable rows.",
6 | "main": "index.js",
7 | "author": "Allen Hernandez",
8 | "license": "MIT",
9 | "scripts": {
10 | "test": "jest"
11 | },
12 | "keywords": [
13 | "react-component"
14 | ],
15 | "jest": {
16 | "scriptPreprocessor": "./preprocessor.js",
17 | "unmockedModulePathPatterns": [
18 | "./node_modules/react"
19 | ]
20 | },
21 | "dependencies": {
22 | "react": "^0.12.2",
23 | "react-tools": "^0.12.2"
24 | },
25 | "devDependencies": {
26 | "browserify": "^7.0.3",
27 | "gulp": "^3.8.10",
28 | "jest-cli": "^0.2.1",
29 | "reactify": "^0.17.1",
30 | "vinyl-source-stream": "^1.0.0"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/preprocessor.js:
--------------------------------------------------------------------------------
1 | var ReactTools = require('react-tools');
2 | module.exports = {
3 | process: function(src) {
4 | return ReactTools.transform(src);
5 | }
6 | };
7 |
--------------------------------------------------------------------------------
/src/__tests__/table-body-test.js:
--------------------------------------------------------------------------------
1 | jest
2 | .dontMock('../table-body');
3 |
4 | describe('table body', function() {
5 | it ('should render a tr for every entry', function() {
6 | var React = require('react/addons');
7 | var TableBody = require('../table-body');
8 | var TestUtils = React.addons.TestUtils;
9 | var mock = jest.genMockFunction();
10 |
11 | var fixtures = [{}, {}, {}];
12 |
13 | // test fails if checkedRows or fields is not supplied
14 | var tbody = TestUtils.renderIntoDocument(
15 |
16 | );
17 |
18 | var rows = TestUtils.scryRenderedDOMComponentsWithTag(tbody, 'tr');
19 | expect(rows.length).toBe(fixtures.length);
20 | });
21 | });
22 |
--------------------------------------------------------------------------------
/src/__tests__/table-head-test.js:
--------------------------------------------------------------------------------
1 | jest
2 | .dontMock('../table-head');
3 |
4 | describe('table head', function() {
5 | it ('calls the callback', function() {
6 | var React = require('react/addons');
7 | var TableHead = require('../table-head');
8 | var TestUtils = React.addons.TestUtils;
9 | var mock = jest.genMockFunction();
10 |
11 | var thead = TestUtils.renderIntoDocument(
12 |
13 | );
14 |
15 | var checkbox = TestUtils.findRenderedDOMComponentWithTag(thead, 'input');
16 | expect(checkbox.getDOMNode().checked).toBe(false);
17 |
18 | TestUtils.Simulate.change(checkbox);
19 | expect(mock).toBeCalled();
20 | });
21 | });
22 |
--------------------------------------------------------------------------------
/src/__tests__/table-test.js:
--------------------------------------------------------------------------------
1 | jest
2 | .dontMock('../table')
3 | .dontMock('../table-head')
4 | .dontMock('../table-body');
5 |
6 | describe('table', function() {
7 | var React = require('react/addons');
8 | var Table = require('../table');
9 | var TableHead = require('../table-head');
10 | var TableBody = require('../table-body');
11 | var TestUtils = React.addons.TestUtils;
12 |
13 | it('renders its sub-components', function() {
14 | var mock = jest.genMockFunction();
15 |
16 | var table = TestUtils.renderIntoDocument(
17 |
18 | );
19 |
20 | // make sure the component has head and body sub-components
21 | TestUtils.findRenderedComponentWithType(table, TableHead);
22 | TestUtils.findRenderedComponentWithType(table, TableBody);
23 | });
24 |
25 | it('tells the client it has been changed', function() {
26 | var mock = jest.genMockFunction();
27 |
28 | var table = TestUtils.renderIntoDocument(
29 |
30 | );
31 |
32 | // find the tbody and checkbox of 1st row
33 | var tbody = TestUtils.findRenderedDOMComponentWithTag(table, 'tbody');
34 | var firstRowCheckbox = TestUtils.findRenderedDOMComponentWithTag(tbody, 'input');
35 |
36 | // simulate an event
37 | TestUtils.Simulate.change(firstRowCheckbox);
38 |
39 | // ensure that the callback has been called
40 | expect(mock.mock.calls.length).toEqual(1);
41 | });
42 | });
43 |
--------------------------------------------------------------------------------
/src/table-body.js:
--------------------------------------------------------------------------------
1 | var React = require('react');
2 |
3 | var TableRow = React.createClass({
4 | handleChange: function(e) {
5 | e.stopPropagation();
6 |
7 | this.props.onChange(e.target.checked);
8 | },
9 |
10 | render: function() {
11 | return (
12 | React.createElement('tr', null,
13 | React.createElement('td', null,
14 | React.createElement('input', {
15 | 'type': 'checkbox',
16 | 'aria-label': 'select single row',
17 | 'checked': this.props.checked,
18 | 'onChange': this.handleChange
19 | })
20 | ),
21 | this.props.fields.map(function(field, i) {
22 | return (
23 | React.createElement('td', {
24 | key: i
25 | }, this.props.data[field])
26 | );
27 | }, this)
28 | )
29 | );
30 | }
31 | });
32 |
33 | var TableBody = React.createClass({
34 | render: function() {
35 | return React.createElement('tbody', null,
36 | this.props.data.map(function(datum, i) {
37 | return React.createElement(TableRow, {
38 | key: i,
39 | data: datum,
40 | fields: this.props.fields,
41 | checked: this.props.checkedRows[i],
42 | onChange: this.props.onChange.bind(null, i)
43 | });
44 | }, this)
45 | );
46 | }
47 | });
48 |
49 | module.exports = TableBody;
50 |
--------------------------------------------------------------------------------
/src/table-head.js:
--------------------------------------------------------------------------------
1 | var React = require('react');
2 |
3 | var TableHead = React.createClass({
4 | handleChange: function(e) {
5 | e.stopPropagation();
6 |
7 | this.props.onChange(e.target.checked);
8 | },
9 |
10 | render: function() {
11 | return (
12 | React.createElement('thead', null,
13 | React.createElement('tr', null,
14 | React.createElement('th', null,
15 | React.createElement('input', {
16 | 'type': 'checkbox',
17 | 'aria-label': 'toggle all rows selected',
18 | 'checked': this.props.checked,
19 | 'onChange': this.handleChange
20 | })
21 | ),
22 | this.props.fields.map(function(field, i) {
23 | return (
24 | React.createElement('th', {
25 | 'key': i
26 | }, field)
27 | );
28 | })
29 | )
30 | )
31 | );
32 | }
33 | });
34 |
35 | module.exports = TableHead;
36 |
--------------------------------------------------------------------------------
/src/table.js:
--------------------------------------------------------------------------------
1 | var React = require('react');
2 |
3 | var TableHead = require('./table-head');
4 | var TableBody = require('./table-body');
5 |
6 | var Table = React.createClass({
7 | getInitialState: function() {
8 | return {
9 | selectedRows: []
10 | };
11 | },
12 |
13 | /**
14 | * if any rows have been added or removed set selected rows to none
15 | * @param {Object} nextProps the next properties of the Table (updated data)
16 | */
17 | componentWillReceiveProps: function(nextProps) {
18 | var changingRowCount = this.props.data.length !== nextProps.data.length;
19 |
20 | if (changingRowCount) {
21 | this.setState({ selectedRows: [] });
22 | }
23 | },
24 |
25 | /**
26 | * set selectedRows to all (de)selected and update UI accordingly
27 | * @param {Boolean} checked check status of the all selected checkbox
28 | */
29 | handleHeadChange: function(checked) {
30 | var selectedRows = [];
31 | var i;
32 |
33 | if (checked) {
34 | for (i = 0; i < this.props.data.length; i++) {
35 | selectedRows.push(i);
36 | }
37 | }
38 |
39 | this.onChange(selectedRows);
40 | },
41 |
42 | /**
43 | * respond to a user (de)selecting a single row
44 | * @param {Int} i the index of the row that was clicked
45 | * @param {Boolean} checked the new check status of that row
46 | */
47 | handleBodyChange: function(i, checked) {
48 | var selectedRows = this.state.selectedRows.slice();
49 | var index;
50 |
51 |
52 | function sortedIndex(array, newElement) {
53 | var lessThan = array.filter(function(el, i) {
54 | return el < newElement;
55 | });
56 |
57 | return lessThan.length;
58 | }
59 |
60 | if (checked) {
61 | index = sortedIndex(selectedRows, i);
62 | selectedRows.splice(index, 0, i);
63 |
64 | } else {
65 | index = selectedRows.indexOf(i);
66 | selectedRows.splice(index, 1);
67 | }
68 |
69 | this.onChange(selectedRows);
70 | },
71 |
72 | /**
73 | * if applicable inform parent component of change, in any case
74 | * update the component's internal state
75 | * @param {[type]} selectedRows [description]
76 | */
77 | onChange: function(selectedRows) {
78 | if (this.props.onChange) {
79 | this.props.onChange(selectedRows);
80 | }
81 |
82 | this.setState({
83 | selectedRows: selectedRows
84 | });
85 | },
86 |
87 | render: function() {
88 | var data = this.props.data;
89 |
90 | var customFields = this.props.columns;
91 | var fields = customFields ? customFields : data.map(Object.keys);
92 |
93 | if (!customFields) {
94 | fields = fields.reduce(function(prev, curr) {
95 | return prev.concat(curr);
96 | }).filter(function(field, i, array) {
97 | return array.indexOf(field) === i;
98 | });
99 | }
100 |
101 | var selectedRows = this.state.selectedRows;
102 | var allRowsSelected = data.length === selectedRows.length && data.length !== 0;
103 |
104 | var rowStates = [];
105 | selectedRows.map(function(row) {
106 | rowStates[row] = true;
107 | });
108 |
109 | return (
110 | React.createElement('table', {
111 | className: this.props.className
112 | },
113 | React.createElement(TableHead, {
114 | fields: fields,
115 | onChange: this.handleHeadChange,
116 | checked: allRowsSelected
117 | }),
118 | React.createElement(TableBody, {
119 | data: this.props.data,
120 | fields: fields,
121 | checkedRows: rowStates,
122 | onChange: this.handleBodyChange
123 | })
124 | )
125 | );
126 | }
127 | });
128 |
129 | module.exports = Table;
130 |
--------------------------------------------------------------------------------