├── .gitignore ├── package.json ├── LICENSE ├── gulpfile.js ├── rjt.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 27 | node_modules 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-json-table", 3 | "version": "0.1.1", 4 | "description": "A simple but reactive table react component to display JSON data.", 5 | "main": "rjt.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/arqex/react-json-table.git" 12 | }, 13 | "keywords": [ 14 | "react", 15 | "table", 16 | "json" 17 | ], 18 | "author": "Javier Marquez", 19 | "license": "MIT", 20 | "bugs": { 21 | "url": "https://github.com/arqex/react-json-table/issues" 22 | }, 23 | "homepage": "https://github.com/arqex/react-json-table", 24 | "devDependencies": { 25 | "gulp": "^3.9.0", 26 | "gulp-insert": "^0.4.0", 27 | "gulp-uglify": "^1.2.0", 28 | "gulp-webpack": "^1.4.0" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 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 all 13 | 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 THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'), 2 | uglify = require('gulp-uglify'), 3 | insert = require('gulp-insert'), 4 | webpack = require('gulp-webpack') 5 | ; 6 | 7 | var packageName = 'react-json-table'; 8 | var pack = require( './package.json' ); 9 | 10 | var getWPConfig = function( filename ){ 11 | return { 12 | externals: { 13 | react: { 14 | root: 'React' 15 | } 16 | }, 17 | output: { 18 | libraryTarget: 'umd', 19 | library: 'JsonTable', 20 | filename: filename + '.js' 21 | } 22 | }; 23 | }; 24 | 25 | var cr = ('/*\n%%name%% v%%version%%\n%%homepage%%\n%%license%%: https://github.com/arqex/' + packageName + '/raw/master/LICENSE\n*/\n') 26 | .replace( '%%name%%', pack.name) 27 | .replace( '%%version%%', pack.version) 28 | .replace( '%%license%%', pack.license) 29 | .replace( '%%homepage%%', pack.homepage) 30 | ; 31 | 32 | function build( config, minify ){ 33 | var stream = gulp.src('./rjt.js') 34 | .pipe( webpack( config ) ) 35 | ; 36 | 37 | if( minify ){ 38 | stream.pipe( uglify() ); 39 | } 40 | 41 | return stream.pipe( insert.prepend( cr ) ) 42 | .pipe( gulp.dest('build/') ) 43 | ; 44 | } 45 | 46 | gulp.task("build", function( callback ) { 47 | build( getWPConfig( packageName ) ); 48 | return build( getWPConfig( packageName + '.min' ), true ); 49 | }); 50 | 51 | gulp.task( 'default', ['build'] ); 52 | -------------------------------------------------------------------------------- /rjt.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | 3 | var $ = React.DOM; 4 | 5 | // Some shared attrs for JsonTable and JsonRow 6 | var defaultSettings = { 7 | header: true, 8 | noRowsMessage: 'No items', 9 | classPrefix: 'json' 10 | }, 11 | getSetting = function( name ){ 12 | var settings = this.props.settings; 13 | 14 | if( !settings || typeof settings[ name ] == 'undefined' ) 15 | return defaultSettings[ name ]; 16 | 17 | return settings[ name ]; 18 | } 19 | ; 20 | 21 | var JsonTable = React.createClass({ 22 | getSetting: getSetting, 23 | 24 | render: function(){ 25 | var cols = this.normalizeColumns(), 26 | contents = [this.renderRows( cols )] 27 | ; 28 | 29 | if( this.getSetting('header') ) 30 | contents.unshift( this.renderHeader( cols ) ); 31 | 32 | var tableClass = this.props.className || this.getSetting( 'classPrefix' ) + 'Table'; 33 | 34 | return $.table({ className: tableClass }, contents ); 35 | }, 36 | 37 | renderHeader: function( cols ){ 38 | var me = this, 39 | prefix = this.getSetting( 'classPrefix' ), 40 | headerClass = this.getSetting( 'headerClass' ), 41 | cells = cols.map( function(col){ 42 | var className = prefix + 'Column'; 43 | if( headerClass ) 44 | className = headerClass( className, col.key ); 45 | 46 | return $.th( 47 | { className: className, key: col.key, onClick: me.onClickHeader, "data-key": col.key }, 48 | col.label 49 | ); 50 | }) 51 | ; 52 | 53 | return $.thead({ key: 'th' }, 54 | $.tr({ className: prefix + 'Header' }, cells ) 55 | ); 56 | }, 57 | 58 | renderRows: function( cols ){ 59 | var me = this, 60 | items = this.props.rows, 61 | settings = this.props.settings || {}, 62 | i = 1 63 | ; 64 | 65 | if( !items || !items.length ) 66 | return $.tbody({key:'body'}, [$.tr({key:'row'}, $.td({key:'column'}, this.getSetting('noRowsMessage') ))]); 67 | 68 | var rows = items.map( function( item ){ 69 | var key = me.getKey( item, i ); 70 | return React.createElement(Row, { 71 | key: key, 72 | reactKey: key, 73 | item: item, 74 | settings: settings, 75 | columns: cols, 76 | i: i++, 77 | onClickRow: me.onClickRow, 78 | onClickCell: me.onClickCell 79 | }); 80 | }); 81 | 82 | return $.tbody({key:'body'}, rows); 83 | }, 84 | 85 | getItemField: function( item, field ){ 86 | return item[ field ]; 87 | }, 88 | 89 | normalizeColumns: function(){ 90 | var getItemField = this.props.cellRenderer || this.getItemField, 91 | cols = this.props.columns, 92 | items = this.props.rows 93 | ; 94 | 95 | if( !cols ){ 96 | if( !items || !items.length ) 97 | return []; 98 | 99 | return Object.keys( items[0] ).map( function( key ){ 100 | return { key: key, label: key, cell: getItemField }; 101 | }); 102 | } 103 | 104 | return cols.map( function( col ){ 105 | var key; 106 | if( typeof col == 'string' ){ 107 | return { 108 | key: col, 109 | label: col, 110 | cell: getItemField 111 | }; 112 | } 113 | 114 | if( typeof col == 'object' ){ 115 | key = col.key || col.label; 116 | 117 | // This is about get default column definition 118 | // we use label as key if not defined 119 | // we use key as label if not defined 120 | // we use getItemField as cell function if not defined 121 | return { 122 | key: key, 123 | label: col.label || key, 124 | cell: col.cell || getItemField 125 | }; 126 | } 127 | 128 | return { 129 | key: 'unknown', 130 | name:'unknown', 131 | cell: 'Unknown' 132 | }; 133 | }); 134 | }, 135 | 136 | getKey: function( item, i ){ 137 | var field = this.props.settings && this.props.settings.keyField; 138 | if( field && item[ field ] ) 139 | return item[ field ]; 140 | 141 | if( item.id ) 142 | return item.id; 143 | 144 | if( item._id ) 145 | return item._id; 146 | 147 | return i; 148 | }, 149 | 150 | shouldComponentUpdate: function(){ 151 | return true; 152 | }, 153 | 154 | onClickRow: function( e, item ){ 155 | if( this.props.onClickRow ){ 156 | this.props.onClickRow( e, item ); 157 | } 158 | }, 159 | 160 | onClickHeader: function( e ){ 161 | if( this.props.onClickHeader ){ 162 | this.props.onClickHeader( e, e.target.dataset.key ); 163 | } 164 | }, 165 | 166 | onClickCell: function( e, key, item ){ 167 | if( this.props.onClickCell ){ 168 | this.props.onClickCell( e, key, item ); 169 | } 170 | } 171 | }); 172 | 173 | var Row = React.createClass({ 174 | getSetting: getSetting, 175 | 176 | render: function() { 177 | var me = this, 178 | props = this.props, 179 | cellClass = this.getSetting('cellClass'), 180 | rowClass = this.getSetting('rowClass'), 181 | prefix = this.getSetting('classPrefix'), 182 | cells = props.columns.map( function( col ){ 183 | var content = col.cell, 184 | key = col.key, 185 | className = prefix + 'Cell ' + prefix + 'Cell_' + key 186 | ; 187 | 188 | if( cellClass ) 189 | className = cellClass( className, key, props.item ); 190 | 191 | if( typeof content == 'function' ) 192 | content = content( props.item, key ); 193 | 194 | return $.td( { 195 | className: className, 196 | key: key, 197 | "data-key": key, 198 | onClick: me.onClickCell 199 | }, content ); 200 | }) 201 | ; 202 | 203 | var className = prefix + 'Row ' + prefix + 204 | (props.i % 2 ? 'Odd' : 'Even') 205 | ; 206 | 207 | if( props.reactKey ) 208 | className += ' ' + prefix + 'Row_' + props.reactKey; 209 | 210 | if( rowClass ) 211 | className = rowClass( className, props.item ); 212 | 213 | return $.tr({ 214 | className: className, 215 | onClick: me.onClickRow, 216 | key: this.props.reactKey 217 | }, cells ); 218 | }, 219 | 220 | onClickCell: function( e ){ 221 | this.props.onClickCell( e, e.target.dataset.key, this.props.item ); 222 | }, 223 | 224 | onClickRow: function( e ){ 225 | this.props.onClickRow( e, this.props.item ); 226 | } 227 | }); 228 | 229 | module.exports = JsonTable; 230 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-json-table 2 | A simple but flexible table react component to display JSON data. 3 | 4 | As simple as feeding it with an array of objects. 5 | ```js 6 | var items = [ 7 | { name: 'Louise', age: 27, color: 'red' }, 8 | { name: 'Margaret', age: 15, color: 'blue'}, 9 | { name: 'Lisa', age:34, color: 'yellow'} 10 | ]; 11 | 12 | React.render(, document.body); 13 | ``` 14 | [See the example working](http://codepen.io/arqex/pen/JdWwoe?editors=011) 15 | 16 | Features: 17 | * No dependencies and in a UMD format. 18 | * Customizable cell contents to show your data the way you need. 19 | * Callbacks for clicks on headers, rows or cells. 20 | * Allows to add custom columns. 21 | * Enough `className` attributes to let you style it your own way. 22 | * Pure rendering, no internal state, everything comes from the props. 23 | 24 | ## Motivation 25 | Creating tables in react is a repetitive work: 26 | * Create the table wrapper 27 | * Create a wrapper also for every item 28 | * For every row print all the cells 29 | * Add some classes to let styling 30 | * I also want to listen to clicks in the header of every column in order to sorting. 31 | * Hey, I forgot to add `` tags so it is not working! Add them! 32 | * ... 33 | 34 | I don't want to do it ever again, JsonTable component will do that ugly stuff so on. 35 | 36 | ## Installation 37 | Using node package manager: 38 | ``` 39 | npm install react-json-table --save 40 | ``` 41 | You can also use the built UMD files [react-json-table.js](https://github.com/arqex/react-json-table/blob/master/build/react-json-table.js)(6KB) and [react-json-table.min.js](https://github.com/arqex/react-json-table/blob/master/build/react-json-table.min.js)(3KB) if you want `JsonTable` globally or as an AMD package. 42 | 43 | Half of the built version size is the code to create the UMD module. NPM version is really lightweight. 44 | 45 | ## Usage 46 | You can see the simplest example of use at the top of this page, but probably you would like to customize a bit the behaviour of the table to adapt it to your needs. Have a look at the accepted component props. 47 | 48 | ### props 49 | Prop name | Values | Description 50 | ---|---|--- 51 | rows | Array[Object] | The data you want to display in the table. 52 | columns | Array[String\|Object] | The columns and their order for the table. If it is a `string` the value attribute of the current row that matches it will be shown as cell content. But also it is possible to use an `object` to customize the column, see [column definition](#column-definition). 53 | className | *string* | Class to use for the `` element. 54 | settings | Object | Further customization of the table, see [table settings](#table-settings). 55 | onClickCell | Function | Callback triggered when a cell is clicked: `fn( event, columnName, rowData )`. 56 | onClickRow | Function | Callback triggered when a row is clicked: `fn( event, rowData )` 57 | onClickHeader | Function | Callback triggered when a column header is clicked: `fn( event, columnName )` 58 | 59 | ### Column definition 60 | Using column definitions you can change the behaviour of the column easily. To do so you need to pass an array of the column definitions as the `columns` prop to the JsonTable: 61 | ```js 62 | var items = [ 63 | { name: 'Louise', age: 27, color: 'red' }, 64 | { name: 'Margaret', age: 15, color: 'blue'}, 65 | { name: 'Lisa', age:34, color: 'yellow'} 66 | ]; 67 | 68 | var columns = [ 69 | 'name', 70 | {key: 'age', label: 'Age'}, 71 | {key: 'color', label: 'Colourful', cell: function( item, columnKey ){ 72 | return { item.color }; 73 | }} 74 | ]; 75 | 76 | React.render(, document.body); 77 | ``` 78 | http://codepen.io/arqex/pen/waJREq?editors=011 79 | 80 | As you can see in the example, a column definition can be just a string with the name of the field to display or an object. But if an object is passed the customization can be much more. A column definition can be an object with the following properties: 81 | * `key`: It is the internal name use for the column by JsonTable. It is added to the className of the cells and headers to apply styles to the column. It is also passed as an argument for the click callbacks. If the column definition has no `cell` property, it also represent the property of the current row to be shown as cell content. 82 | * `label`: It is the content of the column header. You can use a `string` or a `ReactComponent` to show inside the header cell. 83 | * `cell`: What is going to be displayed inside the column cells. It can be a `string` or `ReactComponent` to show static contents, but tipically it is a `function( rowData, columnKey )` that return the contents for the cell. This way different contents are shown in the column for different rows. 84 | 85 | ### Table settings 86 | Using the prop `settings` we can customize some details that are not related to columns. It is an object with the following properties: 87 | 88 | Setting name | Values | Description 89 | ---|---|--- 90 | `cellClass` | *function* | It is possible to add custom classes to the cells if you pass a function `fn( currentClass, columnKey, rowData )` in this setting. 91 | `classPrefix` | *string* | JsonTable uses `class` attributes for its markup like `jsonRow` or `jsonCell`. The default prefix is `json` but you can use this setting to change it in the case it is conflicting with other classes in your app. 92 | `header` | *boolean* | If `false`, no header will be shown for the table. Default `true`. 93 | `headerClass` | *function* | It is possible to add custom classes to the column headers if you pass a function `fn( currentClass, columnKey )` in this setting. 94 | `keyField` | *string* | React components that have a list of children need to give to every children a different `key` prop in order to make the diff algorithm check if something has change. You can define here what field of your rows will be used as a row key. JsonTable uses the `id` or `_id` property of your rows automatically if you don't give this setting, but **you must be sure that there is a keyField for your rows** if you don't want strange behaviours on update. [More info](https://facebook.github.io/react/docs/multiple-components.html#dynamic-children). 95 | `noRowsMessage` | *string*, *ReactComponent* | Message shown when the table has no rows. Default *"No items"*. 96 | `rowClass` | *function* | It is possible to add custom classes to the rows if you pass a function `fn( currentClass, rowData )` in this setting. 97 | `cellRenderer` | *function(item,field)* | If provided, this function will be used to render all the cells' content, so it is a way of programatically customize every cell. If no provided, the cell contents will just be `item[field]`, the value of the item for that field. 98 | 99 | [You can play with the table settings here](http://codepen.io/arqex/pen/YXZBKG?editors=011). 100 | 101 | ### Reacting to clicks 102 | It is always useful binding some callbacks when the user clicks on the table. 103 | Click callbacks can be added using the props `onClickCell`, `onClickHeader` and `onClickRow`. In the next example we create a component using JsonTable where rows and cells are selected on click, and columns are sorted when the column header is clicked: 104 | ```js 105 | var SelectTable = React.createClass({ 106 | getInitialState: function(){ 107 | // We will store the selected cell and row, also the sorted column 108 | return {row: false, cell: false, sort: false}; 109 | }, 110 | 111 | render: function(){ 112 | var me = this, 113 | // clone the rows 114 | items = this.props.rows.slice() 115 | ; 116 | // Sort the table 117 | if( this.state.sort ){ 118 | items.sort( function( a, b ){ 119 | return a[ me.state.sort ] > b[ me.state.sort ] ? 1 : -1; 120 | }); 121 | } 122 | 123 | return ; 129 | }, 130 | 131 | getSettings: function(){ 132 | var me = this; 133 | // We will add some classes to the selected rows and cells 134 | return { 135 | keyField: 'name', 136 | cellClass: function( current, key, item){ 137 | if( me.state.cell == key && me.state.row == item.name ) 138 | return current + ' cellSelected'; 139 | return current; 140 | }, 141 | headerClass: function( current, key ){ 142 | if( me.state.sort == key ) 143 | return current + ' headerSelected'; 144 | return current; 145 | }, 146 | rowClass: function( current, item ){ 147 | if( me.state.row == item.name ) 148 | return current + ' rowSelected'; 149 | return current; 150 | } 151 | }; 152 | }, 153 | 154 | onClickCell: function( e, column, item ){ 155 | this.setState( {cell: column} ); 156 | }, 157 | 158 | onClickHeader: function( e, column ){ 159 | this.setState( {sort: column} ); 160 | }, 161 | 162 | onClickRow: function( e, item ){ 163 | this.setState( {row: item.name} ); 164 | } 165 | }); 166 | ``` 167 | http://codepen.io/arqex/pen/pJPzox?editors=011 168 | 169 | ## What's next? 170 | Tests, tests, tests... I need to add tests for the different settings in order to continue the developing of new features. 171 | 172 | Of course, issues reports, feature and pull requests are welcome. If JsonTable can make you not to code a react table again I will be happy to help. 173 | 174 | ## [MIT Licensed](LICENSE) 175 | --------------------------------------------------------------------------------