├── .gitignore ├── .mversionrc ├── .nvmrc ├── .travis.yml ├── CONTRIBUTING.md ├── README.md ├── bower.json ├── dist ├── react-table.js └── react-table.min.js ├── examples ├── app.js ├── data │ ├── names-since-2011.json │ └── names-small.json ├── index.html ├── non-cjs.html └── public │ ├── app.css │ ├── non-cjs.js │ └── react-with-addons.js ├── gulpfile.js ├── index.js ├── karma.conf.js ├── package.json ├── public └── react-table.css ├── src ├── constants.js ├── table-head.js ├── table-header.js ├── table-row.js └── table.js └── test └── unit ├── support ├── data.js ├── globals.js ├── helpers.js └── setup-and-teardown.js ├── table-head-test.js ├── table-header-test.js ├── table-row-test.js └── table-test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | *.built.* 4 | -------------------------------------------------------------------------------- /.mversionrc: -------------------------------------------------------------------------------- 1 | { 2 | "commitMessage": "%s", 3 | "tagName": "v%s", 4 | "scripts": { 5 | "preupdate": "echo 'Bumping version and running tests'", 6 | "precommit": "npm test", 7 | "postcommit": "git push && git push --tags && npm publish", 8 | "postupdate": "echo 'Updated to version %s in manifest files'" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v0.10.36 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | sudo: false 3 | node_js: 4 | - "0.10" 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Please include tests with your changes to help make reviewing and merging easy. 2 | 3 | #### How to test out local changes in another repo with `react-table` as a dependency 4 | 5 | Here's one approach to test out local changes, e.g. for a new branch called `#4-configurable-sort-column` 6 | 7 | 1. fork this repo on GitHub 8 | 2. clone your fork to your local machine 9 | 3. from your fork: `git checkout origin/#4-configurable-sort-column` 10 | 4. from this detached HEAD, checkout into a new local branch: `git checkout -b \#4-configurable-sort-column` 11 | 5. run [`npm link`](https://docs.npmjs.com/cli/link) 12 | 6. inside your project that uses `react-table` run `npm link react-table` to use your local copy 13 | 14 | Or you can also point your package.json to a git url e.g. 15 | 16 | ```json 17 | dependencies: { 18 | "react-table": "nicktomlin/react-table#4-configurable-sort-column" 19 | } 20 | ``` 21 | 22 | Although this won't allow you to run against local changes. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | React Table 2 | --- 3 | 4 | [![Build Status](http://img.shields.io/travis/NickTomlin/react-table.svg?style=flat&branch=master)](https://travis-ci.org/NickTomlin/react-table) 5 | ![NPM package](https://img.shields.io/npm/v/@nicktomlin/react-table.svg) 6 | 7 | A simple sortable table component for react. 8 | 9 | # Usage 10 | 11 | `npm i @nicktomlin/react-table` (not an npm user? see instructions below) 12 | 13 | ```javasript 14 | var React = require('react'); 15 | var ReactTable = require('react-table'); 16 | var data = [ 17 | {favoriteColor:'blue', age: 30, name: "Athos", job: "Musketeer"}, 18 | {favoriteColor: 'red' , age: 33, name: "Porthos", job: "Musketeer"}, 19 | {favoriteColor: 'blue' , age: 27, name: "Aramis", job: "Musketeer"}, 20 | {favoriteColor: 'orange' , age: 25, name: "d'Artagnan", job: "Guard"} 21 | ]; 22 | 23 | React.render(, document.body); 24 | ``` 25 | 26 | See examples for a more full featured use case. 27 | 28 | ## Usage without NPM 29 | 30 | Include the built files in `dist` with a ` 14 | 15 | -------------------------------------------------------------------------------- /examples/non-cjs.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | React Table 6 | 7 | 8 | 9 | 10 |

React Table

11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /examples/public/app.css: -------------------------------------------------------------------------------- 1 | body { 2 | max-width: 80em; 3 | margin: 0 auto; 4 | font-family: monospace; 5 | } 6 | 7 | button { 8 | border: 0; 9 | outline: none; 10 | background: tomato; 11 | padding: .75em .5em; 12 | color: white; 13 | border-radius:3px; 14 | cursor: pointer; 15 | } 16 | 17 | button:disabled { 18 | cursor: not-allowed; 19 | background: hsl(9, 36%, 61%); 20 | } 21 | 22 | .hero-button { 23 | text-align: center; 24 | font-size:3em; 25 | } 26 | 27 | table { 28 | width: 100%; 29 | border-spacing: 0; 30 | text-align: left; 31 | } 32 | 33 | .react-table__th--ascending:before { 34 | content: "↓" 35 | } 36 | 37 | .react-table__th--descending:before { 38 | content: "↑"; 39 | } 40 | 41 | .react-table__th { 42 | border-bottom: 2px solid #6699cc; /* 0d */ 43 | color: #2d2d2d; 44 | font-weight: bolder; 45 | font-size: 1.25em; 46 | padding: 1em 1em; 47 | text-transform: capitalize; 48 | } 49 | 50 | .react-table__tr:nth-child(odd) td { 51 | border-spacing: 0; 52 | background-color: #f2f0ec; 53 | } 54 | 55 | .react-table__td { 56 | color: #393939; 57 | padding: 1em 1em; 58 | } 59 | 60 | /* 61 | * Thanks to Chris Kempson for the wonderful base16 eighties theme 62 | * http://chriskempson.github.io/base16/#eighties 63 | */ 64 | 65 | .base00-background { background-color: #2d2d2d; } 66 | .base01-background { background-color: #393939; } 67 | .base02-background { background-color: #515151; } 68 | .base03-background { background-color: #747369; } 69 | .base04-background { background-color: #a09f93; } 70 | .base05-background { background-color: #d3d0c8; } 71 | .base06-background { background-color: #e8e6df; } 72 | .base07-background { background-color: #f2f0ec; } 73 | .base08-background { background-color: #f2777a; } 74 | .base09-background { background-color: #f99157; } 75 | .base0A-background { background-color: #ffcc66; } 76 | .base0B-background { background-color: #99cc99; } 77 | .base0C-background { background-color: #66cccc; } 78 | .base0D-background { background-color: #6699cc; } 79 | .base0E-background { background-color: #cc99cc; } 80 | .base0F-background { background-color: #d27b53; } 81 | 82 | .base00 { color: #2d2d2d; } 83 | .base01 { color: #393939; } 84 | .base02 { color: #515151; } 85 | .base03 { color: #747369; } 86 | .base04 { color: #a09f93; } 87 | .base05 { color: #d3d0c8; } 88 | .base06 { color: #e8e6df; } 89 | .base07 { color: #f2f0ec; } 90 | .base08 { color: #f2777a; } 91 | .base09 { color: #f99157; } 92 | .base0A { color: #ffcc66; } 93 | .base0B { color: #99cc99; } 94 | .base0C { color: #66cccc; } 95 | .base0D { color: #6699cc; } 96 | .base0E { color: #cc99cc; } 97 | .base0F { color: #d27b53; } 98 | -------------------------------------------------------------------------------- /examples/public/non-cjs.js: -------------------------------------------------------------------------------- 1 | (function awesomeIFFE () { 2 | var data = [ 3 | {favoriteColor:'blue', age: 30, name: "Athos", job: "Musketeer"}, 4 | {favoriteColor: 'red' , age: 33, name: "Porthos", job: "Musketeer"}, 5 | {favoriteColor: 'blue' , age: 27, name: "Aramis", job: "Musketeer"}, 6 | {favoriteColor: 'orange' , age: 25, name: "d'Artagnan", job: "Guard"} 7 | ]; 8 | 9 | var myTable = React.createFactory(ReactTable) 10 | 11 | React.render(myTable({data: data}, null), document.querySelector('#content')); 12 | })(); 13 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var gutil = require('gulp-util'); 3 | var browserify = require('browserify'); 4 | var streamify = require('gulp-streamify'); 5 | var connect = require('connect'); 6 | var livereload = require('gulp-livereload'); 7 | var http = require('http'); 8 | var source = require('vinyl-source-stream'); 9 | var rename = require('gulp-rename'); 10 | var uglify = require('gulp-uglify'); 11 | 12 | var CONFIG = { 13 | namespace: 'ReactTable', 14 | distDir: 'dist', 15 | outfile: 'react-table.js', 16 | examplesPublicDir: './examples/public', 17 | port: 3111 18 | }; 19 | 20 | function bify (src, includeReact) { 21 | var b = browserify({ 22 | entries: src, 23 | standalone: 'ReactTable' 24 | }); 25 | 26 | b.transform('babelify'); 27 | 28 | if (!includeReact) { 29 | b.transform('browserify-shim'); 30 | b.external('react'); 31 | } 32 | 33 | b.on('error', gutil.log.bind(gutil, 'Browserify Error')); 34 | 35 | return b; 36 | } 37 | 38 | function buildScript (bundle, outfileName, dest) { 39 | dest = dest || './dist'; 40 | 41 | return bundle 42 | .bundle() 43 | .pipe(source(outfileName)) 44 | .pipe(gulp.dest(dest)); 45 | } 46 | 47 | gulp.task('build:dev', function () { 48 | return buildScript(bify('./index.js'), CONFIG.outfile); 49 | }); 50 | 51 | gulp.task('build:release', function () { 52 | return buildScript(bify('./index.js'), CONFIG.outfile) 53 | .pipe(streamify(uglify())) 54 | .pipe(rename({suffix: '.min'})) 55 | .pipe(gulp.dest(CONFIG.distDir)); 56 | }); 57 | 58 | gulp.task('build:examples', ['build:release'], function () { 59 | return buildScript( 60 | bify('./examples/app.js', true), 61 | 'app.built.js', 62 | CONFIG.examplesPublicDir 63 | ); 64 | }); 65 | 66 | gulp.task('serve', ['build:examples'], function (cb) { 67 | var connectRoute = require('connect-route'); 68 | var fs = require('fs'); 69 | var data = fs.readFileSync('./examples/data/names-small.json', 'utf8'); 70 | 71 | var app = connect() 72 | .use(connect.logger('dev')) 73 | .use(connect.static('./examples')) 74 | .use(connect.static('./public')) 75 | .use(connect.static('./dist')) 76 | .use(connectRoute(function (router) { 77 | router.get('/data', function (req, res) { 78 | res.setHeader('Content-Type', 'application/json'); 79 | res.end(JSON.stringify(data)); 80 | }); 81 | })); 82 | 83 | http.createServer(app) 84 | .listen(CONFIG.port, function () { 85 | console.log('React Table example served up at', CONFIG.port); 86 | }) 87 | .on('close', cb); 88 | }); 89 | 90 | gulp.task('dev', function () { 91 | livereload.listen(); 92 | 93 | gulp.watch(['./src/**/*.js'], ['build:dev', 'build:examples']) 94 | .on('change', livereload.changed); 95 | gulp.watch(['./examples/*.js'], ['build:examples']) 96 | .on('change', livereload.changed); 97 | gulp.watch(['./examples/public/*.css']) 98 | .on('change', livereload.changed); 99 | 100 | // .start as alternative to .run http://stackoverflow.com/a/23298810/1048479 101 | gulp.start('serve'); 102 | }); 103 | 104 | gulp.task('default', ['dev']); 105 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./src/table'); 2 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function(config) { 2 | config.set({ 3 | // frameworks to use 4 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter 5 | frameworks: ['mocha', 'browserify', 'phantomjs-shim'], 6 | 7 | // list of files / patterns to load in the browser 8 | files: [ 9 | 'test/unit/support/globals.js', 10 | 'test/unit/support/*.js', 11 | 'test/unit/**/*-test.js' 12 | ], 13 | 14 | // preprocess matching files before serving them to the browser 15 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor 16 | preprocessors: { 17 | 'test/**/*.js': ['browserify'] 18 | }, 19 | 20 | browserify: { 21 | debug: true, 22 | // transform: [ 'babelify' ], 23 | // plugin: ['proxyquire-universal'], 24 | // extensions: ['.js', '.jsx'] 25 | }, 26 | 27 | // test results reporter to use 28 | // possible values: 'dots', 'progress' 29 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 30 | reporters: ['dots'], 31 | 32 | 33 | // web server port 34 | port: 9876, 35 | 36 | 37 | // enable / disable colors in the output (reporters and logs) 38 | colors: true, 39 | 40 | 41 | // level of logging 42 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 43 | logLevel: config.LOG_INFO, 44 | 45 | 46 | // enable / disable watching file and executing tests whenever any file changes 47 | autoWatch: true, 48 | 49 | 50 | // start these browsers 51 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 52 | //browsers: ['Chrome', 'Firefox', 'PhantomJS'], 53 | browsers: ['PhantomJS'], 54 | 55 | 56 | // Continuous Integration mode 57 | // if true, Karma captures browsers, runs the tests and exits 58 | singleRun: false 59 | }); 60 | }; 61 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@nicktomlin/react-table", 3 | "version": "1.3.0", 4 | "description": "A simple sortable table component for react", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "gulp", 8 | "test": "karma start karma.conf.js --single-run=true", 9 | "test:dev": "karma start karma.conf.js", 10 | "build": "gulp build:release" 11 | }, 12 | "author": "Nick Tomlin", 13 | "license": "ISC", 14 | "repository": { 15 | "type": "git", 16 | "url": "http://github.com/nicktomlin/react-table" 17 | }, 18 | "devDependencies": { 19 | "babelify": "^6.1.3", 20 | "browserify": "^5.10.0", 21 | "browserify-shim": "^3.8.2", 22 | "chai": "^3.2.0", 23 | "connect": "^2.25.7", 24 | "connect-route": "^0.1.4", 25 | "gulp": "^3.8.7", 26 | "gulp-livereload": "^2.1.1", 27 | "gulp-rename": "^1.2.0", 28 | "gulp-streamify": "0.0.5", 29 | "gulp-uglify": "^0.3.2", 30 | "gulp-util": "^3.0.0", 31 | "jshint": "^2.5.4", 32 | "karma": "^0.13.8", 33 | "karma-browserify": "^4.3.0", 34 | "karma-chrome-launcher": "^0.2.0", 35 | "karma-firefox-launcher": "^0.1.4", 36 | "karma-mocha": "^0.1.10", 37 | "karma-phantomjs-launcher": "^0.2.1", 38 | "karma-phantomjs-shim": "^1.0.0", 39 | "lodash": "^2.4.1", 40 | "morgan": "^1.2.3", 41 | "phantom-ownpropertynames": "^1.0.0", 42 | "proxyquire": "^1.6.0", 43 | "proxyquire-universal": "^1.0.8", 44 | "proxyquireify": "^3.0.0", 45 | "serve-static": "^1.5.3", 46 | "sinon": "^1.15.4", 47 | "sinon-chai": "^2.8.0", 48 | "superagent": "^0.18.2", 49 | "vinyl-source-stream": "^0.1.1", 50 | "watchify": "^1.0.2" 51 | }, 52 | "browserify": { 53 | "transform": [ 54 | "babelify" 55 | ] 56 | }, 57 | "browserify-shim": { 58 | "react": "global:React" 59 | }, 60 | "dependencies": { 61 | "react": "^0.12.1" 62 | }, 63 | "keywords": [ 64 | "react", 65 | "react-component", 66 | "table", 67 | "sortable-table", 68 | "sortable" 69 | ] 70 | } 71 | -------------------------------------------------------------------------------- /public/react-table.css: -------------------------------------------------------------------------------- 1 | .react-table { 2 | border-collapse: collapse; 3 | border-spacing: 0; 4 | empty-cells: show; 5 | text-align: left; 6 | } 7 | 8 | .react-table__thead { 9 | vertical-align: bottom; 10 | } 11 | 12 | .react-table__th { 13 | padding: .75em 1em; 14 | } 15 | 16 | .react-table caption { 17 | padding: 1em 0; 18 | text-align: center; 19 | } 20 | 21 | .react-table td, 22 | .react-table th { 23 | font-size: inherit; 24 | margin: 0; 25 | overflow: visible; 26 | padding: .75em 1em; 27 | } 28 | -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | moduleClass: 'react-table', 3 | thClass: 'th', 4 | headClass: 'thead', 5 | trClass: 'tr', 6 | tdClass: 'td' 7 | }; 8 | -------------------------------------------------------------------------------- /src/table-head.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var TableHeader = require('./table-header'); 3 | var constants = require('./constants'); 4 | 5 | module.exports = React.createClass({ 6 | className: constants.moduleClass + '__' + constants.headClass, 7 | getDefaultProps: function () { 8 | return { 9 | columns: [], 10 | columnDisplay: {} 11 | }; 12 | }, 13 | handleHeadingClick: function () { 14 | if (this.props.clickHandler) { 15 | this.props.clickHandler.apply(null, arguments); 16 | } 17 | }, 18 | renderHeader: function () { 19 | return this.props.columns.map(function (column) { 20 | var mappedValue = this.props.columnDisplay[column]; 21 | return ( 22 | 28 | {mappedValue ? mappedValue : column} 29 | 30 | ); 31 | }.bind(this)); 32 | }, 33 | render: function () { 34 | return ( 35 | 36 | {this.renderHeader()} 37 | 38 | ); 39 | } 40 | }); 41 | -------------------------------------------------------------------------------- /src/table-header.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var constants = require('./constants'); 3 | 4 | module.exports = React.createClass({ 5 | className: constants.moduleClass + '__' + constants.thClass, 6 | getDefaultProps: function () { 7 | return { 8 | isActive: false, 9 | sortDirection: 'ascending' 10 | }; 11 | }, 12 | handleClick: function () { 13 | if (this.props.clickHandler) { 14 | this.props.clickHandler({ 15 | sortKey: this.props.sortKey 16 | }); 17 | } 18 | }, 19 | getClassName: function () { 20 | var activeClass = this.props.isActive ? 21 | this.className + '--' + this.props.sortDirection : ''; 22 | return [this.className, activeClass].join(' '); 23 | }, 24 | render: function () { 25 | return ( 26 | 30 | {this.props.children} 31 | 32 | ); 33 | } 34 | }); 35 | -------------------------------------------------------------------------------- /src/table-row.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var constants = require('./constants'); 3 | 4 | module.exports = React.createClass({ 5 | className: constants.moduleClass + '__' + constants.trClass, 6 | getDefaultProps: function () { 7 | return { 8 | data: [] 9 | }; 10 | }, 11 | renderRowData: function () { 12 | var tds = []; 13 | var trClass = constants.moduleClass + '__' + constants.tdClass; 14 | 15 | for (var td in this.props.data) { 16 | tds.push( 17 | 18 | {this.props.data[td]} 19 | 20 | ); 21 | } 22 | 23 | return tds; 24 | }, 25 | render: function () { 26 | var rowData = this.renderRowData(); 27 | return ( 28 | 29 | {rowData} 30 | 31 | ); 32 | } 33 | }); 34 | -------------------------------------------------------------------------------- /src/table.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var TableRow = require('./table-row'); 3 | var TableHead = require('./table-head'); 4 | 5 | module.exports = React.createClass({ 6 | getDefaultProps: function () { 7 | return { 8 | data: [] 9 | }; 10 | }, 11 | getInitialState: function () { 12 | return { 13 | sortDirection: 'ascending', 14 | activeSortKey: this.props.initialSortKey 15 | }; 16 | }, 17 | handleHeadingClick: function (data) { 18 | var activeKey = this.state.activeSortKey; 19 | 20 | if (activeKey && activeKey === data.sortKey) { 21 | this.setState({ 22 | sortDirection: this.state.sortDirection === 23 | 'ascending' ? 'descending' : 'ascending' 24 | }); 25 | } else { 26 | this.setState({ 27 | activeSortKey: data.sortKey 28 | }, function () { 29 | }.bind(this)); 30 | } 31 | }, 32 | filterObject: function (obj) { 33 | var filteredData; 34 | var includedColumns = this.props.includedColumns; 35 | 36 | if (includedColumns) { 37 | filteredData = {}; 38 | 39 | includedColumns.forEach(function (k) { 40 | filteredData[k] = obj[k]; 41 | }); 42 | } else { 43 | filteredData = obj; 44 | } 45 | 46 | return filteredData; 47 | }, 48 | generateHeadersFromRow: function (row) { 49 | var data; 50 | var keys = []; 51 | 52 | if (row) { 53 | data = this.filterObject(row); 54 | keys = Object.keys(data); 55 | } 56 | 57 | return keys; 58 | }, 59 | renderHead: function () { 60 | var columns = this.generateHeadersFromRow(this.props.data[0]); 61 | return ( 62 | 69 | ); 70 | }, 71 | sortRow: function (options, rowA, rowB) { 72 | var a = rowA[options.key]; 73 | var b = rowB[options.key]; 74 | 75 | if (options.direction === 'ascending') { 76 | if (options.type === 'number') { 77 | return a - b; 78 | } else { 79 | if (a < b) return -1; 80 | if (a > b) return 1; 81 | return 0; 82 | } 83 | } else { 84 | if (options.type === 'number') { 85 | return b - a; 86 | } else { 87 | if (a > b) return -1; 88 | if (a < b) return 1; 89 | return 0; 90 | } 91 | } 92 | }, 93 | sortRows: function (data) { 94 | if (!data.length) { return data; } 95 | var sortConfig = {}; 96 | 97 | sortConfig.direction = this.state.sortDirection; 98 | 99 | if (this.state.activeSortKey) { 100 | sortConfig.key = this.state.activeSortKey; 101 | } else { 102 | sortConfig.key = data[0] ? 103 | Object.keys(data[0])[0] 104 | : undefined; 105 | } 106 | 107 | sortConfig.type = sortConfig.key ? typeof data[0][sortConfig.key] : undefined; 108 | 109 | return data 110 | .sort(this.sortRow.bind(this, sortConfig)); 111 | 112 | }, 113 | renderRow: function (row) { 114 | return ( 115 | 116 | ); 117 | }, 118 | renderRows: function () { 119 | // keep things immutable-ish 120 | var data = this.props.data.slice(); 121 | 122 | return this.sortRows(data) 123 | .map(this.renderRow); 124 | }, 125 | render: function () { 126 | return ( 127 | 128 | {this.renderHead()}, 129 | {this.renderRows()} 130 |
131 | ); 132 | } 133 | }); 134 | -------------------------------------------------------------------------------- /test/unit/support/data.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | data: [ 3 | {name: "bob", age: 41, eyeColor: 'blue'}, 4 | {name: "susan", age: 30, eyeColor: 'green'}, 5 | {name: "gerald", age: 15, eyeColor: 'blue'}, 6 | {name: "billy", age: 80, eyeColor: 'chrome'}, 7 | {name: "cal", age: 50, eyeColor: 'yellow'} 8 | ], 9 | dataWithMeta: [ 10 | {_private: "I should't be displayed", name: "bob", age: 41, eyeColor: 'blue'}, 11 | {_private: "I should't be displayed", name: "susan", age: 30, eyeColor: 'green'} 12 | ], 13 | headings: [ 14 | 'heading1', 15 | 'heading2', 16 | 'heading3' 17 | ] 18 | }; 19 | -------------------------------------------------------------------------------- /test/unit/support/globals.js: -------------------------------------------------------------------------------- 1 | require('phantom-ownpropertynames/implement'); // required to allow proxyquire deps to work with phantom :( 2 | var data = require('./data'); 3 | var React = require('react/addons'); 4 | var sinon = require('sinon'); 5 | var sinonChai = require('sinon-chai'); 6 | var chai = require('chai'); 7 | var TestUtils = React.addons.TestUtils; 8 | 9 | chai.use(sinonChai); 10 | 11 | var fixtures = {}; 12 | 13 | // mix data into fixtures 14 | for (var prop in data) { 15 | fixtures[prop] = data[prop]; 16 | } 17 | 18 | global.React = React; 19 | global.TestUtils = TestUtils; 20 | global.render = TestUtils.renderIntoDocument; 21 | 22 | global.sinon = sinon; 23 | global.sandbox = sinon.sandbox.create(); 24 | global.fixtures = fixtures; 25 | global.expect = chai.expect; 26 | -------------------------------------------------------------------------------- /test/unit/support/helpers.js: -------------------------------------------------------------------------------- 1 | function simulate (eventType, node) { 2 | TestUtils.Simulate[eventType](node); 3 | } 4 | 5 | module.exports = { 6 | render: function (Component, options) { 7 | _.extend({}, options); 8 | 9 | return TestUtils.renderIntoDocument(Component(options)); 10 | }, 11 | click: function (node) { 12 | simulate('click', node); 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /test/unit/support/setup-and-teardown.js: -------------------------------------------------------------------------------- 1 | afterEach(function () { 2 | global.sandbox.restore(); 3 | }); 4 | -------------------------------------------------------------------------------- /test/unit/table-head-test.js: -------------------------------------------------------------------------------- 1 | describe('TableHead', function () { 2 | var TableHead = require('../../src/table-head'); 3 | var TableHeader = require('../../src/table-header'); 4 | var helpers = require('./support/helpers'); 5 | 6 | function queryHeadings (Component) { 7 | return TestUtils.scryRenderedComponentsWithType(Component, TableHeader); 8 | } 9 | 10 | it('renders a TableHeader element for each item in the columns prop', function () { 11 | var head = render(); 12 | var headings = queryHeadings(head); 13 | 14 | expect(headings.length).to.eql(fixtures.headings.length); 15 | }); 16 | 17 | it('calls clickHandler handler when TableHeader is clicked', function () { 18 | var mockHandler = sandbox.spy(); 19 | var head = render( 20 | 21 | 26 |
27 | ); 28 | 29 | var secondHeading = queryHeadings(head)[1].getDOMNode(); 30 | helpers.click(secondHeading); 31 | 32 | expect(mockHandler).to.have.been.called; 33 | }); 34 | 35 | it('remaps table header display if object is passed in', function () { 36 | var display = { 37 | 'heading1': 'first heading', 38 | 'heading2': 'second heading', 39 | 'heading3': 'third heading' 40 | }; 41 | var head = render( 42 | 43 | 48 |
49 | ); 50 | 51 | var secondHeadingNode = queryHeadings(head)[1].getDOMNode(); 52 | expect(secondHeadingNode.textContent).to.eql('second heading'); 53 | }); 54 | 55 | it('uses the column name as the header display if no mapping is given', function () { 56 | var display = { 57 | 'heading1': 'first heading', 58 | }; 59 | var head = render( 60 | 61 | 66 |
67 | ); 68 | 69 | var headings = queryHeadings(head); 70 | var firstHeadingNode = headings[0].getDOMNode(); 71 | var secondHeadingNode = headings[1].getDOMNode(); 72 | 73 | expect(firstHeadingNode.textContent).to.eql('first heading'); 74 | expect(secondHeadingNode.textContent).to.eql(fixtures.headings[1]); 75 | }); 76 | }); 77 | -------------------------------------------------------------------------------- /test/unit/table-header-test.js: -------------------------------------------------------------------------------- 1 | describe('Table Header', function () { 2 | var helpers = require('./support/helpers'); 3 | var TableHeader = require('../../src/table-header'); 4 | 5 | function renderHeader (header) { 6 | var table = render( 7 | 8 | {header} 9 |
10 | ); 11 | 12 | return TestUtils.findRenderedComponentWithType(table, TableHeader); 13 | } 14 | 15 | it('renders a table header', function () { 16 | var header = renderHeader(); 17 | expect(header.getDOMNode().tagName).to.eql('TH'); 18 | }); 19 | 20 | it('calls clickHandler prop an event when it is clicked', function () { 21 | var mock = sandbox.spy(); 22 | var header = renderHeader(); 23 | 24 | helpers.click(header.getDOMNode()); 25 | 26 | expect(mock).to.have.been.called; 27 | }); 28 | 29 | it('calls clickHandler with the value of its props', function () { 30 | var mock = sandbox.spy(); 31 | var header = renderHeader( 32 | 36 | ); 37 | 38 | helpers.click(header.getDOMNode()); 39 | 40 | expect(mock).to.have.been.calledWith({ 41 | sortKey: 'name' 42 | }); 43 | }); 44 | 45 | it('adds a className based on its sort order', function () { 46 | var header = renderHeader(); 47 | var expectedClassName = header.getClassName(); 48 | 49 | TestUtils.findRenderedDOMComponentWithClass(header, expectedClassName); 50 | }); 51 | 52 | it('defaults to inactive', function () { 53 | var header = renderHeader(); 54 | 55 | expect(header.props.isActive).to.eql(false); 56 | }); 57 | 58 | it('adds an active class if props.isActive is true', function () { 59 | var header = renderHeader( 60 | 64 | ); 65 | var expectedClassName = header.getClassName(); 66 | 67 | TestUtils.findRenderedDOMComponentWithClass(header, expectedClassName); 68 | }); 69 | 70 | it('adds does not add an active class if props.isActive is false', function () { 71 | var sortDirection = 'descending'; 72 | var header = renderHeader( 73 | 77 | ); 78 | var expectedClassName = header.getClassName(); 79 | 80 | expect(expectedClassName).to.not.contain(sortDirection); 81 | TestUtils.findRenderedDOMComponentWithClass(header, expectedClassName); 82 | }); 83 | }); 84 | -------------------------------------------------------------------------------- /test/unit/table-row-test.js: -------------------------------------------------------------------------------- 1 | describe('Table Row', function () { 2 | var helpers = require('./support/helpers'); 3 | var TableRow = require('../../src/table-row'); 4 | 5 | function objectLength (obj) { 6 | return Object.keys(obj).length; 7 | } 8 | 9 | var data = { 10 | name: 'bob', 11 | occupation: 'steward', 12 | favoriteColor: 'blue' 13 | }; 14 | 15 | it('creates a for each element of supplied data object', function () { 16 | var tr = render(); 17 | var expectedTds = objectLength(data); 18 | 19 | var tds = TestUtils.scryRenderedDOMComponentsWithTag(tr, 'td'); 20 | expect(tds.length).to.eql(expectedTds); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /test/unit/table-test.js: -------------------------------------------------------------------------------- 1 | describe('Table', function () { 2 | var Table = require('../../src/table'); 3 | var TableHeader = require('../../src/table-header'); 4 | var TableHead = require('../../src/table-head'); 5 | var helpers = require('./support/helpers'); 6 | var _ = require('lodash'); 7 | 8 | function selecTrs (table) { 9 | return TestUtils.scryRenderedDOMComponentsWithTag(table, 'tr'); 10 | } 11 | 12 | function trsContain(table, array) { 13 | var trs = selecTrs(table); 14 | _.zip(trs, array) 15 | .forEach(function (pair) { 16 | var actual = parseInt(pair[0].getDOMNode().textContent); 17 | var expected = pair[1]; 18 | expect(actual).to.eql(expected); 19 | }); 20 | } 21 | 22 | it('renders a table', function () { 23 | var table = render(); 24 | expect(table.getDOMNode().tagName).to.eql('TABLE'); 25 | }); 26 | 27 | it('sets data prop to an an empty array if none is specified', function () { 28 | var table = render(
); 29 | expect(table.props.data).to.eql([]); 30 | }); 31 | 32 | it('uses an optional array to filter which properties show up in rows', function () { 33 | var table, filteredData; 34 | var data = [ 35 | {_dontInclude: 'no', include: 'yes', alsoInclude: 'yay'} 36 | ]; 37 | var row = data[0]; 38 | var columns = ['include', 'alsoInclude']; 39 | 40 | table = render(
); 41 | filteredData = table.filterObject(row); 42 | 43 | expect(filteredData).to.eql(_.omit(row, '_dontInclude')); 44 | }); 45 | 46 | describe('#renderHeader', function () { 47 | it('defaults to creating headers based on key names of data', function () { 48 | var table = render(
); 49 | var headers = table.generateHeadersFromRow(fixtures.data[0]); 50 | 51 | expect(headers).to.have.members(Object.keys(fixtures.data[0])); 52 | }); 53 | }); 54 | 55 | describe('sorting', function () { 56 | var sortData = [ 57 | {id: 10, name: "z"}, 58 | {id: 2000, name: "d"}, 59 | {id: 104, name: "a"}, 60 | {id: 90, name: "b"} 61 | ]; 62 | 63 | it('does not blow up if no data is provided', function () { 64 | expect(function () { 65 | render(
); 66 | }).to.not.throw(); 67 | }); 68 | 69 | it('takes an optional default sort parameter', function () { 70 | var table = render(
); 71 | var trs = selecTrs(table); 72 | 73 | expect(trs[0].getDOMNode().textContent).to.eql('104a'); 74 | }); 75 | 76 | it('defaults to sorting by first key in data', function () { 77 | var table = render(
); 78 | var trs = selecTrs(table); 79 | 80 | expect(trs[0].getDOMNode().textContent).to.eql('104a'); 81 | }); 82 | 83 | it('properly sorts numerical items', function () { 84 | var key = 'id'; 85 | var numericalData = [15, 47, 7, 7, 12, 15, 7, 15, 15, 27, 47].map(function (x) { return {id: x}; }); 86 | var sorted = [7, 7, 7, 12, 15, 15, 15, 15, 27, 47, 47]; 87 | 88 | var table = render(
); 89 | trsContain(table, sorted); 90 | }); 91 | 92 | 93 | it('properly sorts alphabetical items', function () { 94 | var key = 'id'; 95 | var data = ["SUFFOLK", "NASSAU", "SUFFOLK", "WESTCHESTER", "WESTCHESTER", "ONONDAGA", "WESTCHESTER", "WESTCHESTER", "SUFFOLK", "ONONDAGA", "ONONDAGA", "ONONDAGA", "WESTCHESTER", "WESTCHESTER", "SUFFOLK", "ONONDAGA", "ONONDAGA", "ONONDAGA", "ONONDAGA", "SUFFOLK", "SUFFOLK", "SUFFOLK", "SUFFOLK", "SUFFOLK", "WESTCHESTER"].map(function (x){ return {name: x}; }); 96 | 97 | var sorted = ["NASSAU", "ONONDAGA", "ONONDAGA", "ONONDAGA", "ONONDAGA", "ONONDAGA", "ONONDAGA", "ONONDAGA", "ONONDAGA", "SUFFOLK", "SUFFOLK", "SUFFOLK", "SUFFOLK", "SUFFOLK", "SUFFOLK", "SUFFOLK", "SUFFOLK", "SUFFOLK", "WESTCHESTER", "WESTCHESTER", "WESTCHESTER", "WESTCHESTER", "WESTCHESTER", "WESTCHESTER", "WESTCHESTER"]; 98 | 99 | var table = render(
); 100 | var actual = table.sortRows(data).map(function (x) { return x.name; }); 101 | 102 | expect(actual.every(function (x, index) { 103 | return x === sorted[index]; 104 | })).to.eql(true); 105 | }); 106 | 107 | it('sorts rows in ascending order by default, using the 1st key of a row as comparator', function () { 108 | var table = render(
); 109 | var trs = selecTrs(table); 110 | 111 | var smallestId = sortData[0].id + sortData[0].name; 112 | var largestId = sortData[1].id + sortData[1].name; 113 | 114 | expect(trs[0].getDOMNode().textContent).to.contain(smallestId); 115 | expect(trs[trs.length - 1].getDOMNode().textContent).to.eql(largestId); 116 | }); 117 | 118 | // TODO 119 | // the way we are checking this is ganky 120 | // is there a way we can isolate the sorting from the components 121 | // so we don't have to interact with the rendered component? 122 | 123 | it('sorts rows in descending order, if state.sortKey is descending, using the 1st key of a row as comparator', function () { 124 | var table = render(
); 125 | 126 | table.setState({sortDirection: 'descending'}); 127 | 128 | var sorted = sortData 129 | .sort(function (x,y) {return y.id - x.id; }) 130 | .map(function (x) { return x.id; }); 131 | 132 | trsContain(table, sorted); 133 | }); 134 | 135 | it('sorts with a key provided by state.sortKey', function () { 136 | var data = [ 137 | { 138 | 'id': 100, 139 | 'age': 1 140 | }, 141 | { 142 | 'id': 3, 143 | 'age': 300 144 | } 145 | ]; 146 | var table = render(
); 147 | table.setState({activeSortKey: 'age'}); 148 | 149 | trsContain(table, [1001, 3300]); 150 | }); 151 | 152 | describe('#handleHeadingClick', function () { 153 | var table; 154 | 155 | beforeEach(function () { 156 | table = render(
); 157 | }); 158 | 159 | afterEach(function () { 160 | table = null; 161 | }); 162 | 163 | it('changes sort order if called by active key', function () { 164 | var sortKey = 'name'; 165 | table.setState({activeSortKey: sortKey}); 166 | 167 | expect(table.state.sortDirection).to.eql('ascending'); 168 | 169 | table.handleHeadingClick({ 170 | sortKey: sortKey 171 | }); 172 | 173 | expect(table.state.sortDirection).to.eql('descending'); 174 | }); 175 | 176 | it('does not change sort order if called with non active header', function () { 177 | var key = 'foo'; 178 | 179 | expect(table.state.sortDirection).to.eql('ascending'); 180 | 181 | table.handleHeadingClick({ 182 | sortKey: key 183 | }); 184 | 185 | expect(table.state.sortDirection).to.eql('ascending'); 186 | }); 187 | 188 | it('changes active sort key if called with non active key', function () { 189 | var key = 'newKey'; 190 | 191 | expect(table.state.sortDirection).to.eql('ascending'); 192 | 193 | table.handleHeadingClick({ 194 | sortKey: key 195 | }); 196 | 197 | expect(table.state.activeSortKey).to.eql(key); 198 | }); 199 | 200 | it('toggles in between ascending and descending for same key', function () { 201 | var key = 'foo'; 202 | 203 | table.setState({ 204 | activeSortKey: key 205 | }); 206 | 207 | expect(table.state.sortDirection).to.eql('ascending'); 208 | 209 | table.handleHeadingClick({ 210 | sortKey: key 211 | }); 212 | 213 | expect(table.state.sortDirection).to.eql('descending'); 214 | 215 | table.handleHeadingClick({ 216 | sortKey: key 217 | }); 218 | 219 | expect(table.state.sortDirection).to.eql('ascending'); 220 | }); 221 | }); 222 | }); 223 | 224 | describe('#renderHead', function () { 225 | it('passes columnDisplay to head', function () { 226 | // can't wait for computed properties! 227 | var columnDisplay = {}; 228 | columnDisplay[fixtures.headings[0]] = fixtures.headings[0].toUpperCase(); 229 | columnDisplay[fixtures.headings[1]] = fixtures.headings[1].toUpperCase(); 230 | 231 | var table = render( 232 |
236 | ); 237 | 238 | var header = TestUtils.findRenderedComponentWithType(table, TableHead); 239 | var mappedHeaders = Object.keys(header.props.columnDisplay); 240 | 241 | expect(mappedHeaders.length).to.eql(2); 242 | }); 243 | }); 244 | 245 | describe('#renderRows', function () { 246 | it('returns an array containg tr components', function () { 247 | var table = render(
); 248 | expect(table.renderRows().length).to.eql(fixtures.data.length); 249 | }); 250 | }); 251 | }); 252 | --------------------------------------------------------------------------------