├── .babelrc ├── .circleci └── config.yml ├── .eslintignore ├── .eslintrc ├── .github ├── CONTRIBUTING.MD ├── CONTRIBUTORS.MD ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .npmignore ├── .prettierrc ├── .storybook ├── addons.js ├── config.js ├── manager-head.html ├── preview-head.html ├── theme │ ├── dark.js │ ├── index.js │ └── light.js └── webpack.config.js ├── LICENSE ├── README.md ├── fixtures └── data.js ├── package.json ├── public ├── compress-logo-dark.svg ├── compress-logo.svg └── favicon.ico ├── setup-test.js ├── src ├── components │ ├── __snapshots__ │ │ ├── pagination.spec.js.snap │ │ └── table.spec.js.snap │ ├── index.js │ ├── pagination.js │ ├── pagination.spec.js │ ├── table.js │ └── table.spec.js ├── index.js └── styles │ ├── _pagination.scss │ ├── _table.scss │ └── index.scss ├── stories ├── index.stories.js ├── pagination.stories.js └── sorting.stories.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-react", "@babel/preset-env"], 3 | "plugins": ["@babel/plugin-proposal-class-properties"] 4 | } -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | defaults: &defaults 2 | working_directory: ~/rdt 3 | docker: 4 | - image: circleci/node:latest 5 | jobs: 6 | test-build: 7 | <<: *defaults 8 | steps: 9 | # Checkout the code from the branch into the working_directory 10 | - checkout 11 | # Log the current branch 12 | - run: 13 | name: Show current branch 14 | command: echo ${CIRCLE_BRANCH} 15 | # Download and cache dependencies 16 | - restore_cache: 17 | name: Restore YARN Package Cache 18 | keys: 19 | - v1-dependencies-{{ checksum "yarn.lock" }} 20 | # Install project dependencies 21 | - run: 22 | name: Install Dependencies 23 | command: yarn install 24 | # Cache local dependencies if they don't exist 25 | - save_cache: 26 | name: Save YARN Package Cache 27 | key: v1-dependencies-{{ checksum "yarn.lock" }} 28 | paths: 29 | - ./node_modules 30 | # Lint the server source code 31 | - run: 32 | name: Linting 33 | command: yarn lint 34 | - run: 35 | name: Setup Code Climate test-reporter 36 | command: | 37 | curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter 38 | chmod +x ./cc-test-reporter 39 | # Run tests 40 | - run: 41 | name: Run tests 42 | command: | 43 | ./cc-test-reporter before-build 44 | yarn test:coverage 45 | ./cc-test-reporter after-build --exit-code $? 46 | - store_test_results: 47 | path: reports/jest 48 | - store_artifacts: 49 | path: reports/jest 50 | - persist_to_workspace: 51 | root: ~/rdt 52 | paths: 53 | - . 54 | docs-deploy: 55 | <<: *defaults 56 | steps: 57 | - attach_workspace: 58 | at: ~/rdt 59 | - add_ssh_keys 60 | - run: 61 | name: Add SSH Host 62 | command: ssh-keyscan ${SSH_HOST} >> ~/.ssh/known_hosts 63 | - run: 64 | name: Buid docs 65 | command: yarn build-storybook 66 | - run: 67 | name: Configure dependencies 68 | command: git config --global user.name "${GH_NAME}" && git config --global user.email "${GH_EMAIL}" 69 | - run: 70 | name: Deploy docs to gh-pages branch 71 | command: yarn run deploy-docs --message "[skip ci] generate docs" 72 | - run: 73 | name: Deployed docs success 74 | command: echo "Successfully deployed docs" 75 | when: on_success 76 | workflows: 77 | version: 2 78 | build_and_deploy: 79 | jobs: 80 | - test-build: 81 | filters: 82 | branches: 83 | ignore: 84 | - gh-pages 85 | - docs-deploy: 86 | requires: 87 | - test-build 88 | filters: 89 | branches: 90 | only: master 91 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /build 2 | node_modules 3 | storybook-static 4 | coverage 5 | lib -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "parserOptions": { 4 | "sourceType": "module", 5 | "allowImportExportEverywhere": false, 6 | "codeFrame": false 7 | }, 8 | "extends": ["airbnb", "prettier"], 9 | "env": { 10 | "browser": true, 11 | "jest": true, 12 | "node": true, 13 | "es6": true 14 | }, 15 | "globals": { 16 | "mount": true, 17 | "render": true, 18 | "shallow": true, 19 | }, 20 | "rules": { 21 | "max-len": ["error", {"code": 100}], 22 | "prefer-promise-reject-errors": ["off"], 23 | "react/jsx-filename-extension": ["off"], 24 | "react/prop-types": ["warn"], 25 | "no-return-assign": ["off"], 26 | "import/no-extraneous-dependencies": ["error", {"devDependencies": true}] 27 | } 28 | } -------------------------------------------------------------------------------- /.github/CONTRIBUTING.MD: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | We'd love to accept your patches and contributions to this project. 4 | We want to make contributing to this project as easy and transparent as possible, whether it's: 5 | 6 | - Reporting a bug 7 | - Discussing the current state of the code 8 | - Submitting a fix 9 | - Proposing new features 10 | - Becoming a maintainer 11 | 12 | ## Code reviews 13 | 14 | All submissions, including submissions by project members, require review. We 15 | use GitHub pull requests for this purpose. Consult 16 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more 17 | information on using pull requests. 18 | 19 | ## Community Guidelines 20 | 21 | This project follows [Google's Open Source Community 22 | Guidelines](https://opensource.google.com/conduct/). 23 | 24 | ## License 25 | 26 | By contributing, you agree that your contributions will be licensed under its MIT License. 27 | -------------------------------------------------------------------------------- /.github/CONTRIBUTORS.MD: -------------------------------------------------------------------------------- 1 | # Contributors 2 | 3 | [Empress](https://github.com/emp-daisy) | 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | # Submitting an issue 2 | 3 | We have some functionalities in mind and we have issued them and there is a milestone label available on the issue. If there is a bug or a feature that is not listed in the issues page or there is no one assigned to the issue, feel free to fix/add it! Although it's better to discuss it in the issue or create a new issue for it so there is no confilcting code. 4 | 5 | ## Before submitting an issue, check the following checklist 6 | 7 | - [ ] To-Do. 8 | - [ ] Another To-Do. 9 | 10 | ## The problem 11 | 12 | ### What are the steps to reproduce this issue 13 | 14 | - [ ] Step 1 15 | - [ ] Step 2 16 | 17 | ### Expected Results 18 | 19 | ### Actual Results 20 | 21 | ### Is there anything else you would like to report 22 | 23 | ## Environment 24 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | # Pull Request 2 | 3 | ## Before submitting a pull request 4 | 5 | ## General steps for completing this pull request 6 | 7 | Please review the [guidelines for contributing](https://github.com/emp-daisy/react-table-it/blob/master/.github/CONTRIBUTING.MD) to this repository. 8 | 9 | ### Checklist 10 | 11 | Ensure that your `pull request` has followed all the steps below: 12 | 13 | - [ ] Code compilation 14 | - [ ] All tests passing 15 | - [ ] Extended the documentation, if applicable 16 | - [ ] I have added myself to the [CONTRIBUTORS file](https://github.com/emp-daisy/react-table-it/blob/master/.github/CONTRIBUTORS.MD) 17 | 18 | ### Description 19 | 20 | ## Proposed changes 21 | 22 | ### Types of changes 23 | 24 | - [ ] New feature 25 | - [ ] Bugfix 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | node_modules 3 | storybook-static 4 | coverage 5 | .DS_Store 6 | lib -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | demo 3 | .babelrc 4 | webpack.config.js 5 | .eslintrc 6 | .gitignore 7 | .prettierrc 8 | coverage -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "trailingComma": "all", 4 | "tabWidth": 2, 5 | "semi": true, 6 | "singleQuote": true 7 | } -------------------------------------------------------------------------------- /.storybook/addons.js: -------------------------------------------------------------------------------- 1 | import '@storybook/addon-actions/register'; 2 | import '@storybook/addon-links/register'; 3 | import '@storybook/addon-viewport/register'; 4 | -------------------------------------------------------------------------------- /.storybook/config.js: -------------------------------------------------------------------------------- 1 | import { configure, addParameters, addDecorator } from '@storybook/react'; 2 | import { dark_theme } from './theme'; 3 | import { withInfo } from '@storybook/addon-info'; 4 | import { INITIAL_VIEWPORTS } from '@storybook/addon-viewport'; 5 | import '../src/styles/index.scss'; 6 | 7 | // automatically import all files ending in *.stories.js 8 | const req = require.context('../stories', true, /\.stories\.js$/); 9 | function loadStories() { 10 | req.keys().forEach(filename => req(filename)); 11 | } 12 | 13 | addDecorator( 14 | withInfo({ 15 | styles: { 16 | button: { 17 | base: { 18 | fontFamily: 'sans-serif', 19 | fontSize: '14px', 20 | fontWeight: '500', 21 | display: 'block', 22 | position: 'fixed', 23 | border: 'none', 24 | background: '#000', 25 | color: '#fff', 26 | padding: '5px 15px', 27 | cursor: 'pointer', 28 | }, 29 | topRight: { 30 | bottom: 0, 31 | right: 0, 32 | top: 'unset', 33 | borderRadius: '5px 0 0 0', 34 | }, 35 | }, 36 | }, 37 | }), 38 | ); 39 | 40 | addParameters({ 41 | options: { 42 | theme: dark_theme, 43 | panelPosition: 'right', 44 | showPanel: false, 45 | }, 46 | }); 47 | 48 | addParameters({ viewport: { viewports: INITIAL_VIEWPORTS } }); 49 | 50 | configure(loadStories, module); 51 | -------------------------------------------------------------------------------- /.storybook/manager-head.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | -------------------------------------------------------------------------------- /.storybook/preview-head.html: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /.storybook/theme/dark.js: -------------------------------------------------------------------------------- 1 | import { create } from '@storybook/theming'; 2 | import logo from '../../public/compress-logo.svg'; 3 | 4 | export default create({ 5 | base: 'dark', 6 | 7 | brandImage: logo, 8 | brandName: 'react-table-it', 9 | brandUrl: 'https://github.com/emp-daisy/react-table-it', 10 | }); 11 | -------------------------------------------------------------------------------- /.storybook/theme/index.js: -------------------------------------------------------------------------------- 1 | import light_theme from './light'; 2 | import dark_theme from './dark'; 3 | 4 | export { light_theme, dark_theme }; 5 | -------------------------------------------------------------------------------- /.storybook/theme/light.js: -------------------------------------------------------------------------------- 1 | import { create } from '@storybook/theming'; 2 | import logo from '../../public/compress-logo-dark.svg'; 3 | 4 | export default create({ 5 | base: 'normal', 6 | 7 | brandImage: logo, 8 | brandName: 'react-table-it', 9 | brandUrl: 'https://github.com/emp-daisy/react-table-it', 10 | }); 11 | -------------------------------------------------------------------------------- /.storybook/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = async ({ config }) => { 4 | config.module.rules.push({ 5 | test: /\.scss$/, 6 | use: ['style-loader', 'css-loader', 'sass-loader'], 7 | include: path.resolve(__dirname, '../'), 8 | }); 9 | config.resolve.modules.push(path.resolve(__dirname, '../fixtures')); 10 | config.resolve.extensions.push('.ts', '.tsx'); 11 | return config; 12 | }; 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Jessica M 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Table It 2 | 3 | Logo 4 | 5 | [![CircleCI](https://circleci.com/gh/emp-daisy/react-table-it.svg?style=svg)](https://circleci.com/gh/emp-daisy/react-table-it) 6 | [![Test Coverage](https://api.codeclimate.com/v1/badges/16a560e1dcd231b3ef99/test_coverage)](https://codeclimate.com/github/emp-daisy/react-table-it/test_coverage) 7 | [![Maintainability](https://api.codeclimate.com/v1/badges/16a560e1dcd231b3ef99/maintainability)](https://codeclimate.com/github/emp-daisy/react-table-it/maintainability) 8 | 9 | Data table component with React 10 | 11 | ## [Demo](https://emp-daisy.github.io/react-table-it) 12 | 13 | ### Installation 14 | 15 | `$ npm i @emp/reactable` 16 | 17 | or 18 | 19 | `$ yarn add @emp/reactable` 20 | 21 | ### Usage 22 | 23 | ```js 24 | import Table from '@empd/reactable'; 25 | import '@empd/reactable/lib/styles.css'; 26 | ``` 27 | 28 | ### Props 29 | 30 | | Props | Required | Description | Type | Default | 31 | | --------------------- | :------: | ----------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-------------: | 32 | | `data` | - | Data to load on table | `[]` | `[]` | 33 | | `columns` | - | Table column settings | `array` of objects
Object contains some/ all properties below
`name: string or element (required)`
`className: string`
`attributes: Object of html attributes`
`selector: string`
`sortable: boolean`
`unsearchable: boolean`
`cell: element or function` function passes current row data and returns element
| `[]` | 34 | | `emptyPlaceholder` | - | Data to load on table | `[]` | `[]` | 35 | | `dataLength` | - | Specify length of all data when loading from server. `Note: server props must true` | `[]` | `[]` | 36 | | `emptyPlaceholder` | - | Placeholder when table is empty. | `string | element` | 'No data found' | 37 | | `pageOptions` | - | Data to load on table | `array` of numbers | `[10, 30, 50]` | 38 | | `server` | - | Set if pagination is handled by asynchronously | `boolean` | `false` | 39 | | `customPagination` | - | Render custom pagination | `boolean` | `false` | 40 | | `paginationComponent` | - | Custom pagination component | `function` | `undefined` | 41 | | `paginationPosition` | - | Position of pagination component | One of `['top', 'bottom', 'both', 'none']` | 'top' | 42 | | `onPageChange` | - | Custom page change function | `function` with params `(offset, limit, searchValue)`
`Note: server props must true` | - | 43 | | `onSort` | - | Custom sort function | `function` with params `(selector/key, ascending(boolean))` | `undefined` | 44 | | `searchPlaceholder` | - | Search box placeholder | `string` | 'Search' | 45 | | `search` | - | Set visibility of search box | `boolean` | `true` | 46 | | `containerClass` | - | CSS class for package component | `string` | '' | 47 | | `tableClass` | - | CSS class for table | `string` | '' | 48 | | `headerClass` | - | CSS class for table header | `string` | '' | 49 | | `rowClass` | - | CSS class for table row | `string` | '' | 50 | | `header` | - | Custom header component | `element` | `null` | 51 | | `footer` | - | Custom footer component | `element` | `null` | 52 | 53 | ### Technology 54 | 55 | Table It uses a number of open source projects to work properly: 56 | 57 | - [ReactJS] - HTML enhanced for web apps! 58 | - [Bootstrap] - great UI boilerplate for modern web apps 59 | - [Storybook] - great UI docs 60 | 61 | ### Development 62 | 63 | Want to contribute? Great! 64 | 65 | We use Webpack for fast developing. 66 | Make a change in your file and instantanously see your updates! 67 | 68 | Open your favorite Terminal and run these commands. 69 | 70 | Install dependenies: `$ yarn` 71 | 72 | Start application (opens storybook which uses the table component):`$ yarn start` 73 | 74 | Ensure the tests are stable with good coverage: `$ yarn test` 75 | 76 | Ensure the test have good coverage: `$ yarn test:coverage` 77 | 78 | ### License 79 | 80 | [MIT](https://github.com/emp-daisy/react-table-it/blob/master/LICENSE) 81 | -------------------------------------------------------------------------------- /fixtures/data.js: -------------------------------------------------------------------------------- 1 | import faker from 'faker'; 2 | 3 | const ColumnsFixtures = [ 4 | { name: 'Name', selector: 'name', className: 'w-50', sortable: true }, 5 | { name: 'Description', selector: 'description' }, 6 | ]; 7 | 8 | const TableFixtures = (size = 1) => 9 | new Array(size).fill({}).map(() => ({ 10 | name: faker.commerce.productName(), 11 | description: faker.lorem.sentences(), 12 | })); 13 | 14 | export { ColumnsFixtures, TableFixtures }; 15 | export default TableFixtures; 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@empd/reactable", 3 | "version": "1.2.1", 4 | "description": "React data-table", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "start": "yarn storybook", 8 | "build": "yarn clean:build && yarn build:js && yarn build:style", 9 | "build:js": "babel ./src --out-dir ./lib -s inline --ignore **/*.spec.js", 10 | "build:style": "node-sass src/styles/index.scss lib/styles.css --include-path ./node_modules/", 11 | "test": "jest", 12 | "test:watch": "jest --watch", 13 | "test:coverage": "jest --collectCoverage=true", 14 | "lint": "eslint --fix . && echo 'Lint complete.'", 15 | "clean:build": "rm -rf lib/", 16 | "prepublishOnly": "yarn build", 17 | "storybook": "start-storybook -p 6006 -s public", 18 | "build-storybook": "build-storybook -s public", 19 | "deploy-docs": "gh-pages -d storybook-static" 20 | }, 21 | "keywords": [ 22 | "react", 23 | "datatable", 24 | "data-table", 25 | "table" 26 | ], 27 | "author": "Empress", 28 | "repository": { 29 | "type": "git", 30 | "url": "https://github.com/emp-daisy/data-table-react.git" 31 | }, 32 | "bugs": { 33 | "url": "https://github.com/emp-daisy/data-table-react/issues" 34 | }, 35 | "homepage": "https://emp-daisy.github.io/data-table-react/", 36 | "license": "MIT", 37 | "engines": { 38 | "node": ">= 10.16.0", 39 | "npm": ">= 6.9.0" 40 | }, 41 | "peerDependencies": { 42 | "react": "^16.x", 43 | "react-dom": "^16.6.3" 44 | }, 45 | "dependencies": {}, 46 | "devDependencies": { 47 | "@babel/cli": "^7.1.5", 48 | "@babel/core": "^7.1.6", 49 | "@babel/plugin-proposal-class-properties": "^7.5.5", 50 | "@babel/preset-env": "^7.1.6", 51 | "@babel/preset-react": "^7.0.0", 52 | "@fortawesome/fontawesome-free": "^5.10.1", 53 | "@fortawesome/fontawesome-svg-core": "^1.2.21", 54 | "@fortawesome/free-solid-svg-icons": "^5.10.1", 55 | "@fortawesome/react-fontawesome": "^0.1.4", 56 | "@storybook/addon-a11y": "^5.1.10", 57 | "@storybook/addon-actions": "^5.1.10", 58 | "@storybook/addon-contexts": "^5.1.11", 59 | "@storybook/addon-info": "^5.1.11", 60 | "@storybook/addon-links": "^5.1.10", 61 | "@storybook/addon-storysource": "^5.1.10", 62 | "@storybook/addon-viewport": "^5.1.11", 63 | "@storybook/addons": "^5.1.10", 64 | "@storybook/react": "^5.1.10", 65 | "babel-eslint": "^10.0.2", 66 | "babel-jest": "^24.9.0", 67 | "babel-loader": "^8.0.6", 68 | "bootstrap": "^4.3.1", 69 | "css-loader": "^3.2.0", 70 | "enzyme": "^3.10.0", 71 | "enzyme-adapter-react-16": "^1.14.0", 72 | "eslint": "^6.3.0", 73 | "eslint-config-airbnb": "^17.1.1", 74 | "eslint-config-prettier": "^6.0.0", 75 | "eslint-plugin-import": "^2.18.2", 76 | "eslint-plugin-jsx-a11y": "^6.2.3", 77 | "eslint-plugin-react": "^7.14.3", 78 | "faker": "^4.1.0", 79 | "gh-pages": "^2.1.0", 80 | "husky": "^3.0.2", 81 | "jest": "^24.9.0", 82 | "lint-staged": "^9.2.1", 83 | "node-sass": "^4.12.0", 84 | "prettier": "^1.18.2", 85 | "pretty-quick": "^1.11.1", 86 | "prop-types": "^15.7.2", 87 | "react": "^16.x", 88 | "react-dom": "^16.6.3", 89 | "react-js-pagination": "^3.0.2", 90 | "react-paginate": "^6.3.0", 91 | "sass-loader": "^7.2.0", 92 | "webpack-cli": "^3.3.9" 93 | }, 94 | "files": [ 95 | "lib/*" 96 | ], 97 | "publishConfig": { 98 | "access": "public" 99 | }, 100 | "husky": { 101 | "hooks": { 102 | "pre-commit": "lint-staged" 103 | } 104 | }, 105 | "lint-staged": { 106 | "src/**/*.{js,jsx}": [ 107 | "eslint", 108 | "pretty-quick — staged", 109 | "git add" 110 | ] 111 | }, 112 | "jest": { 113 | "setupFilesAfterEnv": [ 114 | "/setup-test.js" 115 | ], 116 | "verbose": true, 117 | "testMatch": [ 118 | "/src/**/*.spec.js" 119 | ], 120 | "collectCoverageFrom": [ 121 | "**/*.{js,jsx}", 122 | "!**/node_modules/**", 123 | "!**/coverage/**", 124 | "!**/stories/**", 125 | "!**/storybook-static/**", 126 | "!**/setup-test.js" 127 | ] 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /public/compress-logo-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 29 | 35 | 36 | 37 | 61 | 63 | 64 | 66 | image/svg+xml 67 | 69 | 70 | 71 | 72 | 73 | 78 | 81 | 84 | 87 | 90 | 93 | 96 | 99 | 102 | 105 | 108 | 111 | 114 | 117 | 120 | 123 | 137 | 144 | 149 | Table It 161 | 162 | 163 | 164 | -------------------------------------------------------------------------------- /public/compress-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 29 | 35 | 36 | 37 | 61 | 63 | 64 | 66 | image/svg+xml 67 | 69 | 70 | 71 | 72 | 73 | 78 | 81 | 84 | 87 | 90 | 93 | 96 | 99 | 102 | 105 | 108 | 111 | 114 | 117 | 120 | 123 | 137 | 144 | 149 | Table It 161 | 162 | 163 | 164 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emp-daisy/react-table-it/8eae0639c5efd39681fdb0976b0188910e886b4c/public/favicon.ico -------------------------------------------------------------------------------- /setup-test.js: -------------------------------------------------------------------------------- 1 | import { configure, shallow, render, mount } from 'enzyme'; 2 | import Adapter from 'enzyme-adapter-react-16'; 3 | 4 | configure({ adapter: new Adapter() }); 5 | 6 | global.shallow = shallow; 7 | global.render = render; 8 | global.mount = mount; 9 | -------------------------------------------------------------------------------- /src/components/__snapshots__/pagination.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Pagination Component should renders correctly 1`] = `ShallowWrapper {}`; 4 | -------------------------------------------------------------------------------- /src/components/__snapshots__/table.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Table Component should renders correctly 1`] = `ShallowWrapper {}`; 4 | -------------------------------------------------------------------------------- /src/components/index.js: -------------------------------------------------------------------------------- 1 | import Table from './table'; 2 | import Pagination from './pagination'; 3 | 4 | export { Table, Pagination }; 5 | -------------------------------------------------------------------------------- /src/components/pagination.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/no-array-index-key */ 2 | import React from 'react'; 3 | import PropTypes from 'prop-types'; 4 | import { faSearch, faChevronRight, faChevronLeft } from '@fortawesome/free-solid-svg-icons'; 5 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 6 | 7 | class Pagination extends React.Component { 8 | constructor(props) { 9 | super(props); 10 | const perPage = props.pageOptions[0]; 11 | const totalPages = Math.ceil(props.itemsLength / perPage); 12 | this.state = { 13 | perPage, 14 | currentPage: 1, 15 | totalPages, 16 | searchText: '', 17 | }; 18 | } 19 | 20 | componentDidMount = () => { 21 | const { currentPage } = this.state; 22 | this.switchPage(currentPage); 23 | }; 24 | 25 | componentDidUpdate = props => { 26 | const { currentPage } = this.state; 27 | const { itemsLength, data } = this.props; 28 | const { itemsLength: prevItemsLength, data: prevData } = props; 29 | if (prevItemsLength !== itemsLength) { 30 | this.setTotalPages(); 31 | } 32 | if (prevData !== data) { 33 | this.switchPage(currentPage); 34 | } 35 | }; 36 | 37 | setTotalPages = () => { 38 | const { perPage } = this.state; 39 | const { itemsLength } = this.props; 40 | this.setState({ totalPages: Math.ceil(itemsLength / perPage) }); 41 | }; 42 | 43 | switchPage = page => { 44 | const { perPage, searchText } = this.state; 45 | const { onPageChange } = this.props; 46 | this.setState({ currentPage: page }); 47 | onPageChange(perPage * (page - 1), perPage, searchText); 48 | }; 49 | 50 | previous = () => { 51 | const { currentPage } = this.state; 52 | if (currentPage !== 1) { 53 | this.switchPage(currentPage - 1); 54 | } 55 | }; 56 | 57 | next = () => { 58 | const { currentPage, totalPages } = this.state; 59 | if (currentPage < totalPages) { 60 | this.switchPage(currentPage + 1); 61 | } 62 | }; 63 | 64 | changeLimit = num => { 65 | const { searchText } = this.state; 66 | const { onPageChange, itemsLength } = this.props; 67 | this.setState({ 68 | perPage: num, 69 | totalPages: Math.ceil(itemsLength / num), 70 | currentPage: 1, 71 | }); 72 | onPageChange(0, num, searchText); 73 | }; 74 | 75 | onPageChange = value => { 76 | const { totalPages } = this.state; 77 | if (Number.isNaN(value) || value > totalPages || value < 1) { 78 | return; 79 | } 80 | this.switchPage(value); 81 | }; 82 | 83 | handleSearch = ev => { 84 | const { onPageChange } = this.props; 85 | const { perPage } = this.state; 86 | const text = ev.target.value.trim().toLowerCase(); 87 | this.setState({ searchText: text, currentPage: 1 }); 88 | onPageChange(0, perPage, text); 89 | }; 90 | 91 | render() { 92 | const { 93 | showSearch, 94 | pageOptions, 95 | searchPlaceholder, 96 | data, 97 | paginationComponent, 98 | customPagination, 99 | itemsLength, 100 | } = this.props; 101 | const { totalPages, currentPage, perPage } = this.state; 102 | return ( 103 | 104 | {!customPagination ? ( 105 |
106 |
107 | {showSearch && ( 108 |
109 | 119 |
120 | 128 |
129 |
130 | )} 131 |
132 | {totalPages > 0 && ( 133 |
134 | 153 |
157 |
158 | 159 | 160 | Prev 161 | 162 |
163 |
164 |
165 | 182 |
183 |
184 |
= totalPages ? ' disabled' : ''}`}> 185 | 186 | Next 187 | 188 | 189 |
190 |
191 |
192 | )} 193 |
194 | ) : ( 195 | 196 | {paginationComponent({ 197 | itemsLength, 198 | currentPage, 199 | perPage, 200 | next: this.next, 201 | previous: this.previous, 202 | pageChange: this.onPageChange, 203 | limitChange: this.changeLimit, 204 | searchPage: this.handleSearch, 205 | })} 206 | 207 | )} 208 |
209 | ); 210 | } 211 | } 212 | 213 | Pagination.propTypes = { 214 | /** Length of the data in the view. */ 215 | itemsLength: PropTypes.number, 216 | /** View data. */ 217 | data: PropTypes.arrayOf(PropTypes.any), 218 | /** Page change function. 219 | * params => (offset, limit, search) 220 | * *** offset => offset for new page 221 | * *** limit => current limit of table 222 | * *** search => optional search term 223 | */ 224 | onPageChange: PropTypes.func, 225 | /** Page limit options. */ 226 | pageOptions: PropTypes.arrayOf(PropTypes.any), 227 | /** Placeholder for search box. */ 228 | searchPlaceholder: PropTypes.string, 229 | /** Set visibility of search box */ 230 | showSearch: PropTypes.bool, 231 | /** Render custom pagination */ 232 | customPagination: PropTypes.bool, 233 | /** Custom pagination component. */ 234 | paginationComponent: PropTypes.func, 235 | }; 236 | 237 | Pagination.defaultProps = { 238 | itemsLength: 0, 239 | data: [], 240 | onPageChange: () => {}, 241 | pageOptions: [10, 25, 50], 242 | searchPlaceholder: 'Search...', 243 | showSearch: true, 244 | customPagination: false, 245 | paginationComponent: null, 246 | }; 247 | 248 | export default Pagination; 249 | -------------------------------------------------------------------------------- /src/components/pagination.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Pagination from './pagination'; 3 | 4 | describe('Pagination Component', () => { 5 | it('should renders correctly', () => { 6 | const wrapper = shallow(); 7 | expect(wrapper).toMatchSnapshot(); 8 | }); 9 | 10 | it('should switch to previous page', () => { 11 | const wrapper = shallow(); 12 | const switchPageSpy = jest.spyOn(wrapper.instance(), 'switchPage'); 13 | wrapper.setState({ currentPage: 2 }); 14 | wrapper.instance().previous(); 15 | expect(switchPageSpy).toHaveBeenCalled(); 16 | }); 17 | 18 | it('should not switch to previos page', () => { 19 | const wrapper = shallow(); 20 | const switchPageSpy = jest.spyOn(wrapper.instance(), 'switchPage'); 21 | wrapper.setState({ currentPage: 1 }); 22 | wrapper.instance().previous(); 23 | expect(switchPageSpy).not.toHaveBeenCalled(); 24 | }); 25 | 26 | it('should switch to next page', () => { 27 | const wrapper = shallow(); 28 | const switchPageSpy = jest.spyOn(wrapper.instance(), 'switchPage'); 29 | wrapper.setState({ currentPage: 1 }); 30 | wrapper.instance().next(); 31 | expect(switchPageSpy).toHaveBeenCalled(); 32 | }); 33 | 34 | it('should not switch to next page', () => { 35 | const wrapper = shallow(); 36 | const switchPageSpy = jest.spyOn(wrapper.instance(), 'switchPage'); 37 | wrapper.setState({ currentPage: 2 }); 38 | wrapper.instance().next(); 39 | expect(switchPageSpy).not.toHaveBeenCalled(); 40 | }); 41 | 42 | it('should switch to selected page', () => { 43 | const wrapper = shallow(); 44 | const switchPageSpy = jest.spyOn(wrapper.instance(), 'switchPage'); 45 | wrapper.instance().onPageChange(2); 46 | expect(switchPageSpy).toHaveBeenCalled(); 47 | }); 48 | 49 | it('should not switch to selected page', () => { 50 | const wrapper = shallow(); 51 | const switchPageSpy = jest.spyOn(wrapper.instance(), 'switchPage'); 52 | wrapper.instance().onPageChange(3); 53 | expect(switchPageSpy).not.toHaveBeenCalled(); 54 | }); 55 | 56 | it('should switch to change page', () => { 57 | const wrapper = shallow(); 58 | const switchPageSpy = jest.spyOn(wrapper.instance(), 'switchPage'); 59 | wrapper.instance().onPageChange(2); 60 | expect(switchPageSpy).toHaveBeenCalled(); 61 | }); 62 | 63 | it('should not switch to change page', () => { 64 | const wrapper = shallow(); 65 | const switchPageSpy = jest.spyOn(wrapper.instance(), 'switchPage'); 66 | wrapper.instance().onPageChange(3); 67 | expect(switchPageSpy).not.toHaveBeenCalled(); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /src/components/table.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/no-array-index-key */ 2 | import * as React from 'react'; 3 | import PropTypes from 'prop-types'; 4 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 5 | import { faSort, faSortUp, faSortDown } from '@fortawesome/free-solid-svg-icons'; 6 | import PaginationBar from './pagination'; 7 | 8 | class Table extends React.Component { 9 | constructor(props) { 10 | super(props); 11 | this.state = { 12 | sortAscending: false, 13 | stateData: [], 14 | filteredData: [], 15 | itemsLength: 0, 16 | sortKey: undefined, 17 | }; 18 | } 19 | 20 | componentDidMount = () => { 21 | this.setItemLength(); 22 | }; 23 | 24 | componentDidUpdate = prevProps => { 25 | const { data } = this.props; 26 | const { data: prevData } = prevProps; 27 | if (prevData !== data) { 28 | this.setItemLength(); 29 | } 30 | }; 31 | 32 | setItemLength = () => { 33 | const { server, dataLength, data } = this.props; 34 | this.setState({ 35 | itemsLength: server ? dataLength : data.length, 36 | stateData: data, 37 | }); 38 | }; 39 | 40 | onPageChange = (offset, limit, search) => { 41 | const { server, onPageChange, columns } = this.props; 42 | const { stateData: data } = this.state; 43 | let filteredData; 44 | let searchData; 45 | if (server) { 46 | onPageChange(offset, limit, search); 47 | searchData = data; 48 | filteredData = data.slice(0, limit); 49 | } else { 50 | const searchable = columns.reduce( 51 | (result, item) => [...result, ...(!item.unsearchable ? [item.selector] : [])], 52 | [], 53 | ); 54 | searchData = 55 | search.trim().length === 0 56 | ? data 57 | : [...data].filter( 58 | item => 59 | searchable 60 | .reduce((result, key) => [...result, item[key]], []) 61 | .map(mitem => { 62 | let itemValue = mitem; 63 | if (typeof mitem === 'object') { 64 | itemValue = Object.values(mitem); 65 | } 66 | return [itemValue || ''].toString().toLowerCase(); 67 | }) 68 | .filter(fitem => fitem.includes(search)).length, 69 | ); 70 | filteredData = searchData.slice(offset).slice(0, limit); 71 | } 72 | this.setState({ filteredData, itemsLength: searchData.length }); 73 | }; 74 | 75 | sortPage = key => { 76 | const { sortAscending: sortOption } = this.state; 77 | const { onSort, data } = this.props; 78 | const sortAscending = !sortOption; 79 | let stateData = data; 80 | if (onSort) { 81 | onSort(key, sortAscending); 82 | } else { 83 | stateData = [...data].sort((a, b) => { 84 | const first = a[key] || ''; 85 | const second = b[key] || ''; 86 | return sortAscending 87 | ? first.toString().localeCompare(second.toString()) 88 | : second.toString().localeCompare(first.toString()); 89 | }); 90 | } 91 | this.setState({ stateData, sortKey: key, sortAscending }); 92 | }; 93 | 94 | sortIcon = selector => { 95 | const { sortKey, sortAscending } = this.state; 96 | if (!sortKey || sortKey !== selector) { 97 | return faSort; 98 | } 99 | if (sortAscending) { 100 | return faSortUp; 101 | } 102 | return faSortDown; 103 | }; 104 | 105 | pagination = () => { 106 | const { 107 | searchPlaceholder, 108 | search, 109 | pageOptions, 110 | paginationComponent, 111 | customPagination, 112 | } = this.props; 113 | const { itemsLength, stateData } = this.state; 114 | return ( 115 | 125 | ); 126 | }; 127 | 128 | tableCell = (item, data) => (typeof item.cell === 'function' ? item.cell(data) : item.cell); 129 | 130 | render() { 131 | const { 132 | columns, 133 | emptyPlaceholder, 134 | paginationPosition, 135 | header, 136 | footer, 137 | containerClass, 138 | tableClass, 139 | rowClass, 140 | } = this.props; 141 | const { filteredData } = this.state; 142 | return ( 143 |
144 | {header} 145 | {(paginationPosition === 'top' || paginationPosition === 'both') && this.pagination()} 146 | 147 | 148 | 149 | {columns && 150 | columns.map((item, index) => ( 151 | 165 | ))} 166 | 167 | 168 | 169 | {filteredData && 170 | filteredData.map((data, index) => ( 171 | 172 | {columns && 173 | columns.map((item, tdIndex) => ( 174 | 177 | ))} 178 | 179 | ))} 180 | {filteredData.length === 0 && ( 181 | 182 | 185 | 186 | )} 187 | 188 |
this.sortPage(item.selector) })} 155 | > 156 | {item.name} 157 | {item.sortable && ( 158 | 163 | )} 164 |
175 | {item.cell ? this.tableCell(item, data) : data[item.selector]} 176 |
183 | {emptyPlaceholder} 184 |
189 | {(paginationPosition === 'bottom' || paginationPosition === 'both') && this.pagination()} 190 | {footer} 191 |
192 | ); 193 | } 194 | } 195 | 196 | Table.propTypes = { 197 | /** Placeholder when table is empty. */ 198 | emptyPlaceholder: PropTypes.oneOfType([PropTypes.string, PropTypes.element]), 199 | /** Data to load on table */ 200 | data: PropTypes.arrayOf(PropTypes.any), 201 | /** Table column settings. */ 202 | columns: PropTypes.arrayOf( 203 | PropTypes.shape({ 204 | name: PropTypes.oneOfType([PropTypes.string, PropTypes.element]).isRequired, 205 | className: PropTypes.string, 206 | attributes: PropTypes.objectOf(PropTypes.any), 207 | selector: PropTypes.string, 208 | sortable: PropTypes.bool, 209 | unsearchable: PropTypes.bool, 210 | cell: PropTypes.oneOfType([PropTypes.func, PropTypes.element]), 211 | }), 212 | ), 213 | /** Specify length of all data when loading from server. 214 | * Note: server props must true 215 | */ 216 | dataLength: PropTypes.number, 217 | /** Position of pagination component. */ 218 | paginationPosition: PropTypes.oneOf(['top', 'bottom', 'both', 'none']), 219 | /** Page limit options. */ 220 | pageOptions: PropTypes.arrayOf(PropTypes.number), 221 | /** Custom pagination component. 222 | * paginationComponent must be set 223 | * PROPS: *** itemsLength => size of data 224 | * *** currentPage => current page 225 | * *** perPage => page limit 226 | * *** next => next page function 227 | * *** previous => previous page function 228 | * *** pageChange => change page function 229 | * *** limitChange => change page limit function 230 | * *** searchPage => search page 231 | */ 232 | paginationComponent: PropTypes.func, 233 | /** 234 | * Render custom pagination 235 | */ 236 | customPagination: PropTypes.bool, 237 | /** Set if pagination is handled by asynchronously */ 238 | server: PropTypes.bool, 239 | /** Custom page change function. 240 | * Note: server props must true 241 | * params => (offset, limit, search) 242 | * *** offset => offset for new page 243 | * *** limit => current limit of table 244 | * *** search => optional search term 245 | */ 246 | onPageChange: PropTypes.func, 247 | // SORTING 248 | /** Custom sort function. 249 | * params => (selector, direction) 250 | * *** selector => the column selector specified 251 | * *** direction => ascendingOrder === true 252 | */ 253 | onSort: PropTypes.func, 254 | /** Search box placehiolder */ 255 | searchPlaceholder: PropTypes.string, 256 | /** Set visibility of search box */ 257 | search: PropTypes.bool, 258 | /** CSS class for package component. */ 259 | containerClass: PropTypes.string, 260 | /** CSS class for table. */ 261 | tableClass: PropTypes.string, 262 | /** CSS class for table header. */ 263 | headerClass: PropTypes.string, 264 | /** CSS class for table row. */ 265 | rowClass: PropTypes.string, 266 | /** Custom header component. */ 267 | header: PropTypes.element, 268 | /** Custom footer component. */ 269 | footer: PropTypes.element, 270 | }; 271 | 272 | Table.defaultProps = { 273 | // TABLE 274 | emptyPlaceholder: 'No data found', 275 | dataLength: 0, 276 | columns: [], 277 | data: [], 278 | // PAGINATION 279 | pageOptions: [10, 30, 50], 280 | server: false, 281 | customPagination: false, 282 | paginationComponent: undefined, 283 | paginationPosition: 'top', 284 | onPageChange: () => {}, 285 | // SORTING 286 | onSort: undefined, 287 | // SEARCH 288 | searchPlaceholder: 'Search', 289 | search: true, 290 | // STYLING 291 | containerClass: '', 292 | tableClass: '', 293 | headerClass: '', 294 | rowClass: '', 295 | // MISC 296 | header: null, 297 | footer: null, 298 | }; 299 | 300 | export default Table; 301 | -------------------------------------------------------------------------------- /src/components/table.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Pagination from 'react-js-pagination'; 3 | import Table from './table'; 4 | import { TableFixtures, ColumnsFixtures } from '../../fixtures/data'; 5 | 6 | const fixtures = TableFixtures(20); 7 | describe.only('Table Component', () => { 8 | it('should renders correctly', () => { 9 | const wrapper = shallow(); 10 | expect(wrapper).toMatchSnapshot(); 11 | }); 12 | 13 | it('should renders empty row', () => { 14 | const wrapper = mount(
); 15 | expect(wrapper.find('table td')).toHaveLength(1); 16 | expect(wrapper.find('table td').text()).toEqual('No data found'); 17 | wrapper.setProps({ emptyPlaceholder: 'No product found!' }); 18 | wrapper.update(); 19 | expect(wrapper.find('table td').text()).toEqual('No product found!'); 20 | }); 21 | 22 | it('should get correct ficture size', () => { 23 | expect(TableFixtures(10).length).toEqual(10); 24 | expect(TableFixtures().length).toEqual(1); 25 | }); 26 | 27 | it('renders data on table', () => { 28 | const wrapper = mount(
); 29 | expect(wrapper.find('table thead th')).toHaveLength(2); 30 | expect(wrapper.find('table tbody tr')).toHaveLength(10); 31 | }); 32 | 33 | it('renders pagination on top and bottom', () => { 34 | const wrapper = mount( 35 |
, 36 | ); 37 | expect(wrapper.find('.pagination')).toHaveLength(2); 38 | }); 39 | 40 | it('renders data on custom data column', () => { 41 | const wrapper = mount( 42 |
}, 47 | ]} 48 | />, 49 | ); 50 | expect(wrapper.find('table thead th')).toHaveLength(3); 51 | expect(wrapper.find('table tbody tr .product-checkbox')).toHaveLength(10); 52 | }); 53 | 54 | it('renders data on custom data column with function', () => { 55 | const wrapper = mount( 56 |
, 63 | }, 64 | ]} 65 | />, 66 | ); 67 | expect(wrapper.find('table thead th')).toHaveLength(3); 68 | expect( 69 | wrapper.containsMatchingElement(), 70 | ).toBeTruthy(); 71 | expect(wrapper.find('table tbody tr .product-checkbox')).toHaveLength(10); 72 | }); 73 | 74 | it('watches for update on data', () => { 75 | const wrapper = mount(
); 76 | wrapper.setProps({ data: fixtures.slice(0, 5) }); 77 | wrapper.update(); 78 | expect(wrapper.find('table thead th')).toHaveLength(2); 79 | expect(wrapper.find('table tbody tr')).toHaveLength(5); 80 | }); 81 | 82 | it('sorts table should not crash on absent key', () => { 83 | const fixturesMod = fixtures.map(i => ({ name: i.name })); 84 | const columnMod = ColumnsFixtures.map(i => ({ ...i, sortable: true })); 85 | const wrapper = mount(
); 86 | const onSortSpy = jest.spyOn(wrapper.instance(), 'sortPage'); 87 | // Ascending order 88 | wrapper 89 | .find('table thead th') 90 | .at(1) 91 | .simulate('click', { target: { name: 0 } }); 92 | expect(onSortSpy).toHaveBeenCalled(); 93 | wrapper.update(); 94 | expect(wrapper.state('sortAscending')).toBeTruthy(); 95 | }); 96 | 97 | it('sorts table', () => { 98 | const names = fixtures.map(item => item.name).sort(); 99 | const wrapper = mount(
); 100 | const onSortSpy = jest.spyOn(wrapper.instance(), 'sortPage'); 101 | // Ascending order 102 | wrapper 103 | .find('table thead th') 104 | .at(0) 105 | .simulate('click', { target: { name: 0 } }); 106 | expect(onSortSpy).toHaveBeenCalled(); 107 | wrapper.update(); 108 | expect(wrapper.state('sortAscending')).toBeTruthy(); 109 | expect( 110 | wrapper 111 | .find('table tbody tr') 112 | .first() 113 | .find('td') 114 | .first() 115 | .text(), 116 | ).toEqual(names[0]); 117 | expect( 118 | wrapper 119 | .find('table tbody tr') 120 | .last() 121 | .find('td') 122 | .first() 123 | .text(), 124 | ).toEqual(names[9]); 125 | }); 126 | 127 | it('sorts tablein descending order', () => { 128 | const names = fixtures.map(item => item.name).sort(); 129 | const wrapper = mount(
); 130 | const onSortSpy = jest.spyOn(wrapper.instance(), 'sortPage'); 131 | wrapper.setState({ sortAscending: true }); 132 | wrapper.instance().forceUpdate(); 133 | wrapper.update(); 134 | wrapper 135 | .find('table thead th') 136 | .at(0) 137 | .simulate('click', { target: { name: 0 } }); 138 | wrapper.update(); 139 | expect(wrapper.state('sortAscending')).not.toBeTruthy(); 140 | expect(onSortSpy).toHaveBeenCalled(); 141 | expect( 142 | wrapper 143 | .find('table tbody tr') 144 | .first() 145 | .find('td') 146 | .first() 147 | .text(), 148 | ).toEqual(names[names.length - 1]); 149 | expect( 150 | wrapper 151 | .find('table tbody tr') 152 | .last() 153 | .find('td') 154 | .first() 155 | .text(), 156 | ).toEqual(names[names.length - 10]); 157 | }); 158 | 159 | it('sorts table with custom function', () => { 160 | const wrapper = mount(
); 161 | const onSort = jest.fn(); 162 | wrapper.setProps({ onSort }); 163 | wrapper.update(); 164 | wrapper 165 | .find('table thead th') 166 | .at(0) 167 | .simulate('click', { target: { name: 0 } }); 168 | expect(onSort).toHaveBeenCalled(); 169 | }); 170 | 171 | it('should not sorts table', () => { 172 | const wrapper = mount(
); 173 | const onSortSpy = jest.spyOn(wrapper.instance(), 'sortPage'); 174 | wrapper 175 | .find('table thead th') 176 | .at(1) 177 | .simulate('click', { target: { name: 0 } }); 178 | expect(onSortSpy).not.toHaveBeenCalled(); 179 | }); 180 | 181 | it('should display search box', () => { 182 | const wrapper = mount(
); 183 | expect(wrapper.find('.input-group.search')).toHaveLength(1); 184 | }); 185 | 186 | it('should not display search box', () => { 187 | const wrapper = mount(
); 188 | expect(wrapper.find('.input-group.search')).toHaveLength(0); 189 | }); 190 | 191 | it('should show search result', () => { 192 | const wrapper = mount(
); 193 | wrapper.find('.input-group.search input').simulate('change', { 194 | target: { value: fixtures[0].name }, 195 | }); 196 | wrapper.update(); 197 | expect(wrapper.find('table tbody tr')).toHaveLength(1); 198 | // show all result on clear 199 | wrapper.find('.input-group.search input').simulate('change', { target: { value: '' } }); 200 | wrapper.update(); 201 | expect(wrapper.find('table tbody tr')).toHaveLength(10); 202 | }); 203 | 204 | it('should not search column', () => { 205 | const fixturesMod = fixtures.map(i => ({ name: i.name, availablity: 'still-in-stock' })); 206 | const columnMod = [ 207 | ...ColumnsFixtures, 208 | { name: 'Availablity', selector: 'availablity', unsearchable: true }, 209 | ]; 210 | const wrapper = mount(
); 211 | wrapper.find('.input-group.search input').simulate('change', { 212 | target: { value: 'still-in-stock' }, 213 | }); 214 | expect(wrapper.find('table td').text()).toEqual('No data found'); 215 | }); 216 | 217 | it('should display/change correct page option', () => { 218 | const wrapper = mount( 219 |
, 220 | ); 221 | expect(wrapper.find('.pagination .page-option').props().value).toEqual(25); 222 | wrapper 223 | .find('.pagination .page-option') 224 | .find('option') 225 | .at(1) 226 | .simulate('change', 1); 227 | expect(wrapper.find('.pagination .page-option').props().value).toEqual('40'); 228 | wrapper 229 | .find('.pagination .page-option') 230 | .find('option') 231 | .at(2) 232 | .simulate('change', 1); 233 | expect(wrapper.find('.pagination .page-option').props().value).toEqual('50'); 234 | }); 235 | 236 | it('should change the table limit', () => { 237 | const wrapper = mount( 238 |
, 239 | ); 240 | expect(wrapper.find('table tbody tr')).toHaveLength(5); 241 | wrapper 242 | .find('.pagination .page-option') 243 | .find('option') 244 | .at(1) 245 | .simulate('change', 1); 246 | 247 | expect(wrapper.find('table tbody tr')).toHaveLength(10); 248 | wrapper 249 | .find('.pagination .page-option') 250 | .find('option') 251 | .at(2) 252 | .simulate('change', 1); 253 | 254 | expect(wrapper.find('table tbody tr')).toHaveLength(20); 255 | }); 256 | 257 | it('should display/change correct page option', () => { 258 | const wrapper = mount( 259 |
, 260 | ); 261 | expect(wrapper.find('.pagination .page-option').props().value).toEqual(25); 262 | wrapper 263 | .find('.pagination .page-option') 264 | .find('option') 265 | .at(1) 266 | .simulate('change', 1); 267 | expect(wrapper.find('.pagination .page-option').props().value).toEqual('40'); 268 | wrapper 269 | .find('.pagination .page-option') 270 | .find('option') 271 | .at(2) 272 | .simulate('change', 1); 273 | expect(wrapper.find('.pagination .page-option').props().value).toEqual('50'); 274 | }); 275 | 276 | it('should change the table items on prev click', () => { 277 | const wrapper = mount( 278 |
, 279 | ); 280 | 281 | wrapper.find('.pagination .next span').simulate('click'); 282 | expect(wrapper.find('table tbody tr')).toHaveLength(5); 283 | 284 | wrapper.find('.pagination .prev span').simulate('click'); 285 | expect(wrapper.find('table tbody tr')).toHaveLength(15); 286 | }); 287 | 288 | it('should change the table items on next click', () => { 289 | const wrapper = mount( 290 |
, 291 | ); 292 | 293 | expect(wrapper.find('table tbody tr')).toHaveLength(15); 294 | 295 | wrapper.find('.pagination .next span').simulate('click'); 296 | expect(wrapper.find('table tbody tr')).toHaveLength(5); 297 | }); 298 | 299 | it('should change page by input field', () => { 300 | const wrapper = mount( 301 |
, 302 | ); 303 | expect(wrapper.find('table tbody tr')).toHaveLength(15); 304 | 305 | wrapper.find('.pagination input.current-page').simulate('change', { target: { value: 2 } }); 306 | wrapper.update(); 307 | expect(wrapper.find('table tbody tr')).toHaveLength(5); 308 | }); 309 | 310 | it('should render custom pagination', () => { 311 | const wrapper = mount( 312 |
( 318 | pageChange(page, perPage)} 327 | /> 328 | )} 329 | />, 330 | ); 331 | expect(wrapper.find('table tbody tr')).toHaveLength(15); 332 | 333 | expect(wrapper.find('.pagination.custom-paging')).toHaveLength(1); 334 | }); 335 | 336 | it('load from server', () => { 337 | const fixturess = TableFixtures(35); 338 | const wrapper = mount( 339 |
, 340 | ); 341 | wrapper.update(); 342 | expect(wrapper.state('itemsLength')).toEqual(35); 343 | expect(wrapper.find('table tbody tr')).toHaveLength(10); 344 | }); 345 | }); 346 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | // import './styles/index.scss'; 2 | import { Table } from './components'; 3 | 4 | export default Table; 5 | -------------------------------------------------------------------------------- /src/styles/_pagination.scss: -------------------------------------------------------------------------------- 1 | .pagination { 2 | font-size: 14px; 3 | vertical-align: middle; 4 | width: 100%; 5 | 6 | input { 7 | &:focus { 8 | box-shadow: none; 9 | border-color: unset; 10 | } 11 | } 12 | 13 | .search { 14 | border: none; 15 | } 16 | 17 | .page-option { 18 | width: auto; 19 | background: none; 20 | } 21 | 22 | .navigation { 23 | .pages { 24 | display: inline-block; 25 | 26 | span { 27 | padding: 3px; 28 | margin: -3px 0px; 29 | border-radius: 50px; 30 | height: auto; 31 | width: 30px; 32 | display: inline-block; 33 | text-align: center; 34 | cursor: pointer; 35 | 36 | // &.active { 37 | // @include themify($themes) { 38 | // color: themed("headerColor"); 39 | // background-color: themed("buttonSecondaryColor") !important; 40 | // } 41 | // } 42 | // &:hover { 43 | // @include themify($themes) { 44 | // color: themed("headerColor"); 45 | // } 46 | // } 47 | } 48 | 49 | .current-page { 50 | margin: 0 !important; 51 | text-align: center; 52 | } 53 | input { 54 | margin: 0 !important; 55 | text-align: center; 56 | } 57 | } 58 | 59 | .control { 60 | cursor: pointer; 61 | 62 | &.prev { 63 | float: left; 64 | margin-right: 10px; 65 | } 66 | 67 | &.next { 68 | float: right; 69 | margin-left: 10px; 70 | } 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/styles/_table.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emp-daisy/react-table-it/8eae0639c5efd39681fdb0976b0188910e886b4c/src/styles/_table.scss -------------------------------------------------------------------------------- /src/styles/index.scss: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css?family=Roboto:300,400,700'); 2 | 3 | @import '../../node_modules/bootstrap/scss/bootstrap.scss'; 4 | 5 | @import 'pagination.scss'; 6 | @import 'table.scss'; 7 | -------------------------------------------------------------------------------- /stories/index.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { storiesOf } from '@storybook/react'; 3 | import Table from '../src'; 4 | import { ColumnsFixtures, TableFixtures } from '../fixtures/data'; 5 | 6 | storiesOf('Table', module) 7 | .add('Basic', () =>
) 8 | .add('Custom', () => ( 9 |
15 | )) 16 | .add('Custom header and footer', () => ( 17 |
22 |

23 | Product List 24 |

25 | 26 | )} 27 | footer={( 28 |
29 |
30 | {`© `} 31 | 32 | My custom footer 33 | 34 |
35 | )} 36 | /> 37 | )) 38 | .add('With no data', () => ( 39 |
40 | )); 41 | -------------------------------------------------------------------------------- /stories/pagination.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { storiesOf } from '@storybook/react'; 3 | import Pagination from 'react-js-pagination'; 4 | import Table from '../src'; 5 | import { ColumnsFixtures, TableFixtures } from '../fixtures/data'; 6 | 7 | storiesOf('Pagination', module) 8 | .add('Bottom Pagination', () => ( 9 |
10 | )) 11 | .add('Double Pagination', () => ( 12 |
13 | )) 14 | .add('Custom Pagination', () => ( 15 |
( 19 | pageChange(page, perPage)} 28 | /> 29 | )} 30 | /> 31 | )); 32 | -------------------------------------------------------------------------------- /stories/sorting.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { storiesOf } from '@storybook/react'; 3 | import Table from '../src'; 4 | import { ColumnsFixtures, TableFixtures } from '../fixtures/data'; 5 | 6 | storiesOf('Sorting', module).add('Sort column', () => ( 7 |
({ ...data, sortable: true }))} 9 | data={TableFixtures(20)} 10 | /> 11 | )); 12 | --------------------------------------------------------------------------------