├── .eslintrc.js ├── .gitignore ├── .npmignore ├── .prettierrc ├── .travis.yml ├── LICENSE.md ├── README.md ├── examples └── uncontrolled-ts │ ├── .gitignore │ ├── README.md │ ├── package.json │ ├── public │ ├── dev │ │ └── index.html │ └── prod │ │ └── index.html │ ├── src │ ├── App.tsx │ ├── DataTable.tsx │ ├── data.ts │ ├── index.tsx │ └── typings.d.ts │ ├── tsconfig.json │ └── yarn.lock ├── jest.config.js ├── other ├── raf-polyfill.js └── setupEnzyme.ts ├── package.json ├── rollup.config.js ├── src ├── __tests__ │ ├── __snapshots__ │ │ ├── index.controlled.test.tsx.snap │ │ ├── index.initial.test.tsx.snap │ │ └── index.uncontrolled.test.tsx.snap │ ├── index.controlled.test.tsx │ ├── index.initial.test.tsx │ ├── index.root.test.tsx │ ├── index.uncontrolled.test.tsx │ └── utils.test.ts ├── react-data-sort.tsx └── utils.ts ├── tsconfig.json └── yarn.lock /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: 'babel-eslint', 3 | extends: ['standard', 'prettier'], 4 | plugins: ['react', 'import'], 5 | env: { 6 | browser: true, 7 | jest: true 8 | }, 9 | rules: { 10 | 'react/jsx-uses-react': 'error', 11 | 'react/jsx-uses-vars': 'error' 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | .rpt2_cache 5 | coverage 6 | *.log 7 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | coverage 3 | .babelrc 4 | .eslintrc.js 5 | .prettierrc 6 | jest.config.js 7 | rollup.config.js 8 | other -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "printWidth": 140, 4 | "singleQuote": true 5 | } 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | cache: yarn 4 | notifications: 5 | email: false 6 | node_js: '10' 7 | install: yarn install 8 | before_script: 9 | - yarn install 10 | - yarn run test 11 | script: yarn run build 12 | after_success: 13 | - npx semantic-release 14 | branches: 15 | only: 16 | - master -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2017 Corjen Moll 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 4 | "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, 5 | distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the 6 | following conditions: 7 | 8 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 11 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 12 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 13 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React data sort 2 | 3 | A simple react component that helps you sort and paginate a list of data. 4 | 5 | [![GitHub license](https://img.shields.io/github/license/Corjen/react-data-sort.svg)](https://github.com/Corjen/react-data-sort/blob/master/LICENSE.md) 6 | [![Build Status](https://travis-ci.org/Corjen/react-data-sort.svg?branch=master)](https://travis-ci.org/Corjen/react-data-sort) 7 | ![](https://img.shields.io/badge/size-7.19%20kB-brightgreen.svg) ![](https://img.shields.io/badge/gzip%20size-2.02%20kB-brightgreen.svg) 8 | 9 | # The problem 10 | 11 | You want to display a custom set of data in a table or list and want to be able to sort and/or paginate it. You also want to have freedom of 12 | styling and a simple API. 13 | 14 | # This solution 15 | 16 | Components with a [render prop](https://cdb.reacttraining.com/use-a-render-prop-50de598f11ce) like [Downshift](downshift) and React Router's 17 | [Route](https://reacttraining.com/react-router/web/api/Route) are gaining popularity. The render prop pattern gives you maximum flexibility 18 | in the way you render and style your components because the render prop itself doens't render anything. 19 | 20 | I've made this component because I was looking for a React table component that would give me as much control as possible over rendering and 21 | styling. I couldn't find it, so I decided to build something myself. This is my first open source React Component, any feedback or 22 | contributions are very welcome! 23 | 24 | > Note: If you need to render a really large dataset where performance is vital, something like 25 | > [react-virtualized](https://github.com/bvaughn/react-virtualized) is probably a better fit. 26 | 27 | # Table of Contents 28 | 29 | * [Installation](#installation) 30 | * [Usage](#usage) 31 | * [Props](#props) 32 | * [Render prop function](#render-prop-function) 33 | * [Examples](#examples) 34 | * [Todo](#todo) 35 | * [License](#license) 36 | 37 | # Installation 38 | 39 | This modules is distributed via [npm](https://www.npmjs.com/package/react-data-sort). You can install it with npm: 40 | 41 | ``` 42 | npm install --save react-data-sort 43 | ``` 44 | 45 | This package has `react` and `prop-types` as [peerDependencies](https://nodejs.org/en/blog/npm/peer-dependencies/). Make sure to install 46 | them if you haven't. 47 | 48 | # Usage 49 | 50 | ```javascript 51 | import Datasort from 'react-data-sort' 52 | 53 | const tableData = [{ id: 1, name: 'b', id: 2, name: 'c', id: 3, name: 'a' }] 54 | 55 | function App() { 56 | return ( 57 | ( 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | {data.map(({ id, name }) => ( 70 | 71 | 72 | 73 | 74 | ))} 75 | 76 |
IdName
{id}{name}
77 | )} 78 | /> 79 | ) 80 | } 81 | 82 | export default App 83 | ``` 84 | 85 | By default, it will return the data in the same order that you've given it. The above code will result in this table: 86 | 87 | | ID | Name | 88 | | --- | ---- | 89 | | 1 | b | 90 | | 2 | c | 91 | | 3 | a | 92 | 93 | # Props 94 | 95 | ## data 96 | 97 | > `array` | defaults to `[]` An array of data that you want to render 98 | 99 | ## defaultDirection 100 | 101 | > `string` | defaults to `desc` | can be `asc` or `desc` This is the direction in which the data is sorted by default. 102 | 103 | ## defaultSortBy 104 | 105 | > `string` | defaults to `null` | can be null or an object key in your data array. This is the key by which your data is sorted. 106 | 107 | ## itemsPerPage 108 | 109 | > `number` | defaults to `10` The number of items to show on one page. Only works if `paginate` prop is `true`. 110 | 111 | ## paginate 112 | 113 | > `boolean` | defaults to `false` 114 | 115 | Enables pagination functionality and slices your data to the current page. 116 | 117 | ## searchInKeys 118 | 119 | > `array` | defaults to the keys of the first item in `data` 120 | 121 | Sets the keys to search in 122 | 123 | # Controlled vs Uncontrolled 124 | 125 | The internal state manages `direction`, `sortBy`, `searchQuery` and `activePage`. In some cases, you want to control that state outside the component, for 126 | example if you use `redux` or `mobx` to manage your state. You can set `direction`, `sortBy`, `searchQuery` and `active` as props, thus making that part of 127 | the state 'controlled'. 128 | 129 | # Render Prop Function 130 | 131 | The render prop expects a function and doesn't render anything. It's argument is an object, with the internal state and a couple of actions. 132 | 133 | ```javascript 134 | ( 145 | // Render jsx stuff here 146 | )} 147 | /> 148 | ``` 149 | 150 | ## actions 151 | 152 | You can change the internal state with these actions. 153 | 154 | | property | type | description | 155 | | --------------- | ----------------------------- | ------------------------------------------------------ | 156 | | toggleDirection | `function()` | toggle the direction from `asc` to `desc` or viceversa | 157 | | setDirection | `function(direction: string)` | set the direction to `asc` or `desc` | 158 | | prevPage | `function()` | go to the previous page (only if `paginate` is true) | 159 | | nextPage | `function()` | go to the next page (only if `paginate` is true) | 160 | | goToPage | `function(index: number)` | go to a specific page | 161 | | setSortBy | `function(key: string)` | set the key to sort the data by | 162 | | reset | `function()` | reset to the initial state | 163 | | search | `function(query: string)` | search for a query in given data | 164 | 165 | ## state 166 | 167 | These are the internal state values 168 | 169 | | property | type | description | 170 | | ----------- | ----------------- | ------------------------------------------------- | 171 | | activePage | `number` | the current active page | 172 | | pages | `number` | the total amount of pages The current active page | 173 | | sortBy | `string` / `null` | the current key where the data is sorted by | 174 | | direction | `string` | the current direction where the data is sorted by | 175 | | searchQuery | `string` | the current search query | 176 | 177 | # Examples 178 | 179 | * [Uncontrolled example](https://codesandbox.io/s/2zmrjm564r) 180 | * [Controlled example](https://codesandbox.io/s/4r08q2vx94) 181 | * [Without pagination](https://codesandbox.io/s/4rw4pvykzx) 182 | 183 | # TODO 184 | 185 | * UMD build 186 | * Add helpers for aria labels 187 | * Change the name to something fancier? 188 | 189 | # License 190 | 191 | MIT 192 | -------------------------------------------------------------------------------- /examples/uncontrolled-ts/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | .cache/ 4 | .vscode 5 | build/ 6 | coverage/ -------------------------------------------------------------------------------- /examples/uncontrolled-ts/README.md: -------------------------------------------------------------------------------- 1 | ## React-data-sort uncontrolled typescript example 2 | 3 | This is an example app with react-data-sort built with TypeScript. 4 | 5 | ``` 6 | yarn 7 | yarn start 8 | ``` 9 | -------------------------------------------------------------------------------- /examples/uncontrolled-ts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "uncontrolled-ts", 3 | "version": "0.1.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "start": "parcel ./public/dev/index.html --open", 8 | "build": "parcel build ./src/index.tsx --out-dir build/ && cp ./public/prod/index.html ./build/index.html", 9 | "serve:build": "serve --open ./build/" 10 | }, 11 | "dependencies": { 12 | "react": "^16.5.2", 13 | "react-dom": "^16.5.2", 14 | "react-data-sort": "latest" 15 | }, 16 | "devDependencies": { 17 | "@types/react": "^16.4.15", 18 | "@types/react-dom": "^16.0.8", 19 | "babel-core": "^6.26.3", 20 | "babel-plugin-transform-class-properties": "^6.24.1", 21 | "babel-preset-env": "^1.7.0", 22 | "babel-preset-react": "^6.24.1", 23 | "comment-json": "^1.1.3", 24 | "parcel-bundler": "^1.10.1", 25 | "prettier": "^1.14.3", 26 | "react-test-renderer": "^16.5.2", 27 | "serve": "latest", 28 | "typescript": "^3.1.1" 29 | }, 30 | "description": "data-sort-example", 31 | "repository": { 32 | "type": "git", 33 | "url": "https://github.com/Ludvig Lundgren/uncontrolled-ts" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /examples/uncontrolled-ts/public/dev/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 |
9 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/uncontrolled-ts/public/prod/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/uncontrolled-ts/src/App.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import DataTable from './DataTable' 3 | 4 | export const App: React.StatelessComponent<{}> = () => ( 5 |
6 |

Uncontrolled react-data-sort

7 | 8 |
9 | ) 10 | -------------------------------------------------------------------------------- /examples/uncontrolled-ts/src/DataTable.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import Datasort from 'react-data-sort' 3 | import tableData from './data' 4 | 5 | const DataTable = () => { 6 | return ( 7 | { 13 | return ( 14 |
15 | 16 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 |
31 | ) 32 | }} 33 | /> 34 | ) 35 | } 36 | 37 | export default DataTable 38 | 39 | function Search({ search }: any) { 40 | return ( 41 |
42 | Search 43 | search(e.target.value)} /> 44 |
45 | ) 46 | } 47 | 48 | function TableHead({ setSortBy, sortBy, direction, toggleDirection }: any) { 49 | const columns = [{ key: 'id', title: 'ID' }, { key: 'name', title: 'Name' }, { key: 'email', title: 'Email' }] 50 | const items = columns.map(({ key, title }) => { 51 | const active = key === sortBy 52 | return ( 53 | { 57 | if (active) { 58 | toggleDirection() 59 | } 60 | setSortBy(key) 61 | }} 62 | > 63 | {title} {active ? (direction === 'asc' ? '▲' : '▼') : null} 64 | 65 | ) 66 | }) 67 | return ( 68 | 69 | {items} 70 | 71 | ) 72 | } 73 | 74 | function HeadToggle({ children, active, onClick }: any) { 75 | return ( 76 | 77 | {children} 78 | 79 | ) 80 | } 81 | 82 | function TableBody({ data }: any) { 83 | return ( 84 | 85 | {data.map(({ id, name, email }: any) => ( 86 | 87 | {id} 88 | {name} 89 | {email} 90 | 91 | ))} 92 | 93 | ) 94 | } 95 | 96 | function Flex({ children, style }: any) { 97 | return
{children}
98 | } 99 | 100 | function GoToPage({ goToPage, pages }: any) { 101 | const options = [] 102 | for (let i = 0; i < pages; i++) { 103 | options.push( 104 | 107 | ) 108 | } 109 | return ( 110 |
111 | Go to page 112 |
113 | ) 114 | } 115 | 116 | function Navigation({ activePage, goToPage, nextPage, prevPage, pages }: any) { 117 | return ( 118 | 119 | 122 | 125 | 126 | 129 | 132 | 133 | ) 134 | } 135 | 136 | function PageIndicator({ pages, activePage }: any) { 137 | return ( 138 |
139 | {activePage + 1} / {pages} 140 |
141 | ) 142 | } 143 | -------------------------------------------------------------------------------- /examples/uncontrolled-ts/src/data.ts: -------------------------------------------------------------------------------- 1 | const data = [ 2 | { id: 0, name: 'Edgardo', email: 'Kade.Steuber32@hotmail.com' }, 3 | { id: 1, name: 'Mathilde', email: 'Ervin.Tremblay7@yahoo.com' }, 4 | { id: 2, name: 'Augustus', email: 'Queenie87@yahoo.com' }, 5 | { id: 3, name: 'Myrtice', email: 'Salvatore.Purdy70@gmail.com' }, 6 | { id: 4, name: 'Madisyn', email: 'Lesly.Prohaska17@hotmail.com' }, 7 | { id: 5, name: 'Miller', email: 'Eldora13@hotmail.com' }, 8 | { id: 6, name: 'Ronaldo', email: 'Scot.Mayert@yahoo.com' }, 9 | { id: 7, name: 'Eliane', email: 'Dale.White@hotmail.com' }, 10 | { id: 8, name: 'Victor', email: 'Gerard39@hotmail.com' }, 11 | { id: 9, name: 'Keenan', email: 'Lyda56@hotmail.com' }, 12 | { id: 10, name: 'Margot', email: 'Donnell11@yahoo.com' }, 13 | { id: 11, name: 'Gaston', email: 'Daren.Von68@hotmail.com' }, 14 | { id: 12, name: 'Yadira', email: 'Ebony.McDermott@hotmail.com' }, 15 | { id: 13, name: 'Enrico', email: 'Anthony_Connelly76@hotmail.com' }, 16 | { id: 14, name: 'Justyn', email: 'Max_Feest@yahoo.com' }, 17 | { id: 15, name: 'Rudolph', email: 'Citlalli_Boyer90@hotmail.com' }, 18 | { id: 16, name: 'Kirsten', email: 'Paula_Marvin@hotmail.com' }, 19 | { id: 17, name: 'Hermann', email: 'Lucile91@hotmail.com' }, 20 | { id: 18, name: 'Einar', email: 'Ozella.Skiles@yahoo.com' }, 21 | { id: 19, name: 'Felipe', email: 'Zion53@hotmail.com' }, 22 | { id: 20, name: 'Maryse', email: 'Laurie95@yahoo.com' }, 23 | { id: 21, name: 'Leland', email: 'Kellen23@gmail.com' }, 24 | { id: 22, name: 'Rebekah', email: 'Carmella10@yahoo.com' }, 25 | { id: 23, name: 'Berta', email: 'Cordia15@gmail.com' }, 26 | { id: 24, name: 'Maya', email: 'Scotty32@gmail.com' }, 27 | { id: 25, name: 'Alec', email: 'Israel.Brekke27@yahoo.com' }, 28 | { id: 26, name: 'Kara', email: 'Maximillia70@yahoo.com' }, 29 | { id: 27, name: 'Stephany', email: 'Milan.Bode22@yahoo.com' }, 30 | { id: 28, name: 'Lily', email: 'Jannie55@yahoo.com' }, 31 | { id: 29, name: 'Brandyn', email: 'Andy17@gmail.com' }, 32 | { id: 30, name: 'Tristian', email: 'Cullen76@hotmail.com' }, 33 | { id: 31, name: 'Travon', email: 'Wendy.Jerde@hotmail.com' }, 34 | { id: 32, name: 'Sydnee', email: 'Ethyl.Hammes79@hotmail.com' }, 35 | { id: 33, name: 'Eileen', email: 'Wilbert_Steuber@hotmail.com' }, 36 | { id: 34, name: 'Akeem', email: 'Johnson_Yundt71@yahoo.com' }, 37 | { id: 35, name: 'Izabella', email: 'Marcella55@yahoo.com' }, 38 | { id: 36, name: 'Frida', email: 'Delpha5@hotmail.com' }, 39 | { id: 37, name: 'Angela', email: 'Aida79@gmail.com' }, 40 | { id: 38, name: 'Emanuel', email: 'Darius_Langworth17@hotmail.com' }, 41 | { id: 39, name: 'Brooks', email: 'Dagmar73@yahoo.com' }, 42 | { id: 40, name: 'Clint', email: 'Luella_Bechtelar@hotmail.com' }, 43 | { id: 41, name: 'Lilla', email: 'Larissa2@gmail.com' }, 44 | { id: 42, name: 'Prince', email: 'Shany_Flatley@gmail.com' }, 45 | { id: 43, name: 'Josie', email: 'Gabe99@yahoo.com' }, 46 | { id: 44, name: 'Geo', email: 'Alisha.Rosenbaum14@yahoo.com' }, 47 | { id: 45, name: 'Edgar', email: 'Ruby.Friesen@gmail.com' }, 48 | { id: 46, name: 'London', email: 'Jerrold_VonRueden80@yahoo.com' }, 49 | { id: 47, name: 'Llewellyn', email: 'Tabitha.Boyle7@yahoo.com' }, 50 | { id: 48, name: 'Jasmin', email: 'Friedrich35@gmail.com' }, 51 | { id: 49, name: 'Andres', email: 'Frankie_VonRueden@gmail.com' }, 52 | { id: 50, name: 'Suzanne', email: 'Sheila.Torp@gmail.com' }, 53 | { id: 51, name: 'Mervin', email: 'Mortimer.Bogan55@hotmail.com' }, 54 | { id: 52, name: 'Reymundo', email: 'Cielo_Koelpin@gmail.com' }, 55 | { id: 53, name: 'Brielle', email: 'Kirsten.Schultz80@hotmail.com' }, 56 | { id: 54, name: 'Destiny', email: 'Jeff_Turcotte@yahoo.com' }, 57 | { id: 55, name: 'Gerald', email: 'Heath.Haag@yahoo.com' }, 58 | { id: 56, name: 'Verdie', email: 'Melody.Fritsch74@gmail.com' }, 59 | { id: 57, name: 'Otto', email: 'Maybell50@gmail.com' }, 60 | { id: 58, name: 'Curt', email: 'Jamaal_Baumbach28@hotmail.com' }, 61 | { id: 59, name: 'Caleb', email: 'Dedric42@hotmail.com' }, 62 | { id: 60, name: 'Amanda', email: 'Gaston.Little@hotmail.com' }, 63 | { id: 61, name: 'Ruth', email: 'Edwardo.Dach@yahoo.com' }, 64 | { id: 62, name: 'Annette', email: 'Daisha.Effertz5@gmail.com' }, 65 | { id: 63, name: 'Thora', email: 'Elza.Romaguera@yahoo.com' }, 66 | { id: 64, name: 'Zetta', email: 'Erica.Berge@gmail.com' }, 67 | { id: 65, name: 'Merle', email: 'Flo40@yahoo.com' }, 68 | { id: 66, name: 'Winifred', email: 'Layla6@gmail.com' }, 69 | { id: 67, name: 'Olaf', email: 'Chaim_Steuber72@hotmail.com' }, 70 | { id: 68, name: 'Reynold', email: 'Kyleigh_Bechtelar@yahoo.com' }, 71 | { id: 69, name: 'Tristin', email: 'Wendy_Kuphal71@yahoo.com' }, 72 | { id: 70, name: 'Roosevelt', email: 'Alex72@yahoo.com' }, 73 | { id: 71, name: 'Felicity', email: 'Keith52@hotmail.com' }, 74 | { id: 72, name: 'Fritz', email: 'Ettie.Sauer48@yahoo.com' }, 75 | { id: 73, name: 'Emiliano', email: 'Crystal.Herman@hotmail.com' }, 76 | { id: 74, name: 'Sierra', email: 'Kellie_Schumm55@hotmail.com' }, 77 | { id: 75, name: 'Ansel', email: 'Ryley.Durgan77@hotmail.com' }, 78 | { id: 76, name: 'Jaron', email: 'Marley.Armstrong15@gmail.com' }, 79 | { id: 77, name: 'Scotty', email: 'Kristina_Willms@gmail.com' }, 80 | { id: 78, name: 'Alta', email: 'Frederic_Rippin@yahoo.com' }, 81 | { id: 79, name: 'Jo', email: 'Demetrius95@gmail.com' }, 82 | { id: 80, name: 'Deshaun', email: 'Jacquelyn_Gleichner9@hotmail.com' }, 83 | { id: 81, name: 'Jimmy', email: 'Beryl_Skiles74@gmail.com' }, 84 | { id: 82, name: 'Valentina', email: 'Rubie59@yahoo.com' }, 85 | { id: 83, name: 'Jerry', email: 'Jude_Graham@hotmail.com' }, 86 | { id: 84, name: 'Zoey', email: 'Santiago_Rice@hotmail.com' }, 87 | { id: 85, name: 'Iliana', email: 'Orlo_Dietrich@gmail.com' }, 88 | { id: 86, name: 'Sylvester', email: 'Marisa.Wolff0@gmail.com' }, 89 | { id: 87, name: 'Kenya', email: 'Beth_Kutch66@yahoo.com' }, 90 | { id: 88, name: 'Crystel', email: 'Enola86@gmail.com' }, 91 | { id: 89, name: 'Linda', email: 'Samanta_Roberts53@gmail.com' }, 92 | { id: 90, name: 'Daren', email: 'Brenna29@gmail.com' }, 93 | { id: 91, name: 'Jaylin', email: 'Trisha90@hotmail.com' }, 94 | { id: 92, name: 'Valentine', email: 'Cameron43@hotmail.com' }, 95 | { id: 93, name: 'Elza', email: 'Jeanie_Gutmann86@hotmail.com' }, 96 | { id: 94, name: 'Mollie', email: 'May.Sipes88@hotmail.com' }, 97 | { id: 95, name: 'Keagan', email: 'Lacy.Douglas96@gmail.com' }, 98 | { id: 96, name: 'Wiley', email: 'Gerson96@gmail.com' }, 99 | { id: 97, name: 'Kennith', email: 'Ara.Schmeler@hotmail.com' }, 100 | { id: 98, name: 'Damian', email: 'Nicola.Huel@yahoo.com' }, 101 | { id: 99, name: 'Delmer', email: 'Chester_Bayer79@hotmail.com' } 102 | ] 103 | 104 | export default data 105 | -------------------------------------------------------------------------------- /examples/uncontrolled-ts/src/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import * as ReactDOM from 'react-dom' 3 | 4 | import { App } from './App' 5 | 6 | ReactDOM.render(, document.getElementById('app')) 7 | -------------------------------------------------------------------------------- /examples/uncontrolled-ts/src/typings.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.json' { 2 | const value: any 3 | export default value 4 | } 5 | -------------------------------------------------------------------------------- /examples/uncontrolled-ts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | "target": "ES5" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'. */, 5 | "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */, 6 | "lib": ["dom", "es2018"] /* Specify library files to be included in the compilation: */, 7 | // "allowJs": true, /* Allow javascript files to be compiled. */ 8 | // "checkJs": true, /* Report errors in .js files. */ 9 | "jsx": "react" /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */, 10 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 11 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 12 | // "outFile": "./", /* Concatenate and emit output to single file. */ 13 | // "outDir": "./", /* Redirect output structure to the directory. */ 14 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 15 | // "removeComments": true, /* Do not emit comments to output. */ 16 | // "noEmit": true, /* Do not emit outputs. */ 17 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 18 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 19 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 20 | 21 | "resolveJsonModule": true, 22 | /* Strict Type-Checking Options */ 23 | "strict": true /* Enable all strict type-checking options. */ 24 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 25 | // "strictNullChecks": true, /* Enable strict null checks. */ 26 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 27 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 28 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 29 | 30 | /* Additional Checks */ 31 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 32 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 33 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 34 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 35 | 36 | /* Module Resolution Options */ 37 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 38 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 39 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 40 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 41 | // "typeRoots": [], /* List of folders to include type definitions from. */ 42 | // "types": [], /* Type declaration files to be included in compilation. */ 43 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 44 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 45 | 46 | /* Source Map Options */ 47 | // "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 48 | // "mapRoot": "./", /* Specify the location where debugger should locate map files instead of generated locations. */ 49 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 50 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 51 | 52 | /* Experimental Options */ 53 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 54 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | roots: ['/src'], 3 | transform: { 4 | '^.+\\.tsx?$': 'ts-jest' 5 | }, 6 | testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.tsx?$', 7 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], 8 | snapshotSerializers: ['enzyme-to-json/serializer'], 9 | setupFilesAfterEnv: ['/other/setupEnzyme.ts'] 10 | } 11 | -------------------------------------------------------------------------------- /other/raf-polyfill.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /** 3 | * requestAnimationFrame polyfill from https://gist.github.com/paulirish/1579671 4 | * http://paulirish.com/2011/requestanimationframe-for-smart-animating/ 5 | * http://my.opera.com/emoller/blog/2011/12/20/requestanimationframe-for-smart-er-animating 6 | * requestAnimationFrame polyfill by Erik Möller. fixes from Paul Irish and Tino Zijdel 7 | * MIT license 8 | */ 9 | 10 | var lastTime = 0 11 | var vendors = ['ms', 'moz', 'webkit', 'o'] 12 | for (var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) { 13 | window.requestAnimationFrame = window[vendors[x] + 'RequestAnimationFrame'] 14 | window.cancelAnimationFrame = window[vendors[x] + 'CancelAnimationFrame'] || window[vendors[x] + 'CancelRequestAnimationFrame'] 15 | } 16 | 17 | if (!window.requestAnimationFrame) { 18 | window.requestAnimationFrame = function(callback, element) { 19 | var currTime = new Date().getTime() 20 | var timeToCall = Math.max(0, 16 - (currTime - lastTime)) 21 | var id = window.setTimeout(function() { 22 | // eslint-disable-next-line consumerweb/no-callback-literal 23 | callback(currTime + timeToCall) 24 | }, timeToCall) 25 | lastTime = currTime + timeToCall 26 | return id 27 | } 28 | global.requestAnimationFrame = window.requestAnimationFrame 29 | } 30 | 31 | if (!window.cancelAnimationFrame) { 32 | window.cancelAnimationFrame = function(id) { 33 | clearTimeout(id) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /other/setupEnzyme.ts: -------------------------------------------------------------------------------- 1 | import Enzyme from 'enzyme' 2 | import EnzymeAdapter from 'enzyme-adapter-react-16' 3 | 4 | Enzyme.configure({ adapter: new EnzymeAdapter() }) 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-data-sort", 3 | "version": "0.0.0-development", 4 | "description": "A React component to sort and paginate data", 5 | "main": "dist/react-data-sort.cjs.js", 6 | "module": "dist/react-data-sort.esm.js", 7 | "types": "dist/react-data-sort.d.ts", 8 | "scripts": { 9 | "test": "jest", 10 | "prebuild": "rimraf dist", 11 | "start": "rollup --config --watch", 12 | "build": "rollup --config", 13 | "semantic-release": "semantic-release pre && npm publish && semantic-release post" 14 | }, 15 | "files": [ 16 | "dist" 17 | ], 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/Corjen/react-data-sort.git" 21 | }, 22 | "keywords": [ 23 | "react", 24 | "data", 25 | "sort", 26 | "pagination" 27 | ], 28 | "author": "Corjen Moll", 29 | "license": "MIT", 30 | "bugs": { 31 | "url": "https://github.com/Corjen/react-data-sort/issues" 32 | }, 33 | "homepage": "https://github.com/Corjen/react-data-sort#readme", 34 | "devDependencies": { 35 | "@types/enzyme": "^3.9.4", 36 | "@types/enzyme-adapter-react-16": "^1.0.5", 37 | "@types/jest": "^24.0.15", 38 | "@types/lodash.sortby": "^4.7.6", 39 | "@types/match-sorter": "^2.3.0", 40 | "@types/react": "^16.8.22", 41 | "@types/react-dom": "^16.8.4", 42 | "cz-conventional-changelog": "2.1.0", 43 | "enzyme": "^3.10.0", 44 | "enzyme-adapter-react-16": "^1.14.0", 45 | "enzyme-to-json": "^3.3.5", 46 | "eslint": "^6.0.1", 47 | "eslint-config-prettier": "^6.0.0", 48 | "eslint-config-standard": "^12.0.0", 49 | "eslint-plugin-import": "^2.18.0", 50 | "eslint-plugin-node": "^9.1.0", 51 | "eslint-plugin-promise": "^4.2.1", 52 | "eslint-plugin-react": "^7.14.2", 53 | "eslint-plugin-standard": "^4.0.0", 54 | "jest": "^24.8.0", 55 | "prettier": "^1.18.2", 56 | "react": "^16.8.6", 57 | "react-dom": "^16.8.6", 58 | "rimraf": "^2.6.3", 59 | "rollup": "^1.16.2", 60 | "rollup-plugin-typescript2": "^0.21.2", 61 | "semantic-release": "^15.13.18", 62 | "ts-jest": "^24.0.2", 63 | "tslib": "^1.10.0", 64 | "typescript": "^3.5.2" 65 | }, 66 | "peerDependencies": { 67 | "prop-types": "^15.6.1", 68 | "react": "^16.3.2" 69 | }, 70 | "dependencies": { 71 | "lodash.sortby": "^4.7.0", 72 | "match-sorter": "^3.1.1" 73 | }, 74 | "config": { 75 | "commitizen": { 76 | "path": "./node_modules/cz-conventional-changelog" 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import typescript from 'rollup-plugin-typescript2' 2 | 3 | export default { 4 | input: './src/react-data-sort.tsx', 5 | output: [ 6 | { 7 | file: './dist/react-data-sort.cjs.js', 8 | format: 'cjs', 9 | sourcemap: true 10 | }, 11 | { 12 | file: './dist/react-data-sort.esm.js', 13 | format: 'es', 14 | sourcemap: true 15 | } 16 | ], 17 | external: ['react', 'prop-types', 'lodash.sortby', 'match-sorter'], 18 | plugins: [typescript()] 19 | } 20 | -------------------------------------------------------------------------------- /src/__tests__/__snapshots__/index.controlled.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Pagination 1`] = ` 4 | Object { 5 | "activePage": 0, 6 | "data": Array [ 7 | Object { 8 | "id": 1, 9 | "name": "b", 10 | }, 11 | Object { 12 | "id": 2, 13 | "name": "a", 14 | }, 15 | Object { 16 | "id": 3, 17 | "name": "c", 18 | }, 19 | ], 20 | "direction": "asc", 21 | "goToPage": [Function], 22 | "nextPage": [Function], 23 | "pages": 1, 24 | "prevPage": [Function], 25 | "reset": [Function], 26 | "search": [Function], 27 | "searchQuery": "", 28 | "setDirection": [Function], 29 | "setSortBy": [Function], 30 | "sortBy": null, 31 | "toggleDirection": [Function], 32 | } 33 | `; 34 | -------------------------------------------------------------------------------- /src/__tests__/__snapshots__/index.initial.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`initialState 1`] = ` 4 | Object { 5 | "activePage": 0, 6 | "data": Array [], 7 | "direction": "asc", 8 | "pages": null, 9 | "searchQuery": "", 10 | "sortBy": null, 11 | } 12 | `; 13 | -------------------------------------------------------------------------------- /src/__tests__/__snapshots__/index.uncontrolled.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Pagination 1`] = ` 4 | Object { 5 | "activePage": 0, 6 | "data": Array [ 7 | Object { 8 | "id": 1, 9 | "name": "b", 10 | }, 11 | ], 12 | "direction": "asc", 13 | "goToPage": [Function], 14 | "nextPage": [Function], 15 | "pages": 3, 16 | "prevPage": [Function], 17 | "reset": [Function], 18 | "search": [Function], 19 | "searchQuery": "", 20 | "setDirection": [Function], 21 | "setSortBy": [Function], 22 | "sortBy": null, 23 | "toggleDirection": [Function], 24 | } 25 | `; 26 | -------------------------------------------------------------------------------- /src/__tests__/index.controlled.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import DataSort from '../react-data-sort' 3 | import { shallow } from 'enzyme' 4 | 5 | const data = [{ id: 1, name: 'b' }, { id: 2, name: 'a' }, { id: 3, name: 'c' }] 6 | 7 | test('Pagination', () => { 8 | let renderArgs: any = renderAndReturnArgs() 9 | expect(renderArgs).toMatchSnapshot() 10 | 11 | // Render with activePage prop 12 | renderArgs = renderAndReturnArgs({ itemsPerPage: 1, activePage: 1 }) 13 | expect(renderArgs.activePage).toEqual(1) 14 | expect(renderArgs.data).toEqual([data[1]]) 15 | 16 | renderArgs = renderAndReturnArgs({ itemsPerPage: 1, activePage: 2 }) 17 | expect(renderArgs.activePage).toEqual(2) 18 | }) 19 | 20 | test('Direction & sortBy', () => { 21 | let renderArgs: any = renderAndReturnArgs({ sortBy: 'id', direction: 'desc' }) 22 | expect(renderArgs.sortBy).toEqual('id') 23 | expect(renderArgs.direction).toEqual('desc') 24 | expect(renderArgs.data).toEqual([data[2], data[1], data[0]]) 25 | 26 | renderArgs = renderAndReturnArgs({ sortBy: 'name' }) 27 | expect(renderArgs.sortBy).toEqual('name') 28 | expect(renderArgs.direction).toEqual('asc') 29 | expect(renderArgs.data).toEqual([data[1], data[0], data[2]]) 30 | }) 31 | 32 | test('Search query', () => { 33 | let searchQuery = 'b' 34 | let renderArgs: any = renderAndReturnArgs({ searchQuery, searchInKeys: ['name'] }) 35 | expect(renderArgs.searchQuery).toEqual(searchQuery) 36 | expect(renderArgs.data).toEqual([data[0]]) 37 | 38 | searchQuery = '' 39 | renderArgs = renderAndReturnArgs({ searchQuery }) 40 | expect(renderArgs.searchQuery).toEqual(searchQuery) 41 | expect(renderArgs.data).toEqual(data) 42 | }) 43 | 44 | function renderAndReturnArgs(props = {}) { 45 | let renderArgs 46 | const renderSpy = jest.fn(args => { 47 | renderArgs = { ...args } 48 | return null 49 | }) 50 | shallow() 51 | return renderArgs 52 | } 53 | -------------------------------------------------------------------------------- /src/__tests__/index.initial.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import DataSort from '../react-data-sort' 3 | import { shallow } from 'enzyme' 4 | 5 | const data = [{ id: 1, name: 'b' }, { id: 2, name: 'a' }, { id: 3, name: 'c' }] 6 | 7 | test('initialState', () => { 8 | const wrapper = shallow( null} />) 9 | expect(wrapper.state()).toMatchSnapshot() 10 | }) 11 | 12 | test('pages should be null if pagination is disabled', () => { 13 | const wrapper = shallow( null} />) 14 | expect(wrapper.state('pages')).toBeNull() 15 | }) 16 | 17 | test('pages should be set if pagination is enabled', () => { 18 | const wrapper = shallow( null} />) 19 | expect(wrapper.state('pages')).toEqual(1) 20 | }) 21 | 22 | test('defaultSortBy should set the sortBy state', () => { 23 | const wrapper = shallow( null} />) 24 | expect(wrapper.state('sortBy')).toEqual('id') 25 | }) 26 | 27 | test('defaultDirection should set the direction', () => { 28 | const wrapper = shallow( null} />) 29 | expect(wrapper.state('direction')).toEqual('desc') 30 | }) 31 | 32 | test('pagination should be uncontrolled', () => { 33 | const instance: any = shallow().instance() 34 | expect(instance.isPaginationControlled()).toBeFalsy() 35 | }) 36 | 37 | test('pagination should be controlled', () => { 38 | const instance: any = shallow().instance() 39 | expect(instance.isPaginationControlled()).toBeTruthy() 40 | }) 41 | 42 | test('sortBy should be uncontrolled', () => { 43 | const instance: any = shallow().instance() 44 | expect(instance.isSortByControlled()).toBeFalsy() 45 | }) 46 | 47 | test('sortBy should be controlled', () => { 48 | const instance: any = shallow().instance() 49 | expect(instance.isSortByControlled()).toBeTruthy() 50 | }) 51 | 52 | test('direction should be uncontrolled', () => { 53 | const instance: any = shallow().instance() 54 | expect(instance.isDirectionControlled()).toBeFalsy() 55 | }) 56 | 57 | test('direction should be controlled', () => { 58 | const instance: any = shallow().instance() 59 | expect(instance.isDirectionControlled()).toBeTruthy() 60 | }) 61 | 62 | test('search should be uncontrolled', () => { 63 | const instance: any = shallow().instance() 64 | expect(instance.isSearchControlled()).toBeFalsy() 65 | }) 66 | 67 | test('search should be controlled', () => { 68 | const instance: any = shallow().instance() 69 | expect(instance.isSearchControlled()).toBeTruthy() 70 | }) 71 | -------------------------------------------------------------------------------- /src/__tests__/index.root.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import DataSort from '../react-data-sort' 3 | import { mount } from 'enzyme' 4 | 5 | const data = [{ id: 1, name: 'b' }, { id: 2, name: 'a' }, { id: 3, name: 'c' }] 6 | 7 | test('render nothing', () => { 8 | const Component = () => 9 | expect(mount().html()).toEqual('') 10 | }) 11 | 12 | test('render prop is null, then render null', () => { 13 | const Component = () => null} /> 14 | expect(mount().html()).toEqual('') 15 | }) 16 | 17 | test('render fine', () => { 18 | const Test = () =>
test
19 | const Component = () => } /> 20 | expect(() => mount()).not.toThrow() 21 | }) 22 | -------------------------------------------------------------------------------- /src/__tests__/index.uncontrolled.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import DataSort from '../react-data-sort' 3 | import { shallow } from 'enzyme' 4 | const data = [{ id: 1, name: 'b' }, { id: 2, name: 'a' }, { id: 3, name: 'c' }] 5 | 6 | test('Pagination', () => { 7 | let renderArgs!: any 8 | const renderSpy = jest.fn(args => { 9 | renderArgs = { ...args } 10 | return null 11 | }) 12 | shallow() 13 | 14 | // Expect some initial defaults 15 | expect(renderArgs).toMatchSnapshot() 16 | 17 | // Go to a none existing page 18 | renderArgs.goToPage(renderArgs.pages + 1) 19 | expect(renderArgs.activePage).toEqual(0) 20 | 21 | // Go to an existing page 22 | renderArgs.goToPage(renderArgs.pages - 1) 23 | expect(renderArgs.activePage).toEqual(2) 24 | 25 | // Reset 26 | renderArgs.goToPage(0) 27 | 28 | // Go to the next page 29 | renderArgs.nextPage() 30 | expect(renderArgs.activePage).toEqual(1) 31 | 32 | // Should stay on the same page if next page doesn't exist 33 | renderArgs.goToPage(2) 34 | renderArgs.nextPage() 35 | expect(renderArgs.activePage).toEqual(2) 36 | 37 | // Should stay on the same page if next page doesn't exist 38 | renderArgs.goToPage(0) 39 | renderArgs.prevPage() 40 | expect(renderArgs.activePage).toEqual(0) 41 | renderArgs.goToPage(2) 42 | renderArgs.prevPage() 43 | expect(renderArgs.activePage).toEqual(1) 44 | }) 45 | 46 | test('Set & toggle direction', () => { 47 | let renderArgs!: any 48 | const renderSpy = jest.fn(args => { 49 | renderArgs = { ...args } 50 | return null 51 | }) 52 | shallow() 53 | 54 | // Should keep the same direction if input is invalid 55 | renderArgs.setDirection('invalid') 56 | expect(renderArgs.direction).toEqual('asc') 57 | 58 | // Set to desc 59 | renderArgs.setDirection('desc') 60 | expect(renderArgs.direction).toEqual('desc') 61 | 62 | // Set to asc 63 | renderArgs.setDirection('asc') 64 | expect(renderArgs.direction).toEqual('asc') 65 | 66 | // Toggle direction 67 | renderArgs.toggleDirection() 68 | expect(renderArgs.direction).toEqual('desc') 69 | renderArgs.toggleDirection() 70 | expect(renderArgs.direction).toEqual('asc') 71 | 72 | // Validate data by direction 73 | renderArgs.setSortBy('id') // We need to set this, or we won't know what to sort 74 | expect(renderArgs.data).toEqual(data) 75 | renderArgs.toggleDirection() 76 | expect(renderArgs.data).toEqual(data.reverse()) 77 | }) 78 | 79 | test('Sort by & direction', () => { 80 | let renderArgs!: any 81 | const renderSpy = jest.fn(args => { 82 | renderArgs = { ...args } 83 | return null 84 | }) 85 | shallow() 86 | 87 | // Default is null 88 | expect(renderArgs.sortBy).toBeNull() 89 | renderArgs.setSortBy('name') 90 | expect(renderArgs.sortBy).toEqual('name') 91 | expect(renderArgs.data).toEqual([data[1], data[2], data[0]]) 92 | // SortBy should stay the same if direction changes 93 | renderArgs.toggleDirection() 94 | expect(renderArgs.sortBy).toEqual('name') 95 | expect(renderArgs.data).toEqual([data[0], data[2], data[1]]) 96 | }) 97 | 98 | test('Reset', () => { 99 | let renderArgs!: any 100 | const renderSpy = jest.fn(args => { 101 | renderArgs = { ...args } 102 | return null 103 | }) 104 | shallow() 105 | renderArgs.toggleDirection() 106 | renderArgs.nextPage() 107 | renderArgs.reset() 108 | expect(renderArgs.activePage).toEqual(0) 109 | expect(renderArgs.direction).toEqual('asc') 110 | expect(renderArgs.sortBy).toBeNull() 111 | }) 112 | 113 | test('Search', () => { 114 | let renderArgs!: any 115 | const renderSpy = jest.fn(args => { 116 | renderArgs = { ...args } 117 | return null 118 | }) 119 | 120 | shallow() 121 | renderArgs.search('') 122 | expect(renderArgs.searchQuery).toEqual('') 123 | expect(renderArgs.data).toEqual(data) 124 | 125 | renderArgs.search('a') 126 | expect(renderArgs.searchQuery).toEqual('a') 127 | expect(renderArgs.data).toEqual([data[1]]) 128 | }) 129 | -------------------------------------------------------------------------------- /src/__tests__/utils.test.ts: -------------------------------------------------------------------------------- 1 | import { calculatePages, sortData, paginateData } from '../utils' 2 | 3 | describe('calculatePages', () => { 4 | it('should calculate the right amount of pages', () => { 5 | expect(calculatePages(10, 2)).toEqual(5) 6 | expect(calculatePages(20, 4)).toEqual(5) 7 | expect(calculatePages(10, 3)).toEqual(4) 8 | expect(calculatePages(1, 10)).toEqual(1) 9 | }) 10 | }) 11 | 12 | describe('sortData', () => { 13 | const data = [{ id: 2, name: 'b' }, { id: 1, name: 'c' }, { id: 3, name: 'a' }] 14 | 15 | it('should order the data by id', () => { 16 | expect(sortData(data, 'id')).toEqual([data[1], data[0], data[2]]) 17 | expect(sortData(data, 'id', 'asc')).toEqual([data[1], data[0], data[2]]) 18 | }) 19 | 20 | it('should order the data by id in reversed order', () => { 21 | expect(sortData(data, 'id', 'desc')).toEqual([data[2], data[0], data[1]]) 22 | }) 23 | 24 | it('should order the data by name', () => { 25 | expect(sortData(data, 'name')).toEqual([data[2], data[0], data[1]]) 26 | }) 27 | it('should order the data by name in reversed order', () => { 28 | expect(sortData(data, 'name', 'desc')).toEqual([data[1], data[0], data[2]]) 29 | }) 30 | it('should return the data if the key does not exist', () => { 31 | expect(sortData(data, 'key')).toEqual(data) 32 | }) 33 | }) 34 | 35 | describe('paginateData', () => { 36 | const data = [{ id: 2 }, { id: 1 }, { id: 3 }] 37 | it('should return the right slice', () => { 38 | expect(paginateData(data, 0, 1)).toEqual([data[0]]) 39 | expect(paginateData(data, 2, 1)).toEqual([data[2]]) 40 | expect(paginateData(data, 0, 2)).toEqual([data[0], data[1]]) 41 | }) 42 | it('should return the unsliced data', () => { 43 | expect(paginateData(data, 0, data.length)).toEqual(data) 44 | }) 45 | }) 46 | -------------------------------------------------------------------------------- /src/react-data-sort.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import * as PropTypes from 'prop-types' 3 | import matchSorter from 'match-sorter' 4 | import { calculatePages, sortData, paginateData } from './utils' 5 | 6 | type Direction = 'asc' | 'desc' 7 | 8 | interface RenderProp { 9 | data: any[] 10 | activePage: number 11 | pages: number 12 | sortBy: string 13 | direction: string 14 | searchQuery: string 15 | toggleDirection: () => void 16 | reset: () => void 17 | prevPage: () => void 18 | nextPage: () => void 19 | goToPage: (activePage: number) => void 20 | setDirection: (direction: Direction) => void 21 | setSortBy: (sortBy: string) => void 22 | search: (value: string) => void 23 | } 24 | 25 | interface DataSortProps { 26 | data: any[] 27 | render?: ({ }: RenderProp) => React.ReactNode 28 | paginate?: boolean 29 | sortBy?: string 30 | direction?: string 31 | itemsPerPage?: number 32 | activePage?: number 33 | defaultSortBy?: string 34 | defaultDirection?: string 35 | defaultActivePage?: number 36 | searchQuery?: string 37 | searchInKeys?: any[] 38 | } 39 | 40 | interface DataSortState { 41 | sortBy: string | null 42 | direction: string 43 | pages: number | null 44 | activePage: number 45 | data: any[] 46 | searchQuery: string 47 | } 48 | 49 | class DataSort extends React.Component { 50 | static propTypes = { 51 | data: PropTypes.array.isRequired, 52 | render: PropTypes.func, 53 | paginate: PropTypes.bool, 54 | sortBy: PropTypes.string, 55 | direction: PropTypes.string, 56 | itemsPerPage: PropTypes.number, 57 | activePage: PropTypes.number, 58 | defaultSortBy: PropTypes.string, 59 | defaultDirection: PropTypes.string, 60 | searchQuery: PropTypes.string, 61 | searchInKeys: PropTypes.array 62 | } 63 | 64 | static defaultProps = { 65 | itemsPerPage: 10, 66 | paginate: false 67 | } 68 | 69 | state: DataSortState = { 70 | sortBy: this.props.defaultSortBy || null, 71 | direction: this.props.defaultDirection || 'asc', 72 | pages: null, 73 | activePage: this.props.defaultActivePage || 0, 74 | data: [], 75 | searchQuery: '' 76 | } 77 | 78 | componentDidMount() { 79 | const { itemsPerPage, paginate, data } = this.props 80 | if (paginate) { 81 | this.setState({ pages: calculatePages(data.length, itemsPerPage) }) 82 | } 83 | } 84 | 85 | isPaginationControlled() { 86 | return typeof this.props.activePage !== 'undefined' 87 | } 88 | 89 | isSortByControlled() { 90 | return typeof this.props.sortBy !== 'undefined' 91 | } 92 | 93 | isDirectionControlled() { 94 | return typeof this.props.direction !== 'undefined' 95 | } 96 | 97 | isSearchControlled() { 98 | return typeof this.props.searchQuery !== 'undefined' 99 | } 100 | 101 | reset = () => { 102 | this.setState({ 103 | sortBy: null, 104 | direction: 'asc', 105 | activePage: 0 106 | }) 107 | } 108 | 109 | prevPage = () => { 110 | if (this.props.paginate === null) { 111 | return 112 | } 113 | const { activePage } = this.isPaginationControlled() ? this.props : this.state 114 | if (activePage === 0) { 115 | return 116 | } 117 | this.goToPage(activePage - 1) 118 | } 119 | 120 | nextPage = () => { 121 | if (this.props.paginate === null) { 122 | return 123 | } 124 | const { activePage } = this.isPaginationControlled() ? this.props : this.state 125 | const { pages } = this.state 126 | if (activePage + 1 < pages) { 127 | this.goToPage(activePage + 1) 128 | } 129 | } 130 | 131 | goToPage = (activePage: number) => { 132 | if (this.props.paginate === null) { 133 | return 134 | } 135 | if (typeof activePage !== 'number' || activePage < 0 || activePage > this.state.pages) { 136 | return 137 | } 138 | this.setState({ activePage }) 139 | } 140 | 141 | setSortBy = (sortBy: string) => { 142 | this.setState({ sortBy }) 143 | } 144 | 145 | setDirection = (direction: Direction) => { 146 | if (direction === 'asc' || direction === 'desc') { 147 | this.setState({ direction }) 148 | } 149 | } 150 | 151 | toggleDirection = () => { 152 | this.setState({ 153 | direction: this.state.direction === 'asc' ? 'desc' : 'asc' 154 | }) 155 | } 156 | 157 | /** 158 | * Search dataset with given query 159 | * 160 | * @param value: string 161 | */ 162 | search = (value: string) => { 163 | this.setState({ searchQuery: value }) 164 | } 165 | 166 | render() { 167 | const { render, paginate, itemsPerPage, data } = this.props 168 | const { activePage } = this.isPaginationControlled() ? this.props : this.state 169 | const { sortBy } = this.isSortByControlled() ? this.props : this.state 170 | const { direction } = this.isDirectionControlled() ? this.props : this.state 171 | const { searchQuery } = this.isSearchControlled() ? this.props : this.state 172 | const { pages } = this.state 173 | const keys = this.props.searchInKeys ? this.props.searchInKeys : Object.keys(data[0]) 174 | 175 | // Search & sort data 176 | const searched = searchQuery === '' ? data : matchSorter(data, searchQuery, { keys }) 177 | const sorted = sortBy === null ? searched : sortData(searched, sortBy, direction) 178 | 179 | return typeof render !== 'function' 180 | ? null 181 | : render({ 182 | data: paginate ? paginateData(sorted, activePage, itemsPerPage) : sorted, 183 | activePage, 184 | pages, 185 | sortBy, 186 | direction, 187 | searchQuery, 188 | toggleDirection: this.toggleDirection, 189 | reset: this.reset, 190 | prevPage: this.prevPage, 191 | nextPage: this.nextPage, 192 | goToPage: this.goToPage, 193 | setDirection: this.setDirection, 194 | setSortBy: this.setSortBy, 195 | search: this.search 196 | }) 197 | } 198 | } 199 | 200 | export default DataSort 201 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import sortBy from 'lodash.sortby' 2 | 3 | export const calculatePages = (items: number, itemsPerPage: number) => { 4 | return Math.ceil(items / itemsPerPage) 5 | } 6 | 7 | export const sortData = (data: any[], key: string, direction = 'asc') => { 8 | const sorted = sortBy(data, key) 9 | return direction === 'desc' ? sorted.reverse() : sorted 10 | } 11 | 12 | export const paginateData = (data: any[], activePage: number, itemsPerPage: number) => { 13 | const from = activePage * itemsPerPage 14 | const to = from + itemsPerPage 15 | return data.slice(from, to) 16 | } 17 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "emitDecoratorMetadata": true, 4 | "experimentalDecorators": true, 5 | "forceConsistentCasingInFileNames": true, 6 | "jsx": "react", 7 | "allowSyntheticDefaultImports": true, 8 | "esModuleInterop": true, 9 | "module": "es6", 10 | "moduleResolution": "node", 11 | "noImplicitAny": true, 12 | "declaration": true, 13 | "outDir": "./dist", 14 | "preserveConstEnums": true, 15 | "target": "es5" 16 | }, 17 | "exclude": ["node_modules", "examples", "other", "src/__tests__"] 18 | } 19 | --------------------------------------------------------------------------------