├── .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 | ![Selectable table screenshot](media/react-table-select.gif) 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 | 93 | 96 |
97 |

Selected rows: {this.state.selected.join(', ')}

98 |
99 |
100 |
101 |
102 | 108 | 109 | 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 | --------------------------------------------------------------------------------