├── .npmignore ├── index.js ├── .babelrc ├── .gitignore ├── src ├── index.js ├── utils │ ├── handleHtmlProp.js │ ├── injectProp.js │ ├── handleCustomRender.js │ ├── search.js │ └── paginate.js ├── __tests__ │ └── utils │ │ ├── utilities.js │ │ ├── injectProp.spec.js │ │ ├── handleHtmlProp.spec.js │ │ ├── handleCustomRender.spec.js │ │ ├── search.spec.js │ │ └── paginate.spec.js └── lib │ └── mui-data-table.js ├── .eslintrc.js ├── LICENSE.md ├── package.json └── README.md /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | coverage 3 | .coveralls.yml 4 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./dist/index'); 2 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-2", "react"] 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | coverage 4 | npm-debug.log 5 | .coveralls.yml 6 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import MuiDataTable from './lib/mui-data-table'; 2 | 3 | export { MuiDataTable }; 4 | -------------------------------------------------------------------------------- /src/utils/handleHtmlProp.js: -------------------------------------------------------------------------------- 1 | const hasHtml = (prop, arr) => ( 2 | !!(arr.filter((item) => item.property === prop)[0].html) 3 | ); 4 | 5 | const extractHtml = (prop, arr) => ( 6 | arr.filter((item) => item.property === prop)[0].html 7 | ); 8 | 9 | export { extractHtml, hasHtml }; 10 | -------------------------------------------------------------------------------- /src/utils/injectProp.js: -------------------------------------------------------------------------------- 1 | const injectProp = (arr) => { 2 | let count = 0, res = arr.slice(0); 3 | 4 | res.forEach((obj) => { 5 | if (!obj.property) { 6 | count += 1; 7 | obj.property = 'MuiDataTableProp-' + count; 8 | } 9 | }); 10 | 11 | return res; 12 | }; 13 | 14 | export default injectProp; 15 | -------------------------------------------------------------------------------- /src/__tests__/utils/utilities.js: -------------------------------------------------------------------------------- 1 | export const objCopy = (arr) => { 2 | const res = []; 3 | 4 | arr.forEach(function (item) { 5 | if (typeof item === 'object') res.push(Object.assign({}, item, {})); 6 | }); 7 | 8 | return res; 9 | }; 10 | 11 | export const lastElem = (arr) => { 12 | return arr[arr.length - 1]; 13 | }; 14 | -------------------------------------------------------------------------------- /src/utils/handleCustomRender.js: -------------------------------------------------------------------------------- 1 | const hasCustomRender = (prop, columns) => ( 2 | !!columns.filter((item) => item.property === prop && item.hasOwnProperty('renderAs'))[0] 3 | ); 4 | 5 | const callCustomRender = (prop, columns, obj) => { 6 | const property = columns.filter((item) => ( 7 | item.property === prop && item.hasOwnProperty('renderAs')) 8 | )[0]; 9 | 10 | return property.renderAs(obj); 11 | }; 12 | 13 | export { hasCustomRender, callCustomRender }; 14 | -------------------------------------------------------------------------------- /src/utils/search.js: -------------------------------------------------------------------------------- 1 | const search = (key, word, data) => { 2 | if (word.length < 1) return data; 3 | 4 | const res = []; 5 | const regex = new RegExp(word, 'i'); 6 | const keys = key.split('|'); 7 | 8 | data.forEach((item) => { 9 | for (let i = 0; i < keys.length; i += 1) { 10 | if ( String(item[keys[i]]).match(regex) ) { 11 | res.push(item); 12 | break; 13 | } 14 | } 15 | 16 | }); 17 | 18 | return res; 19 | }; 20 | 21 | export default search; 22 | -------------------------------------------------------------------------------- /src/__tests__/utils/injectProp.spec.js: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; 2 | import injectProp from '../../utils/injectProp'; 3 | 4 | const expect = chai.expect; 5 | const data = [ 6 | {name: 'ade', location: 'Lagos', age: 21, mood: 'happy', property: 'id'}, 7 | {name: 'tunde', location: 'Edo', age: 25, mood: 'angry'}, 8 | {name: 'bayo', location: 'Ibadan', age: 28, mood: 'scared'} 9 | ]; 10 | 11 | describe('The injectProp function', function () { 12 | it('should return a new array containing custom property attribute if it is not set in the config', function () { 13 | expect(injectProp(data)).to.eql([ 14 | {name: 'ade', location: 'Lagos', age: 21, mood: 'happy', property: 'id'}, 15 | {name: 'tunde', location: 'Edo', age: 25, mood: 'angry', property: 'MuiDataTableProp-1'}, 16 | {name: 'bayo', location: 'Ibadan', age: 28, mood: 'scared', property: 'MuiDataTableProp-2'} 17 | ]); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "browser": true, 4 | "commonjs": true, 5 | "es6": true 6 | }, 7 | "extends": ["eslint:recommended", "plugin:react/recommended"], 8 | "parserOptions": { 9 | "ecmaFeatures": { 10 | "experimentalObjectRestSpread": true, 11 | "jsx": true 12 | }, 13 | "sourceType": "module" 14 | }, 15 | "plugins": [ 16 | "react" 17 | ], 18 | "rules": { 19 | "indent": [ 20 | "error", 21 | 2 22 | ], 23 | "linebreak-style": [ 24 | "error", 25 | "unix" 26 | ], 27 | "quotes": [ 28 | "error", 29 | "single" 30 | ], 31 | "semi": [ 32 | "error", 33 | "always" 34 | ] 35 | }, 36 | "globals": { 37 | "describe": 0, 38 | "it": 0 39 | } 40 | }; 41 | -------------------------------------------------------------------------------- /src/__tests__/utils/handleHtmlProp.spec.js: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; 2 | import { extractHtml, hasHtml } from '../../utils/handleHtmlProp'; 3 | import { objCopy } from './utilities'; 4 | 5 | const expect = chai.expect; 6 | 7 | const data = [{ property: 'client_name', title: 'Client', html: '' }]; 8 | 9 | describe('The html utility functions', function () { 10 | describe('#hasHtml', function () { 11 | it('should return true if the property in the object array contains an html property', function () { 12 | expect(hasHtml('client_name', data)).to.be.true; 13 | }); 14 | 15 | it('should return false if the property does not contain an html property', function () { 16 | const clone = objCopy(data); 17 | delete clone[0].html; 18 | 19 | expect(hasHtml('client_name', clone)).to.be.false; 20 | }); 21 | }); 22 | 23 | describe('#extractHtml', function () { 24 | it('should extract and return the value of the html property', function () { 25 | 26 | expect(extractHtml('client_name', data)).to.equal(''); 27 | }); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Daniel Chinedu 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 NON INFRINGEMENT. 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 | -------------------------------------------------------------------------------- /src/__tests__/utils/handleCustomRender.spec.js: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; 2 | import { hasCustomRender, callCustomRender } from '../../utils/handleCustomRender'; 3 | import { objCopy } from './utilities'; 4 | 5 | const expect = chai.expect; 6 | 7 | const data = [{ property: 'client_name', title: 'Client', renderAs: function (data) { 8 | return data.message; 9 | } }]; 10 | 11 | describe('The render utility functions', function () { 12 | describe('#hasCustomRender', function () { 13 | it('should return true if the property in the object array contains a renderAs property', function () { 14 | expect(hasCustomRender('client_name', data)).to.be.true; 15 | }); 16 | 17 | it('should return false if the property does not contain an html property', function () { 18 | const clone = objCopy(data); 19 | delete clone[0].renderAs; 20 | 21 | expect(hasCustomRender('client_name', clone)).to.be.false; 22 | }); 23 | }); 24 | 25 | describe('#callCustomRender', function () { 26 | it('should call the callback function and return it\'s result', function () { 27 | 28 | expect(callCustomRender('client_name', data, {message: 'hello world'})).to.equal('hello world'); 29 | }); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /src/__tests__/utils/search.spec.js: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; 2 | import search from '../../utils/search'; 3 | 4 | const expect = chai.expect; 5 | const data = [ 6 | {name: 'ade', location: 'Lagos', age: 21, mood: 'happy'}, 7 | {name: 'tunde', location: 'Edo', age: 25, mood: 'angry'}, 8 | {name: 'bayo', location: 'Ibadan', age: 28, mood: 'scared'} 9 | ]; 10 | 11 | describe('The search function', function () { 12 | it('should return the original array if the word being searched for is empty', function () { 13 | expect(search('name', '', data)).to.eql(data); 14 | }); 15 | 16 | it('should return an empty array if the word isn\'t found in the list', function () { 17 | expect(search('name', 'damilare', data)).to.eql([]); 18 | }); 19 | 20 | it('should return the approriate result that match the word if found in the array', function () { 21 | expect(search('name', 'ade', data)).to.eql([data[0]]); 22 | }); 23 | 24 | it('should return results when there is a partial match', function () { 25 | expect(search('name', 'a', data)).to.eql([data[0], data[2]]); 26 | }); 27 | 28 | it('should be able to search using multiple keys', function () { 29 | expect(search('location|name|mood', 'angry', data)).to.eql([data[1]]); 30 | expect(search('location|name|mood', 'Lagos', data)).to.eql([data[0]]); 31 | expect(search('location|name|mood|age', '28', data)).to.eql([data[2]]); 32 | }); 33 | 34 | it('should perform case insensitive search(es)', function () { 35 | expect(search('name', 'TUNDE', data)).to.eql([data[1]]); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /src/utils/paginate.js: -------------------------------------------------------------------------------- 1 | class Paginate { 2 | constructor(arr) { 3 | this.arr = arr; 4 | } 5 | 6 | showingCalc(arr) { 7 | if (arr.length <= 1) return `1 - ${arr[0]}`; 8 | 9 | const start = (arr.slice(0, arr.length - 1).reduce((curr, next) => curr + next)) + 1; 10 | const stop = arr.reduce((curr, next) => curr + next); 11 | 12 | return `${start} - ${stop}`; 13 | } 14 | 15 | perPage(n = 5) { 16 | const clone = Array.from(this.arr); 17 | const totalNumOfPages = Math.ceil(this.arr.length / n); 18 | const total = clone.length; 19 | const currentlyShowing = []; 20 | const res = []; 21 | 22 | let count = 0; 23 | let temp = null; 24 | 25 | while (clone.length > 0) { 26 | count += 1; 27 | 28 | temp = clone.splice(0, n); 29 | currentlyShowing.push(temp.length); 30 | 31 | temp.push({ 32 | paginationInfo: { 33 | currentPage: count, 34 | nextPage: (count === totalNumOfPages ? null : (count + 1)), 35 | previousPage: ((count - 1) === 0 ? null : (count - 1)), 36 | currentlyShowing: `${this.showingCalc(currentlyShowing)} of ${total}`, 37 | isLastPage: (count === totalNumOfPages), 38 | totalNumOfPages, 39 | total 40 | } 41 | }); 42 | 43 | res.push(temp); 44 | } 45 | 46 | return new Paginate(res); 47 | } 48 | 49 | page(n) { 50 | const requestedPage = this.arr[n - 1]; 51 | const lastPage = this.arr[this.arr.length - 1]; 52 | 53 | return requestedPage || lastPage || []; 54 | } 55 | } 56 | 57 | export default Paginate; 58 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mui-data-table", 3 | "version": "0.1.7", 4 | "description": "Data table for react material-ui", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/andela-cdaniel/mui-data-table" 8 | }, 9 | "author": "Daniel Chinedu", 10 | "license": "MIT", 11 | "bugs": { 12 | "url": "https://github.com/andela-cdaniel/mui-data-table/issues" 13 | }, 14 | "homepage": "https://github.com/andela-cdaniel/mui-data-table", 15 | "keywords": [ 16 | "react", 17 | "react material-ui data table", 18 | "react material-ui", 19 | "react data table", 20 | "material ui", 21 | "material-ui data table" 22 | ], 23 | "scripts": { 24 | "prepublish": "babel src --ignore __tests__ --out-dir ./dist", 25 | "lint": "eslint ./src", 26 | "lintfix": "eslint ./src --fix", 27 | "test": "mocha --require babel-register src/__tests__/**/*.js", 28 | "test:coverage": "istanbul cover _mocha -- --require babel-register src/__tests__/**/*.js && istanbul-coveralls" 29 | }, 30 | "devDependencies": { 31 | "babel-cli": "^6.6.4", 32 | "babel-core": "^6.7.4", 33 | "babel-eslint": "^6.0.2", 34 | "babel-plugin-transform-es2015-modules-umd": "^6.6.5", 35 | "babel-polyfill": "^6.7.4", 36 | "babel-preset-es2015": "^6.6.0", 37 | "babel-preset-react": "^6.5.0", 38 | "babel-preset-stage-2": "^6.5.0", 39 | "babel-register": "^6.11.6", 40 | "chai": "^3.5.0", 41 | "enzyme": "^2.2.0", 42 | "eslint": "^2.7.0", 43 | "eslint-plugin-babel": "^3.1.0", 44 | "eslint-plugin-react": "^4.2.3", 45 | "istanbul": "1.0.0-alpha.2", 46 | "istanbul-coveralls": "^1.0.3", 47 | "jsdom": "^8.1.0", 48 | "mocha": "^2.4.5", 49 | "nodemon": "^1.9.1", 50 | "react": "^15.0.0", 51 | "react-addons-test-utils": "^15.0.0", 52 | "react-dom": "^15.3.0", 53 | "sinon": "^1.17.3" 54 | }, 55 | "peerDependencies": { 56 | "react": "~0.14.8 || ^15.0.0", 57 | "material-ui": "^0.15.1" 58 | }, 59 | "dependencies": { 60 | "babel-runtime": "^6.6.1", 61 | "dotenv": "^2.0.0" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/__tests__/utils/paginate.spec.js: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; 2 | import Paginate from '../../utils/paginate'; 3 | 4 | import { lastElem } from './utilities'; 5 | 6 | const expect = chai.expect; 7 | const data = [ 8 | {name: 'ade', location: 'Lagos', age: 21, mood: 'happy'}, 9 | {name: 'tunde', location: 'Edo', age: 25, mood: 'angry'}, 10 | {name: 'bayo', location: 'Ibadan', age: 28, mood: 'scared'}, 11 | {name: 'ade', location: 'Lagos', age: 21, mood: 'happy'}, 12 | {name: 'tunde', location: 'Edo', age: 25, mood: 'angry'}, 13 | {name: 'bayo', location: 'Ibadan', age: 28, mood: 'curious'} 14 | ]; 15 | const paginateObj = new Paginate(data); 16 | 17 | describe('The Paginate class', function () { 18 | it('should raise an error if called without the new keyword', function () { 19 | expect(Paginate).to.throw(TypeError); 20 | }); 21 | 22 | 23 | describe('#showingCalc', function () { 24 | it('should return a string after execution', function () { 25 | expect(typeof paginateObj.showingCalc([1,2,3,4,5])).to.eql('string'); 26 | }); 27 | 28 | it('should return 1 up to first argument of array if the length of the array isn\'t greater than 1', function () { 29 | expect(paginateObj.showingCalc([1])).to.eql('1 - 1'); 30 | }); 31 | 32 | it('should add one to the result of adding all items, with the exception of the\ 33 | last item in the array to get the start value', function () { 34 | expect(paginateObj.showingCalc([1,2,3]).split('-')[0].trim()).to.eql('4'); 35 | }); 36 | 37 | it('should add all items in the array to get the end value', function () { 38 | expect(paginateObj.showingCalc([1,2,3]).split('-')[1].trim()).to.eql('6'); 39 | }); 40 | 41 | it('should return the correct page range based on array argument', function () { 42 | expect(paginateObj.showingCalc([1,2,3])).to.eql('4 - 6'); 43 | expect(paginateObj.showingCalc([5,5,4])).to.eql('11 - 14'); 44 | expect(paginateObj.showingCalc([3,2,2])).to.eql('6 - 7'); 45 | }); 46 | }); 47 | 48 | describe('#perPage', function () { 49 | it('should return a new instance of Paginate', function () { 50 | expect(paginateObj.perPage(2) instanceof Paginate).to.be.true; 51 | }); 52 | 53 | it('should append a pagination info object to every item in the paginated array', function () { 54 | const val = paginateObj.perPage(3).arr; 55 | 56 | expect(val[0]).to.have.lengthOf(4); 57 | expect(val[1]).to.have.lengthOf(4); 58 | expect(lastElem(val[0])).have.ownProperty('paginationInfo'); 59 | }); 60 | 61 | it('should paginate with a default of 5 if no argument is passed in', function () { 62 | const val = paginateObj.perPage().arr[0]; 63 | 64 | expect(val.slice(0, val.length - 1)).to.have.lengthOf(5); 65 | }); 66 | 67 | }); 68 | 69 | describe('#page', function () { 70 | it('should return the item if it finds the item at a given index', function () { 71 | const val = paginateObj.perPage(3).page(1); 72 | 73 | expect(val.slice(0, val.length - 1)).to.eql([ data[0], data[1], data[2] ]); 74 | }); 75 | 76 | it('should return the last item if a number higher than the total amount of pages is passed in', function () { 77 | const val = paginateObj.perPage(3).page(5); 78 | 79 | expect(val.slice(0, val.length - 1)).to.eql([ data[3], data[4], data[5] ]); 80 | }); 81 | 82 | it('should return an empty array if the original array is empty', function () { 83 | const temp = []; 84 | const val = new Paginate(temp).perPage(5).page(1); 85 | 86 | expect(val).to.eql([]); 87 | }); 88 | }); 89 | 90 | }); 91 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mui Data Table 2 | 3 | ## Important Notice: 4 | 5 | This project is no longer actively maintained and as such I won't be be able to respond to issues or accept new or existing pull requests. 6 | 7 | Alternatives are listed below: 8 | 9 | - https://github.com/gregnb/mui-datatables 10 | - https://github.com/hyojin/material-ui-datatables 11 | 12 |
13 | 14 | Data table implementation for [react material-ui](http://www.material-ui.com/#/) 15 | 16 | [![CircleCI](https://circleci.com/gh/blueyedgeek/mui-data-table/tree/master.svg?style=shield)](https://circleci.com/gh/blueyedgeek/mui-data-table/tree/master) [![Coverage Status](https://coveralls.io/repos/github/andela-cdaniel/mui-data-table/badge.svg?branch=ch-add-coverage-info)](https://coveralls.io/github/andela-cdaniel/mui-data-table?branch=ch-add-coverage-info) 17 | [![npm version](https://badge.fury.io/js/mui-data-table.svg)](https://badge.fury.io/js/mui-data-table) 18 | 19 | Mui data table was borne out of a need to integrate 20 | [Material design data tables](https://material.google.com/components/data-tables.html) with 21 | [react material ui](http://www.material-ui.com/#/). It achieves this by extending the table component already provided 22 | by material ui with new behaviour. 23 | 24 | ~Mui data table is still in active development and there is still a plan to add even more features to make it more robust 25 | and flexible.~ 26 | 27 | ## Demo 28 | 29 | [demo](https://blueyedgeek.github.io/mui-data-table/build/) 30 | 31 | ## Features 32 | 33 | * Pagination 34 | * Search / Filter 35 | * Custom renderer / formatter 36 | 37 | ## Dependencies 38 | 39 | * React 40 | * Material-ui 41 | 42 | Mui data table is a React component and as such, you'd need to have react installed before installing this package. 43 | You'll also need to ensure that you are using the material-ui component library in your application. 44 | 45 | ## Installation 46 | 47 | Mui data table is available as an npm package, you can install from the command line using this command: 48 | 49 | ```bash 50 | npm install mui-data-table --save 51 | ``` 52 | 53 | ## Usage 54 | 55 | Mui data table is designed to be easy to use. It has a ridiculously simple api and all you have to do is to pass a 56 | configuration object as a property and that is all the setup you need to get started. 57 | 58 | ### Example Usage 59 | 60 | ```javascript 61 | import React from 'react'; 62 | import ReactDOM from 'react-dom'; 63 | import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider'; 64 | import { MuiDataTable } from 'mui-data-table'; 65 | 66 | const data = [ 67 | { id: 1, name: 'Chikwa Eligson', age: 24, location: 'Lagos', level: 'stage-1', mood: 'happy' }, 68 | { id: 2, name: 'Bamidele Johnson', age: 18, location: 'Anambra', level: 'stage-4', mood: 'anxious' }, 69 | { id: 3, name: 'John Lee', age: 20, location: 'Abuja', level: 'stage-2', mood: 'indifferent' }, 70 | { id: 4, name: 'Binta Pelumi', age: 22, location: 'Jos', level: 'stage-3', mood: 'sad' }, 71 | { id: 5, name: 'Cassidy Ferangamo', age: 30, location: 'Lagos', level: 'stage-4', mood: 'angry' }, 72 | { id: 6, name: 'Damian Swaggbag', age: 35, location: 'PortHarcourt', level: 'stage-1', mood: 'bitter' }, 73 | { id: 7, name: 'Loveth Sweetstick', age: 20, location: 'Imo', level: 'stage-3', mood: 'happy' }, 74 | { id: 8, name: 'Zzaz Zuzzi', age: 19, location: 'Bayelsa', level: 'stage-2', mood: 'party-mood' }, 75 | { id: 9, name: 'Ian Sweetmouth', age: 18, location: 'Enugu', level: 'stage-4', mood: 'happy' }, 76 | { id: 10, name: 'Elekun Bayo', age: 21, location: 'Zamfara', level: 'stage-4', mood: 'anxious' }, 77 | ]; 78 | 79 | const config = { 80 | paginated: true, 81 | search: 'name', 82 | data: data, 83 | columns: [ 84 | { property: 'id', title: 'S/N'}, 85 | { property: 'name', title: 'Name' }, 86 | { property: 'age', title: 'Age' }, 87 | { property: 'location', title: 'Location' }, 88 | { property: 'level', title: 'level' }, 89 | { title: 'Mood', renderAs: function (data) { 90 | return `${data.name} is in a ${data.mood} mood.`; 91 | }}, 92 | ] 93 | viewSearchBarOnload: true // set to true or false. Default it is set to false. Shows the search bar or not depending on the value set 94 | }; 95 | 96 | class App extends React.Component { 97 | render() { 98 | return ( 99 | 100 | 101 | 102 | ); 103 | } 104 | } 105 | 106 | ReactDOM.render(, document.querySelector('#root')); 107 | ``` 108 | 109 | This will populate the table with the data specified in the `data` array with pagination and filtering turned on. 110 | 111 | ### Configuration options 112 | 113 | | Attribute | Type | Default | Description | 114 | |------------|------| ---------| -------------| 115 | | paginated | boolean or object | false | Determines if the generated table should be paginated. When it is passed the boolean value `true`, it paginates the table using the defaults: 5 rows per page and the table contains options to switch to 10 or 15 per page. You can also pass it an object with two available properties: `rowsPerPage` and `menuOptions`. `rowsPerPage` takes a number and will be used to determine how many values in the data object should be displayed per page while `menuOptions` which takes an array of numbers is used to populate the select field component with menu items, if this property is not declared or is set to false, the select field component will not show up on the table. You can check the demo to see how it is used.| 116 | |search | string | empty string ('') | Determines which property from the data object should be used to filter the values in the table when search is made. You can filter using multiple columns by seperating each value in the string with a pipe(\|) e.g: `'name\|mood\|location'`.| 117 | |data | array (containing only objects)| empty array ([]) | Determines the data that is used to populate the table with values. 118 | |column | array (containing only objects)| | Determines the mapping between the data object, table headers, table columns and table rows. | 119 | 120 | ### Column configuration 121 | 122 | The column attribute is used to determine how data in the `data` object should be displayed within the table. 123 | 124 | | Attribute | Type | Default | Description| 125 | |-----------|------|--------|-------------| 126 | |property| string | | Determines the attribute in the `data` object to be used to populate a column. It is ignored if a `renderAs` attribute is present as well.| 127 | |title| string | | Determines the header name for a particular column. It can be set to an empty string or totally removed if the header doesn't need to have a name.| 128 | |renderAs| function | | Overwrites the `property` attribute and is used instead to display data for a particular column. It takes one argument which is passed down from the data object. The argument will contain one individual object from the `data` object at a time and the `renderAs` method will be used to format the data before displaying it on the table | 129 | 130 | ## Contributing 131 | 132 | Feature requests can be made using [Github issues](https://github.com/andela-cdaniel/mui-data-table/issues) 133 | 134 | Pull requests are totally encouraged and you are welcome to contribute to the development of `mui-data-table`. Please do raise an issue before making a pull request so as to determine if a particular feature is already being worked on or is currently out of the scope of this project. 135 | 136 | 1. [Fork mui-data-table](https://github.com/andela-cdaniel/mui-data-table/fork) 137 | 2. Create a feature branch (git checkout -b my-new-fature) 138 | 3. Write tests if the feature needs to be tested 139 | 4. Ensure the code lints successfully 140 | 5. Commit your changes 141 | 6. Push to your branch 142 | 7. Make a pull request 143 | 144 | ## License 145 | 146 | `mui-data-table` is released under the [MIT License](https://github.com/andela-cdaniel/mui-data-table/blob/master/LICENSE.md) and by contributing, you agree that your contributions will be licensed under it. 147 | -------------------------------------------------------------------------------- /src/lib/mui-data-table.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Table, TableBody, TableHeader, TableHeaderColumn, TableRow, TableRowColumn, TableFooter } from 'material-ui/Table'; 3 | 4 | import SelectField from 'material-ui/SelectField'; 5 | import MenuItem from 'material-ui/MenuItem'; 6 | 7 | import Paper from 'material-ui/Paper'; 8 | 9 | import FilterList from 'material-ui/svg-icons/content/filter-list'; 10 | import SearchIcon from 'material-ui/svg-icons/action/search'; 11 | import NavigateRight from 'material-ui/svg-icons/image/navigate-next'; 12 | import NavigateLeft from 'material-ui/svg-icons/image/navigate-before'; 13 | 14 | import injectProp from '../utils/injectProp'; 15 | import { hasHtml, extractHtml } from '../utils/handleHtmlProp'; 16 | import { hasCustomRender, callCustomRender } from '../utils/handleCustomRender'; 17 | import arraySearch from '../utils/search.js'; 18 | import Paginate from '../utils/paginate'; 19 | 20 | const iconStyleFilter = { 21 | color: '#757575', 22 | cursor: 'pointer', 23 | transform: 'translateY(5px) translateX(-20px)' 24 | }; 25 | 26 | const searchHeaderColumnStyle = { 27 | position: 'relative', 28 | textAlign: 'right' 29 | }; 30 | 31 | const searchStyle = { 32 | color: '#777777', 33 | opacity: 1, 34 | transitionDuration: '0.6s', 35 | transitionProperty: 'opacity', 36 | border: 0, 37 | outline: 0, 38 | fontSize: 16, 39 | width: '100%', 40 | marginLeft: -22, 41 | padding: '7px 12px', 42 | textIndent: 3, 43 | cursor: 'text' 44 | }; 45 | 46 | const iconStyleSearch = { 47 | color: '#757575', 48 | position: 'absolute', 49 | top: '30%', 50 | opacity: 0, 51 | marginLeft: -76 52 | }; 53 | 54 | const navigationStyle = { 55 | cursor: 'pointer' 56 | }; 57 | 58 | export default class MuiDataTable extends React.Component { 59 | constructor(props) { 60 | super(); 61 | let tableData = props.config.data || []; 62 | let rowsPerPage = props.config.paginated.constructor === Object ? props.config.paginated.rowsPerPage : 5; 63 | let viewSearchBarOnload = props.config.viewSearchBarOnload === undefined ? true : props.config.viewSearchBarOnload; 64 | 65 | tableData = props.config.paginated ? new Paginate(tableData).perPage(rowsPerPage) : tableData; 66 | 67 | if (tableData instanceof Paginate) { 68 | tableData = tableData.page(1); 69 | } 70 | 71 | this.state = { 72 | disabled: !viewSearchBarOnload, 73 | style: !viewSearchBarOnload ? {...searchStyle, opacity: 0} : searchStyle, 74 | idempotentData: props.config.data, 75 | paginatedIdempotentData: new Paginate(props.config.data), 76 | perPageSelection: props.config.paginated.rowsPerPage || 5, 77 | tableData: tableData, 78 | searchData: [], 79 | isSearching: false, 80 | navigationStyle, 81 | iconStyleSearch: iconStyleSearch, 82 | }; 83 | 84 | this.columns = injectProp(props.config.columns); 85 | this.toggleSearch = this.toggleSearch.bind(this); 86 | this.searchData = this.searchData.bind(this); 87 | this.handlePerPageChange = this.handlePerPageChange.bind(this); 88 | this.navigateRight = this.navigateRight.bind(this); 89 | this.navigateLeft = this.navigateLeft.bind(this); 90 | } 91 | 92 | handlePerPageChange(evt, index, val) { 93 | const paginationInfo = this.paginationObject(); 94 | let data = this.state.paginatedIdempotentData; 95 | 96 | if (this.state.isSearching) { 97 | const tableData = this.state.searchData; 98 | data = new Paginate(tableData); 99 | } 100 | 101 | this.setState({ 102 | tableData: data.perPage(val).page(paginationInfo.currentPage), 103 | perPageSelection: val 104 | }); 105 | } 106 | 107 | paginationObject() { 108 | const res = this.state.tableData[this.state.tableData.length - 1]; 109 | 110 | if (!res || !res.paginationInfo) { 111 | return { 112 | perPage: 5, 113 | currentPage: 1, 114 | previousPage: null, 115 | nextPage: null, 116 | currentlyShowing: '0 - 0 of 0', 117 | isLastPage: true, 118 | totalNumOfPages: 0, 119 | total: 0 120 | }; 121 | } 122 | 123 | res.paginationInfo.perPage = this.state.perPageSelection; 124 | 125 | return res.paginationInfo; 126 | } 127 | 128 | showPaginationInfo() { 129 | return this.paginationObject().currentlyShowing; 130 | } 131 | 132 | navigateRight() { 133 | const paginationInfo = this.paginationObject(); 134 | let data = this.state.paginatedIdempotentData; 135 | 136 | if (this.state.isSearching) { 137 | const tableData = this.state.searchData; 138 | data = new Paginate(tableData); 139 | } 140 | 141 | this.setState({ 142 | tableData: data.perPage(paginationInfo.perPage).page(paginationInfo.nextPage) 143 | }); 144 | } 145 | 146 | navigateLeft() { 147 | const paginationInfo = this.paginationObject(); 148 | let data = this.state.paginatedIdempotentData; 149 | 150 | if (!paginationInfo.previousPage) return; 151 | 152 | if (this.state.isSearching) { 153 | const tableData = this.state.searchData; 154 | data = new Paginate(tableData); 155 | } 156 | 157 | this.setState({ 158 | tableData: data.perPage(paginationInfo.perPage).page(paginationInfo.previousPage) 159 | }); 160 | } 161 | 162 | mapColumnsToElems(cols) { 163 | return cols.map((item, index) => ( 164 | {item.title} 165 | )); 166 | } 167 | 168 | 169 | mapDataToProperties(properties, obj) { 170 | return properties.map((prop, index) => ( 171 | 172 | {this.renderTableData(obj, prop)} 173 | 174 | )); 175 | } 176 | 177 | populateTableWithdata(data, cols) { 178 | const properties = cols.map(item => item.property); 179 | 180 | return data.map((item, index) => { 181 | if (item.paginationInfo) return undefined; 182 | return ( 183 | 184 | {this.mapDataToProperties(properties, item)} 185 | 186 | ); 187 | }); 188 | } 189 | 190 | calcColSpan(cols) { 191 | return cols.length; 192 | } 193 | 194 | shouldShowItem(item) { 195 | const styleObj = { 196 | display: (item ? '' : 'none') 197 | }; 198 | 199 | return styleObj; 200 | } 201 | 202 | shouldShowMenu(defaultStyle) { 203 | if (this.props.config.paginated && this.props.config.paginated.constructor === Boolean) return defaultStyle; 204 | 205 | const menuOptions = this.props.config.paginated.menuOptions; 206 | 207 | return menuOptions ? defaultStyle : { display: 'none' }; 208 | } 209 | 210 | toggleOpacity(val) { 211 | return val === 0 ? 1 : 0; 212 | } 213 | 214 | toggleSearch() { 215 | const style = Object.assign({}, this.state.style, {}); 216 | const searchIconStyle = Object.assign({}, this.state.iconStyleSearch, {}); 217 | let disabledState = this.state.disabled; 218 | 219 | style.opacity = this.toggleOpacity(style.opacity); 220 | searchIconStyle.opacity = this.toggleOpacity(searchIconStyle.opacity); 221 | 222 | disabledState = !disabledState; 223 | 224 | this.setState({ 225 | style, 226 | iconStyleSearch: searchIconStyle, 227 | disabled: disabledState 228 | }); 229 | } 230 | 231 | searchData(e) { 232 | const key = this.props.config.search; 233 | const word = e.target.value; 234 | const data = this.state.idempotentData; 235 | let paginationInfo; 236 | 237 | let res = arraySearch(key, word, data); 238 | 239 | this.setState({ searchData: res }); 240 | 241 | if (word.length > 0) { 242 | this.setState({ isSearching: true }); 243 | } else { 244 | this.setState({ isSearching: false }); 245 | } 246 | 247 | if (this.props.config.paginated) { 248 | paginationInfo = this.paginationObject(); 249 | res = new Paginate(res).perPage(paginationInfo.perPage).page(1); 250 | } 251 | 252 | this.setState({ 253 | tableData: res 254 | }); 255 | } 256 | 257 | renderTableData(obj, prop) { 258 | const columns = this.columns; 259 | 260 | if (hasCustomRender(prop, columns)) { 261 | return callCustomRender(prop, columns, obj); 262 | } else if (obj[prop] && hasHtml(prop, columns)) { 263 | return ( 264 |
265 | {obj[prop]} 266 | {extractHtml(prop, columns)} 267 |
268 | ); 269 | } else if (!obj[prop] && hasHtml(prop, columns)) { 270 | return extractHtml(prop, columns); 271 | } else if (obj[prop] && !hasHtml(prop, columns)) { 272 | return obj[prop]; 273 | } 274 | 275 | return undefined; 276 | } 277 | 278 | setRowSelection(type, obj) { 279 | const menuOptions = type === 'object' ? obj.menuOptions : [5, 10, 15]; 280 | 281 | return menuOptions.map((num, index) => ( 282 | 283 | )); 284 | } 285 | 286 | handleRowSelection(obj) { 287 | if ( obj && obj.constructor === Boolean ) { 288 | return this.setRowSelection('', obj); 289 | } else if ( obj && obj.constructor === Object ) { 290 | return this.setRowSelection('object', obj); 291 | } else { 292 | return; 293 | } 294 | } 295 | 296 | render() { 297 | return ( 298 | 299 | 300 | 301 | 302 | 306 | 307 | 314 | 315 | 316 | 317 | 318 | 319 | {this.mapColumnsToElems(this.columns)} 320 | 321 | 322 | 323 | 324 | {this.populateTableWithdata(this.state.tableData, this.columns)} 325 | 326 | 327 | 328 | 329 | 332 | Rows per page: 333 | 338 | { this.handleRowSelection(this.props.config.paginated) } 339 | 340 | 341 | 342 | 343 | {this.showPaginationInfo()} 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 |
354 |
355 | ); 356 | } 357 | } 358 | 359 | MuiDataTable.propTypes = { 360 | config: React.PropTypes.object.isRequired 361 | }; 362 | --------------------------------------------------------------------------------