├── 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 | [](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 |
161 | );
162 | }
163 | }
164 |
--------------------------------------------------------------------------------