├── js └── .gitkeeper ├── .npmignore ├── index.js ├── .babelrc ├── .gitignore ├── .eslintrc.json ├── example ├── webpack.config.js ├── index.html ├── package.json └── src │ └── index.js ├── package.json ├── src ├── icons.js ├── sortable-table-body.js ├── sortable-table-header.js └── sortable-table.js └── README.md /js/.gitkeeper: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src/ 2 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require("./js/sortable-table"); 2 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015", 4 | "react", 5 | "stage-1" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | .#* 3 | 4 | node_modules/ 5 | dist/ 6 | js/*.js 7 | sample/bundle.js 8 | example/bundle.js 9 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es6": true 4 | }, 5 | "parser": "babel-eslint", 6 | "parserOptions": { 7 | "sourceType": "module", 8 | "ecmaFeatures": { 9 | "jsx": true 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /example/webpack.config.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | 3 | module.exports = { 4 | entry: './src/index.js', 5 | output: { 6 | filename: 'bundle.js' 7 | }, 8 | module: { 9 | loaders: [ 10 | { 11 | test: /\.js$/, 12 | exclude: /node_modules/, 13 | loader: "babel" 14 | } 15 | ] 16 | }, 17 | resolveLoader: { 18 | modulesDirectories: [ "node_modules" ] 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | React Sortable Table Example 5 | 6 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-sortable-table-example", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "Rudolph-Miller", 10 | "license": "MIT", 11 | "dependencies": { 12 | "react": "^0.14.7", 13 | "react-dom": "^0.14.7", 14 | "react-sortable-table": "file:.." 15 | }, 16 | "devDependencies": { 17 | "babel-core": "^6.7.2", 18 | "babel-loader": "^6.2.4", 19 | "webpack": "^1.12.14" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Rudolph-Miller", 3 | "name": "react-sortable-table", 4 | "version": "1.4.0", 5 | "keywords": [ 6 | "react", 7 | "react-component", 8 | "table", 9 | "sortable" 10 | ], 11 | "description": "sortable table component in React.js", 12 | "main": "index.js", 13 | "scripts": { 14 | "prepublish": "node_modules/.bin/babel src --out-dir js", 15 | "build": "node_modules/.bin/babel src --out-dir js" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git://github.com/Rudolph-Miller/react-sortable-table" 20 | }, 21 | "dependencies": { 22 | "react": "^0.14.7" 23 | }, 24 | "license": "MIT", 25 | "devDependencies": { 26 | "babel-cli": "^6.26.0", 27 | "babel-preset-es2015": "^6.6.0", 28 | "babel-preset-react": "^6.5.0", 29 | "babel-preset-stage-1": "^6.5.0" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/icons.js: -------------------------------------------------------------------------------- 1 | import { Component, PropTypes } from 'react'; 2 | 3 | class FaIcon extends Component { 4 | static propTypes = { 5 | icon: PropTypes.string.isRequired 6 | } 7 | 8 | render() { 9 | const className = `fa fa-lg ${this.props.icon}` 10 | return ( 11 | 15 | ); 16 | } 17 | } 18 | 19 | export class SortIconBoth extends Component { 20 | render() { 21 | return ( 22 | 23 | ); 24 | } 25 | } 26 | 27 | export class SortIconAsc extends Component { 28 | render() { 29 | return ( 30 | 31 | ); 32 | } 33 | } 34 | 35 | export class SortIconDesc extends Component { 36 | render() { 37 | return ( 38 | 39 | ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/sortable-table-body.js: -------------------------------------------------------------------------------- 1 | import { Component, PropTypes } from 'react'; 2 | 3 | class SortableTableRow extends Component { 4 | render() { 5 | var tds = this.props.columns.map(function (item, index) { 6 | var value = this.props.data[item.key]; 7 | if ( item.render ) { 8 | value = item.render(value) 9 | } 10 | if ( item.renderWithData ) { 11 | value = item.renderWithData(this.props.data) 12 | } 13 | return ( 14 | 18 | {value} 19 | 20 | ); 21 | }.bind(this)); 22 | 23 | return ( 24 | 25 | {tds} 26 | 27 | ); 28 | } 29 | } 30 | 31 | export default class SortableTableBody extends Component { 32 | static propTypes = { 33 | data: PropTypes.array.isRequired, 34 | columns: PropTypes.array.isRequired, 35 | sortings: PropTypes.array.isRequired 36 | } 37 | 38 | render() { 39 | var bodies = this.props.data.map(((item, index) => { 40 | return ( 41 | 45 | ); 46 | }).bind(this)); 47 | 48 | return ( 49 | 50 | {bodies} 51 | 52 | ); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /example/src/index.js: -------------------------------------------------------------------------------- 1 | window.React = require('react'); 2 | import { render } from 'react-dom'; 3 | import React, { Component, PropTypes } from 'react'; 4 | import SortableTable from 'react-sortable-table'; 5 | 6 | function getFamilyName(name) { 7 | return name.split(' ').slice(-1)[0] 8 | } 9 | 10 | const FamilyNameSorter = { 11 | desc: (data, key) => { 12 | var result = data.sort(function (_a, _b) { 13 | const a = getFamilyName(_a[key]); 14 | const b = getFamilyName(_b[key]); 15 | if ( a <= b ) { 16 | return 1; 17 | } else if ( a > b) { 18 | return -1; 19 | } 20 | }); 21 | return result; 22 | }, 23 | 24 | asc: (data, key) => { 25 | return data.sort(function (_a, _b) { 26 | const a = getFamilyName(_a[key]); 27 | const b = getFamilyName(_b[key]); 28 | if ( a >= b ) { 29 | return 1; 30 | } else if ( a < b) { 31 | return -1; 32 | } 33 | }) 34 | } 35 | }; 36 | 37 | 38 | class App extends Component { 39 | constructor() { 40 | super() 41 | this.state = { 42 | data: [ 43 | { id: 3, name: 'Satoshi Yamamoto', class: 'B' }, 44 | { id: 1, name: 'Taro Tanak', class: 'A' }, 45 | { id: 2, name: 'Ken Asada', class: 'A' }, 46 | { id: 4, name: 'Masaru Tokunaga', class: 'C' } 47 | ] 48 | }; 49 | } 50 | 51 | render() { 52 | const columns = [ 53 | { 54 | header: 'ID', 55 | key: 'id', 56 | defaultSorting: 'ASC', 57 | headerStyle: { fontSize: '15px', backgroundColor: '#FFDAB9', width: '100px' }, 58 | dataStyle: { fontSize: '15px', backgroundColor: '#FFDAB9'}, 59 | dataProps: { className: 'align-right' }, 60 | render: (id) => { return {id}; } 61 | 62 | }, 63 | { 64 | header: 'NAME', 65 | key: 'name', 66 | headerStyle: { fontSize: '15px' }, 67 | headerProps: { className: 'align-left' }, 68 | descSortFunction: FamilyNameSorter.desc, 69 | ascSortFunction: FamilyNameSorter.asc 70 | }, 71 | { 72 | header: 'CLASS', 73 | key: 'class', 74 | headerStyle: { fontSize: '15px' }, 75 | sortable: false 76 | } 77 | ]; 78 | 79 | const style = { 80 | backgroundColor: '#eee' 81 | }; 82 | 83 | const iconStyle = { 84 | color: '#aaa', 85 | paddingLeft: '5px', 86 | paddingRight: '5px' 87 | }; 88 | 89 | return ( 90 | 95 | ); 96 | } 97 | } 98 | 99 | render(, document.getElementById('app')); 100 | -------------------------------------------------------------------------------- /src/sortable-table-header.js: -------------------------------------------------------------------------------- 1 | import { Component, PropTypes } from 'react'; 2 | 3 | import { SortIconBoth, SortIconDesc, SortIconAsc } from './icons'; 4 | 5 | class SortableTableHeaderItem extends Component { 6 | static propTypes = { 7 | headerProps: PropTypes.object, 8 | sortable: PropTypes.bool, 9 | sorting: PropTypes.oneOf(['desc', 'asc', 'both']), 10 | iconStyle: PropTypes.object, 11 | iconDesc: PropTypes.node, 12 | iconAsc: PropTypes.node, 13 | iconBoth: PropTypes.node 14 | } 15 | 16 | static defaultProps = { 17 | headerProps: {}, 18 | sortable: true 19 | } 20 | 21 | onClick(e) { 22 | if (this.props.sortable) 23 | this.props.onClick(this.props.index); 24 | } 25 | 26 | render() { 27 | let sortIcon; 28 | if (this.props.sortable) { 29 | if (this.props.iconBoth) { 30 | sortIcon = this.props.iconBoth; 31 | } else { 32 | sortIcon = ; 33 | } 34 | if (this.props.sorting == "desc") { 35 | if (this.props.iconDesc) { 36 | sortIcon = this.props.iconDesc; 37 | } else { 38 | sortIcon = ; 39 | } 40 | } else if (this.props.sorting == "asc") { 41 | if (this.props.iconAsc) { 42 | sortIcon = this.props.iconAsc; 43 | } else { 44 | sortIcon = ; 45 | } 46 | } 47 | } 48 | 49 | return ( 50 | 54 | {this.props.header} 55 | {sortIcon} 56 | 57 | ); 58 | } 59 | } 60 | 61 | export default class SortableTableHeader extends Component { 62 | static propTypes = { 63 | columns: PropTypes.array.isRequired, 64 | sortings: PropTypes.array.isRequired, 65 | onStateChange: PropTypes.func, 66 | iconStyle: PropTypes.object, 67 | iconDesc: PropTypes.node, 68 | iconAsc: PropTypes.node, 69 | iconBoth: PropTypes.node 70 | } 71 | 72 | onClick(index) { 73 | this.props.onStateChange.bind(this)(index); 74 | } 75 | 76 | render() { 77 | const headers = this.props.columns.map(((column, index) => { 78 | const sorting = this.props.sortings[index]; 79 | return ( 80 | 93 | ); 94 | }).bind(this)); 95 | 96 | return ( 97 | 98 | 99 | {headers} 100 | 101 | 102 | ); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sortable Table in React.js 2 | 3 | [![npm version](https://badge.fury.io/js/react-sortable-table.svg)](http://badge.fury.io/js/react-sortable-table) 4 | 5 | # Feature 6 | 7 | - Simple API 8 | - Customizable style 9 | - Customizable sorting functions 10 | 11 | __This component is depends on [Font Awesome](http://fortawesome.github.io/Font-Awesome/)__ 12 | Please activate Font Awesome. [Get started with Font Awesome](http://fortawesome.github.io/Font-Awesome/get-started/) 13 | [LICENSE of Font Awesome](http://fortawesome.github.io/Font-Awesome/license/) 14 | 15 | # Example 16 | 17 | https://rudolph-miller.github.io/react-sortable-table 18 | - ID: default sorting 19 | - rendered as `` tag. 20 | - NAME: custom sorting function that sort names by the family name 21 | - CLASS: unsortable 22 | 23 | # Install 24 | 25 | ``` 26 | npm install react-sortable-table 27 | ``` 28 | 29 | # Usage 30 | 31 | ```js 32 | window.React = require('react'); 33 | import { render } from 'react-dom'; 34 | import React, { Component, PropTypes } from 'react'; 35 | import SortableTable from 'react-sortable-table'; 36 | 37 | function getFamilyName(name) { 38 | return name.split(' ').slice(-1)[0] 39 | } 40 | 41 | const FamilyNameSorter = { 42 | desc: (data, key) => { 43 | var result = data.sort(function (_a, _b) { 44 | const a = getFamilyName(_a[key]); 45 | const b = getFamilyName(_b[key]); 46 | if ( a <= b ) { 47 | return 1; 48 | } else if ( a > b) { 49 | return -1; 50 | } 51 | }); 52 | return result; 53 | }, 54 | 55 | asc: (data, key) => { 56 | return data.sort(function (_a, _b) { 57 | const a = getFamilyName(_a[key]); 58 | const b = getFamilyName(_b[key]); 59 | if ( a >= b ) { 60 | return 1; 61 | } else if ( a < b) { 62 | return -1; 63 | } 64 | }) 65 | } 66 | }; 67 | 68 | 69 | class App extends Component { 70 | constructor() { 71 | super() 72 | this.state = { 73 | data: [ 74 | { id: 3, name: 'Satoshi Yamamoto', class: 'B' }, 75 | { id: 1, name: 'Taro Tanak', class: 'A' }, 76 | { id: 2, name: 'Ken Asada', class: 'A' }, 77 | { id: 4, name: 'Masaru Tokunaga', class: 'C' } 78 | ] 79 | }; 80 | } 81 | 82 | render() { 83 | const columns = [ 84 | { 85 | header: 'ID', 86 | key: 'id', 87 | defaultSorting: 'ASC', 88 | headerStyle: { fontSize: '15px', backgroundColor: '#FFDAB9', width: '100px' }, 89 | dataStyle: { fontSize: '15px', backgroundColor: '#FFDAB9'}, 90 | dataProps: { className: 'align-right' }, 91 | render: (id) => { return {id}; } 92 | }, 93 | { 94 | header: 'NAME', 95 | key: 'name', 96 | headerStyle: { fontSize: '15px' }, 97 | headerProps: { className: 'align-left' }, 98 | descSortFunction: FamilyNameSorter.desc, 99 | ascSortFunction: FamilyNameSorter.asc 100 | }, 101 | { 102 | header: 'CLASS', 103 | key: 'class', 104 | headerStyle: { fontSize: '15px' }, 105 | sortable: false 106 | } 107 | ]; 108 | 109 | const style = { 110 | backgroundColor: '#eee' 111 | }; 112 | 113 | const iconStyle = { 114 | color: '#aaa', 115 | paddingLeft: '5px', 116 | paddingRight: '5px' 117 | }; 118 | 119 | return ( 120 | 125 | ); 126 | } 127 | } 128 | 129 | render(, document.getElementById('app')); 130 | ``` 131 | 132 | # PropTypes 133 | 134 | - data: React.PropTypes.array.isRequired 135 | - columns: React.PropTypes.array.isRequired 136 | 137 | # Copyright 138 | 139 | Copyright (c) 2015 Rudolph-Miller (chopsticks.tk.ppfm@gmail.com) 140 | 141 | #License 142 | 143 | Licensed under the MIT License. 144 | -------------------------------------------------------------------------------- /src/sortable-table.js: -------------------------------------------------------------------------------- 1 | import { Component, PropTypes } from 'react'; 2 | import SortableTableHeader from './sortable-table-header'; 3 | import SortableTableBody from './sortable-table-body'; 4 | 5 | export default class SortableTable extends Component { 6 | static propTypes = { 7 | data: PropTypes.array.isRequired, 8 | columns: PropTypes.array.isRequired, 9 | style: PropTypes.object, 10 | iconStyle: PropTypes.object, 11 | iconDesc: PropTypes.node, 12 | iconAsc: PropTypes.node, 13 | iconBoth: PropTypes.node 14 | } 15 | 16 | constructor(props) { 17 | super(props) 18 | 19 | this.state = { 20 | sortings: this.getDefaultSortings(props) 21 | }; 22 | } 23 | 24 | getDefaultSortings(props) { 25 | return props.columns.map((column) => { 26 | let sorting = "both"; 27 | if (column.defaultSorting) { 28 | const defaultSorting = column.defaultSorting.toLowerCase(); 29 | 30 | if (defaultSorting == "desc") { 31 | sorting = "desc"; 32 | } else if (defaultSorting == "asc") { 33 | sorting = "asc"; 34 | } 35 | } 36 | return sorting; 37 | }); 38 | } 39 | 40 | sortData(data, sortings) { 41 | let sortedData = this.props.data; 42 | for (var i in sortings) { 43 | const sorting = sortings[i]; 44 | const column = this.props.columns[i]; 45 | const key = this.props.columns[i].key; 46 | switch (sorting) { 47 | case "desc": 48 | if (column.descSortFunction && 49 | typeof(column.descSortFunction) == "function") { 50 | sortedData = column.descSortFunction(sortedData, key); 51 | } else { 52 | sortedData = this.descSortData(sortedData, key); 53 | } 54 | break; 55 | case "asc": 56 | if (column.ascSortFunction && 57 | typeof(column.ascSortFunction) == "function") { 58 | sortedData = column.ascSortFunction(sortedData, key); 59 | } else { 60 | sortedData = this.ascSortData(sortedData, key); 61 | } 62 | break; 63 | } 64 | } 65 | return sortedData; 66 | } 67 | 68 | ascSortData(data, key) { 69 | return this.sortDataByKey(data, key, ((a, b) => { 70 | if ( this.parseFloatable(a) && this.parseFloatable(b) ) { 71 | a = this.parseIfFloat(a); 72 | b = this.parseIfFloat(b); 73 | } 74 | if ( a >= b ) { 75 | return 1; 76 | } else if ( a < b) { 77 | return -1; 78 | } 79 | }).bind(this)); 80 | } 81 | 82 | descSortData(data, key) { 83 | return this.sortDataByKey(data, key, ((a, b) => { 84 | if ( this.parseFloatable(a) && this.parseFloatable(b) ) { 85 | a = this.parseIfFloat(a); 86 | b = this.parseIfFloat(b); 87 | } 88 | if ( a <= b ) { 89 | return 1; 90 | } else if ( a > b) { 91 | return -1; 92 | } 93 | }).bind(this)); 94 | } 95 | 96 | parseFloatable(value) { 97 | return ( typeof(value) === "string" && ( /^\d+$/.test(value) || /^\d+$/.test(value.replace(/[,.%$]/g, "")) ) ) ? true : false; 98 | } 99 | 100 | parseIfFloat(value) { 101 | return parseFloat(value.replace(/,/g, "")); 102 | } 103 | 104 | sortDataByKey(data, key, fn) { 105 | const clone = Array.apply(null, data); 106 | 107 | return clone.sort((a, b) => { 108 | return fn(a[key], b[key]); 109 | }); 110 | } 111 | 112 | onStateChange(index) { 113 | const sortings = this.state.sortings.map(((sorting, i) => { 114 | if (i == index) 115 | sorting = this.nextSortingState(sorting); 116 | 117 | return sorting; 118 | }).bind(this)); 119 | 120 | this.setState({ 121 | sortings 122 | }); 123 | } 124 | 125 | nextSortingState(state) { 126 | let next; 127 | switch (state) { 128 | case "both": 129 | next = "desc"; 130 | break; 131 | case "desc": 132 | next = "asc"; 133 | break; 134 | case "asc": 135 | next= "both" 136 | break; 137 | } 138 | return next; 139 | } 140 | 141 | render() { 142 | const sortedData = this.sortData(this.props.data, this.state.sortings); 143 | 144 | return ( 145 | 148 | 156 | 160 |
161 | ); 162 | } 163 | } 164 | --------------------------------------------------------------------------------