├── .babelrc ├── .eslintrc ├── .gitignore ├── .npmignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── SUMMARY.md ├── blog └── Release.md ├── book.json ├── docs ├── ChangeLog.md ├── Concepts.md ├── Contribute.md ├── Demo.md ├── GettingStarted.md ├── Idea.md ├── Motivation.md ├── Patrons.md ├── README.md ├── features │ ├── Empty.md │ ├── Filter.md │ ├── Infinite.md │ ├── MagicColumn.md │ ├── Pagination.md │ ├── Plain.md │ ├── README.md │ ├── Select.md │ └── Sort.md └── recipes │ ├── Composition.md │ └── Consumer.md ├── examples ├── README.md ├── RealWorld │ ├── .gitignore │ ├── .nvmrc │ ├── Procfile │ ├── README.md │ ├── app.js │ ├── app.json │ ├── dist │ │ └── index.html │ ├── package.json │ ├── src │ │ ├── data.js │ │ ├── example.js │ │ ├── filter.js │ │ ├── index.js │ │ └── store.js │ ├── webpack.config.js │ └── webpack.prod.config.js └── Showcases │ ├── .gitignore │ ├── .nvmrc │ ├── Procfile │ ├── README.md │ ├── app.js │ ├── app.json │ ├── dist │ └── index.html │ ├── package.json │ ├── src │ ├── data.js │ ├── examples │ │ ├── custom-style.js │ │ ├── filter.js │ │ ├── infinite.js │ │ ├── magic-column.js │ │ ├── multiple-filter-or.js │ │ ├── multiple-filter.js │ │ ├── pagination.js │ │ ├── plain.js │ │ ├── responsive.js │ │ ├── select-plain.js │ │ ├── select-preselectables.js │ │ ├── select-selected.js │ │ ├── select-sort.js │ │ ├── select-unselectables.js │ │ └── sort.js │ ├── index.js │ └── store.js │ ├── webpack.config.js │ └── webpack.prod.config.js ├── index.js ├── mocha.config.js ├── package.json ├── src ├── components │ ├── Cell │ │ ├── index.js │ │ └── presenter.js │ ├── CellMagic │ │ ├── container.js │ │ ├── index.js │ │ └── presenter.js │ ├── CellMagicHeader │ │ ├── container.js │ │ ├── index.js │ │ ├── presenter.js │ │ └── style.less │ ├── CellSelected │ │ ├── container.js │ │ ├── index.js │ │ └── presenter.js │ ├── Enhanced │ │ ├── index.js │ │ └── presenter.js │ ├── HeaderCell │ │ ├── index.js │ │ └── presenter.js │ ├── Pagination │ │ ├── container.js │ │ ├── index.js │ │ ├── presenter.js │ │ └── style.less │ ├── Row │ │ ├── container.js │ │ ├── index.js │ │ ├── presenter.js │ │ └── style.less │ ├── Sort │ │ ├── container.js │ │ ├── index.js │ │ ├── presenter.js │ │ └── style.less │ ├── SortSelected │ │ ├── container.js │ │ └── index.js │ ├── index.js │ └── style.less ├── ducks │ ├── filter │ │ ├── index.js │ │ └── spec.js │ ├── index.js │ ├── magic │ │ ├── index.js │ │ └── spec.js │ ├── paginate │ │ ├── index.js │ │ └── spec.js │ ├── reset │ │ ├── index.js │ │ └── spec.js │ ├── select │ │ ├── index.js │ │ └── spec.js │ └── sort │ │ ├── index.js │ │ └── spec.js ├── enhancements │ ├── index.js │ ├── withEmpty │ │ └── index.js │ ├── withFilter │ │ └── index.js │ ├── withFilterOr │ │ └── index.js │ ├── withPaginate │ │ └── index.js │ ├── withPreselectables │ │ └── index.js │ ├── withSelectables │ │ └── index.js │ ├── withSort │ │ └── index.js │ └── withUnselectables │ │ └── index.js ├── helper │ ├── components │ │ └── SortCaret │ │ │ └── index.js │ ├── services │ │ ├── index.js │ │ ├── select │ │ │ └── index.js │ │ └── sort │ │ │ └── index.js │ └── util │ │ ├── empty.js │ │ ├── empty.spec.js │ │ ├── every.js │ │ ├── every.spec.js │ │ ├── filter.js │ │ ├── filter.spec.js │ │ ├── find.js │ │ ├── find.spec.js │ │ ├── getContext.js │ │ ├── noop.js │ │ ├── noop.spec.js │ │ ├── omit.js │ │ ├── omit.spec.js │ │ ├── partition.js │ │ ├── partition.spec.js │ │ ├── some.js │ │ ├── some.spec.js │ │ ├── uniq.js │ │ └── uniq.spec.js └── index.js ├── webpack.config.dev.js └── webpack.config.prod.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "react", "stage-2"], 3 | } -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true 4 | }, 5 | "parser": "babel-eslint", 6 | "plugins": [ 7 | "react" 8 | ], 9 | "extends": [ 10 | "airbnb-base", 11 | "eslint:recommended", 12 | "plugin:react/recommended" 13 | ], 14 | "rules": { 15 | "import/prefer-default-export": 0, 16 | "react/display-name": 0, 17 | "react/prop-types": 0 18 | }, 19 | "globals": { 20 | "describe": true, 21 | "fdescribe": true, 22 | "before": true, 23 | "beforeEach": true, 24 | "it": true, 25 | "fit": true, 26 | "xit": true, 27 | "expect": true, 28 | "after": true, 29 | "afterEach": true 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /dist 2 | node_modules 3 | _book 4 | bundle.js 5 | npm-debug.log 6 | package-lock.json 7 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | node_modules 3 | _book 4 | bundle.js 5 | npm-debug.log 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 0.8.0 2 | 3 | ### Changes 4 | 5 | ⚠️ Some of these changes are potentially breaking changes if you are using Select/Sort and CellMagicHeader! 6 | 7 | - **select:** revamp magic column selector (putting both sorting and column choosing into the dropdown) 8 | - **styles:** use font-weight 600 for "active" styles (instead of 700) 9 | - **styles:** smaller margins for CellMagicHeader 10 | 11 | 12 | ### Bugfixes 13 | 14 | - **a11y:** remove unneeded `tabindex="0"` on sort button 15 | 16 | # 0.7.0 17 | 18 | - initial version (since the beginning of this changelog) 19 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at petercrona89@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Robin Wieruch 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-redux-composable-list 2 | 3 | The react-redux-composable-list offers you a solution to display a list of complex items. That sounds simple. Why would you need a library to deal with it? 4 | 5 | The library comes with various opt-in features to manipulate the list of items or to change the representation of the list. These opt-in features are called **enhancements** or to stay in the React world: higher order components. Multiple enhancements can be composed to opt-in multiple features like **sorting, filtering or pagination**. After all, it gives you only an entry point to these enhancements. You can come up with enhancements on your own and just compose them into the set of enhancements that come with the library. 6 | 7 | In addition, in order to manipulate the state of those enhancements, you can use (built-in) **enhancer** components. They can be used everywhere in your application and allow you to manipulate the sorting, filtering etc. state. There again the library stays extendable. You can write your own enhancer components. 8 | 9 | ![Demo](https://media.giphy.com/media/l1J3SfGrltemdEX5e/giphy.gif) 10 | 11 | With the mental model behind this [idea](/docs/Idea.md) and [concepts](/docs/Concepts.md), you can come up with great opt-in features on your own. All features, coming from the library or from yourself, can be used to be composed into each other. The library comes with several features that you can already use, but it is not bound to a rigid endgame solution. 12 | 13 | ## Demo 14 | 15 | You can checkout the live demonstrations ([Showcases](https://react-redux-composable-list-showcases.wieruch.com/), [Real World](https://react-redux-composable-list-realworld.wieruch.com/)) that show several features of the library. 16 | 17 | In addition, you can checkout the [examples/](https://github.com/SmallImprovements/react-redux-composable-list/tree/master/examples) folder in the GitHub repository. In each sub folder you will find instructions to set up the demonstrated project. 18 | 19 | ## Why should you use it? 20 | 21 | The react-redux-composable-list solved a real problem for us at [Small Improvements](https://www.small-improvements.com/). By using this approach, we were able to display lists of items in a powerful yet flexible way. It always depends on your use case if you want to add a powerful set of features to your displayed list (pagination, filter, sorting) or if you want to keep it simple by only adding the selection feature. 22 | 23 | In the end, we thought we are not the only ones who need this common set of features in such a composable and extendable way. 24 | 25 | ### Composable 26 | 27 | The library builds up on the React concept of composability. You can weave multiple higher order components into each other to manipulate your list, for instance with sorting, or to alter the final representation of the list, for instance with pagination. 28 | 29 | ### Extendable 30 | 31 | Since the library API, that is basically an API for your Redux store, is well documented, you can come up with your own enhancements and enhancer components. Only a few enhancements and enhancers come with the library. Yet, with the mental model behind it, you can extend the feature set of the library on your own. 32 | 33 | ### Built-in Features 34 | 35 | The library comes already with several features like sorting, filtering, pagination, extendable columns and selecting of items. These are common features when displaying a list of items. But you can come up with your own opt-in features as well. 36 | 37 | ## Getting Started 38 | 39 | If you want to jump right away into using the library, you should checkout the [Getting Started](/docs/GettingStarted.md) section in the documentation. 40 | 41 | If you want to dive deeper into the library, you can checkout the whole [documentation](/docs/) to get to know what the library is about and how to use it. 42 | 43 | ## Contribute 44 | 45 | Please let us know if you have any feedback. The repository is open for contribution, please read the [contribution guidelines](/docs/Contribute.md). 46 | -------------------------------------------------------------------------------- /SUMMARY.md: -------------------------------------------------------------------------------- 1 | docs/README.md -------------------------------------------------------------------------------- /blog/Release.md: -------------------------------------------------------------------------------- 1 | # Displaying a List of Items in React, but Composed 2 | 3 | Displaying a list of items is a challenge you will encounter in most web applications. When using a view layer library such as React, you only have to iterate over the list of items and return elements. However, often you want a couple of more features such as filtering, sorting or pagination. Not every list component needs it though, but it would be great to have these functionalities as opt-in features whenever displaying a list of items. 4 | 5 | We are excited to open source our in-house solution at Small Improvements which handles this use case: [react-redux-composable-list](https://github.com/SmallImprovements/react-redux-composable-list). In our web application, customers often need to work with lists of data while managing their feedback, objectives or other content in our system. At Small Improvements our customers range from an active user count of 20 to 2000 users. Thus it can happen that we need to display a lot of data yet have to keep it accessible for people managing it. 6 | 7 | ![Demo](https://media.giphy.com/media/l1J3SfGrltemdEX5e/giphy.gif) 8 | 9 | The requirements for each list of data are different. One list is just fine with a filter functionality. Another list mixes together selectable and filterable items. Each displayed list has different requirements. The library we are open sourcing solves all the requirements we had in-house at Small Improvements. The library is highly extendable and builds up on composition. You can come up with your own opt-in features. 10 | 11 | ## Demo and Features 12 | 13 | The react-redux-composable-list comes with the following features: 14 | 15 | * Filtering (AND filter, OR filter, multiple filters) 16 | * Selecting 17 | * Sorting 18 | * Magic Column (collapsing multiple columns in one column) 19 | * Pagination 20 | 21 | There are two demo applications up and running to show the features of react-redux-composable-list. 22 | 23 | * [Real World](https://react-redux-composable-list-realworld.wieruch.com/) 24 | * [Showcases](https://react-redux-composable-list-showcases.wieruch.com/) 25 | 26 | While the former demonstrates all features in one real world example, the latter separates the examples by feature. 27 | 28 | The Real World example shows that all features can be used altogether by composing them. To specify the opt-in features of your list components you use React's [higher order components](https://www.robinwieruch.de/gentle-introduction-higher-order-components/). 29 | 30 | ```javascript 31 | const List = ({ list, stateKey }) => { 32 | ... 33 | } 34 | 35 | ... 36 | 37 | const EmptyBecauseFilter = () => 38 |
39 |

No Filter Result

40 |

Sorry, there was no item matching your filter.

41 |
42 | 43 | export default compose( 44 | withEmpty({ component: EmptyBecauseNoList }), 45 | withSelectables({ ids: [0] }), 46 | withPreselectables({ ids: [2, 3] }), 47 | withUnselectables({ ids: [4, 6] }), 48 | withFilter(), 49 | withEmpty({ component: EmptyBecauseFilter }), 50 | withSort(), 51 | withPaginate({ size: 10 }), 52 | )(List); 53 | ``` 54 | 55 | You can find the implementation of both demo applications in the official [GitHub repository](https://github.com/SmallImprovements/react-redux-composable-list/tree/master/examples). Further details about specific features can be found in the [official documentation](https://github.com/SmallImprovements/react-redux-composable-list/tree/master/docs/features). 56 | 57 | ## Getting Started 58 | 59 | If you want to jump right into using the library, you should checkout the [Getting Started](https://github.com/SmallImprovements/react-redux-composable-list/blob/master/docs/GettingStarted.md) section in the official documentation. 60 | 61 | For instance having a list of items with the option to select items can be accomplished with the following component: 62 | 63 | ```javascript 64 | import { components, enhancements } from 'react-redux-composable-list'; 65 | const { Enhanced, Row, Cell } = components; 66 | const { withSelectables } = enhancements; 67 | 68 | const ListComponent = ({ list, stateKey }) => 69 | 70 | {list.map(item => 71 | 72 | {item.title} 73 | {item.comment} 74 | 75 | )} 76 | 77 | 78 | export default withSelectables()(ListComponent); 79 | ``` 80 | 81 | Afterwards it can be simply used by passing a list of items and a state key to identify the table state. 82 | 83 | ```javascript 84 | import SelectableListComponent from 'path/to/ListComponent'; 85 | 86 | const list = [ 87 | { id: '1', title: 'foo', comment: 'foo foo' }, 88 | { id: '2', title: 'bar', comment: 'bar bar' }, 89 | ]; 90 | 91 | const App = () => 92 | 96 | ``` 97 | 98 | If you want to dive deeper into the library, you can checkout the whole [documentation](https://github.com/SmallImprovements/react-redux-composable-list/tree/master/docs) to learn more about the library and how to use it. 99 | 100 | ## Extend it & Contribute 101 | 102 | You can write your own enhancements and enhancers, because you have provide you with access to the library's API. To be more specific, the library API is nothing more than action creators and selectors for the Redux store. All the state that is managed for the tables is organized in a Redux store. You will find everything you need to know about the API in each [documented feature](https://github.com/SmallImprovements/react-redux-composable-list/tree/master/docs/features). In general, the documentation is a good place to get started and to read up everything about all the features. 103 | 104 | We would love, if you would give it a shot and give us feedback about it. In addition, [we welcome you to make contributions to the library](https://github.com/SmallImprovements/react-redux-composable-list/blob/master/docs/Contribute.md). 105 | 106 | ## Resources 107 | 108 | * [GitHub Repository](https://github.com/SmallImprovements/react-redux-composable-list) 109 | * Demo Applications: 110 | * [Real World](https://react-redux-composable-list-realworld.wieruch.com/) ([Source Code](https://github.com/SmallImprovements/react-redux-composable-list/tree/master/examples/RealWorld)) 111 | * [Showcases](https://react-redux-composable-list-showcases.wieruch.com/) ([Source Code](https://github.com/SmallImprovements/react-redux-composable-list/tree/master/examples/Showcases)) 112 | * [Official Documentation](https://github.com/SmallImprovements/react-redux-composable-list/tree/master/docs) 113 | * [Getting Started](https://github.com/SmallImprovements/react-redux-composable-list/blob/master/docs/GettingStarted.md) 114 | -------------------------------------------------------------------------------- /book.json: -------------------------------------------------------------------------------- 1 | { 2 | "gitbook": "3.2.2", 3 | "title": "react-redux-compose-table", 4 | "plugins": ["edit-link", "prism", "-highlight", "github", "anchorjs"], 5 | "pluginsConfig": { 6 | "edit-link": { 7 | "base": "https://github.com/rwieruch/react-redux-compose-table/tree/master", 8 | "label": "Edit This Page" 9 | }, 10 | "github": { 11 | "url": "https://github.com/rwieruch/react-redux-compose-table/" 12 | }, 13 | "theme-default": { 14 | "styles": { 15 | "website": "build/gitbook.css" 16 | } 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /docs/ChangeLog.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | No Changes yet :'( 4 | -------------------------------------------------------------------------------- /docs/Concepts.md: -------------------------------------------------------------------------------- 1 | # Concepts 2 | 3 | It makes sense to know the concepts behind the library in order to use it. First, the general terms and the three ingredients Enhancements, Enhanced Component and Enhancer Components will be explained. Second, you will get to know the flow, the connection in context, between all ingredients. 4 | 5 | ## Terms 6 | 7 | * **basic component:** 8 | * a plain functional stateless or ES6 class component in React 9 | * **enhancement:** 10 | * a higher order order component that takes a basic component (or already enhanced component) as input and returns an enhanced component 11 | * **enhanced component:** 12 | * a component that is the output of one enhancement or multiple enhancements 13 | * **enhancer component:** 14 | * a component that alters an enhancement that can be anywhere in the application yet it can be in an enhanced component itself 15 | * **state key:** 16 | * an [identifier](https://www.robinwieruch.de/redux-state-keys/) for the enhanced component to track all its enhancements 17 | 18 | ### Enhancements 19 | 20 | The library comes with higher order components described as **enhancements**. These enhancements get applied on a **basic component** that becomes an **enhanced component**. Multiple enhancements can be used to create an enhanced component. Already enhanced components can get further enhancements too. 21 | 22 | The enhancements either **manipulate** your list of items, add **conditional rendering** or **extend** the component that gets enhanced. In the following the 3 kinds of enhancements will be explained. 23 | 24 | A **manipulation** could be sorting or filtering. Your enhanced component wouldn't get the plain list of items anymore, but an altered version of it. The list would be sorted and filtered before it reaches your enhanced component. The enhanced component would have the manipulated list of data at its disposal. 25 | 26 | A **conditional rendering** could be to show a placeholder when the list is empty or the list is not empty but the filter enhancement returns an filtered list that is empty. Giving the user feedback about why no data shows up would improve the user experience. 27 | 28 | An **extension** of the basic component could be pagination. Whenever you see a paginated list of data, you get controls to navigate through the pages. It can be useful for huge lists of data. The extension would wrap around the basic component to give you these controls. 29 | 30 | Composability makes it possible to enhance a component. You can use the third-party library [recompose](https://github.com/acdlite/recompose) to compose multiple enhancements into an enhanced component. 31 | 32 | ### Enhanced Component 33 | 34 | Your enhanced component can show the list of data in any way. After all, it only gets a (manipulated) list of data. However, a list of items is shown most of the time in a kind of table component. That's why the library comes with useful components to compose a table of data. A mandatory wrapper component, the Enhanced Component, is used to identify the applied enhancements with a state key. In addition, the library provides you with Row, Cell and HeaderCell components to layout your enhanced component. But as mentioned, you don't need to use them apart from the Enhanced Component. 35 | 36 | ### Enhancer Components 37 | 38 | In addition, there are enhancer components. They can be used inside or outside of your enhanced component. In fact, they can be used anywhere in your application. They will be responsible to alter enhancements. These enhancements will be stored and flow back to the enhanced component via its composed enhancements. 39 | 40 | There are several active enhancer that already come with the library. However, since the library builds up on extendability, you can use your own enhancer components too. There is a library API to manipulate the enhancements in the Redux store by using Redux actions. So you can write your own enhancer components that lead to changes in (built-in) enhancements. 41 | 42 | ## The Flow 43 | 44 | After all, how is the general integration across enhancements, enhanced component and enhancer components? You can create an enhanced component by using enhancements. Multiple enhancements can be composed into each other to create a more powerful enhanced component. The enhanced component decides how to show the data. It can use components from the library yet you can decide on your own how to show it. To close the loop, enhancer components can be used inside or outside of the enhanced component to manipulate the enhancements. All enhancements will apply the manipulations to the enhanced component. The manipulated data flows through all enhancements to the enhanced component. In the end, the enhanced component shows the (manipulated) list of data again. 45 | -------------------------------------------------------------------------------- /docs/Contribute.md: -------------------------------------------------------------------------------- 1 | # Contribute 2 | 3 | Please let us know if you have any feedback. You can create Issues to provide feedback, to point out bugs or to provide information about improvements that you want to contribute. 4 | 5 | ## Primary Goals: 6 | 7 | Basically it would be great to improve the core of the library to make it more robust. 8 | 9 | * exceptional test coverage for the core functionalities 10 | * functionality to use custom styles (e.g. row selected style, general row style, pagination style) 11 | * performance (e.g. usage of shouldComponentUpdate), because there were no performance optimizations made yet 12 | * introduce own sort by functionality and get rid of lodash.sortby 13 | * introduce a default filter for the filter enhancements that can be passed in as configuration (similar to the sort enhancement that is already having this functionality) 14 | 15 | For now, we don't want to encourage you to add new functionalities. The library comes with a fixed set of enhancements (filter, sort, ...) that you can use to enhance your composable list. However, since you can opt-in every time an own enhancement, because the library is extendable, we are curious about the enhancements you come up with. Perhaps they can be added in the future to this library. 16 | 17 | ## Installation 18 | 19 | * `git clone git@github.com:SmallImprovements/react-redux-composable-list.git` 20 | * `cd react-redux-composable-list` 21 | * `npm install` 22 | * `npm run test` 23 | * `npm start` 24 | * visit `http://localhost:8080/` 25 | -------------------------------------------------------------------------------- /docs/Demo.md: -------------------------------------------------------------------------------- 1 | # Demo 2 | 3 | You can checkout the live demonstrations ([Showcases](https://react-redux-composable-list-showcases.wieruch.com/), [Real World](https://react-redux-composable-list-realworld.wieruch.com/)) that show several features of the library. 4 | 5 | In addition, you can checkout the [examples/](https://github.com/SmallImprovements/react-redux-composable-list/tree/master/examples) folder in the GitHub repository. In each sub folder you will find instructions to set up the demonstrated project. 6 | -------------------------------------------------------------------------------- /docs/GettingStarted.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | You can use npm to install the library: `npm install react-redux-composable-list` 4 | 5 | There are **two requirements** in order to use the library: 6 | 7 | First, since it depends to save the state in the Redux store, you have to connect the reducers provided by the library to your Redux store. 8 | 9 | ```javascript 10 | import { createStore, combineReducers } from 'redux'; 11 | 12 | import reducers from 'react-redux-composable-list'; 13 | 14 | const rootReducer = combineReducers({ 15 | ...reducers, 16 | // add your own reducers 17 | }); 18 | 19 | const configureStore = (initialState) => createStore(rootReducer, initialState); 20 | 21 | export default configureStore; 22 | ``` 23 | 24 | Second, you need to use the [react-redux](https://github.com/reactjs/react-redux) Provider component, that takes your Redux store as input, in a top level component. Maybe you already do so, because you have a React + Redux application. Afterward, you can use the functionalities of the library in your component tree. 25 | 26 | ```javascript 27 | import configureStore from 'path/to/store'; 28 | 29 | const initialState = {}; 30 | const store = configureStore(initialState); 31 | 32 | 33 | 34 | 35 | ``` 36 | 37 | Now you can start to write your first [plain](/docs/features/Plain.md) component: 38 | 39 | ```javascript 40 | import { components } from 'react-redux-composable-list'; 41 | const { Enhanced, Row, Cell } = components; 42 | 43 | const Plain = ({ list, stateKey }) => 44 | 45 | {list.map(item => 46 | 47 | {item.title} 48 | {item.comment} 49 | 50 | )} 51 | 52 | 53 | export default Plain; 54 | ``` 55 | 56 | And use it in your application: 57 | 58 | ```javascript 59 | import Plain from 'path/to/component'; 60 | 61 | const list = [ 62 | { id: '1', title: 'foo', comment: 'foo foo' }, 63 | { id: '2', title: 'bar', comment: 'bar bar' }, 64 | ]; 65 | 66 | const App = () => 67 | 71 | ``` 72 | 73 | That's it. You show the list of data. But that's dull, isn't it? You might want to use the functionalities of the library. There should be an enhancement ([higher order component](https://www.robinwieruch.de/gentle-introduction-higher-order-components/)) in between, otherwise the library doesn't bring you any benefits. 74 | 75 | Let's define an Enhanced Component that enables you to select items from the list: 76 | 77 | ```javascript 78 | import { components, enhancements } from 'react-redux-composable-list'; 79 | const { Enhanced, Row, Cell } = components; 80 | const { withSelectables } = enhancements; 81 | 82 | const Selectable = ({ list, stateKey }) => 83 | 84 | {list.map(item => 85 | 86 | {item.title} 87 | {item.comment} 88 | 89 | )} 90 | 91 | 92 | export default withSelectables()(Selectable); 93 | ``` 94 | 95 | That should already do the magic to make your items in the list selectable. You can use it again in your application: 96 | 97 | ```javascript 98 | import Selectable from 'path/to/component'; 99 | 100 | const list = [ 101 | { id: '1', title: 'foo', comment: 'foo foo' }, 102 | { id: '2', title: 'bar', comment: 'bar bar' }, 103 | ]; 104 | 105 | const App = () => 106 | 110 | ``` 111 | 112 | The enhancement can be configured by passing a configuration object. You could pass `['1']` to initially select the item with the `id: '1'`: 113 | 114 | ```javascript 115 | ... 116 | 117 | export default withSelectables({ ids: ['1'] })(Selectable); 118 | ``` 119 | 120 | That's it. Your items in the list should be selectable now. Optionally you can have already selected items. Refer to the [Select enhancement](/docs/features/Select.md) to get to know more about it. After all, you would need to retrieve the selected items at some point to do further things. 121 | 122 | Before you dive into the set of features of the library, you should read the [Concepts](/docs/Concepts.md) behind it. 123 | -------------------------------------------------------------------------------- /docs/Idea.md: -------------------------------------------------------------------------------- 1 | # Idea 2 | 3 | React has several great concepts and patterns that can be used to achieve a flexible yet sophisticated approach to show and manipulate data. The main idea behind the library is composability. 4 | 5 | On the one hand, [higher order components](https://www.robinwieruch.de/gentle-introduction-higher-order-components/) (HOCs) can be used and re-used to **opt-in various enhancements**. A basic component, that simply shows a list of items, can become an enhanced version of the component. The enhancement could be to make the list filterable or sortable. Or it could be to add the ability to select items from the list. These opt-in enhancements can get stacked by using composition. Multiple enhancements have a compound effect of features on your basic component. With composition the library enables you to sort, filter and paginate your list. 6 | The library comes with various in-house enhancements, but you can opt-in your own higher order components. 7 | 8 | On the other hand, the children property keeps the components that show the data composable, reusable and extendable. It's not one component that takes a big configuration object. You can **decide on your own what you want to do with the data** that comes from all your opt-in enhancements. You can show it in a table layout, but you are not forced to. Yet it is a common way to show a lot of data in a table to keep it accessible. That's why the library comes with a bunch of in-house components to layout a table. These components are used to show the data. But you can of course use your own components too. 9 | -------------------------------------------------------------------------------- /docs/Motivation.md: -------------------------------------------------------------------------------- 1 | # Motivation 2 | 3 | Small Improvements builds a product that makes feedback happen at companies. The software enables a company and its people to grow with feedback and objectives. It is a continuous loop between giving feedback and striving towards a goal. It allows you to grow as an individual in a company yet helps the company to focus on long term objectives. 4 | 5 | Companies that are using Small Improvements range from 20 to 2000 users. When so many people set up short and long term objectives or write feedback about each other, a lot of data is available and needs to be displayed. In Small Improvements we are using lists and tables to make the data accessible. Our customers are keen to explore this data, that's why there needs to be an approach to make this data accessible by adding a rich set of features on top of just displaying data. 6 | 7 | Back in the days, Small Improvements migrated from Angular to React. The migration came with the drawback that many of the components had to be re-written. Thus the way to show a lot of data in a table or list would be a requirement in React too. 8 | 9 | In Angular, Small Improvements had a table component to show the data and to access it with filtering, sorting etc. However, it was a rigid implementation that nobody wanted to touch anymore. Basically, like you would have been used in Angular, it came with one monstrousness configuration object to show a table component. 10 | 11 | In React, we wanted to make it better. According to the React way of doing things, we wanted to keep it composable, reusable and simple in its usage. We came up with a solution for ourselves to make the data available with all desired functionalities. Since we were convinced that the solution would be beneficial for everyone, we wanted to open source it. 12 | -------------------------------------------------------------------------------- /docs/Patrons.md: -------------------------------------------------------------------------------- 1 | # Patrons 2 | 3 | Patrons that contributed and supported the open source project. 4 | 5 | - [**Small Improvements**](https://www.small-improvements.com): The functionalities of the library were developed for a real use case at Small Improvements. Now we extracted the functionality to enable everyone to use it. Thanks at Small Improvements for being a place of innovation and contribution. 6 | 7 | - [**Robin Wieruch**](https://github.com/rwieruch): Introduced the functionality at Small Improvements when the company migrated from Angular to React. There was an Angular version before, yet with the feedback of his peers Robin was able to improve the functionality. -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Table of Contents 2 | 3 | * [Read Me](/README.md) 4 | * [Motivation](/docs/Motivation.md) 5 | * [Idea](/docs/Idea.md) 6 | * [Demo](/docs/Demo.md) 7 | * [Getting Started](/docs/GettingStarted.md) 8 | * [Concepts](/docs/Concepts.md) 9 | * [Enhancements](/docs/features/README.md) 10 | * [Plain](/docs/features/Plain.md) 11 | * [Filter](/docs/features/Filter.md) 12 | * [Select](/docs/features/Select.md) 13 | * [Sort](/docs/features/Sort.md) 14 | * [Magic Column](/docs/features/MagicColumn.md) 15 | * [Pagination](/docs/features/Pagination.md) 16 | * [Infinite](/docs/features/Infinite.md) 17 | * [Empty](/docs/features/Empty.md) 18 | * [Recipes](/docs/recipes/README.md) 19 | * [Composition](/docs/recipes/Composition.md) 20 | * [API Consumer](/docs/recipes/Consumer.md) 21 | * [Patrons](/docs/Patrons.md) 22 | * [Contribute](/docs/Contribute.md) 23 | -------------------------------------------------------------------------------- /docs/features/Empty.md: -------------------------------------------------------------------------------- 1 | # Empty Enhancement 2 | 3 | The Empty enhancement allows you to opt-in conditional rendered components when the list is empty. It can happen because the list is initially empty or after another enhancement (Filter Enhancement) affected the list. It improves the user experience. 4 | 5 | * **General Requirements:** 6 | * pass a stateKey to Enhanced component 7 | * items need a stable id as identifier 8 | * **Empty Requirements:** 9 | * use withEmpty enhancement with configuration object 10 | 11 | ## Demo 12 | 13 | * [Real World](https://react-redux-composable-list-realworld.wieruch.com/) 14 | 15 | ## Definition 16 | 17 | ```javascript 18 | import { components, enhancements } from 'react-redux-composable-list'; 19 | const { Enhanced, Row, Cell } = components; 20 | const { withEmpty } = enhancements; 21 | 22 | const CanBeEmpty = ({ list, stateKey }) => 23 | 24 | {list.map(item => 25 | 26 | {item.title} 27 | {item.comment} 28 | 29 | )} 30 | 31 | 32 | const EmptyBecauseNoList = () => 33 |
34 |

Nothing to see!

35 |

Sorry, there is no content.

36 |
37 | 38 | export default withEmpty({ component: EmptyBecauseNoList })(CanBeEmpty); 39 | ``` 40 | 41 | ## Usage 42 | 43 | ```javascript 44 | import CanBeEmpty from 'path/to/component'; 45 | 46 | const list = []; 47 | 48 | const App = () => 49 | 53 | ``` 54 | 55 | ## Configuration: 56 | 57 | The configuration takes a component that will be conditional rendered in case the list is empty. 58 | 59 | ## More Combinations 60 | 61 | You can use multiple `withEmpty` enhancements to achieve an improved user experience. For instance, you can use an initial `withEmpty` enhancement to show a message when there is no list at all, but show another `withEmpty` when the list is empty due to a set filter. 62 | 63 | 64 | ```javascript 65 | import { compose } from recompose; 66 | 67 | ... 68 | 69 | const EmptyBecauseFilter = () => 70 |
71 |

No Filter Result

72 |

Sorry, there was no item matching your filter.

73 |
74 | 75 | const EmptyBecauseNoList = () => 76 |
77 |

Nothing to see!

78 |

Sorry, there is no content.

79 |
80 | 81 | export default compose( 82 | withEmpty({ component: EmptyBecauseNoList }), 83 | withFilter(), 84 | withEmpty({ component: EmptyBecauseFilter }), 85 | )(TodoList); 86 | ``` 87 | 88 | You can have a look into the [Filter enhancement](/docs/features/Filter.md) to get to know how to filter items between your `withEmpty` enhancements. 89 | -------------------------------------------------------------------------------- /docs/features/Filter.md: -------------------------------------------------------------------------------- 1 | # Filter Enhancement 2 | 3 | The Filter enhancement is an enabler to filter items in your list. 4 | 5 | * **General Requirements:** 6 | * pass a stateKey to Enhanced component 7 | * items need a stable id as identifier 8 | * **Filter Requirements:** 9 | * use withFilter enhancement 10 | 11 | ## Demo 12 | 13 | * [Showcases](https://react-redux-composable-list-showcases.wieruch.com/) 14 | * With Filter 15 | * With Multiple Filters AND 16 | * With Multiple Filters OR 17 | * [Real World](https://react-redux-composable-list-realworld.wieruch.com/) 18 | 19 | ## Definition 20 | 21 | ```javascript 22 | import { components, enhancements } from 'react-redux-composable-list'; 23 | const { Enhanced, Row, Cell } = components; 24 | const { withFilter } = enhancements; 25 | 26 | const Filterable = ({ list, stateKey }) => 27 | 28 | {list.map(item => 29 | 30 | {item.title} 31 | {item.comment} 32 | 33 | )} 34 | 35 | 36 | export default withFilter()(Filterable); 37 | ``` 38 | 39 | (Not Built-In) Enhancer Component: 40 | 41 | ```javascript 42 | import { connect } from 'react-redux'; 43 | import { actionCreators } from 'react-redux-composable-list'; 44 | 45 | const Filters = ({ onTitleFilterChange, onCommentFilterChange }) => 46 |
47 |

Filters

48 |
49 | Title: onTitleFilterChange(e.target.value)} 52 | /> 53 |
54 |
55 | Comment: onCommentFilterChange(e.target.value)} 58 | /> 59 |
60 |
61 | 62 | const titleFilterFn = query => item => 63 | item.title.toLowerCase().indexOf(query.toLowerCase()) !== -1; 64 | 65 | const commentFilterFn = query => item => 66 | item.comment.toLowerCase().indexOf(query.toLowerCase()) !== -1; 67 | 68 | const mapDispatchToProps = (dispatch, props) => ({ 69 | onTitleFilterChange: (query) => query !== '' 70 | ? dispatch(actionCreators.doSetFilter(props.stateKey, 'TITLE_FILTER', titleFilterFn(query))) 71 | : dispatch(actionCreators.doRemoveFilter(props.stateKey, 'TITLE_FILTER')), 72 | 73 | onCommentFilterChange: (query) => query !== '' 74 | ? dispatch(actionCreators.doSetFilter(props.stateKey, 'COMMENT_FILTER', commentFilterFn(query))) 75 | : dispatch(actionCreators.doRemoveFilter(props.stateKey, 'COMMENT_FILTER')) 76 | }); 77 | 78 | export default connect(null, mapDispatchToProps)(Filters); 79 | ``` 80 | 81 | ## Usage 82 | 83 | ```javascript 84 | import Filters from 'path/to/component'; 85 | import Filterable from 'path/to/component'; 86 | 87 | const list = [ 88 | { id: '1', title: 'foo', comment: 'foo foo' }, 89 | { id: '2', title: 'bar', comment: 'bar bar' }, 90 | ]; 91 | 92 | const App = () => 93 |
94 | 97 | 101 |
102 | ``` 103 | 104 | ## More Enhancements 105 | 106 | When using the `withFilter` enhancement, all set filters are AND concatenated. There are cases, where you want to concatenate them OR. In order to do so, you only have to exchange the enhancement yet you can keep the Filter Enhancer Components. 107 | 108 | ```javascript 109 | import { components, enhancements } from 'react-redux-composable-list'; 110 | const { Enhanced, Row, Cell } = components; 111 | const { withFilterOr } = enhancements; 112 | 113 | const Filterable = ({ list, stateKey }) => 114 | 115 | {list.map(item => 116 | 117 | {item.title} 118 | {item.comment} 119 | 120 | )} 121 | 122 | 123 | export default withFilterOr()(Filterable); 124 | ``` 125 | 126 | ## Redux API 127 | 128 | You can import action creators and selectors from the library: 129 | 130 | ```javascript 131 | import { actionCreators, selectors } from 'react-redux-composable-list'; 132 | ``` 133 | 134 | You can use Redux actions to update the Redux store. The library API offers the following action creators that can be dispatched: 135 | 136 | * **actionCreators.function doSetFilter(stateKey, key, fn):** 137 | * sets a filter function identified by a key 138 | * **actionCreators.function doRemoveFilter(stateKey, key):** 139 | * removes a filter identified by a key 140 | * **actionCreators.function doResetFilter(stateKey):** 141 | * removes all filters 142 | 143 | You can use Redux selectors to retrieve state from the Redux store. The library API offers the following selectors: 144 | 145 | * **getFilters(state, stateKey):** 146 | * retrieves all filter functions 147 | 148 | ## Enhancer Components 149 | 150 | There are no built-in enhancer components in the library. We believed that Filter components look and behave always different. You can use the library API, like in the example above, to build your own filter enhancer component. 151 | -------------------------------------------------------------------------------- /docs/features/Infinite.md: -------------------------------------------------------------------------------- 1 | # Infinite 2 | 3 | The Infinite feature is no real enhancement. Since the library works with composition, you can use third-party components too. The following scenario shows you how you can use `react-infinite` ([docs](https://github.com/seatgeek/react-infinite)) to achieve infinite scroll. It is used as alternative to the [Pagination enhancement](/docs/features/Pagination.md). 4 | 5 | * **General Requirements:** 6 | * pass a stateKey to Enhanced component 7 | * items need a stable id as identifier 8 | 9 | * [Showcases](https://react-redux-composable-list-showcases.wieruch.com/) 10 | * With Infinite Scroll 11 | 12 | ## Definition 13 | 14 | ```javascript 15 | import Infinite from 'react-infinite'; 16 | 17 | import { components } from 'react-redux-composable-list'; 18 | const { Enhanced, Row, Cell } = components; 19 | 20 | const Infinity = ({ list, stateKey }) => 21 | 22 | 23 | {list.map(item => 24 | 25 | {item.title} 26 | {item.comment} 27 | 28 | )} 29 | 30 | 31 | 32 | export default Infinity; 33 | ``` 34 | 35 | ## Usage 36 | 37 | ```javascript 38 | import Infinity from 'path/to/component'; 39 | 40 | const list = [ 41 | { id: '1', title: 'foo', comment: 'foo foo' }, 42 | { id: '2', title: 'bar', comment: 'bar bar' }, 43 | ]; 44 | 45 | const App = () => 46 | 50 | ``` 51 | -------------------------------------------------------------------------------- /docs/features/MagicColumn.md: -------------------------------------------------------------------------------- 1 | # Magic Column Enhancement 2 | 3 | The Magic Column enhancement gives you a flexbile column in your table. The column can show multiple values. It is especially useful when your item has a lot of properties that you want to show. Before you apply this enhancement, you should be familiar with the [Sort enhancement](/docs/features/Sort.md) 4 | 5 | * **General Requirements:** 6 | * pass a stateKey to Enhanced component 7 | * items need a stable id as identifier 8 | * **Magic Column Requirements:** 9 | * use withSort enhancement 10 | 11 | ## Demo 12 | 13 | * [Showcases](https://react-redux-composable-list-showcases.wieruch.com/) 14 | * With Magic Column 15 | * [Real World](https://react-redux-composable-list-realworld.wieruch.com/) 16 | 17 | ## Definition 18 | 19 | ```javascript 20 | import { components, enhancements } from 'react-redux-composable-list'; 21 | const { Enhanced, Row, Cell, CellMagicHeader, CellMagic } = components; 22 | const { withSelectables } = enhancements; 23 | 24 | const SORTS_ASC_DESC = { 25 | ASC: (asc), 26 | DESC: (desc), 27 | }; 28 | 29 | const titleSort = item => item.title; 30 | const commentSort = item => item.comment; 31 | const votesSort = item => item.votes; 32 | const likesSort = item => item.likes; 33 | 34 | const magicSorts = [ 35 | { 36 | label: 'Comment', 37 | sortKey: 'comment', 38 | sortFn: commentSort, 39 | resolve: (item) => item.comment, 40 | }, 41 | { 42 | label: 'Votes', 43 | sortKey: 'votes', 44 | sortFn: votesSort, 45 | resolve: (item) => item.votes, 46 | }, 47 | { 48 | label: 'Likes', 49 | sortKey: 'likes', 50 | sortFn: likesSort, 51 | resolve: (item) => item.likes, 52 | }, 53 | ]; 54 | 55 | const MagicColumnsList = ({ list, stateKey }) => 56 | 57 | 58 | 59 | 63 | Title 64 | 65 | 66 | 67 | 70 | (Magic!) 71 | 72 | 73 | 74 | {list.map(item => 75 | 76 | {item.title} 77 | 78 | 79 | 80 | 81 | )} 82 | 83 | 84 | export default withSort()(MagicColumnsList); 85 | ``` 86 | 87 | ## Usage 88 | 89 | ```javascript 90 | import MagicColumnsList from 'path/to/component'; 91 | 92 | const list = [ 93 | { id: '1', title: 'foo', comment: 'foo foo', likes: 1, votes: 2 }, 94 | { id: '2', title: 'bar', comment: 'bar bar', likes: 3, votes: 4 }, 95 | ]; 96 | 97 | const App = () => 98 | 102 | ``` 103 | 104 | ## Enhancer Components 105 | 106 | The CellMagicHeader component, when using the `withSort` enhancement, is an [Enhancer Component](/docs/recipes/Consumer.md) that wraps the library API and alters the Sort enhancement state. 107 | -------------------------------------------------------------------------------- /docs/features/Pagination.md: -------------------------------------------------------------------------------- 1 | # Pagination Enhancement 2 | 3 | The Pagination enhancement allows you to show a large list of data split up to pages. 4 | 5 | * **General Requirements:** 6 | * pass a stateKey to Enhanced component 7 | * items need a stable id as identifier 8 | * **Pagination Requirements:** 9 | * use withPaginate enhancement with configuration object 10 | 11 | ## Demo 12 | 13 | * [Showcases](https://react-redux-composable-list-showcases.wieruch.com/) 14 | * With Pagination 15 | * [Real World](https://react-redux-composable-list-realworld.wieruch.com/) 16 | 17 | ## Definition 18 | 19 | ```javascript 20 | import { components, enhancements } from 'react-redux-composable-list'; 21 | const { Enhanced, Row, Cell } = components; 22 | const { withPaginate } = enhancements; 23 | 24 | const Paginated = ({ list, stateKey }) => 25 | 26 | {list.map(item => 27 | 28 | {item.title} 29 | {item.comment} 30 | 31 | )} 32 | 33 | 34 | export default withPaginate({ size: 10 })(Paginated); 35 | ``` 36 | 37 | ## Usage 38 | 39 | ```javascript 40 | import Paginated from 'path/to/component'; 41 | 42 | const list = [ 43 | { id: '1', title: 'foo', comment: 'foo foo' }, 44 | { id: '2', title: 'bar', comment: 'bar bar' }, 45 | ]; 46 | 47 | const App = () => 48 | 52 | ``` 53 | 54 | ## Configuration 55 | 56 | The configuration allows you to define a size for your pages. You want to show 10 items per page? You can define it by using the configuration object `{ size: 10 }`. 57 | 58 | ## More Combinations 59 | 60 | You can combine the Pagination enhancement with other enhancements. For instance, it can be combined with the [Sort enhancement](/docs/features/Sort.md) and [Filter enhancement](/docs/features/Filter.md). But you should be aware, that when applying the enhancements the order matters. You should first apply all the list manipulation enhancements (Sort, Filter) and afterwards paginate the list. 61 | 62 | ## Redux API 63 | 64 | You can import action creators and selectors from the library: 65 | 66 | ```javascript 67 | import { actionCreators } from 'react-redux-composable-list'; 68 | ``` 69 | 70 | You can use Redux actions to update the Redux store. The library API offers the following action creators that can be dispatched: 71 | 72 | * **actionCreators.doSetPage(stateKey, page):** 73 | * your pagination should have multiple pages, the method allows you to set one of these pages 74 | 75 | ## Enhancer Components 76 | 77 | The enhanced component, when using the `withPaginate` enhancement, gets extended by pagination controls to alter the Pagination enhancements by using the library API. 78 | -------------------------------------------------------------------------------- /docs/features/Plain.md: -------------------------------------------------------------------------------- 1 | # Plain 2 | 3 | Without any enhancement, you can still use layout components in your pseudo enhanced component from the library to render a list of data. The `Enhanced` component always takes a `stateKey` as identifier. The Row, Cell and HeaderCell components layout the items of the list in a Table. 4 | 5 | ## Demo 6 | 7 | * [Showcases](https://react-redux-composable-list-showcases.wieruch.com/) 8 | * Plain 9 | 10 | ## Definition 11 | 12 | ```javascript 13 | import { components } from 'react-redux-composable-list'; 14 | const { Enhanced, Row, Cell, HeaderCell } = components; 15 | 16 | const Plain = ({ list, stateKey }) => 17 | 18 | 19 | Title 20 | Comment 21 | 22 | {list.map(item => 23 | 24 | {item.title} 25 | {item.comment} 26 | 27 | )} 28 | 29 | 30 | export default Plain; 31 | ``` 32 | 33 | ## Usage 34 | 35 | ```javascript 36 | import Plain from 'path/to/component'; 37 | 38 | const list = [ 39 | { id: '1', title: 'foo', comment: 'foo foo' }, 40 | { id: '2', title: 'bar', comment: 'bar bar' }, 41 | ]; 42 | 43 | const App = () => 44 | 48 | ``` 49 | -------------------------------------------------------------------------------- /docs/features/README.md: -------------------------------------------------------------------------------- 1 | # Enhancements 2 | 3 | You will find all enhancements that can be used with the library. 4 | 5 | * [Plain](/docs/features/Plain.md) 6 | * [Filter](/docs/features/Filter.md) 7 | * [Select](/docs/features/Select.md) 8 | * [Sort](/docs/features/Sort.md) 9 | * [Magic Column](/docs/features/MagicColumn.md) 10 | * [Pagination](/docs/features/Pagination.md) 11 | * [Infinite](/docs/features/Infinite.md) 12 | * [Empty](/docs/features/Empty.md) 13 | -------------------------------------------------------------------------------- /docs/features/Select.md: -------------------------------------------------------------------------------- 1 | # Select Enhancement 2 | 3 | The Select enhancement is an enabler to select items in your list. 4 | 5 | * **General Requirements:** 6 | * pass a stateKey to Enhanced component 7 | * items need a stable id as identifier 8 | * **Select Requirements:** 9 | * use withSelectables enhancement with configuration object 10 | 11 | ## Demo 12 | 13 | * [Showcases](https://react-redux-composable-list-showcases.wieruch.com/) 14 | * With Select 15 | * With Select with Selected 16 | * With Select with Unselectables 17 | * With Select with Preselectables 18 | * With Select with Sort 19 | * [Real World](https://react-redux-composable-list-realworld.wieruch.com/) 20 | 21 | ## Definition 22 | 23 | ```javascript 24 | import { components, enhancements } from 'react-redux-composable-list'; 25 | const { Enhanced, Row, Cell } = components; 26 | const { withSelectables } = enhancements; 27 | 28 | const Selectable = ({ list, stateKey }) => 29 | 30 | {list.map(item => 31 | 32 | {item.title} 33 | {item.comment} 34 | 35 | )} 36 | 37 | 38 | export default withSelectables()(Selectable); 39 | ``` 40 | 41 | ## Usage 42 | 43 | ```javascript 44 | import Selectable from 'path/to/component'; 45 | 46 | const list = [ 47 | { id: '1', title: 'foo', comment: 'foo foo' }, 48 | { id: '2', title: 'bar', comment: 'bar bar' }, 49 | ]; 50 | 51 | const App = () => 52 | 56 | ``` 57 | 58 | ## Configuration: 59 | 60 | The configuration allows you to define selected items on initialization. In order to select the items with the `id: '1'` and `id: '2'`, you would use the configuration `withSelectables({ ids: ['1', '2'] })`. 61 | 62 | ## More Enhancements and Combinations 63 | 64 | You can use two more enhancements to spice up your selectable list. 65 | 66 | First, `withUnselectables` defines items in your list that are not selectable. 67 | 68 | ```javascript 69 | import { compose } from recompose; 70 | 71 | import { components, enhancements } from 'react-redux-composable-list'; 72 | const { Enhanced, Row, Cell } = components; 73 | const { withSelectables, withUnselectables } = enhancements; 74 | 75 | const Selectable = ({ list, stateKey }) => 76 | 77 | {list.map(item => 78 | 79 | {item.title} 80 | {item.comment} 81 | 82 | )} 83 | 84 | 85 | export default compose( 86 | withSelectables(), 87 | withUnselectables({ ids: ['1'] }) 88 | )(Selectable); 89 | ``` 90 | 91 | Second, `withPreselectables` defines items in your list that are selected yet cannot be unselected. 92 | 93 | ```javascript 94 | import { compose } from recompose; 95 | 96 | import { components, enhancements } from 'react-redux-composable-list'; 97 | const { Enhanced, Row, Cell } = components; 98 | const { withSelectables, withPreselectables } = enhancements; 99 | 100 | const Selectable = ({ list, stateKey }) => 101 | 102 | {list.map(item => 103 | 104 | {item.title} 105 | {item.comment} 106 | 107 | )} 108 | 109 | 110 | export default compose( 111 | withSelectables(), 112 | withPreselectables({ ids: ['1'] }) 113 | )(Selectable); 114 | ``` 115 | 116 | Both, `withUnselectables` and `withPreselectables`, can be used in a composition. 117 | 118 | ```javascript 119 | import { compose } from recompose; 120 | 121 | ... 122 | 123 | export default compose( 124 | withSelectables(), 125 | withPreselectables({ ids: ['1'] }), 126 | withUnselectables({ ids: ['2'] }) 127 | )(Selectable); 128 | ``` 129 | 130 | You can have a look into the [Sort enhancement](/docs/features/Sort.md) to get to know how to sort selected items. 131 | 132 | ## Redux API 133 | 134 | You can import action creators and selectors from the library: 135 | 136 | ```javascript 137 | import { actionCreators, selectors } from 'react-redux-composable-list'; 138 | ``` 139 | 140 | You can use Redux actions to update the Redux store. The library API offers the following action creators that can be dispatched: 141 | 142 | * **actionCreators.doSelectItem(stateKey, id):** 143 | * selects an item in the list 144 | * **actionCreators.doSelectItems(stateKey, ids, isSelect):** 145 | * selects or deselects multiple items in the list 146 | * **actionCreators.doSelectItemsExclusively(stateKey, ids, isSelect):** 147 | * selects or deselects multiple items exclusively in the list, meaning that only these items get selected and all previous selected items get unselected 148 | * **actionCreators.doSelectItemsReset(stateKey):** 149 | * resets all selected items 150 | 151 | You can use Redux selectors to retrieve state from the Redux store. The library API offers the following selectors: 152 | 153 | * **getSelection(state, stateKey):** 154 | * retrieves all selected items 155 | * **getIsSelected(state, stateKey, id):** 156 | * checks if an item is selected 157 | 158 | ## Enhancer Components 159 | 160 | The Row component, when using the `withSelectables` enhancement, becomes an [Enhancer Component](/docs/recipes/Consumer.md) that wraps the library API and alters the Select enhancement state. When using `withSelectables` the Row component automatically becomes selectable. 161 | -------------------------------------------------------------------------------- /docs/features/Sort.md: -------------------------------------------------------------------------------- 1 | # Sort Enhancement 2 | 3 | The Sort enhancement is an enabler to sort items in your list. 4 | 5 | * **General Requirements:** 6 | * pass a stateKey to Enhanced component 7 | * items need a stable id as identifier 8 | * **Sort Requirements:** 9 | * use withSort enhancement 10 | 11 | ## Demo 12 | 13 | * [Showcases](https://react-redux-composable-list-showcases.wieruch.com/) 14 | * With Sort 15 | * With Select with Sort 16 | * With Magic Column 17 | * [Real World](https://react-redux-composable-list-realworld.wieruch.com/) 18 | 19 | ## Definition 20 | 21 | ```javascript 22 | import { components, enhancements } from 'react-redux-composable-list'; 23 | const { Enhanced, Row, Cell, HeaderCell, Sort } = components; 24 | const { withSort } = enhancements; 25 | 26 | const titleSort = item => item.title; 27 | const commentSort = item => item.comment; 28 | 29 | const Sortable = ({ list, stateKey }) => 30 | 31 | 32 | 33 | 36 | Title 37 | 38 | 39 | 40 | 43 | Comment 44 | 45 | 46 | 47 | {list.map(item => 48 | 49 | {item.title} 50 | {item.comment} 51 | 52 | )} 53 | 54 | 55 | export default withSort()(Sortable); 56 | ``` 57 | 58 | ## Usage 59 | 60 | ```javascript 61 | import Sortable from 'path/to/component'; 62 | 63 | const list = [ 64 | { id: '1', title: 'foo', comment: 'foo foo' }, 65 | { id: '2', title: 'bar', comment: 'bar bar' }, 66 | ]; 67 | 68 | const App = () => 69 | 73 | ``` 74 | 75 | ## More Combinations 76 | 77 | You can use the `suffix` property to add components that reflect the ascending and descending sort. 78 | 79 | ```javascript 80 | import { components, enhancements } from 'react-redux-composable-list'; 81 | const { Enhanced, Row, Cell, HeaderCell, Sort } = components; 82 | const { withSort } = enhancements; 83 | 84 | const titleSort = item => item.title; 85 | const commentSort = item => item.comment; 86 | 87 | const SORTS_ASC_DESC = { 88 | ASC: (asc), 89 | DESC: (desc), 90 | }; 91 | 92 | // or 93 | // const SORTS_ASC_DESC = { 94 | // ASC: , 95 | // DESC: , 96 | // }; 97 | 98 | const Sortable = ({ list, stateKey }) => 99 | 100 | 101 | 102 | 106 | Title 107 | 108 | 109 | 110 | 114 | Comment 115 | 116 | 117 | 118 | {list.map(item => 119 | 120 | {item.title} 121 | {item.comment} 122 | 123 | )} 124 | 125 | 126 | export default withSort()(Sortable); 127 | ``` 128 | 129 | In case you use the [Select enhancement](/docs/features/Select.md), you can sort the select status of the items too. There exist two built-in components to accomplish it: `SortSelected` and `CellSelected`. 130 | 131 | ```javascript 132 | import { components, enhancements } from 'react-redux-composable-list'; 133 | const { Enhanced, Row, Cell, HeaderCell, Sort, SortSelected, CellSelected } = components; 134 | const { withSort } = enhancements; 135 | 136 | const titleSort = item => item.title; 137 | const commentSort = item => item.comment; 138 | 139 | const SORTS_ASC_DESC = { 140 | ASC: (asc), 141 | DESC: (desc), 142 | }; 143 | 144 | const Sortable = ({ list, stateKey }) => 145 | 146 | 147 | 148 | 152 | Title 153 | 154 | 155 | 156 | 160 | Comment 161 | 162 | 163 | 164 | 167 | Selected 168 | 169 | 170 | 171 | {list.map(item => 172 | 173 | {item.title} 174 | {item.comment} 175 | 176 | 177 | {{ 178 | SELECTED: SELECTED, 179 | NOT_SELECTED: NOT_SELECTED, 180 | PRE_SELECTED: PRE_SELECTED, 181 | UNSELECTABLE: UNSELECTABLE, 182 | }} 183 | 184 | 185 | 186 | )} 187 | 188 | 189 | export default withSort()(Sortable); 190 | ``` 191 | 192 | ## Redux API 193 | 194 | You can import action creators and selectors from the library: 195 | 196 | ```javascript 197 | import { actionCreators, selectors } from 'react-redux-composable-list'; 198 | ``` 199 | 200 | You can use Redux actions to update the Redux store. The library API offers the following action creators that can be dispatched: 201 | 202 | * **actionCreators.doTableSort(stateKey, sortKey, sortFn):** 203 | * sorts items in the list by key and sort function, e.g. `item => item.title` 204 | 205 | You can use Redux selectors to retrieve state from the Redux store. The library API offers the following selectors: 206 | 207 | * **getSort(state, stateKey):** 208 | * retrieves the activated sort object with key and sort function 209 | 210 | ## Enhancer Components 211 | 212 | The Sort component, when using the `withSort` enhancement, is an [Enhancer Component](/docs/recipes/Consumer.md) that wraps the library API and alters the Sort enhancement state. 213 | -------------------------------------------------------------------------------- /docs/recipes/Composition.md: -------------------------------------------------------------------------------- 1 | # Composition 2 | 3 | The library builds up on the [Idea](/docs/Idea.md) of composition. The idea applies for components in the enhanced component but also for the enhancements themselves. If you didn't read the [Concepts](/docs/Concepts.md) of the library, you should do so before you continue to read. 4 | 5 | ## Components 6 | 7 | The library gives you a handful of basic components for your list of items: 8 | 9 | ```javascript 10 | import { components } from 'react-redux-composable-list'; 11 | const { Enhanced, Row, Cell, HeaderCell } = components; 12 | ``` 13 | 14 | You can use them to layout your list: 15 | 16 | ```javascript 17 | import { components } from 'react-redux-composable-list'; 18 | const { Enhanced, Row, Cell } = components; 19 | 20 | const Plain = ({ list, stateKey }) => 21 | 22 | 23 | Title 24 | Comment 25 | 26 | {list.map(item => 27 | 28 | {item.title} 29 | {item.comment} 30 | 31 | )} 32 | 33 | 34 | export default Plain; 35 | ``` 36 | 37 | In addition, there are [Enhancer Components](/docs/recipes/Consumer.md) that consume the library API to alter the enhancements. These enhancers can be inside the enhanced component, like the `Sort` Enhancer Component in the [Sort Enhancement](/docs/features/Sort.md) example, or anywhere outside of your enhanced component, like the custom build `Filter` Enhancer Component in the [Filter Enhancement](/docs/features/Filter.md) example. 38 | 39 | After all, you can use all these built-in layout and enhancer components to compose your enhanced component. In addition, you can come up with custom layout and enhancer components. 40 | 41 | ## Higher Order Components 42 | 43 | The higher order components, called enhancements in the library, can be used to create enhanced components. Multiple enhancements can be composed to opt-in multiple features. You can have a look into each [Enhancement](/docs/features/README.md) to get to know the different enhancements. 44 | 45 | In order to compose multiple of these enhancements into one enhanced component, you can use a helper library like [recompose](https://github.com/acdlite/recompose) with its compose functionality. 46 | 47 | ```javascript 48 | import { compose } from recompose; 49 | 50 | import { enhancements } from 'react-redux-composable-list'; 51 | const { withSelectables, withUnselectables } = enhancements; 52 | 53 | ... 54 | 55 | export default compose( 56 | withSelectables(), 57 | withUnselectables({ ids: ['1'] }) 58 | )(MyListComponent); 59 | ``` 60 | 61 | ### Multiple Enhancements 62 | 63 | In case you are using multiple enhancements, you can compose them in a appropriate order to **improve the performance** and to **avoid bugs**. 64 | 65 | For instance, you can improve the performance when the enhancements `withFilter` and `withSort` are used. You should apply the `withFilter` before the `withSort` enhancement. It makes more sense to filter the list first in order to sort them with less items afterward. 66 | 67 | Regarding the bugs, a few enhancements need to have the correct order. For instance, the `withPaginate` enhancement needs to be after the `withFilter` and `withSort` enhancements, because the pagination needs to be applied on the shown list. 68 | 69 | ```javascript 70 | export default compose( 71 | withFilter(), 72 | withSort(), 73 | withPaginate({ size: 10 }), 74 | )(MyListComponent); 75 | ``` -------------------------------------------------------------------------------- /docs/recipes/Consumer.md: -------------------------------------------------------------------------------- 1 | # Consumer 2 | 3 | The library already brings implicit API consumer in terms of [Enhancer Components](/docs/Concepts.md). They already use the library API to alter enhancements. You can have a look into the [Enhancements](/docs/features/README.md) section to find them depending on the enhancement. For instance, the `Sort` component is such an enhancer component that comes with the library. 4 | 5 | In addition, you can manually consume the library API. Each enhancement offers an API to alter it in the Redux store. To get to know all of these APIs, you can check again the [Enhancements](/docs/features/README.md) section. Each enhancement has its own action and selector API to the Redux store. 6 | 7 | After all, you can extend the library with your own Enhancements and Enhancer Components. You only need to use the library API to access and update enhancements in the Redux store. You can find one of these custom Enhancer Components in the [Filter Enhancement](/docs/features/Filter.md). 8 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | Follow the instructions in each example folder. 4 | -------------------------------------------------------------------------------- /examples/RealWorld/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /examples/RealWorld/.nvmrc: -------------------------------------------------------------------------------- 1 | 8.1.2 -------------------------------------------------------------------------------- /examples/RealWorld/Procfile: -------------------------------------------------------------------------------- 1 | web: node app.js -------------------------------------------------------------------------------- /examples/RealWorld/README.md: -------------------------------------------------------------------------------- 1 | # Real World Example 2 | 3 | A real world example that uses most of the features from the react-redux-composable-list library. [DEMO](https://react-redux-composable-list-realworld.wieruch.com/) 4 | 5 | ## Installation 6 | 7 | * `git clone git@github.com:rwieruch/react-redux-composable-list.git` 8 | * cd react-redux-composable-list/examples/RealWorld 9 | * npm install 10 | * npm start 11 | * visit `http://localhost:8080/` 12 | -------------------------------------------------------------------------------- /examples/RealWorld/app.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var bodyParser = require('body-parser'); 3 | 4 | var path = require('path'); 5 | 6 | app = express(); 7 | 8 | app.use(bodyParser.json()); 9 | app.use(express.static(path.join(__dirname, 'dist'))); 10 | 11 | app.listen(process.env.PORT || 5000); 12 | -------------------------------------------------------------------------------- /examples/RealWorld/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "dokku": { 4 | "predeploy": "npm run build" 5 | } 6 | } 7 | } -------------------------------------------------------------------------------- /examples/RealWorld/dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The Minimal React Webpack Babel Setup 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /examples/RealWorld/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-redux-composable-list-real-world", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "NODE_ENV=production webpack -p --progress --colors --config ./webpack.prod.config.js", 8 | "start": "webpack-dev-server --progress --colors --hot --config ./webpack.config.js", 9 | "test": "echo \"Error: no test specified\" && exit 1" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "ISC", 14 | "babel": { 15 | "presets": [ 16 | "es2015", 17 | "react", 18 | "stage-2" 19 | ] 20 | }, 21 | "devDependencies": { 22 | "babel-core": "6.23.1", 23 | "babel-loader": "6.4.1", 24 | "babel-preset-es2015": "6.22.0", 25 | "babel-preset-react": "6.23.0", 26 | "babel-preset-stage-2": "6.22.0", 27 | "css-loader": "0.28.4", 28 | "exports-loader": "0.6.4", 29 | "extract-text-webpack-plugin": "2.1.2", 30 | "file-loader": "0.11.2", 31 | "font-awesome-webpack": "0.0.5-beta.2", 32 | "imports-loader": "0.7.1", 33 | "react-hot-loader": "1.3.1", 34 | "sass-loader": "6.0.6", 35 | "style-loader": "0.18.2", 36 | "url-loader": "0.5.9", 37 | "webpack": "2.2.1", 38 | "webpack-dev-server": "2.4.1" 39 | }, 40 | "dependencies": { 41 | "body-parser": "1.17.2", 42 | "classnames": "2.2.5", 43 | "express": "4.15.3", 44 | "lodash": "4.17.4", 45 | "lorem-ipsum": "1.0.3", 46 | "path": "0.12.7", 47 | "react": "15.4.2", 48 | "react-dom": "15.4.2", 49 | "react-fa": "4.2.0", 50 | "react-infinite": "0.10.0", 51 | "react-redux": "5.0.3", 52 | "react-redux-composable-list": "^0.1.1", 53 | "recompose": "0.22.0", 54 | "redux": "3.6.0", 55 | "redux-logger": "2.8.2" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /examples/RealWorld/src/data.js: -------------------------------------------------------------------------------- 1 | import loremIpsum from 'lorem-ipsum'; 2 | 3 | const generateList = (count) => { 4 | const list = []; 5 | for (let i = 0; i <= count; i++) { 6 | list.push(generateItem(i)); 7 | } 8 | return list; 9 | }; 10 | 11 | const generateItem = (id) => ({ 12 | id, 13 | title: loremIpsum(3, 'words'), 14 | comment: loremIpsum(1, 'sentences'), 15 | likes: Math.random(), 16 | votes: Math.random(), 17 | }); 18 | 19 | export default generateList; 20 | -------------------------------------------------------------------------------- /examples/RealWorld/src/example.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { compose } from 'recompose'; 4 | import { Icon } from 'react-fa'; 5 | 6 | import { components, enhancements } from 'react-redux-composable-list'; 7 | 8 | const { 9 | Enhanced, 10 | Row, 11 | Cell, 12 | HeaderCell, 13 | Sort, 14 | SortSelected, 15 | CellSelected, 16 | CellMagicHeader, 17 | CellMagic, 18 | } = components; 19 | 20 | const { 21 | withSelectables, 22 | withUnselectables, 23 | withPreselectables, 24 | withSort, 25 | withFilter, 26 | withPaginate, 27 | withEmpty, 28 | } = enhancements; 29 | 30 | const WIDTHS = { 31 | SMALL: { 32 | width: '10%', 33 | }, 34 | MEDIUM: { 35 | width: '20%', 36 | }, 37 | LARGE: { 38 | width: '70%', 39 | }, 40 | }; 41 | 42 | const SORTS_ASC_DESC = { 43 | ASC: , 44 | DESC: , 45 | }; 46 | 47 | const titleSort = item => item.title; 48 | const commentSort = item => item.comment; 49 | const votesSort = item => item.votes; 50 | const likesSort = item => item.likes; 51 | 52 | const magicSorts = [ 53 | { 54 | label: 'Comment', 55 | sortKey: 'comment', 56 | sortFn: commentSort, 57 | resolve: (item) => item.comment, 58 | }, 59 | { 60 | label: 'Votes', 61 | sortKey: 'votes', 62 | sortFn: votesSort, 63 | resolve: (item) => item.votes, 64 | }, 65 | { 66 | label: 'Likes', 67 | sortKey: 'likes', 68 | sortFn: likesSort, 69 | resolve: (item) => item.likes, 70 | }, 71 | ]; 72 | 73 | const RealWorlTable = ({ list, stateKey }) => 74 | 75 | 76 | 77 | 81 | Title 82 | 83 | 84 | 85 | 88 | 89 | 90 | 91 | 92 | 95 | Selected 96 | 97 | 98 | 99 | {list.map(item => 100 | 101 | {item.title} 102 | 103 | 104 | 105 | 106 | 107 | {{ 108 | SELECTED: , 109 | NOT_SELECTED: , 110 | PRE_SELECTED: , 111 | UNSELECTABLE: , 112 | }} 113 | 114 | 115 | 116 | )} 117 | 118 | 119 | // Empty Components, if filter result or in general list is empty or null 120 | 121 | const EmptyBecauseFilter = () => 122 |
123 |

No Filter Result

124 |

Sorry, there was no item matching your filter.

125 |
126 | 127 | const EmptyBecauseNoList = () => 128 |
129 |

Nothing to see!

130 |

Sorry, there is no content.

131 |
132 | 133 | export default compose( 134 | withEmpty({ component: EmptyBecauseNoList }), 135 | withSelectables({ ids: [0] }), 136 | withPreselectables({ ids: [2, 3] }), 137 | withUnselectables({ ids: [4, 6] }), 138 | withFilter(), 139 | withEmpty({ component: EmptyBecauseFilter }), 140 | withSort(), 141 | withPaginate({ size: 10 }), 142 | )(RealWorlTable); -------------------------------------------------------------------------------- /examples/RealWorld/src/filter.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | 4 | import { actionCreators } from 'react-redux-composable-list'; 5 | 6 | const InputField = ({ onChange }) => 7 |
8 | Filter: onChange(e.target.value)} 12 | /> 13 |
14 | 15 | const getStringFilterFn = query => item => 16 | item.title.toLowerCase().indexOf(query.toLowerCase()) !== -1 || 17 | item.comment.toLowerCase().indexOf(query.toLowerCase()) !== -1; 18 | 19 | const mapDispatchToPropsStringFilter = (dispatch, props) => ({ 20 | onChange: (query) => query !== '' 21 | ? dispatch(actionCreators.doSetFilter(props.stateKey, 'SOME_FILTER', getStringFilterFn(query))) 22 | : dispatch(actionCreators.doRemoveFilter(props.stateKey, 'SOME_FILTER')) 23 | }); 24 | 25 | export default connect(null, mapDispatchToPropsStringFilter)(InputField); 26 | -------------------------------------------------------------------------------- /examples/RealWorld/src/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component, } from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { Provider } from 'react-redux'; 4 | import generateList from './data'; 5 | 6 | import configureStore from './store'; 7 | 8 | import MyEverythingDataTable from './example'; 9 | import SomeFilter from './filter'; 10 | 11 | const store = configureStore(); 12 | const list = generateList(100); 13 | 14 | const App = () => 15 |
16 | 19 | 23 |
24 | 25 | ReactDOM.render( 26 | 27 | 28 | , 29 | document.getElementById('app') 30 | ); 31 | 32 | module.hot.accept(); 33 | -------------------------------------------------------------------------------- /examples/RealWorld/src/store.js: -------------------------------------------------------------------------------- 1 | import { applyMiddleware, createStore, combineReducers } from 'redux'; 2 | import createLogger from 'redux-logger'; 3 | 4 | import reducers from 'react-redux-composable-list'; 5 | 6 | const logger = createLogger(); 7 | 8 | const rootReducer = combineReducers({ 9 | ...reducers, 10 | // add your own app reducers 11 | }); 12 | 13 | const configureStore = (initialState) => createStore(rootReducer, applyMiddleware(logger), initialState); 14 | 15 | export default configureStore; 16 | -------------------------------------------------------------------------------- /examples/RealWorld/webpack.config.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | var ExtractTextPlugin = require('extract-text-webpack-plugin'); 3 | 4 | module.exports = { 5 | entry: [ 6 | 'webpack-dev-server/client?http://localhost:8080', 7 | 'webpack/hot/only-dev-server', 8 | './src/index.js' 9 | ], 10 | module: { 11 | loaders: [ 12 | { 13 | test: /\.jsx?$/, 14 | exclude: /node_modules/, 15 | loader: 'react-hot-loader!babel-loader' 16 | }, 17 | { 18 | test: /\.css$/, 19 | loader: ExtractTextPlugin.extract({ fallback: 'style-loader', use: 'css-loader' }) 20 | }, 21 | { 22 | test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/, 23 | loader: 'url-loader?limit=10000&mimetype=application/font-woff' 24 | }, 25 | { 26 | test: /\.(ttf|eot|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/, 27 | loader: 'file-loader' 28 | } 29 | ] 30 | }, 31 | resolve: { 32 | extensions: ['*', '.js', '.jsx'] 33 | }, 34 | output: { 35 | path: __dirname + '/dist', 36 | publicPath: '/', 37 | filename: 'bundle.js' 38 | }, 39 | devServer: { 40 | contentBase: './dist', 41 | hot: true 42 | }, 43 | plugins: [ 44 | new ExtractTextPlugin('bundle.css'), 45 | new webpack.DefinePlugin({ 46 | 'process.env': { 47 | 'NODE_ENV': '"development"' 48 | } 49 | }) 50 | ] 51 | }; 52 | -------------------------------------------------------------------------------- /examples/RealWorld/webpack.prod.config.js: -------------------------------------------------------------------------------- 1 | var ExtractTextPlugin = require('extract-text-webpack-plugin'); 2 | var webpack = require('webpack'); 3 | 4 | module.exports = { 5 | entry: { 6 | main: './src/index.js' 7 | }, 8 | resolve: { 9 | extensions: ['*', '.js', '.jsx'] 10 | }, 11 | output: { 12 | path: __dirname + '/dist', 13 | publicPath: '/', 14 | filename: 'bundle.js' 15 | }, 16 | module: { 17 | loaders: [{ 18 | test: /\.(js|jsx)$/, 19 | exclude: /node_modules/, 20 | loader: "babel-loader" 21 | }, 22 | { 23 | test: /\.css$/, 24 | loader: ExtractTextPlugin.extract({ fallback: 'style-loader', use: 'css-loader' }) 25 | }, 26 | { 27 | test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/, 28 | loader: 'url-loader?limit=10000&mimetype=application/font-woff' 29 | }, 30 | { 31 | test: /\.(ttf|eot|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/, 32 | loader: 'file-loader' 33 | }] 34 | }, 35 | plugins: [ 36 | new webpack.optimize.UglifyJsPlugin({ 37 | compress: { 38 | warnings: false 39 | } 40 | }), 41 | new ExtractTextPlugin('bundle.css'), 42 | new webpack.DefinePlugin({ 43 | 'process.env': { 44 | 'NODE_ENV': '"production"' 45 | } 46 | }) 47 | ] 48 | }; -------------------------------------------------------------------------------- /examples/Showcases/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /examples/Showcases/.nvmrc: -------------------------------------------------------------------------------- 1 | 8.1.2 -------------------------------------------------------------------------------- /examples/Showcases/Procfile: -------------------------------------------------------------------------------- 1 | web: node app.js -------------------------------------------------------------------------------- /examples/Showcases/README.md: -------------------------------------------------------------------------------- 1 | # Showcases 2 | 3 | Showcases different usages of the react-redux-composable-list library. [DEMO](https://react-redux-composable-list-showcases.wieruch.com/) 4 | 5 | ## Installation 6 | 7 | * `git clone git@github.com:rwieruch/react-redux-composable-list.git` 8 | * cd react-redux-composable-list/examples/Showcases 9 | * npm install 10 | * npm start 11 | * visit `http://localhost:8080/` 12 | -------------------------------------------------------------------------------- /examples/Showcases/app.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var bodyParser = require('body-parser'); 3 | 4 | var path = require('path'); 5 | 6 | app = express(); 7 | 8 | app.use(bodyParser.json()); 9 | app.use(express.static(path.join(__dirname, 'dist'))); 10 | 11 | app.listen(process.env.PORT || 5000); 12 | -------------------------------------------------------------------------------- /examples/Showcases/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "dokku": { 4 | "predeploy": "npm run build" 5 | } 6 | } 7 | } -------------------------------------------------------------------------------- /examples/Showcases/dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The Minimal React Webpack Babel Setup 5 | 6 | 7 |
8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /examples/Showcases/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-redux-composable-list-showcases", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "NODE_ENV=production webpack -p --progress --colors --config ./webpack.prod.config.js", 8 | "start": "webpack-dev-server --progress --colors --hot --config ./webpack.config.js", 9 | "test": "echo \"Error: no test specified\" && exit 1" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "ISC", 14 | "babel": { 15 | "presets": [ 16 | "es2015", 17 | "react", 18 | "stage-2" 19 | ] 20 | }, 21 | "devDependencies": { 22 | "babel-core": "6.23.1", 23 | "babel-loader": "6.4.1", 24 | "babel-preset-es2015": "6.22.0", 25 | "babel-preset-react": "6.23.0", 26 | "babel-preset-stage-2": "6.22.0", 27 | "css-loader": "0.27.3", 28 | "react-hot-loader": "1.3.1", 29 | "style-loader": "0.15.0", 30 | "webpack": "2.2.1", 31 | "webpack-dev-server": "2.4.1" 32 | }, 33 | "dependencies": { 34 | "body-parser": "^1.17.2", 35 | "classnames": "2.2.5", 36 | "express": "^4.15.3", 37 | "lodash": "4.17.4", 38 | "lorem-ipsum": "1.0.3", 39 | "path": "^0.12.7", 40 | "react": "15.4.2", 41 | "react-dom": "15.4.2", 42 | "react-infinite": "0.10.0", 43 | "react-redux": "5.0.3", 44 | "react-redux-composable-list": "^0.1.1", 45 | "recompose": "0.22.0", 46 | "redux": "3.6.0", 47 | "redux-logger": "2.8.2" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /examples/Showcases/src/data.js: -------------------------------------------------------------------------------- 1 | import loremIpsum from 'lorem-ipsum'; 2 | 3 | const generateList = (count) => { 4 | const list = []; 5 | for (let i = 0; i <= count; i++) { 6 | list.push(generateItem(i)); 7 | } 8 | return list; 9 | }; 10 | 11 | const generateItem = (id) => ({ 12 | id, 13 | title: loremIpsum(3, 'words'), 14 | comment: loremIpsum(1, 'sentences'), 15 | likes: Math.random(), 16 | votes: Math.random(), 17 | }); 18 | 19 | export default generateList; 20 | -------------------------------------------------------------------------------- /examples/Showcases/src/examples/custom-style.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmallImprovements/react-redux-composable-list/aeb97b1396869ed3a5c485fe6e6250bd4bca6b98/examples/Showcases/src/examples/custom-style.js -------------------------------------------------------------------------------- /examples/Showcases/src/examples/filter.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { compose } from 'recompose'; 4 | 5 | import { components, enhancements, actionCreators } from 'react-redux-composable-list'; 6 | const { Enhanced, Row, Cell, HeaderCell } = components; 7 | const { withFilter } = enhancements; 8 | 9 | const WIDTHS = { 10 | SMALL: { 11 | width: '25%', 12 | }, 13 | MEDIUM: { 14 | width: '50%', 15 | }, 16 | LARGE: { 17 | width: '75%', 18 | }, 19 | }; 20 | 21 | const FilterEnhanced = ({ list, stateKey }) => 22 | 23 | 24 | 25 | Title 26 | 27 | 28 | Comment 29 | 30 | 31 | {list.map(item => 32 | 33 | {item.title} 34 | {item.comment} 35 | 36 | )} 37 | 38 | 39 | // External Filter Component that consumes the action API of the library 40 | 41 | const ExternalFilter = ({ onChange }) => 42 |
43 | Filter Title: onChange(e.target.value)} 46 | /> 47 |
48 | 49 | const titleFilterFn = query => item => 50 | item.title.toLowerCase().indexOf(query.toLowerCase()) !== -1; 51 | 52 | const mapDispatchToProps = (dispatch, props) => ({ 53 | onChange: (query) => query !== '' 54 | ? dispatch(actionCreators.doSetFilter(props.stateKey, 'TITLE_FILTER', titleFilterFn(query))) 55 | : dispatch(actionCreators.doRemoveFilter(props.stateKey, 'TITLE_FILTER')) 56 | }); 57 | 58 | const Filter = connect(null, mapDispatchToProps)(ExternalFilter); 59 | 60 | export { 61 | Filter, 62 | }; 63 | 64 | export default compose( 65 | withFilter() 66 | )(FilterEnhanced); 67 | -------------------------------------------------------------------------------- /examples/Showcases/src/examples/infinite.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Infinite from 'react-infinite'; 3 | 4 | import { components } from 'react-redux-composable-list'; 5 | const { Enhanced, Row, Cell } = components; 6 | 7 | const WIDTHS = { 8 | SMALL: { 9 | width: '25%', 10 | }, 11 | MEDIUM: { 12 | width: '50%', 13 | }, 14 | LARGE: { 15 | width: '75%', 16 | }, 17 | }; 18 | 19 | const InfiniteEnhanced = ({ list, stateKey }) => 20 | 21 | 22 | {list.map(item => 23 | 24 | {item.title} 25 | {item.comment} 26 | 27 | )} 28 | 29 | 30 | 31 | export default InfiniteEnhanced; -------------------------------------------------------------------------------- /examples/Showcases/src/examples/magic-column.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { compose } from 'recompose'; 3 | 4 | import { components, enhancements } from 'react-redux-composable-list'; 5 | const { Enhanced, Row, Cell, HeaderCell, Sort, CellMagicHeader, CellMagic } = components; 6 | const { withSort } = enhancements; 7 | 8 | const WIDTHS = { 9 | SMALL: { 10 | width: '35%', 11 | }, 12 | MEDIUM: { 13 | width: '50%', 14 | }, 15 | LARGE: { 16 | width: '65%', 17 | }, 18 | }; 19 | 20 | const SORTS_ASC_DESC = { 21 | ASC: (asc), 22 | DESC: (desc), 23 | }; 24 | 25 | const titleSort = item => item.title; 26 | const commentSort = item => item.comment; 27 | const votesSort = item => item.votes; 28 | const likesSort = item => item.likes; 29 | 30 | const magicSorts = [ 31 | { 32 | label: 'Comment', 33 | sortKey: 'comment', 34 | sortFn: commentSort, 35 | resolve: (item) => item.comment, 36 | }, 37 | { 38 | label: 'Votes', 39 | sortKey: 'votes', 40 | sortFn: votesSort, 41 | resolve: (item) => item.votes, 42 | }, 43 | { 44 | label: 'Likes', 45 | sortKey: 'likes', 46 | sortFn: likesSort, 47 | resolve: (item) => item.likes, 48 | }, 49 | ]; 50 | 51 | const MagicColumnEnhanced = ({ list, stateKey }) => 52 | 53 | 54 | 55 | 59 | Title 60 | 61 | 62 | 63 | 66 | (Magic!) 67 | 68 | 69 | 70 | {list.map(item => 71 | 72 | {item.title} 73 | 74 | 75 | 76 | 77 | )} 78 | 79 | 80 | export default compose( 81 | withSort() 82 | )(MagicColumnEnhanced); 83 | -------------------------------------------------------------------------------- /examples/Showcases/src/examples/multiple-filter-or.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { compose } from 'recompose'; 4 | 5 | import { components, enhancements, actionCreators } from 'react-redux-composable-list'; 6 | const { Enhanced, Row, Cell, HeaderCell } = components; 7 | const { withFilterOr } = enhancements; 8 | 9 | const WIDTHS = { 10 | SMALL: { 11 | width: '25%', 12 | }, 13 | MEDIUM: { 14 | width: '50%', 15 | }, 16 | LARGE: { 17 | width: '75%', 18 | }, 19 | }; 20 | 21 | const FilterEnhanced = ({ list, stateKey }) => 22 | 23 | 24 | 25 | Title 26 | 27 | 28 | Comment 29 | 30 | 31 | {list.map(item => 32 | 33 | {item.title} 34 | {item.comment} 35 | 36 | )} 37 | 38 | 39 | // External Filter Component that consumes the action API of the library 40 | 41 | const ExternalFilters = ({ onFilterChange }) => 42 |
43 | Filter Title or Comment: onFilterChange(e.target.value)} 46 | /> 47 |
48 | 49 | const titleOrCommentFilterFn = query => item => 50 | item.title.toLowerCase().indexOf(query.toLowerCase()) !== -1 || 51 | item.comment.toLowerCase().indexOf(query.toLowerCase()) !== -1; 52 | 53 | const mapDispatchToProps = (dispatch, props) => ({ 54 | onFilterChange: (query) => query !== '' 55 | ? dispatch(actionCreators.doSetFilter(props.stateKey, 'TITLE_COMMENT_FILTER', titleOrCommentFilterFn(query))) 56 | : dispatch(actionCreators.doRemoveFilter(props.stateKey, 'TITLE_COMMENT_FILTER')) 57 | }); 58 | 59 | const Filters = connect(null, mapDispatchToProps)(ExternalFilters); 60 | 61 | export { 62 | Filters, 63 | }; 64 | 65 | export default compose( 66 | withFilterOr() 67 | )(FilterEnhanced); 68 | -------------------------------------------------------------------------------- /examples/Showcases/src/examples/multiple-filter.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { compose } from 'recompose'; 4 | 5 | import { components, enhancements, actionCreators } from 'react-redux-composable-list'; 6 | const { Enhanced, Row, Cell, HeaderCell } = components; 7 | const { withFilter } = enhancements; 8 | 9 | const WIDTHS = { 10 | SMALL: { 11 | width: '25%', 12 | }, 13 | MEDIUM: { 14 | width: '50%', 15 | }, 16 | LARGE: { 17 | width: '75%', 18 | }, 19 | }; 20 | 21 | const FilterEnhanced = ({ list, stateKey }) => 22 | 23 | 24 | 25 | Title 26 | 27 | 28 | Comment 29 | 30 | 31 | {list.map(item => 32 | 33 | {item.title} 34 | {item.comment} 35 | 36 | )} 37 | 38 | 39 | // External Filter Component that consumes the action API of the library 40 | 41 | const ExternalFilters = ({ onTitleFilterChange, onCommentFilterChange }) => 42 |
43 |

Filters

44 |
45 | Title: onTitleFilterChange(e.target.value)} 48 | /> 49 |
50 |
51 | Comment: onCommentFilterChange(e.target.value)} 54 | /> 55 |
56 |
57 | 58 | const titleFilterFn = query => item => 59 | item.title.toLowerCase().indexOf(query.toLowerCase()) !== -1; 60 | 61 | const commentFilterFn = query => item => 62 | item.comment.toLowerCase().indexOf(query.toLowerCase()) !== -1; 63 | 64 | const mapDispatchToProps = (dispatch, props) => ({ 65 | onTitleFilterChange: (query) => query !== '' 66 | ? dispatch(actionCreators.doSetFilter(props.stateKey, 'TITLE_FILTER', titleFilterFn(query))) 67 | : dispatch(actionCreators.doRemoveFilter(props.stateKey, 'TITLE_FILTER')), 68 | 69 | onCommentFilterChange: (query) => query !== '' 70 | ? dispatch(actionCreators.doSetFilter(props.stateKey, 'COMMENT_FILTER', commentFilterFn(query))) 71 | : dispatch(actionCreators.doRemoveFilter(props.stateKey, 'COMMENT_FILTER')) 72 | }); 73 | 74 | const Filters = connect(null, mapDispatchToProps)(ExternalFilters); 75 | 76 | export { 77 | Filters, 78 | }; 79 | 80 | export default compose( 81 | withFilter() 82 | )(FilterEnhanced); 83 | -------------------------------------------------------------------------------- /examples/Showcases/src/examples/pagination.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { compose } from 'recompose'; 3 | 4 | import { components, enhancements } from 'react-redux-composable-list'; 5 | const { Enhanced, Row, Cell } = components; 6 | const { withPaginate } = enhancements; 7 | 8 | const WIDTHS = { 9 | SMALL: { 10 | width: '25%', 11 | }, 12 | MEDIUM: { 13 | width: '50%', 14 | }, 15 | LARGE: { 16 | width: '75%', 17 | }, 18 | }; 19 | 20 | const PaginationEnhanced = ({ list, stateKey }) => 21 | 22 | {list.map(item => 23 | 24 | {item.title} 25 | {item.comment} 26 | 27 | )} 28 | 29 | 30 | export default compose( 31 | withPaginate({ size: 10 }) 32 | )(PaginationEnhanced); 33 | -------------------------------------------------------------------------------- /examples/Showcases/src/examples/plain.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { components } from 'react-redux-composable-list'; 4 | const { Enhanced, Row, Cell } = components; 5 | 6 | const WIDTHS = { 7 | SMALL: { 8 | width: '25%', 9 | }, 10 | MEDIUM: { 11 | width: '50%', 12 | }, 13 | LARGE: { 14 | width: '75%', 15 | }, 16 | }; 17 | 18 | const PlainEnhanced = ({ list, stateKey }) => 19 | 20 | {list.map(item => 21 | 22 | {item.title} 23 | {item.comment} 24 | 25 | )} 26 | 27 | 28 | export default PlainEnhanced; -------------------------------------------------------------------------------- /examples/Showcases/src/examples/responsive.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmallImprovements/react-redux-composable-list/aeb97b1396869ed3a5c485fe6e6250bd4bca6b98/examples/Showcases/src/examples/responsive.js -------------------------------------------------------------------------------- /examples/Showcases/src/examples/select-plain.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { compose } from 'recompose'; 3 | 4 | import { components, enhancements } from 'react-redux-composable-list'; 5 | const { Enhanced, Row, Cell } = components; 6 | const { withSelectables } = enhancements; 7 | 8 | const WIDTHS = { 9 | SMALL: { 10 | width: '25%', 11 | }, 12 | MEDIUM: { 13 | width: '50%', 14 | }, 15 | LARGE: { 16 | width: '75%', 17 | }, 18 | }; 19 | 20 | const SelectPlainEnhanced = ({ list, stateKey }) => 21 | 22 | {list.map(item => 23 | 24 | {item.title} 25 | {item.comment} 26 | 27 | )} 28 | 29 | 30 | export default compose( 31 | withSelectables() 32 | )(SelectPlainEnhanced); 33 | -------------------------------------------------------------------------------- /examples/Showcases/src/examples/select-preselectables.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { compose } from 'recompose'; 3 | 4 | import { components, enhancements } from 'react-redux-composable-list'; 5 | const { Enhanced, Row, Cell } = components; 6 | const { withPreselectables, withSelectables } = enhancements; 7 | 8 | const WIDTHS = { 9 | SMALL: { 10 | width: '25%', 11 | }, 12 | MEDIUM: { 13 | width: '50%', 14 | }, 15 | LARGE: { 16 | width: '75%', 17 | }, 18 | }; 19 | 20 | const SelectPreselectablesEnhanced = ({ list, stateKey }) => 21 | 22 | {list.map(item => 23 | 24 | {item.title} 25 | {item.comment} 26 | 27 | )} 28 | 29 | 30 | export default compose( 31 | withSelectables(), 32 | withPreselectables({ ids: [1, 2] }) 33 | )(SelectPreselectablesEnhanced); 34 | -------------------------------------------------------------------------------- /examples/Showcases/src/examples/select-selected.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { compose } from 'recompose'; 3 | 4 | import { components, enhancements } from 'react-redux-composable-list'; 5 | const { Enhanced, Row, Cell } = components; 6 | const { withSelectables } = enhancements; 7 | 8 | const WIDTHS = { 9 | SMALL: { 10 | width: '25%', 11 | }, 12 | MEDIUM: { 13 | width: '50%', 14 | }, 15 | LARGE: { 16 | width: '75%', 17 | }, 18 | }; 19 | 20 | const SelectSelectedEnhanced = ({ list, stateKey }) => 21 | 22 | {list.map(item => 23 | 24 | {item.title} 25 | {item.comment} 26 | 27 | )} 28 | 29 | 30 | export default compose( 31 | withSelectables({ ids: [1, 2] }) 32 | )(SelectSelectedEnhanced); 33 | -------------------------------------------------------------------------------- /examples/Showcases/src/examples/select-sort.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { compose } from 'recompose'; 3 | 4 | import { components, enhancements } from 'react-redux-composable-list'; 5 | const { Enhanced, Row, Cell, HeaderCell, Sort, SortSelected, CellSelected } = components; 6 | const { withSelectables, withUnselectables, withPreselectables, withSort } = enhancements; 7 | 8 | const WIDTHS = { 9 | SMALL: { 10 | width: '25%', 11 | }, 12 | MEDIUM: { 13 | width: '50%', 14 | }, 15 | LARGE: { 16 | width: '75%', 17 | }, 18 | }; 19 | 20 | const SORTS_ASC_DESC = { 21 | ASC: (asc), 22 | DESC: (desc), 23 | }; 24 | 25 | const titleSort = item => item.title; 26 | const commentSort = item => item.comment; 27 | 28 | const SelectSortEnhanced = ({ list, stateKey }) => 29 | 30 | 31 | 32 | 35 | Title 36 | 37 | 38 | 39 | 42 | Comment 43 | 44 | 45 | 46 | 49 | Selected 50 | 51 | 52 | 53 | {list.map(item => 54 | 55 | {item.title} 56 | {item.comment} 57 | 58 | 59 | {{ 60 | SELECTED: SELECTED, 61 | NOT_SELECTED: NOT_SELECTED, 62 | PRE_SELECTED: PRE_SELECTED, 63 | UNSELECTABLE: UNSELECTABLE, 64 | }} 65 | 66 | 67 | 68 | )} 69 | 70 | 71 | const Foo = ({ state }) => { 72 | {SELECT_STATES[state]} 73 | }; 74 | 75 | export default compose( 76 | withSelectables(), 77 | withPreselectables({ ids: [5] }), 78 | withUnselectables({ ids: [1, 2] }), 79 | withSort() 80 | )(SelectSortEnhanced); 81 | -------------------------------------------------------------------------------- /examples/Showcases/src/examples/select-unselectables.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { compose } from 'recompose'; 3 | 4 | import { components, enhancements } from 'react-redux-composable-list'; 5 | const { Enhanced, Row, Cell } = components; 6 | const { withUnselectables, withSelectables } = enhancements; 7 | 8 | const WIDTHS = { 9 | SMALL: { 10 | width: '25%', 11 | }, 12 | MEDIUM: { 13 | width: '50%', 14 | }, 15 | LARGE: { 16 | width: '75%', 17 | }, 18 | }; 19 | 20 | const SelectUnselectablesEnhanced = ({ list, stateKey }) => 21 | 22 | {list.map(item => 23 | 24 | {item.title} 25 | {item.comment} 26 | 27 | )} 28 | 29 | 30 | export default compose( 31 | withSelectables(), 32 | withUnselectables({ ids: [1, 2] }) 33 | )(SelectUnselectablesEnhanced); 34 | -------------------------------------------------------------------------------- /examples/Showcases/src/examples/sort.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { compose } from 'recompose'; 3 | 4 | import { components, enhancements } from 'react-redux-composable-list'; 5 | const { Enhanced, Row, Cell, HeaderCell, Sort } = components; 6 | const { withSort } = enhancements; 7 | 8 | const WIDTHS = { 9 | SMALL: { 10 | width: '25%', 11 | }, 12 | MEDIUM: { 13 | width: '50%', 14 | }, 15 | LARGE: { 16 | width: '75%', 17 | }, 18 | }; 19 | 20 | const SORTS_ASC_DESC = { 21 | ASC: (asc), 22 | DESC: (desc), 23 | }; 24 | 25 | const titleSort = item => item.title; 26 | const commentSort = item => item.comment; 27 | 28 | const SortEnhanced = ({ list, stateKey }) => 29 | 30 | 31 | 32 | 36 | Title 37 | 38 | 39 | 40 | 44 | Comment 45 | 46 | 47 | 48 | {list.map(item => 49 | 50 | {item.title} 51 | {item.comment} 52 | 53 | )} 54 | 55 | 56 | export default compose( 57 | withSort() 58 | )(SortEnhanced); 59 | -------------------------------------------------------------------------------- /examples/Showcases/src/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { Provider } from 'react-redux'; 4 | import { map } from 'lodash'; 5 | import generateList from './data'; 6 | 7 | import configureStore from './store'; 8 | 9 | import PlainEnhanced from './examples/plain'; 10 | import SortEnhanced from './examples/sort'; 11 | import SelectPlainEnhanced from './examples/select-plain'; 12 | import SelectSelectedEnhanced from './examples/select-selected'; 13 | import SelectUnselectablesEnhanced from './examples/select-unselectables'; 14 | import SelectPreselectablesEnhanced from './examples/select-preselectables'; 15 | import SelectSortEnhanced from './examples/select-sort'; 16 | import MagicColumnEnhanced from './examples/magic-column'; 17 | import FilterEnhanced, { Filter as TitleFilter } from './examples/filter'; 18 | import FilterMultipleEnhanced, { Filters as TitleCommentAndFilter } from './examples/multiple-filter'; 19 | import FilterMultipleOrEnhanced, { Filters as TitleCommentOrFilter } from './examples/multiple-filter-or'; 20 | import InfiniteEnhanced from './examples/infinite'; 21 | import PaginationEnhanced from './examples/pagination'; 22 | 23 | const store = configureStore(); 24 | 25 | // Each example will get resolved upon user request - see Showcase Component 26 | // id: identifier for the component in Redux, used as stateKey in the component 27 | // label: button label to select the example 28 | // Component: the Component that uses the library, each component is defined in its own file 29 | // ExternalApiConsumerComponent: optional component that uses the library API to update the list from outside 30 | const SHOWCASE_EXAMPLES = { 31 | PLAIN: { 32 | id: 'PLAIN', 33 | label: 'Plain', 34 | Component: PlainEnhanced, 35 | }, 36 | SORT: { 37 | id: 'SORT', 38 | label: 'With Sort', 39 | Component: SortEnhanced, 40 | }, 41 | SELECT_PLAIN: { 42 | id: 'SELECT_PLAIN', 43 | label: 'With Select', 44 | Component: SelectPlainEnhanced, 45 | }, 46 | SELECT_SELECTED: { 47 | id: 'SELECT_SELECTED', 48 | label: 'With Select With Selected', 49 | Component: SelectSelectedEnhanced, 50 | }, 51 | SELECT_UNSELECTABLES: { 52 | id: 'SELECT_UNSELECTABLES', 53 | label: 'With Select With Unselectables', 54 | Component: SelectUnselectablesEnhanced, 55 | }, 56 | SELECT_PRESELECTED: { 57 | id: 'SELECT_PRESELECTED', 58 | label: 'With Select With Preselectables', 59 | Component: SelectPreselectablesEnhanced, 60 | }, 61 | SELECT_SORT: { 62 | id: 'SELECT_SORT', 63 | label: 'With Select With Sort', 64 | Component: SelectSortEnhanced, 65 | }, 66 | MAGIC: { 67 | id: 'MAGIC', 68 | label: 'With Magic Column', 69 | Component: MagicColumnEnhanced, 70 | }, 71 | FILTER: { 72 | id: 'FILTER', 73 | label: 'With Filter', 74 | Component: FilterEnhanced, 75 | ExternalApiConsumerComponent: TitleFilter, 76 | }, 77 | FILTER_MULTIPLE: { 78 | id: 'FILTER_MULTIPLE', 79 | label: 'With Multiple Filters AND', 80 | Component: FilterMultipleEnhanced, 81 | ExternalApiConsumerComponent: TitleCommentAndFilter, 82 | }, 83 | FILTER_MULTIPLE_OR: { 84 | id: 'FILTER_MULTIPLE_OR', 85 | label: 'With Multiple Filters OR', 86 | Component: FilterMultipleOrEnhanced, 87 | ExternalApiConsumerComponent: TitleCommentOrFilter, 88 | }, 89 | INFINITE: { 90 | id: 'INFINITE', 91 | label: 'With Infinite Scroll', 92 | Component: InfiniteEnhanced, 93 | }, 94 | PAGINATION: { 95 | id: 'PAGINATION', 96 | label: 'With Pagination', 97 | Component: PaginationEnhanced, 98 | }, 99 | }; 100 | 101 | class App extends Component { 102 | constructor(props) { 103 | super(props); 104 | 105 | this.state = { 106 | example: SHOWCASE_EXAMPLES.PLAIN.id, 107 | } 108 | } 109 | 110 | render() { 111 | const example = SHOWCASE_EXAMPLES[this.state.example]; 112 | 113 | return ( 114 |
115 | this.setState({ example: id })} /> 116 | 117 |
118 | 119 |
120 |

121 | {example.label} 122 |

123 | 124 | 125 |
126 |
127 | ); 128 | } 129 | } 130 | 131 | const list = generateList(100); 132 | 133 | // example.Component equals one of the Example components e.g. PlainEnhanced 134 | // input: list of items as list 135 | // input: unique identifier as stateKey 136 | // external API consumer component needs the stateKey to speak to the library API 137 | const Showcase = ({ example }) => 138 |
139 | { example.ExternalApiConsumerComponent 140 | ? 141 | : null 142 | } 143 | 144 | 148 |
; 149 | 150 | const ShowcaseSelector = ({ onSelectShowcase }) => 151 |
152 |

Showcase React Redux Data Grids

153 | {map(SHOWCASE_EXAMPLES, x => 154 |
155 | 161 |
162 | )} 163 |
; 164 | 165 | // needs store on top level 166 | ReactDOM.render( 167 | 168 | 169 | , 170 | document.getElementById('app') 171 | ); 172 | 173 | module.hot.accept(); 174 | -------------------------------------------------------------------------------- /examples/Showcases/src/store.js: -------------------------------------------------------------------------------- 1 | import { applyMiddleware, createStore, combineReducers } from 'redux'; 2 | import createLogger from 'redux-logger'; 3 | 4 | import reducers from 'react-redux-composable-list'; 5 | 6 | const logger = createLogger(); 7 | 8 | const rootReducer = combineReducers({ 9 | ...reducers, 10 | // add your own app reducers 11 | }); 12 | 13 | const configureStore = (initialState) => createStore(rootReducer, applyMiddleware(logger), initialState); 14 | 15 | export default configureStore; 16 | -------------------------------------------------------------------------------- /examples/Showcases/webpack.config.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | 3 | module.exports = { 4 | entry: [ 5 | 'webpack-dev-server/client?http://localhost:8080', 6 | 'webpack/hot/only-dev-server', 7 | './src/index.js' 8 | ], 9 | module: { 10 | loaders: [{ 11 | test: /\.jsx?$/, 12 | exclude: /node_modules/, 13 | loader: 'react-hot-loader!babel-loader' 14 | }] 15 | }, 16 | resolve: { 17 | extensions: ['*', '.js', '.jsx'] 18 | }, 19 | output: { 20 | path: __dirname + '/dist', 21 | publicPath: '/', 22 | filename: 'bundle.js' 23 | }, 24 | devServer: { 25 | contentBase: './dist', 26 | hot: true 27 | }, 28 | plugins: [ 29 | new webpack.DefinePlugin({ 30 | 'process.env': { 31 | 'NODE_ENV': '"development"' 32 | } 33 | }) 34 | ] 35 | }; -------------------------------------------------------------------------------- /examples/Showcases/webpack.prod.config.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | 3 | module.exports = { 4 | entry: { 5 | main: './src/index.js' 6 | }, 7 | resolve: { 8 | extensions: ['*', '.js', '.jsx'] 9 | }, 10 | output: { 11 | path: __dirname + '/dist', 12 | publicPath: '/', 13 | filename: 'bundle.js' 14 | }, 15 | module: { 16 | loaders: [{ 17 | test: /\.(js|jsx)$/, 18 | exclude: /node_modules/, 19 | loader: "babel-loader" 20 | }, 21 | { 22 | test: /\.scss$/, 23 | loader: 'style!css!sass' 24 | }] 25 | }, 26 | plugins: [ 27 | new webpack.optimize.UglifyJsPlugin({ 28 | compress: { 29 | warnings: false 30 | } 31 | }), 32 | new webpack.DefinePlugin({ 33 | 'process.env': { 34 | 'NODE_ENV': '"production"' 35 | } 36 | }) 37 | ] 38 | }; -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./dist/bundle'); -------------------------------------------------------------------------------- /mocha.config.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | 3 | global.fdescribe = (...args) => describe.only(...args); 4 | global.fit = (...args) => it.only(...args); 5 | global.expect = expect; 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-redux-composable-list", 3 | "version": "0.8.2", 4 | "description": "Composable List in React and Redux", 5 | "main": "dist/bundle.js", 6 | "peerDependencies": { 7 | "react": "^0.14.9 || ^15.3.0 || ^16.0.0 || ^17.0.0", 8 | "redux": "^3.0.0", 9 | "react-redux": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" 10 | }, 11 | "dependencies": { 12 | "lodash.sortby": "^4.7.0", 13 | "prop-types": "^15.5.7" 14 | }, 15 | "devDependencies": { 16 | "babel-core": "^6.9.0", 17 | "babel-eslint": "^7.2.0", 18 | "babel-loader": "^6.4.1", 19 | "babel-preset-es2015": "^6.22.0", 20 | "babel-preset-react": "^6.23.0", 21 | "babel-preset-stage-2": "^6.22.0", 22 | "babel-register": "^6.9.0", 23 | "chai": "^3.5.0", 24 | "css-loader": "^0.27.3", 25 | "deep-freeze": "0.0.1", 26 | "eslint": "^3.18.0", 27 | "eslint-config-airbnb": "^14.1.0", 28 | "eslint-loader": "^1.6.3", 29 | "eslint-plugin-import": "^2.2.0", 30 | "eslint-plugin-jsx-a11y": "^4.0.0", 31 | "eslint-plugin-react": "^6.10.3", 32 | "gitbook-cli": "^2.3.0", 33 | "less": "^2.7.2", 34 | "less-loader": "^3.0.0", 35 | "mocha": "^2.5.3", 36 | "react": "^15.4.2", 37 | "react-redux": "^5.0.3", 38 | "redux": "^3.6.0", 39 | "style-loader": "^0.18.1", 40 | "webpack": "1.12.2", 41 | "webpack-visualizer-plugin": "^0.1.11" 42 | }, 43 | "scripts": { 44 | "build-dev": "webpack --progress --profile --colors --config ./webpack.config.dev.js", 45 | "build-dist": "webpack -p --config ./webpack.config.prod.js", 46 | "docs:prepare": "gitbook install", 47 | "docs:watch": "npm run docs:prepare && gitbook serve", 48 | "prepare": "npm run build-dist", 49 | "test": "mocha --compilers js:babel-core/register --require ./mocha.config.js 'src/**/*spec.js'", 50 | "test:watch": "npm run test -- -w", 51 | "lint": "eslint src" 52 | }, 53 | "repository": { 54 | "type": "git", 55 | "url": "git+https://github.com/SmallImprovements/react-redux-composable-list.git" 56 | }, 57 | "keywords": [ 58 | "react", 59 | "redux", 60 | "react-component", 61 | "react-list", 62 | "list", 63 | "redux-list" 64 | ], 65 | "author": "Robin Wieruch (https://www.robinwieruch.de)", 66 | "license": "MIT", 67 | "bugs": { 68 | "url": "https://github.com/SmallImprovements/react-redux-composable-list/issues" 69 | }, 70 | "homepage": "https://github.com/SmallImprovements/react-redux-composable-list#readme", 71 | "tags": [ 72 | "react", 73 | "redux", 74 | "table", 75 | "data", 76 | "grid" 77 | ] 78 | } 79 | -------------------------------------------------------------------------------- /src/components/Cell/index.js: -------------------------------------------------------------------------------- 1 | import Cell from './presenter'; 2 | 3 | export default Cell; 4 | -------------------------------------------------------------------------------- /src/components/Cell/presenter.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | 4 | import '../style.less'; 5 | 6 | const Cell = ({ 7 | style, 8 | className = '', 9 | children 10 | }) => 11 |
16 | {children} 17 |
; 18 | 19 | Cell.propTypes = { 20 | style: PropTypes.object, 21 | className: PropTypes.string, 22 | children: PropTypes.node, 23 | }; 24 | 25 | export default Cell; 26 | -------------------------------------------------------------------------------- /src/components/CellMagic/container.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import { connect } from 'react-redux'; 3 | 4 | import { getContext } from '../../helper/util/getContext'; 5 | import { find } from '../../helper/util/find'; 6 | 7 | import { selectors } from '../../ducks'; 8 | import CellMagic from './presenter'; 9 | 10 | function mapStateToProps(state, { magicSorts, stateKey, item }) { 11 | const sortKey = selectors.getMagicSort(state, stateKey, magicSorts); 12 | const activeMagicSort = find(magicSorts, (s) => s.sortKey === sortKey); 13 | return { 14 | item, 15 | activeMagicSort, 16 | }; 17 | } 18 | 19 | const contextTypes = { 20 | stateKey: PropTypes.string.isRequired 21 | }; 22 | 23 | export default getContext(contextTypes)(connect(mapStateToProps)(CellMagic)); 24 | -------------------------------------------------------------------------------- /src/components/CellMagic/index.js: -------------------------------------------------------------------------------- 1 | import CellMagic from './container'; 2 | 3 | export default CellMagic; 4 | -------------------------------------------------------------------------------- /src/components/CellMagic/presenter.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | 4 | const CellMagic = ({ item, activeMagicSort }) => 5 |
6 | { activeMagicSort.component 7 | ? 10 | : activeMagicSort.resolve(item) 11 | } 12 |
; 13 | 14 | CellMagic.propTypes = { 15 | item: PropTypes.object.isRequired, 16 | activeMagicSort: PropTypes.object.isRequired, 17 | }; 18 | 19 | export default CellMagic; 20 | -------------------------------------------------------------------------------- /src/components/CellMagicHeader/container.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import { connect } from 'react-redux'; 3 | import { bindActionCreators } from 'redux'; 4 | import { getContext } from '../../helper/util/getContext'; 5 | 6 | import { find } from '../../helper/util/find'; 7 | 8 | import { actionCreators, selectors } from '../../ducks'; 9 | import CellMagicHeader from './presenter'; 10 | 11 | function mapStateToProps(state, { magicSorts, stateKey }) { 12 | const { sortKey: stateSortKey, isReverse } = selectors.getSort(state, stateKey); 13 | const isActive = (sortKey) => sortKey === stateSortKey; 14 | const sortKey = selectors.getMagicSort(state, stateKey, magicSorts); 15 | const primarySort = find(magicSorts, (s) => s.sortKey === sortKey); 16 | return { 17 | magicSorts, 18 | primarySort, 19 | isActive, 20 | isReverse, 21 | }; 22 | } 23 | 24 | function mapDispatchToProps(dispatch, { stateKey }) { 25 | const { doTableSort, doSetMagicSort } = actionCreators; 26 | return bindActionCreators({ 27 | onSort: (sortKey, sortFn, isReverse) => doTableSort(stateKey, sortKey, sortFn, isReverse), 28 | onSetMagic: (sortKey) => doSetMagicSort(stateKey, sortKey), 29 | }, dispatch); 30 | } 31 | 32 | const contextTypes = { 33 | stateKey: PropTypes.string.isRequired 34 | }; 35 | 36 | export default getContext(contextTypes)(connect(mapStateToProps, mapDispatchToProps)(CellMagicHeader)); 37 | -------------------------------------------------------------------------------- /src/components/CellMagicHeader/index.js: -------------------------------------------------------------------------------- 1 | import CellMagicHeader from './container'; 2 | 3 | export default CellMagicHeader; 4 | -------------------------------------------------------------------------------- /src/components/CellMagicHeader/presenter.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React, { Component } from 'react'; 3 | import SortCaret from '../../helper/components/SortCaret'; 4 | import { sort } from '../../helper/services'; 5 | import './style.less'; 6 | 7 | const getLinkClass = (active) => { 8 | const linkClasses = ['react-redux-composable-list-row-magic-header-inline']; 9 | if (active) { 10 | linkClasses.push('react-redux-composable-list-row-magic-header-active'); 11 | } 12 | return linkClasses.join(' '); 13 | } 14 | 15 | class CellMagicHeader extends Component { 16 | state = { 17 | isColumnSelectorShown: false, 18 | }; 19 | 20 | setColumnSelectorShown = (isShown) => { 21 | if (isShown) { 22 | document.addEventListener('click', this.handleOutsideClick, false); 23 | } else { 24 | document.removeEventListener('click', this.handleOutsideClick, false); 25 | } 26 | this.setState({ 27 | isColumnSelectorShown: isShown, 28 | }); 29 | }; 30 | 31 | handleOutsideClick = (e) => { 32 | const isClickOnButton = this.buttonNode && this.buttonNode.contains(e.target); 33 | const isClickInsideColumnSelector = this.columnSelectorNode && this.columnSelectorNode.contains(e.target); 34 | if (!(isClickOnButton || isClickInsideColumnSelector)) { 35 | this.setColumnSelectorShown(false); 36 | } 37 | }; 38 | 39 | componentWillUnmount() { 40 | document.removeEventListener('click', this.handleOutsideClick, false); 41 | } 42 | 43 | render() { 44 | const { 45 | primarySort, 46 | magicSorts, 47 | isActive, 48 | isReverse, 49 | onSort, 50 | onSetMagic, 51 | suffix, 52 | children 53 | } = this.props; 54 | const { isColumnSelectorShown } = this.state; 55 | const onSortPrimary = (newIsReverse) => onSort(primarySort.sortKey, primarySort.sortFn, newIsReverse); 56 | const handlePrimarySortClickAsc = () => onSortPrimary(false); 57 | const handlePrimarySortClickDesc = () => onSortPrimary(true); 58 | const handleMagicSortClick = (magicSort) => { 59 | const wasSortingActive = isActive(primarySort.sortKey); 60 | onSetMagic(magicSort.sortKey); 61 | if (wasSortingActive) { 62 | // Sort by the newly-selected column if sorting was active before. 63 | onSort(magicSort.sortKey, magicSort.sortFn, isReverse); 64 | } 65 | }; 66 | const toggleColumnSelector = () => this.setColumnSelectorShown(!isColumnSelectorShown); 67 | return ( 68 |
75 | { this.buttonNode = ref }}> 86 | {primarySort.label} 87 | {children} 88 | 89 | 90 |
{ this.columnSelectorNode = node; }} 95 | role="menu"> 96 | 121 | 139 |
140 |
141 | ); 142 | } 143 | } 144 | 145 | CellMagicHeader.propTypes = { 146 | primarySort: PropTypes.object.isRequired, 147 | magicSorts: PropTypes.array.isRequired, 148 | isActive: PropTypes.func.isRequired, 149 | isReverse: PropTypes.bool, 150 | onSort: PropTypes.func.isRequired, 151 | onSetMagic: PropTypes.func.isRequired, 152 | children: PropTypes.node, 153 | }; 154 | 155 | export default CellMagicHeader; 156 | -------------------------------------------------------------------------------- /src/components/CellMagicHeader/style.less: -------------------------------------------------------------------------------- 1 | .react-redux-composable-list-row-magic-header { 2 | position: relative; 3 | display: flex; 4 | 5 | a { 6 | cursor: pointer; 7 | 8 | span { 9 | display: inline-block; 10 | } 11 | } 12 | 13 | & > * { 14 | display: inline; 15 | } 16 | } 17 | 18 | .react-redux-composable-list-row-magic-header-inline { 19 | box-sizing: border-box; 20 | 21 | &:hover { 22 | text-decoration: none; 23 | } 24 | } 25 | 26 | .react-redux-composable-list-row-magic-header-active { 27 | font-weight: 600; 28 | } 29 | 30 | .react-redux-composable-list-row-magic-header-custom-column { 31 | .react-redux-composable-list-row-magic-header-custom-column-selector-info { 32 | color: #828282; 33 | } 34 | } 35 | 36 | .react-redux-composable-list-row-magic-header-custom-column-selector { 37 | opacity: 0; 38 | visibility: hidden; 39 | position: absolute; 40 | z-index: 2; 41 | left: -(10px); 42 | top: calc(100% - 5px); 43 | min-width: calc(100% + 10px); 44 | margin: 10px 0 0 0; 45 | padding: 10px 0; 46 | background: #FFFFFF; 47 | box-shadow: 0 1px 5px rgba(0, 0, 0, 0.3); 48 | border-radius: 0 0 3px 3px; 49 | transition: all .2s; 50 | 51 | ul { 52 | list-style: none; 53 | padding: 0; 54 | margin: 0; 55 | 56 | + ul { 57 | margin-top: 10px; 58 | } 59 | } 60 | 61 | li { 62 | margin: 0; 63 | cursor: pointer; 64 | 65 | a, &.react-redux-composable-list-row-magic-header-custom-column-selector-info { 66 | display: block; 67 | width: 100%; 68 | padding: 5px 10px; 69 | } 70 | 71 | a:hover { 72 | background: #EBEDEE; 73 | } 74 | 75 | &.react-redux-composable-list-row-magic-header-custom-column-selector-info { 76 | padding-top: 0; 77 | cursor: default; 78 | } 79 | } 80 | } 81 | 82 | .react-redux-composable-list-row-magic-header-custom-column-selector-shown { 83 | opacity: 1; 84 | visibility: visible; 85 | } 86 | -------------------------------------------------------------------------------- /src/components/CellSelected/container.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import { connect } from 'react-redux'; 3 | import { getContext } from '../../helper/util/getContext'; 4 | 5 | import { selectors } from '../../ducks'; 6 | import CellSelected from './presenter'; 7 | import { select } from '../../helper/services'; 8 | 9 | const mapStateToProps = (state, { stateKey, id, preselected = [], unselectables = [] }) => { 10 | const isSelected = selectors.getIsSelected(state, stateKey, id); 11 | 12 | return { 13 | state: select.getSelectState(id, isSelected, preselected, unselectables), 14 | }; 15 | } 16 | 17 | const contextTypes = { 18 | stateKey: PropTypes.string.isRequired, 19 | preselected: PropTypes.array, 20 | unselectables: PropTypes.array, 21 | }; 22 | 23 | export default getContext(contextTypes)(connect(mapStateToProps)(CellSelected)); 24 | -------------------------------------------------------------------------------- /src/components/CellSelected/index.js: -------------------------------------------------------------------------------- 1 | import CellSelected from './container'; 2 | 3 | export default CellSelected; 4 | -------------------------------------------------------------------------------- /src/components/CellSelected/presenter.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | 4 | const CellSelected = ({ state, children }) => 5 |
6 | {children[state]} 7 |
; 8 | 9 | CellSelected.propTypes = { 10 | state: PropTypes.string.isRequired, 11 | children: PropTypes.object.isRequired, 12 | }; 13 | 14 | export default CellSelected; 15 | -------------------------------------------------------------------------------- /src/components/Enhanced/index.js: -------------------------------------------------------------------------------- 1 | import Enhanced from './presenter'; 2 | 3 | export default Enhanced; 4 | -------------------------------------------------------------------------------- /src/components/Enhanced/presenter.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React, { Component } from 'react'; 3 | import '../style.less'; 4 | 5 | class Enhanced extends Component { 6 | getChildContext() { 7 | const { stateKey } = this.props; 8 | 9 | return { 10 | stateKey, 11 | }; 12 | } 13 | 14 | render() { 15 | const { 16 | style, 17 | className, 18 | children, 19 | } = this.props; 20 | 21 | return ( 22 |
27 | {children} 28 |
29 | ); 30 | } 31 | } 32 | 33 | Enhanced.defaultProps = { 34 | className: '', 35 | }; 36 | 37 | Enhanced.propTypes = { 38 | stateKey: PropTypes.string.isRequired, 39 | style: PropTypes.object, 40 | className: PropTypes.string, 41 | children: PropTypes.node, 42 | }; 43 | 44 | Enhanced.childContextTypes = { 45 | stateKey: PropTypes.string, 46 | }; 47 | 48 | export default Enhanced; 49 | -------------------------------------------------------------------------------- /src/components/HeaderCell/index.js: -------------------------------------------------------------------------------- 1 | import HeaderCell from './presenter'; 2 | 3 | export default HeaderCell; 4 | -------------------------------------------------------------------------------- /src/components/HeaderCell/presenter.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | import '../style.less'; 4 | 5 | const isChildString = children => typeof children === 'string'; 6 | 7 | const HeaderCell = ({ 8 | style, 9 | className = '', 10 | children 11 | }) => 12 |
17 | {children} 18 |
; 19 | 20 | HeaderCell.propTypes = { 21 | style: PropTypes.object, 22 | className: PropTypes.string, 23 | children: PropTypes.node, 24 | }; 25 | 26 | export default HeaderCell; 27 | -------------------------------------------------------------------------------- /src/components/Pagination/container.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { bindActionCreators } from 'redux'; 3 | import { actionCreators } from '../../ducks'; 4 | import Pagination from './presenter'; 5 | 6 | const mapDispatchToProps = (dispatch, { stateKey }) => ({ 7 | onPaginate: bindActionCreators((page) => actionCreators.doSetPage(stateKey, page), dispatch), 8 | }); 9 | 10 | export default connect(null, mapDispatchToProps)(Pagination); 11 | -------------------------------------------------------------------------------- /src/components/Pagination/index.js: -------------------------------------------------------------------------------- 1 | import Pagination from './container'; 2 | 3 | export default Pagination; 4 | -------------------------------------------------------------------------------- /src/components/Pagination/presenter.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | 4 | import { noop } from '../../helper/util/noop'; 5 | 6 | import './style.less'; 7 | 8 | function getTooltip(page, step, get = noop) { 9 | const fromValue = get(step[0]); 10 | const toValue = get(step[step.length - 1]); 11 | 12 | return !(fromValue || toValue) 13 | ? `Page ${page + 1}` 14 | : `Page ${page + 1} - From ${fromValue || 'N/A'} to ${toValue || 'N/A'}`; 15 | } 16 | 17 | const Step = ({ 18 | step, 19 | currentPage, 20 | page, 21 | get, 22 | onPaginate, 23 | dotted 24 | }) => { 25 | const tooltip = getTooltip(page, step, get); 26 | 27 | const btnClass = ['button']; 28 | if (page === currentPage) { 29 | btnClass.push('is-selected'); 30 | } 31 | 32 | return ( 33 | 43 | ); 44 | }; 45 | 46 | Step.propTypes = { 47 | step: PropTypes.array.isRequired, 48 | currentPage: PropTypes.number.isRequired, 49 | page: PropTypes.number.isRequired, 50 | get: PropTypes.func, 51 | onPaginate: PropTypes.func.isRequired, 52 | dotted: PropTypes.bool.isRequired, 53 | }; 54 | 55 | const Pagination = ({ 56 | paginatedLists, 57 | currentPage, 58 | get, 59 | onPaginate, 60 | dotted 61 | }) => { 62 | if (paginatedLists.length < 2) { 63 | return null; 64 | } 65 | 66 | const paginationClass = []; 67 | 68 | dotted 69 | ? paginationClass.push('react-redux-composable-list-row-pagination-dot-container') 70 | : paginationClass.push('react-redux-composable-list-row-pagination-button-container'); 71 | 72 | return ( 73 |
74 | {paginatedLists.map((step, key) => { 75 | const props = { 76 | step, 77 | currentPage, 78 | get, 79 | page: key, 80 | onPaginate, 81 | dotted 82 | }; 83 | return ; 84 | })} 85 |
86 | ); 87 | } 88 | 89 | Pagination.propTypes = { 90 | paginatedLists: PropTypes.array.isRequired, 91 | currentPage: PropTypes.number.isRequired, 92 | get: PropTypes.func, 93 | onPaginate: PropTypes.func.isRequired, 94 | dotted: PropTypes.bool.isRequired, 95 | }; 96 | 97 | export default Pagination; 98 | -------------------------------------------------------------------------------- /src/components/Pagination/style.less: -------------------------------------------------------------------------------- 1 | .react-redux-composable-list-row-pagination-button-container, 2 | .react-redux-composable-list-row-pagination-dot-container { 3 | text-align: center; 4 | 5 | button { 6 | z-index: 3; 7 | outline: none; 8 | display: inline-block; 9 | padding: 10px; 10 | margin: 10px 10px 0 0; 11 | color: #000; 12 | border-radius: 3px; 13 | text-decoration: none; 14 | font-size: 15px; 15 | font-weight: 400; 16 | cursor: pointer; 17 | line-height: 1; 18 | text-shadow: none; 19 | text-align: center; 20 | background: #f7f7f7; 21 | border: 1px solid; 22 | border-color: rgba(0,0,0,.07); 23 | } 24 | } 25 | 26 | .react-redux-composable-list-row-pagination-button-container { 27 | button { 28 | width: 32px; 29 | padding: 5px 8px; 30 | margin: 10px 2px; 31 | 32 | &.is-selected, &:focus { 33 | background-color: #777; 34 | color: white; 35 | } 36 | } 37 | } 38 | 39 | .react-redux-composable-list-row-pagination-dot-container { 40 | height: 0; 41 | 42 | button { 43 | background: none; 44 | border: none; 45 | margin: 0; 46 | padding: 5px; 47 | transform: translateY(-27px); 48 | 49 | span { 50 | display: block; 51 | background: darken(#f7f7f7, 20); 52 | border-radius: 100%; 53 | width: 7px; 54 | height: 7px; 55 | transition: all .1s; 56 | } 57 | 58 | &.is-selected, &:focus { 59 | span { 60 | background: darken(#f7f7f7, 51); 61 | transform: scale(1.25); 62 | } 63 | } 64 | 65 | &:hover, &:focus { 66 | background: none; 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/components/Row/container.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import { connect } from 'react-redux'; 3 | import { bindActionCreators } from 'redux'; 4 | import { getContext } from '../../helper/util/getContext'; 5 | 6 | import { actionCreators, selectors } from '../../ducks'; 7 | import RowSelectable from './presenter'; 8 | import { select } from '../../helper/services'; 9 | import { noop } from '../../helper/util/noop'; 10 | 11 | const isSelectableRow = (isSelectable, id) => 12 | isSelectable && !(id === undefined || id === null); 13 | 14 | const mapStateToProps = ( 15 | state, { 16 | stateKey, 17 | id, 18 | isSelectable = false, 19 | preselected = [], 20 | unselectables = [] 21 | }) => { 22 | const hasSelectableRow = isSelectableRow(isSelectable, id); 23 | 24 | const isSelected = hasSelectableRow 25 | ? selectors.getIsSelected(state, stateKey, id) 26 | : false; 27 | 28 | const selectState = hasSelectableRow 29 | ? select.getSelectState(id, isSelected, preselected, unselectables) 30 | : null; 31 | 32 | return { 33 | isSelectable, 34 | selectState, 35 | }; 36 | } 37 | 38 | const mapDispatchToProps = (dispatch, { stateKey, isSelectable, id, preselected, unselectables, allIds }) => 39 | isSelectableRow(isSelectable, id) ? ({ 40 | onSelect: bindActionCreators(() => actionCreators.doSelectItem(stateKey, id), dispatch), 41 | onShiftSelect: bindActionCreators(() => actionCreators.doSelectItemsRange(stateKey, id, preselected, unselectables, allIds), dispatch), 42 | onSelectItems: bindActionCreators((ids) => actionCreators.doSelectItems(stateKey, ids, true), dispatch), 43 | }) : ({ 44 | onSelect: noop, 45 | onShiftSelect: noop, 46 | onSelectItems: noop, 47 | }); 48 | 49 | const contextTypes = { 50 | stateKey: PropTypes.string.isRequired, 51 | isSelectable: PropTypes.bool, 52 | preselected: PropTypes.array, 53 | unselectables: PropTypes.array, 54 | allIds: PropTypes.array, 55 | }; 56 | 57 | export default getContext(contextTypes)(connect(mapStateToProps, mapDispatchToProps)(RowSelectable)); 58 | -------------------------------------------------------------------------------- /src/components/Row/index.js: -------------------------------------------------------------------------------- 1 | import RowSelectable from './container'; 2 | 3 | export default RowSelectable; 4 | -------------------------------------------------------------------------------- /src/components/Row/presenter.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | 4 | import { select } from '../../helper/services'; 5 | 6 | import './style.less'; 7 | 8 | const CLASS_MAPPING = { 9 | [select.SELECT_STATES.selected]: 'react-redux-composable-list-row-selected', 10 | [select.SELECT_STATES.notSelected]: 'react-redux-composable-list-row-selectable', 11 | [select.SELECT_STATES.preSelected]: 'react-redux-composable-list-row-unselectable', 12 | [select.SELECT_STATES.unselectable]: 'react-redux-composable-list-row-unselectable', 13 | }; 14 | 15 | const Row = ({ 16 | isSelectable, 17 | ...props 18 | }) => 19 | isSelectable 20 | ? 21 | : ; 22 | 23 | Row.propTypes = { 24 | isSelectable: PropTypes.bool, 25 | style: PropTypes.object, 26 | className: PropTypes.string, 27 | children: PropTypes.oneOfType([ 28 | PropTypes.arrayOf(PropTypes.node), 29 | PropTypes.node 30 | ]), 31 | }; 32 | 33 | const RowSelectable = ({ 34 | selectState, 35 | onSelect, 36 | onShiftSelect, 37 | children, 38 | isHeader 39 | }) => { 40 | const rowClass = ['react-redux-composable-list-row', CLASS_MAPPING[selectState]]; 41 | if (isHeader) { 42 | rowClass.push('react-redux-composable-list-row-header'); 43 | } 44 | const hasSelectState = selectState === select.SELECT_STATES.selected || 45 | selectState === select.SELECT_STATES.notSelected; 46 | const handleClick = event => { 47 | if (!hasSelectState) { 48 | return; 49 | } 50 | return event && event.shiftKey ? onShiftSelect() : onSelect(); 51 | }; 52 | return ( 53 |
58 | {children} 59 |
60 | ); 61 | }; 62 | 63 | RowSelectable.propTypes = { 64 | selectState: PropTypes.string, 65 | onSelect: PropTypes.func.isRequired, 66 | onShiftSelect: PropTypes.func.isRequired, 67 | children: PropTypes.oneOfType([ 68 | PropTypes.arrayOf(PropTypes.node), 69 | PropTypes.node 70 | ]), 71 | isHeader: PropTypes.bool 72 | }; 73 | 74 | const RowNormal = ({ 75 | style, 76 | className = '', 77 | children, 78 | isHeader 79 | }) => { 80 | const classNameContainer = ['react-redux-composable-list-row']; 81 | 82 | if (className) { 83 | classNameContainer.push(className); 84 | } 85 | 86 | if (isHeader) { 87 | classNameContainer.push('react-redux-composable-list-row-header'); 88 | } 89 | 90 | return ( 91 |
96 | {children} 97 |
98 | ) 99 | }; 100 | 101 | RowNormal.propTypes = { 102 | style: PropTypes.object, 103 | className: PropTypes.string, 104 | children: PropTypes.oneOfType([ 105 | PropTypes.arrayOf(PropTypes.node), 106 | PropTypes.node 107 | ]), 108 | isHeader: PropTypes.bool 109 | }; 110 | 111 | export default Row; 112 | -------------------------------------------------------------------------------- /src/components/Row/style.less: -------------------------------------------------------------------------------- 1 | .react-redux-composable-list-row-selectable, .react-redux-composable-list-row-selected { 2 | cursor: pointer; 3 | } 4 | 5 | .react-redux-composable-list-row-selectable { 6 | &:hover { 7 | background: #eff7ff; 8 | } 9 | } 10 | 11 | .react-redux-composable-list-row-selected { 12 | background-color: #C9E5FF; 13 | 14 | &:hover { 15 | background: #add9ff; 16 | } 17 | } 18 | 19 | .react-redux-composable-list-row-unselectable { 20 | cursor: default; 21 | color: #aaaaaa; 22 | } 23 | -------------------------------------------------------------------------------- /src/components/Sort/container.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import { connect } from 'react-redux'; 3 | import { bindActionCreators } from 'redux'; 4 | import { getContext } from '../../helper/util/getContext'; 5 | import { actionCreators, selectors } from '../../ducks'; 6 | import Sort from './presenter'; 7 | 8 | function mapStateToProps(state, { sortKey, stateKey }) { 9 | const { sortKey: stateSortKey, isReverse } = selectors.getSort(state, stateKey); 10 | return { 11 | isActive: sortKey === stateSortKey, 12 | isReverse, 13 | }; 14 | } 15 | 16 | function mapDispatchToProps(dispatch, { sortKey, sortFn, stateKey }) { 17 | return { 18 | onSort: bindActionCreators(() => actionCreators.doTableSort(stateKey, sortKey, sortFn), dispatch), 19 | }; 20 | } 21 | 22 | const contextTypes = { 23 | stateKey: PropTypes.string.isRequired 24 | }; 25 | 26 | export default getContext(contextTypes)(connect(mapStateToProps, mapDispatchToProps)(Sort)); 27 | -------------------------------------------------------------------------------- /src/components/Sort/index.js: -------------------------------------------------------------------------------- 1 | import Sort from './container'; 2 | 3 | export default Sort; 4 | -------------------------------------------------------------------------------- /src/components/Sort/presenter.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | 4 | import './style.less'; 5 | 6 | import SortCaret from '../../helper/components/SortCaret'; 7 | import { sort } from '../../helper/services'; 8 | 9 | const Sort = ({ isActive, isReverse, onSort, suffix, children }) => { 10 | const linkClass = ['react-redux-composable-list-sort']; 11 | if (isActive) { 12 | linkClass.push('react-redux-composable-list-sort-active'); 13 | } 14 | return ( 15 | 28 | ); 29 | } 30 | 31 | Sort.propTypes = { 32 | isActive: PropTypes.bool, 33 | isReverse: PropTypes.bool, 34 | onSort: PropTypes.func, 35 | children: PropTypes.node, 36 | }; 37 | 38 | export default Sort; 39 | -------------------------------------------------------------------------------- /src/components/Sort/style.less: -------------------------------------------------------------------------------- 1 | .react-redux-composable-list-sort { 2 | cursor: pointer; 3 | 4 | span { 5 | display: inline-block;; 6 | } 7 | 8 | &:hover { 9 | text-decoration: none; 10 | } 11 | } 12 | 13 | .react-redux-composable-list-sort-active { 14 | font-weight: 600; 15 | } 16 | -------------------------------------------------------------------------------- /src/components/SortSelected/container.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import { connect } from 'react-redux'; 3 | import { bindActionCreators } from 'redux'; 4 | import { getContext } from '../../helper/util/getContext'; 5 | 6 | import { actionCreators, selectors } from '../../ducks'; 7 | import Sort from '../Sort/presenter'; 8 | 9 | const mapStateToProps = (state, { sortKey, stateKey }) => { 10 | const { sortKey: stateSortKey, isReverse } = selectors.getSort(state, stateKey); 11 | const isActive = stateSortKey === sortKey; 12 | const selection = selectors.getSelection(state, stateKey); 13 | return { 14 | isActive, 15 | selection, 16 | isReverse, 17 | }; 18 | }; 19 | 20 | const mapDispatchToProps = (dispatch, { sortKey, stateKey }) => ({ 21 | onSort: bindActionCreators((sortFn) => actionCreators.doTableSort(stateKey, sortKey, sortFn), dispatch), 22 | }); 23 | 24 | const mergeProps = (stateProps, dispatchProps, ownProps) => { 25 | const { selection, ...others } = stateProps; 26 | const { onSort } = dispatchProps; 27 | const sortFn = (item) => selection.indexOf(item.id) !== -1; 28 | return { 29 | ...ownProps, 30 | ...others, 31 | onSort: () => onSort(sortFn), 32 | }; 33 | } 34 | 35 | const contextTypes = { 36 | stateKey: PropTypes.string.isRequired 37 | }; 38 | 39 | export default getContext(contextTypes)(connect(mapStateToProps, mapDispatchToProps, mergeProps)(Sort)); 40 | -------------------------------------------------------------------------------- /src/components/SortSelected/index.js: -------------------------------------------------------------------------------- 1 | import SortSelected from './container'; 2 | 3 | export default SortSelected; 4 | -------------------------------------------------------------------------------- /src/components/index.js: -------------------------------------------------------------------------------- 1 | import Enhanced from './Enhanced'; 2 | import Row from './Row'; 3 | import Cell from './Cell'; 4 | import HeaderCell from './HeaderCell'; 5 | 6 | import Pagination from './Pagination'; 7 | 8 | import Sort from './Sort'; 9 | 10 | import SortSelected from './SortSelected'; 11 | import CellSelected from './CellSelected'; 12 | 13 | import CellMagic from './CellMagic'; 14 | import CellMagicHeader from './CellMagicHeader'; 15 | 16 | export { 17 | Enhanced, 18 | Row, 19 | Cell, 20 | HeaderCell, 21 | 22 | Pagination, 23 | 24 | Sort, 25 | 26 | SortSelected, 27 | CellSelected, 28 | 29 | CellMagicHeader, 30 | CellMagic, 31 | }; 32 | -------------------------------------------------------------------------------- /src/components/style.less: -------------------------------------------------------------------------------- 1 | .react-redux-composable-list-cell-body { 2 | overflow: hidden; 3 | text-overflow: ellipsis; 4 | } 5 | 6 | .react-redux-composable-list-cell { 7 | padding: 5px; 8 | display: inline-block; 9 | } 10 | 11 | .react-redux-composable-list-row { 12 | display: flex; 13 | min-height: 36px; 14 | line-height: 24px; 15 | white-space: nowrap; 16 | } 17 | 18 | .react-redux-composable-list-row-header { 19 | border-bottom: 1px solid #DADEE1; 20 | } 21 | 22 | .react-redux-composable-list { 23 | width: 100%; 24 | border-collapse: collapse; 25 | table-layout: fixed; 26 | } 27 | -------------------------------------------------------------------------------- /src/ducks/filter/index.js: -------------------------------------------------------------------------------- 1 | import { applyResetByStateKeys, RESET_BY_STATE_KEYS } from '../reset'; 2 | 3 | import { omit } from '../../helper/util/omit'; 4 | 5 | const SLICE_NAME = 'tableFilter'; 6 | 7 | const FILTER_SET = `${SLICE_NAME}/FILTER_SET`; 8 | const FILTER_REMOVE = `${SLICE_NAME}/FILTER_REMOVE`; 9 | const FILTER_RESET = `${SLICE_NAME}/FILTER_RESET`; 10 | 11 | const INITIAL_STATE = {}; 12 | 13 | function doSetFilter(stateKey, key, fn) { 14 | return { 15 | type: FILTER_SET, 16 | payload: { 17 | stateKey, 18 | fn, 19 | key, 20 | } 21 | }; 22 | } 23 | 24 | function doRemoveFilter(stateKey, key) { 25 | return { 26 | type: FILTER_REMOVE, 27 | payload: { 28 | stateKey, 29 | key, 30 | } 31 | }; 32 | } 33 | 34 | function doResetFilter(stateKey) { 35 | return { 36 | type: FILTER_RESET, 37 | payload: { 38 | stateKey, 39 | } 40 | }; 41 | } 42 | 43 | const reducer = (state = INITIAL_STATE, action) => { 44 | switch (action.type) { 45 | case FILTER_SET: 46 | return applySetFilter(state, action); 47 | case FILTER_REMOVE: 48 | return applyRemoveFilter(state, action); 49 | case FILTER_RESET: 50 | return applyResetFilter(state, action); 51 | case RESET_BY_STATE_KEYS: 52 | return applyResetByStateKeys(state, action); 53 | } 54 | return state; 55 | }; 56 | 57 | function applySetFilter(state, action) { 58 | const { stateKey, fn, key } = action.payload; 59 | const container = getContainer(state, stateKey); 60 | return { 61 | ...state, 62 | [stateKey]: { 63 | ...container, 64 | [key]: fn, 65 | } 66 | }; 67 | } 68 | 69 | function applyRemoveFilter(state, action) { 70 | const { stateKey, key } = action.payload; 71 | const container = getContainer(state, stateKey); 72 | return { 73 | ...state, 74 | [stateKey]: { 75 | ...omit(container, key), 76 | } 77 | }; 78 | } 79 | 80 | function applyResetFilter(state, action) { 81 | const { stateKey } = action.payload; 82 | return { ...state, [stateKey]: [] }; 83 | } 84 | 85 | function getContainer(state, stateKey) { 86 | return state[stateKey] || []; 87 | } 88 | 89 | function getFilters(state, stateKey) { 90 | const filters = state[SLICE_NAME][stateKey] || {}; 91 | return Object.keys(filters).map(value => filters[value]); 92 | } 93 | 94 | const selectors = { 95 | getFilters 96 | }; 97 | 98 | const actionCreators = { 99 | doSetFilter, 100 | doRemoveFilter, 101 | doResetFilter 102 | }; 103 | 104 | const reducers = { [SLICE_NAME]: reducer }; 105 | 106 | export { 107 | reducers, 108 | selectors, 109 | actionCreators 110 | }; 111 | -------------------------------------------------------------------------------- /src/ducks/filter/spec.js: -------------------------------------------------------------------------------- 1 | import deepFreeze from 'deep-freeze'; 2 | import { reducers, actionCreators } from '../filter'; 3 | 4 | const STATE_KEY = 'SOME_KEY'; 5 | 6 | describe('filter', () => { 7 | describe('FILTER_SET', () => { 8 | it('sets a filter', () => { 9 | const key = 'name'; 10 | const fn = foo => foo; 11 | const previousState = {}; 12 | const expectedState = { [STATE_KEY]: { [key]: fn } }; 13 | const action = actionCreators.doSetFilter(STATE_KEY, key, fn); 14 | deepFreeze(action); 15 | deepFreeze(previousState); 16 | expect(reducers.tableFilter(previousState, action)).to.eql(expectedState); 17 | }); 18 | 19 | it('sets a second filter', () => { 20 | const keyOne = 'name'; 21 | const fnOne = foo => foo; 22 | const keyTwo = 'age'; 23 | const fnTwo = bar => bar; 24 | const previousState = { 25 | [STATE_KEY]: { 26 | [keyOne]: fnOne, 27 | } 28 | }; 29 | const expectedState = { 30 | [STATE_KEY]: { 31 | [keyOne]: fnOne, 32 | [keyTwo]: fnTwo, 33 | } 34 | }; 35 | const action = actionCreators.doSetFilter(STATE_KEY, keyTwo, fnTwo); 36 | deepFreeze(action); 37 | deepFreeze(previousState); 38 | expect(reducers.tableFilter(previousState, action)).to.eql(expectedState); 39 | }); 40 | }); 41 | 42 | describe('FILTER_REMOVE', () => { 43 | it('removes a filter', () => { 44 | const keyOne = 'name'; 45 | const fnOne = foo => foo; 46 | const keyTwo = 'age'; 47 | const fnTwo = bar => bar; 48 | const previousState = { 49 | [STATE_KEY]: { 50 | [keyOne]: fnOne, 51 | [keyTwo]: fnTwo, 52 | } 53 | }; 54 | const expectedState = { 55 | [STATE_KEY]: { 56 | [keyOne]: fnOne, 57 | } 58 | }; 59 | const action = actionCreators.doRemoveFilter(STATE_KEY, keyTwo); 60 | deepFreeze(action); 61 | deepFreeze(previousState); 62 | expect(reducers.tableFilter(previousState, action)).to.eql(expectedState); 63 | }); 64 | }); 65 | 66 | describe('FILTER_RESET', () => { 67 | it('resets all filters', () => { 68 | const keyOne = 'name'; 69 | const fnOne = foo => foo; 70 | const keyTwo = 'age'; 71 | const fnTwo = bar => bar; 72 | const previousState = { 73 | [STATE_KEY]: { 74 | [keyOne]: fnOne, 75 | [keyTwo]: fnTwo, 76 | } 77 | }; 78 | const expectedState = { 79 | [STATE_KEY]: [], 80 | }; 81 | const action = actionCreators.doResetFilter(STATE_KEY); 82 | deepFreeze(action); 83 | deepFreeze(previousState); 84 | expect(reducers.tableFilter(previousState, action)).to.eql(expectedState); 85 | }); 86 | }); 87 | }); 88 | -------------------------------------------------------------------------------- /src/ducks/index.js: -------------------------------------------------------------------------------- 1 | import * as sortDuck from './sort'; 2 | import * as filterDuck from './filter'; 3 | import * as selectDuck from './select'; 4 | import * as magicDuck from './magic'; 5 | import * as paginateDuck from './paginate'; 6 | 7 | const reducers = { 8 | ...sortDuck.reducers, 9 | ...filterDuck.reducers, 10 | ...selectDuck.reducers, 11 | ...magicDuck.reducers, 12 | ...paginateDuck.reducers, 13 | }; 14 | 15 | const actionCreators = { 16 | ...sortDuck.actionCreators, 17 | ...filterDuck.actionCreators, 18 | ...selectDuck.actionCreators, 19 | ...magicDuck.actionCreators, 20 | ...paginateDuck.actionCreators, 21 | }; 22 | 23 | const selectors = { 24 | ...sortDuck.selectors, 25 | ...filterDuck.selectors, 26 | ...selectDuck.selectors, 27 | ...magicDuck.selectors, 28 | ...paginateDuck.selectors, 29 | }; 30 | 31 | export { 32 | reducers, 33 | actionCreators, 34 | selectors, 35 | }; 36 | -------------------------------------------------------------------------------- /src/ducks/magic/index.js: -------------------------------------------------------------------------------- 1 | import { applyResetByStateKeys, RESET_BY_STATE_KEYS } from '../reset'; 2 | 3 | const SLICE_NAME = 'tableMagicSort'; 4 | 5 | const TABLE_SET_MAGIC = `${SLICE_NAME}/TABLE_SET_MAGIC`; 6 | 7 | const INITIAL_STATE = {}; 8 | 9 | function doSetMagicSort(stateKey, sortKey) { 10 | return { 11 | type: TABLE_SET_MAGIC, 12 | payload: { 13 | stateKey, 14 | sortKey 15 | } 16 | } 17 | } 18 | 19 | const reducer = (state = INITIAL_STATE, action) => { 20 | switch (action.type) { 21 | case TABLE_SET_MAGIC: 22 | return applySetMagic(state, action); 23 | case RESET_BY_STATE_KEYS: 24 | return applyResetByStateKeys(state, action); 25 | } 26 | return state; 27 | }; 28 | 29 | function applySetMagic(state, action) { 30 | const { stateKey, sortKey } = action.payload; 31 | return { ...state, [stateKey]: sortKey }; 32 | } 33 | 34 | function getMagicSort(state, stateKey, sorts) { 35 | return state[SLICE_NAME][stateKey] || sorts[0].sortKey; 36 | } 37 | 38 | const selectors = { 39 | getMagicSort, 40 | }; 41 | 42 | const actionCreators = { 43 | doSetMagicSort, 44 | }; 45 | 46 | const reducers = { [SLICE_NAME]: reducer }; 47 | 48 | const actionTypes = { 49 | TABLE_SET_MAGIC, 50 | }; 51 | 52 | export { 53 | reducers, 54 | selectors, 55 | actionCreators, 56 | actionTypes 57 | }; 58 | -------------------------------------------------------------------------------- /src/ducks/magic/spec.js: -------------------------------------------------------------------------------- 1 | import deepFreeze from 'deep-freeze'; 2 | import { reducers, actionCreators } from '../magic'; 3 | 4 | const STATE_KEY = 'SOME_KEY'; 5 | 6 | describe('magic', () => { 7 | describe('TABLE_SET_MAGIC', () => { 8 | it('sets a magic column sort', () => { 9 | const sortKey = 'name'; 10 | const previousState = {}; 11 | const expectedState = { [STATE_KEY]: sortKey }; 12 | const action = actionCreators.doSetMagicSort(STATE_KEY, sortKey); 13 | deepFreeze(action); 14 | deepFreeze(previousState); 15 | expect(reducers.tableMagicSort(previousState, action)).to.eql(expectedState); 16 | }); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/ducks/paginate/index.js: -------------------------------------------------------------------------------- 1 | import { applyResetByStateKeys, RESET_BY_STATE_KEYS } from '../reset'; 2 | 3 | const SLICE_NAME = 'tablePaginate'; 4 | 5 | const PAGINATION_SET = `${SLICE_NAME}/PAGINATION_SET`; 6 | 7 | const INITIAL_STATE = {}; 8 | 9 | function doSetPage(stateKey, page) { 10 | return { 11 | type: PAGINATION_SET, 12 | payload: { 13 | stateKey, 14 | page 15 | } 16 | }; 17 | } 18 | 19 | const reducer = (state = INITIAL_STATE, action) => { 20 | switch (action.type) { 21 | case PAGINATION_SET: 22 | return applyPage(state, action); 23 | case RESET_BY_STATE_KEYS: 24 | return applyResetByStateKeys(state, action); 25 | } 26 | return state; 27 | }; 28 | 29 | function applyPage(state, action) { 30 | const { stateKey, page } = action.payload; 31 | return { ...state, [stateKey]: page }; 32 | } 33 | 34 | function getCurrentPage(globalState, stateKey, paginatedLists) { 35 | const currentPage = globalState[SLICE_NAME][stateKey]; 36 | return currentPage ? fallbackDefault(currentPage, paginatedLists) : 0; 37 | } 38 | 39 | function fallbackDefault(currentPage, paginatedLists) { 40 | return currentPage < paginatedLists.length ? currentPage : paginatedLists.length - 1; 41 | } 42 | 43 | const selectors = { 44 | getCurrentPage 45 | }; 46 | 47 | const actionCreators = { 48 | doSetPage 49 | }; 50 | 51 | const reducers = { [SLICE_NAME]: reducer }; 52 | 53 | const actionTypes = { 54 | PAGINATION_SET 55 | }; 56 | 57 | export { 58 | reducers, 59 | actionCreators, 60 | actionTypes, 61 | selectors 62 | }; 63 | -------------------------------------------------------------------------------- /src/ducks/paginate/spec.js: -------------------------------------------------------------------------------- 1 | import deepFreeze from 'deep-freeze'; 2 | import { reducers, actionCreators } from '../paginate'; 3 | 4 | const STATE_KEY = 'SOME_KEY'; 5 | 6 | describe('pagination', () => { 7 | describe('PAGINATION_SET', () => { 8 | it('sets a page', () => { 9 | const page = 5; 10 | const previousState = {}; 11 | const expectedState = { [STATE_KEY]: page }; 12 | const action = actionCreators.doSetPage(STATE_KEY, page); 13 | deepFreeze(action); 14 | deepFreeze(previousState); 15 | expect(reducers.tablePaginate(previousState, action)).to.eql(expectedState); 16 | }); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/ducks/reset/index.js: -------------------------------------------------------------------------------- 1 | export const RESET_BY_STATE_KEYS = 'RESET_BY_STATE_KEYS'; 2 | 3 | export function applyResetByStateKeys(state, { payload }) { 4 | const toReset = payload.reduce((mem, key) => { 5 | mem[key] = undefined; 6 | return mem; 7 | }, {}); 8 | return { ...state, ...toReset }; 9 | } 10 | -------------------------------------------------------------------------------- /src/ducks/reset/spec.js: -------------------------------------------------------------------------------- 1 | import deepFreeze from 'deep-freeze'; 2 | import { applyResetByStateKeys } from '../reset'; 3 | 4 | const STATE_KEY = 'SOME_KEY'; 5 | const OTHER_STATE_KEY = 'SOME_OTHER_KEY'; 6 | const ANOTHER_STATE_KEY = 'SOME_ANOTHER_KEY'; 7 | 8 | describe('reset', () => { 9 | describe('RESET_BY_STATE_KEYS', () => { 10 | it('resets everything by state key', () => { 11 | 12 | const previousState = { 13 | [STATE_KEY]: { 14 | foo: 'foo', 15 | bar: 'bar', 16 | }, 17 | [OTHER_STATE_KEY]: { 18 | zoo: 'zoo', 19 | }, 20 | [ANOTHER_STATE_KEY]: { 21 | tyi: 'tyi', 22 | }, 23 | }; 24 | const expectedState = { 25 | [STATE_KEY]: undefined, 26 | [OTHER_STATE_KEY]: { 27 | zoo: 'zoo', 28 | }, 29 | [ANOTHER_STATE_KEY]: undefined, 30 | }; 31 | const action = { 32 | payload: [ STATE_KEY, ANOTHER_STATE_KEY ], 33 | }; 34 | 35 | deepFreeze(previousState); 36 | expect(applyResetByStateKeys(previousState, action)).to.eql(expectedState); 37 | }); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /src/ducks/select/index.js: -------------------------------------------------------------------------------- 1 | import { uniq } from '../../helper/util/uniq'; 2 | 3 | import { applyResetByStateKeys, RESET_BY_STATE_KEYS } from '../reset'; 4 | 5 | const SLICE_NAME = 'tableSelect'; 6 | 7 | const SELECT_ITEM = `${SLICE_NAME}/SELECT_ITEM`; 8 | const SELECT_ITEMS = `${SLICE_NAME}/SELECT_ITEMS`; 9 | const SELECT_ITEMS_RANGE = `${SLICE_NAME}/SELECT_ITEM_RANGE`; 10 | const SELECT_ITEMS_EXCLUSIVELY = `${SLICE_NAME}/SELECT_ITEMS_EXCLUSIVELY`; 11 | const SELECT_ITEMS_RESET = `${SLICE_NAME}/SELECT_ITEMS_RESET`; 12 | 13 | const INITIAL_STATE = {}; 14 | 15 | function doSelectItem(stateKey, id) { 16 | return { 17 | type: SELECT_ITEM, 18 | payload: { 19 | stateKey, 20 | id, 21 | } 22 | }; 23 | } 24 | 25 | function doSelectItems(stateKey, ids, isSelect) { 26 | return { 27 | type: SELECT_ITEMS, 28 | payload: { 29 | stateKey, 30 | ids, 31 | isSelect, 32 | } 33 | }; 34 | } 35 | 36 | function doSelectItemsRange(stateKey, id, preselected, unselectables, allIds) { 37 | return { 38 | type: SELECT_ITEMS_RANGE, 39 | payload: { 40 | stateKey, 41 | id, 42 | preselected, 43 | unselectables, 44 | allIds, 45 | } 46 | }; 47 | } 48 | 49 | function doSelectItemsExclusively(stateKey, ids, isSelect) { 50 | return { 51 | type: SELECT_ITEMS_EXCLUSIVELY, 52 | payload: { 53 | stateKey, 54 | ids, 55 | isSelect, 56 | } 57 | }; 58 | } 59 | 60 | function doSelectItemsReset(stateKey) { 61 | return { 62 | type: SELECT_ITEMS_RESET, 63 | payload: { 64 | stateKey, 65 | } 66 | }; 67 | } 68 | 69 | const reducer = (state = INITIAL_STATE, action) => { 70 | switch (action.type) { 71 | case SELECT_ITEM: 72 | return applySelectItem(state, action); 73 | case SELECT_ITEMS: 74 | return applySelectItems(state, action); 75 | case SELECT_ITEMS_RANGE: 76 | return applySelectItemsRange(state, action); 77 | case SELECT_ITEMS_EXCLUSIVELY: 78 | return applySelectItemsExclusively(state, action); 79 | case SELECT_ITEMS_RESET: 80 | return applyResetSelectedItems(state, action); 81 | case RESET_BY_STATE_KEYS: 82 | return applyResetByStateKeys(state, action); 83 | } 84 | return state; 85 | }; 86 | 87 | function applySelectItem(state, action) { 88 | const { stateKey, id } = action.payload; 89 | const currentSelection = state[stateKey] && state[stateKey].selectedItems ? 90 | state[stateKey].selectedItems : 91 | []; 92 | const index = currentSelection.indexOf(id); 93 | const isSelect = index === -1; 94 | const selectedItems = isSelect ? addItem(currentSelection, id) : removeItem(currentSelection, index); 95 | const lastSelectedItem = isSelect ? id : null; 96 | const lastUnselectedItem = isSelect ? null : id; 97 | return { ...state, [stateKey]: { selectedItems, lastSelectedItem, lastUnselectedItem } }; 98 | } 99 | 100 | function applySelectItems(state, action) { 101 | return toggleItems(state, action, false); 102 | } 103 | 104 | function applySelectItemsRange(state, action) { 105 | const { stateKey, id, preselected, unselectables, allIds } = action.payload; 106 | const currentSelection = state[stateKey] && state[stateKey].selectedItems 107 | ? state[stateKey].selectedItems 108 | : []; 109 | const isSelect = currentSelection.indexOf(id) === -1; 110 | if (allIds && allIds.length) { 111 | let newState = {}; 112 | if (isSelect) { 113 | const lastSelectedItem = state[stateKey].lastSelectedItem || id; 114 | const selectedRange = getSelectedRange(id, lastSelectedItem, preselected, unselectables, allIds); 115 | const selectedItems = uniq([...currentSelection, ...selectedRange]); 116 | newState = { selectedItems, lastSelectedItem }; 117 | } else { 118 | const lastUnselectedItem = state[stateKey].lastUnselectedItem || id; 119 | const unselectedRange = getSelectedRange(id, lastUnselectedItem, preselected, unselectables, allIds); 120 | const selectedItems = removeItems(currentSelection, unselectedRange); 121 | newState = { selectedItems, lastUnselectedItem }; 122 | } 123 | return { ...state, [stateKey]: newState }; 124 | } 125 | // Fallback to selecting a single item if allIds is not provided. 126 | return applySelectItem(state, action); 127 | } 128 | 129 | function getSelectedRange(id, lastSelectedItem, preselected = [], unselectables = [], allIds) { 130 | const lastSelectedItemIndex = allIds.indexOf(lastSelectedItem); 131 | const currentSelectedItemIndex = allIds.indexOf(id); 132 | const firstIndex = Math.min(lastSelectedItemIndex, currentSelectedItemIndex); 133 | const lastIndex = Math.max(lastSelectedItemIndex, currentSelectedItemIndex); 134 | return allIds 135 | .slice(firstIndex, lastIndex + 1) 136 | .filter(id => { 137 | const isSelectable = preselected.indexOf(id) === -1 && unselectables.indexOf(id) === -1; 138 | return isSelectable; 139 | }); 140 | } 141 | 142 | function applySelectItemsExclusively(state, action) { 143 | return toggleItems(state, action, true); 144 | } 145 | 146 | function toggleItems(state, action, selectExclusively) { 147 | let { stateKey, isSelect, ids } = action.payload; 148 | let list = state[stateKey] && state[stateKey].selectedItems ? 149 | state[stateKey].selectedItems : 150 | []; 151 | let selectedItems; 152 | 153 | if (isSelect) { 154 | selectedItems = uniq(selectExclusively ? ids : [...list, ...ids]); 155 | } else { 156 | selectedItems = removeItems(list, ids); 157 | } 158 | 159 | return { ...state, [stateKey]: { selectedItems, lastSelectedItem: null } }; 160 | } 161 | 162 | function applyResetSelectedItems(state, action) { 163 | const { stateKey } = action.payload; 164 | return { ...state, [stateKey]: { selectedItems: [], lastSelectedItem: null } }; 165 | } 166 | 167 | function removeItems(list, ids) { 168 | return ids.reduce((result, value) => { 169 | let index = result.indexOf(value); 170 | result = (index !== -1) ? removeItem(result, index) : result; 171 | return result; 172 | }, list); 173 | } 174 | 175 | function removeItem(list, index) { 176 | return [ 177 | ...list.slice(0, index), 178 | ...list.slice(index + 1) 179 | ]; 180 | } 181 | 182 | function addItem(list, id) { 183 | return [...list, id]; 184 | } 185 | 186 | function getSelection(state, stateKey) { 187 | return state[SLICE_NAME][stateKey] && state[SLICE_NAME][stateKey].selectedItems ? 188 | state[SLICE_NAME][stateKey].selectedItems : 189 | []; 190 | } 191 | 192 | function getLastSelectedItem(state, stateKey) { 193 | return state[SLICE_NAME][stateKey].lastSelectedItem; 194 | } 195 | 196 | function getIsSelected(state, stateKey, id) { 197 | return getSelection(state, stateKey).indexOf(id) !== -1; 198 | } 199 | 200 | const selectors = { 201 | getSelection, 202 | getLastSelectedItem, 203 | getIsSelected 204 | }; 205 | 206 | const actionCreators = { 207 | doSelectItem, 208 | doSelectItems, 209 | doSelectItemsRange, 210 | doSelectItemsExclusively, 211 | doSelectItemsReset 212 | }; 213 | 214 | const reducers = { [SLICE_NAME]: reducer }; 215 | 216 | const actionTypes = { 217 | SELECT_ITEM, 218 | SELECT_ITEMS, 219 | SELECT_ITEMS_EXCLUSIVELY, 220 | SELECT_ITEMS_RESET 221 | }; 222 | 223 | export { 224 | reducers, 225 | selectors, 226 | actionCreators, 227 | actionTypes 228 | }; 229 | -------------------------------------------------------------------------------- /src/ducks/select/spec.js: -------------------------------------------------------------------------------- 1 | import deepFreeze from 'deep-freeze'; 2 | import { reducers, actionCreators } from '../select'; 3 | 4 | const STATE_KEY = 'SOME_KEY'; 5 | 6 | describe('select', () => { 7 | describe('SELECT_ITEM', () => { 8 | it('toggles a single item as selected', () => { 9 | const id = 'x'; 10 | const previousState = {}; 11 | const expectedState = { [STATE_KEY]: { selectedItems: [id], lastSelectedItem: id, lastUnselectedItem: null } }; 12 | const action = actionCreators.doSelectItem(STATE_KEY, id); 13 | deepFreeze(action); 14 | deepFreeze(previousState); 15 | expect(reducers.tableSelect(previousState, action)).to.eql(expectedState); 16 | }); 17 | 18 | it('updates the last selected item when selecting another item', () => { 19 | const previousState = { [STATE_KEY]: { selectedItems: ['x'], lastSelectedItem: 'x', lastUnselectedItem: null } }; 20 | const expectedState = { [STATE_KEY]: { selectedItems: ['x', 'y'], lastSelectedItem: 'y', lastUnselectedItem: null } }; 21 | const action = actionCreators.doSelectItem(STATE_KEY, 'y'); 22 | deepFreeze(action); 23 | deepFreeze(previousState); 24 | expect(reducers.tableSelect(previousState, action)).to.eql(expectedState); 25 | }); 26 | 27 | it('clears the last selected item when unselecting an item', () => { 28 | const previousState = { [STATE_KEY]: { selectedItems: ['x', 'y'], lastSelectedItem: 'x', lastUnselectedItem: null } }; 29 | const expectedState = { [STATE_KEY]: { selectedItems: ['x'], lastSelectedItem: null, lastUnselectedItem: 'y' } }; 30 | const action = actionCreators.doSelectItem(STATE_KEY, 'y'); 31 | deepFreeze(action); 32 | deepFreeze(previousState); 33 | expect(reducers.tableSelect(previousState, action)).to.eql(expectedState); 34 | }); 35 | 36 | it('toggles a single item as unselected, when it was already selected', () => { 37 | const id = 'x'; 38 | const previousState = { [STATE_KEY]: { selectedItems: [id], lastSelectedItem: id, lastUnselectedItem: null } }; 39 | const expectedState = { [STATE_KEY]: { selectedItems: [], lastSelectedItem: null, lastUnselectedItem: id } }; 40 | const action = actionCreators.doSelectItem(STATE_KEY, id); 41 | deepFreeze(action); 42 | deepFreeze(previousState); 43 | expect(reducers.tableSelect(previousState, action)).to.eql(expectedState); 44 | }); 45 | }); 46 | 47 | describe('SELECT_ITEMS', () => { 48 | it('toggles multiple items as selected', () => { 49 | const ids = ['x', 'y']; 50 | const previousState = {}; 51 | const expectedState = { [STATE_KEY]: { selectedItems: ids, lastSelectedItem: null } }; 52 | const action = actionCreators.doSelectItems(STATE_KEY, ids, true); 53 | deepFreeze(action); 54 | deepFreeze(previousState); 55 | expect(reducers.tableSelect(previousState, action)).to.eql(expectedState); 56 | }); 57 | 58 | it('toggles multiple items as selected, but uniques them', () => { 59 | const ids = ['x', 'y']; 60 | const previousState = { [STATE_KEY]: { selectedItems: ['x'], lastSelectedItem: null } }; 61 | const expectedState = { [STATE_KEY]: { selectedItems: ids, lastSelectedItem: null } }; 62 | const action = actionCreators.doSelectItems(STATE_KEY, ids, true); 63 | deepFreeze(action); 64 | deepFreeze(previousState); 65 | expect(reducers.tableSelect(previousState, action)).to.eql(expectedState); 66 | }); 67 | 68 | it('toggles multiple items as unselected', () => { 69 | const ids = ['x', 'y']; 70 | const previousState = { [STATE_KEY]: { selectedItems: ids, lastSelectedItem: 'y' } }; 71 | const expectedState = { [STATE_KEY]: { selectedItems: [], lastSelectedItem: null } }; 72 | const action = actionCreators.doSelectItems(STATE_KEY, ids, false); 73 | deepFreeze(action); 74 | deepFreeze(previousState); 75 | expect(reducers.tableSelect(previousState, action)).to.eql(expectedState); 76 | }); 77 | }); 78 | 79 | describe('SELECT_ITEMS_RANGE', () => { 80 | it('selects a range of items when shift-clicking a second item', () => { 81 | const allIds = ['v', 'w', 'x', 'y', 'z']; 82 | const previousState = { [STATE_KEY]: { selectedItems: ['w'], lastSelectedItem: 'w' } }; 83 | const expectedState = { [STATE_KEY]: { selectedItems: ['w', 'x', 'y'], lastSelectedItem: 'w' } }; 84 | const action = actionCreators.doSelectItemsRange(STATE_KEY, 'y', [], [], allIds); 85 | deepFreeze(action); 86 | deepFreeze(previousState); 87 | expect(reducers.tableSelect(previousState, action)).to.eql(expectedState); 88 | }); 89 | 90 | it('selects a range of items when shift-clicking a second item, skipping unselectable items', () => { 91 | const allIds = ['v', 'w', 'x', 'y', 'z']; 92 | const previousState = { [STATE_KEY]: { selectedItems: ['v'], lastSelectedItem: 'v' } }; 93 | const expectedState = { [STATE_KEY]: { selectedItems: ['v', 'w', 'z'], lastSelectedItem: 'v' } }; 94 | const preselected = ['x']; 95 | const unselectables = ['y']; 96 | const action = actionCreators.doSelectItemsRange(STATE_KEY, 'z', preselected, unselectables, allIds); 97 | deepFreeze(action); 98 | deepFreeze(previousState); 99 | expect(reducers.tableSelect(previousState, action)).to.eql(expectedState); 100 | }); 101 | 102 | it('selects a single item when shift-clicking a single item without a previously-selected item', () => { 103 | const allIds = ['v', 'w', 'x', 'y', 'z']; 104 | const previousState = { [STATE_KEY]: { selectedItems: [], lastSelectedItem: null } }; 105 | const expectedState = { [STATE_KEY]: { selectedItems: ['w'], lastSelectedItem: 'w' } }; 106 | const action = actionCreators.doSelectItemsRange(STATE_KEY, 'w', [], [], allIds); 107 | deepFreeze(action); 108 | deepFreeze(previousState); 109 | expect(reducers.tableSelect(previousState, action)).to.eql(expectedState); 110 | }); 111 | 112 | it('unselects a range when shift-clicking an already-selected item', () => { 113 | const allIds = ['v', 'w', 'x', 'y', 'z']; 114 | const previousState = { [STATE_KEY]: { selectedItems: ['x', 'y', 'z'], lastUnselectedItem: 'w' } }; 115 | const expectedState = { [STATE_KEY]: { selectedItems: ['z'], lastUnselectedItem: 'w' } }; 116 | const action = actionCreators.doSelectItemsRange(STATE_KEY, 'y', [], [], allIds); 117 | deepFreeze(action); 118 | deepFreeze(previousState); 119 | expect(reducers.tableSelect(previousState, action)).to.eql(expectedState); 120 | }); 121 | }); 122 | 123 | describe('SELECT_ITEMS_EXCLUSIVELY', () => { 124 | it('toggles multiple items as selected but exclusively', () => { 125 | const ids = ['x', 'y']; 126 | const previousState = { [STATE_KEY]: { selectedItems: ['z'], lastSelectedItem: null } }; 127 | const expectedState = { [STATE_KEY]: { selectedItems: ids, lastSelectedItem: null } }; 128 | const action = actionCreators.doSelectItemsExclusively(STATE_KEY, ids, true); 129 | deepFreeze(action); 130 | deepFreeze(previousState); 131 | expect(reducers.tableSelect(previousState, action)).to.eql(expectedState); 132 | }); 133 | }); 134 | 135 | describe('SELECT_ITEMS_RESET', () => { 136 | it('resets all items that nothing is selected anymore', () => { 137 | const ids = ['x', 'y']; 138 | const previousState = { [STATE_KEY]: { selectedItems: ids, lastSelectedItem: 'x' } }; 139 | const expectedState = { [STATE_KEY]: { selectedItems: [], lastSelectedItem: null } }; 140 | const action = actionCreators.doSelectItemsReset(STATE_KEY); 141 | deepFreeze(action); 142 | deepFreeze(previousState); 143 | expect(reducers.tableSelect(previousState, action)).to.eql(expectedState); 144 | }); 145 | }); 146 | }); 147 | -------------------------------------------------------------------------------- /src/ducks/sort/index.js: -------------------------------------------------------------------------------- 1 | import sortBy from 'lodash.sortby'; 2 | 3 | import { partition } from '../../helper/util/partition'; 4 | 5 | import { applyResetByStateKeys, RESET_BY_STATE_KEYS } from '../reset'; 6 | 7 | const SLICE_NAME = 'tableSort'; 8 | 9 | const TABLE_SORT = `${SLICE_NAME}/TABLE_SORT`; 10 | 11 | const INITIAL_STATE = {}; 12 | 13 | function doTableSort(stateKey, sortKey, sortFn, isReverse) { 14 | return { 15 | type: TABLE_SORT, 16 | payload: { 17 | stateKey, 18 | sortKey, 19 | sortFn, 20 | isReverse, 21 | } 22 | }; 23 | } 24 | 25 | const reducer = (state = INITIAL_STATE, action) => { 26 | switch (action.type) { 27 | case TABLE_SORT: 28 | return applyTableSort(state, action); 29 | case RESET_BY_STATE_KEYS: 30 | return applyResetByStateKeys(state, action); 31 | } 32 | return state; 33 | }; 34 | 35 | function applyTableSort(state, action) { 36 | const { stateKey, sortKey, sortFn, isReverse: explicitReverse } = action.payload; 37 | const isExplicitlyReverse = explicitReverse !== undefined; 38 | const implicitReverse = !!state[stateKey] && state[stateKey].sortKey === sortKey && !state[stateKey].isReverse; 39 | const isReverse = isExplicitlyReverse ? explicitReverse : implicitReverse; 40 | const enhancedSortFn = getEnhancedSortFn(isReverse, sortFn); 41 | return { ...state, [stateKey]: { sortFn: enhancedSortFn, sortKey, isReverse } }; 42 | } 43 | 44 | function getEnhancedSortFn(isReverse, sortFn) { 45 | return function (items) { 46 | const [ filledValues, emptyValues ] = partition(items, (item) => isNotEmpty(sortFn(item))); 47 | 48 | return isReverse 49 | ? sortBy(filledValues, sortFn).reverse().concat(emptyValues) 50 | : sortBy(filledValues, sortFn).concat(emptyValues); 51 | }; 52 | } 53 | 54 | function isNotEmpty(value) { 55 | return value !== undefined && value !== null && value !== ''; 56 | } 57 | 58 | function getSort(state, stateKey) { 59 | return state[SLICE_NAME][stateKey] || {}; 60 | } 61 | 62 | const selectors = { 63 | getSort 64 | }; 65 | 66 | const actionCreators = { 67 | doTableSort 68 | }; 69 | 70 | const reducers = { [SLICE_NAME]: reducer }; 71 | 72 | const actionTypes = { 73 | TABLE_SORT, 74 | }; 75 | 76 | export { 77 | reducers, 78 | selectors, 79 | actionCreators, 80 | actionTypes, 81 | 82 | // test only 83 | getEnhancedSortFn 84 | }; 85 | -------------------------------------------------------------------------------- /src/ducks/sort/spec.js: -------------------------------------------------------------------------------- 1 | import deepFreeze from 'deep-freeze'; 2 | import { reducers, actionCreators, getEnhancedSortFn } from '../sort'; 3 | 4 | const STATE_KEY = 'SOME_KEY'; 5 | 6 | const SAMPLE_DATA_ONE = [ 7 | { name: 'a' }, 8 | { name: 'b' }, 9 | ]; 10 | 11 | const SAMPLE_DATA_TWO = [ 12 | { name: 'a' }, 13 | { name: 'b' }, 14 | { name: '' }, 15 | ]; 16 | 17 | describe('sort', () => { 18 | describe('TABLE_SORT', () => { 19 | it('sets a sort', () => { 20 | const isReverse = false; 21 | const sortKey = 'name'; 22 | const sortFn = item => item.name; 23 | const previousState = {}; 24 | const action = actionCreators.doTableSort(STATE_KEY, sortKey, sortFn); 25 | 26 | deepFreeze(action); 27 | deepFreeze(previousState); 28 | 29 | const nextState = reducers.tableSort(previousState, action); 30 | expect(nextState[STATE_KEY].sortKey).to.eql(sortKey); 31 | expect(nextState[STATE_KEY].isReverse).to.eql(isReverse); 32 | }); 33 | 34 | it('reverses a sort when already set', () => { 35 | const isReverse = false; 36 | const sortKey = 'name'; 37 | const sortFn = item => item.name; 38 | const previousState = {}; 39 | const action = actionCreators.doTableSort(STATE_KEY, sortKey, sortFn); 40 | 41 | deepFreeze(action); 42 | deepFreeze(previousState); 43 | 44 | const nextState = reducers.tableSort(previousState, action); 45 | const beyondNextState = reducers.tableSort(nextState, action); 46 | 47 | expect(beyondNextState[STATE_KEY].sortKey).to.eql(sortKey); 48 | expect(beyondNextState[STATE_KEY].isReverse).to.eql(!isReverse); 49 | }); 50 | 51 | it('keeps the same sort when already set and explicitly providing the same value', () => { 52 | const isReverse = true; 53 | const newIsReverse = true; 54 | const sortKey = 'name'; 55 | const sortFn = item => item.name; 56 | const previousState = {}; 57 | const action = actionCreators.doTableSort(STATE_KEY, sortKey, sortFn, newIsReverse); 58 | 59 | deepFreeze(action); 60 | deepFreeze(previousState); 61 | 62 | const nextState = reducers.tableSort(previousState, action); 63 | const beyondNextState = reducers.tableSort(nextState, action); 64 | 65 | expect(beyondNextState[STATE_KEY].sortKey).to.eql(sortKey); 66 | expect(beyondNextState[STATE_KEY].isReverse).to.eql(isReverse); 67 | }); 68 | 69 | it('generates an enhanced sort fn', () => { 70 | const isReverse = false; 71 | const sortKey = 'name'; 72 | const sortFn = item => item.name; 73 | const previousState = {}; 74 | const action = actionCreators.doTableSort(STATE_KEY, sortKey, sortFn); 75 | 76 | deepFreeze(action); 77 | deepFreeze(previousState); 78 | 79 | const enhancedSortFn = getEnhancedSortFn(isReverse, sortFn); 80 | const nextState = reducers.tableSort(previousState, action); 81 | 82 | expect(enhancedSortFn(SAMPLE_DATA_ONE)).to.eql(nextState[STATE_KEY].sortFn(SAMPLE_DATA_ONE)); 83 | }); 84 | 85 | it('generates an enhanced sort fn that sorts reverse', () => { 86 | const isReverse = true; 87 | const sortKey = 'name'; 88 | const sortFn = item => item.name; 89 | const previousState = {}; 90 | const action = actionCreators.doTableSort(STATE_KEY, sortKey, sortFn); 91 | 92 | deepFreeze(action); 93 | deepFreeze(previousState); 94 | 95 | const enhancedSortFn = getEnhancedSortFn(isReverse, sortFn); 96 | const nextState = reducers.tableSort(previousState, action); 97 | const beyondNextState = reducers.tableSort(nextState, action); 98 | 99 | expect(enhancedSortFn(SAMPLE_DATA_ONE)).to.eql(beyondNextState[STATE_KEY].sortFn(SAMPLE_DATA_ONE)); 100 | }); 101 | 102 | it('generates an enhanced sort fn that has items with undefined sort property always as last item', () => { 103 | const isReverse = false; 104 | const sortFn = item => item.name; 105 | 106 | const enhancedSortFn = getEnhancedSortFn(isReverse, sortFn); 107 | const enhancedSortFnReverse = getEnhancedSortFn(!isReverse, sortFn); 108 | 109 | expect(enhancedSortFn(SAMPLE_DATA_TWO)[2].name).to.eql(''); 110 | expect(enhancedSortFnReverse(SAMPLE_DATA_TWO)[2].name).to.eql(''); 111 | }); 112 | }); 113 | }); 114 | -------------------------------------------------------------------------------- /src/enhancements/index.js: -------------------------------------------------------------------------------- 1 | import withEmpty from './withEmpty'; 2 | import withFilter from './withFilter'; 3 | import withFilterOr from './withFilterOr'; 4 | import withSort from './withSort'; 5 | import withPaginate from './withPaginate'; 6 | import withSelectables from './withSelectables'; 7 | import withPreselectables from './withPreselectables'; 8 | import withUnselectables from './withUnselectables'; 9 | 10 | export { 11 | withEmpty, 12 | withFilter, 13 | withFilterOr, 14 | withSort, 15 | withPaginate, 16 | withSelectables, 17 | withPreselectables, 18 | withUnselectables, 19 | }; 20 | -------------------------------------------------------------------------------- /src/enhancements/withEmpty/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const withEmpty = ( 4 | configuration 5 | ) => (Enhanced) => (props) => 6 | (props.list !== null && props.list.length) 7 | ? 8 | : ; 9 | 10 | export default withEmpty; 11 | -------------------------------------------------------------------------------- /src/enhancements/withFilter/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | 4 | import { filter } from '../../helper/util/filter'; 5 | import { every } from '../../helper/util/every'; 6 | 7 | import { selectors } from '../../ducks'; 8 | 9 | const filterList = (fns) => { 10 | const filterFn = (item) => every(fns, (fn) => fn(item)); 11 | return (list) => fns.length ? filter(list, filterFn) : list; 12 | }; 13 | 14 | const withFilter = ( 15 | /*eslint-disable no-unused-vars*/ 16 | configuration = {}, 17 | /*eslint-enable no-unused-vars*/ 18 | ) => (Enhanced) => { 19 | const WithFilter = (props) => ; 20 | 21 | const mapStateToProps = (state, { list, stateKey }) => { 22 | const filterFns = selectors.getFilters(state, stateKey); 23 | return { 24 | list: filterList(filterFns)(list), 25 | }; 26 | }; 27 | 28 | return connect(mapStateToProps)(WithFilter); 29 | }; 30 | 31 | export default withFilter; 32 | -------------------------------------------------------------------------------- /src/enhancements/withFilterOr/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | 4 | import { filter } from '../../helper/util/filter'; 5 | import { some } from '../../helper/util/some'; 6 | 7 | import { selectors } from '../../ducks'; 8 | 9 | const filterList = (fns) => { 10 | const filterFn = (item) => some(fns, (fn) => fn(item)); 11 | return (list) => fns.length ? filter(list, filterFn) : list; 12 | }; 13 | 14 | const withFilterOr = ( 15 | /*eslint-disable no-unused-vars*/ 16 | configuration = {}, 17 | /*eslint-enable no-unused-vars*/ 18 | ) => (Enhanced) => { 19 | const WithFilterOr = (props) => ; 20 | 21 | const mapStateToProps = (state, { list, stateKey }) => { 22 | const filterFns = selectors.getFilters(state, stateKey); 23 | return { 24 | list: filterList(filterFns)(list), 25 | }; 26 | }; 27 | 28 | return connect(mapStateToProps)(WithFilterOr); 29 | }; 30 | 31 | export default withFilterOr; 32 | -------------------------------------------------------------------------------- /src/enhancements/withPaginate/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | 4 | import { selectors } from '../../ducks'; 5 | import { Pagination } from '../../components'; 6 | 7 | const DEFAULT_PAGINATION_SIZE = 15; 8 | 9 | const paginateList = (list, size) => 10 | list.reduce((memo, item, i) => { 11 | if (i % size) { 12 | memo[memo.length - 1].push(item); 13 | } else { 14 | memo[memo.length] = [item]; 15 | } 16 | return memo; 17 | }, []); 18 | 19 | const withPaginate = (configuration = {}) => (Enhanced) => { 20 | const WithPaginate = (props) => 21 |
22 | 28 | 29 | 35 |
; 36 | 37 | const mapStateToProps = (state, { list, stateKey }) => { 38 | const paginatedLists = paginateList(list, configuration.size || DEFAULT_PAGINATION_SIZE); 39 | const currentPage = selectors.getCurrentPage(state, stateKey, paginatedLists); 40 | const paginatedList = paginatedLists[currentPage]; 41 | return { 42 | paginatedLists, 43 | list: paginatedList || [], 44 | currentPage, 45 | }; 46 | }; 47 | 48 | return connect(mapStateToProps)(WithPaginate); 49 | }; 50 | 51 | export default withPaginate; 52 | -------------------------------------------------------------------------------- /src/enhancements/withPreselectables/index.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React, { Component } from 'react'; 3 | 4 | const withPreselectables = (configuration = {}) => (Enhanced) => { 5 | class WithPreselectables extends Component { 6 | getChildContext() { 7 | return { 8 | preselected: configuration.ids || [], 9 | }; 10 | } 11 | 12 | render() { 13 | return ; 14 | } 15 | } 16 | 17 | WithPreselectables.childContextTypes = { 18 | preselected: PropTypes.array, 19 | }; 20 | 21 | return WithPreselectables; 22 | }; 23 | 24 | export default withPreselectables; 25 | -------------------------------------------------------------------------------- /src/enhancements/withSelectables/index.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React, { Component } from 'react'; 3 | import { connect } from 'react-redux'; 4 | import { bindActionCreators } from 'redux'; 5 | 6 | import { actionCreators } from '../../ducks'; 7 | 8 | const withSelectables = (configuration = {}) => (Enhanced) => { 9 | class WithSelectables extends Component { 10 | getChildContext() { 11 | return { 12 | isSelectable: true, 13 | }; 14 | } 15 | 16 | componentDidMount() { 17 | const { onSelectItems } = this.props; 18 | onSelectItems(configuration.ids || []); 19 | } 20 | 21 | render() { 22 | return ; 23 | } 24 | } 25 | 26 | const mapDispatchToProps = (dispatch, { stateKey }) => ({ 27 | onSelectItems: bindActionCreators((ids) => actionCreators.doSelectItems(stateKey, ids, true), dispatch), 28 | }); 29 | 30 | WithSelectables.childContextTypes = { 31 | isSelectable: PropTypes.bool, 32 | }; 33 | 34 | return connect(null, mapDispatchToProps)(WithSelectables); 35 | }; 36 | 37 | export default withSelectables; 38 | -------------------------------------------------------------------------------- /src/enhancements/withSort/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { bindActionCreators } from 'redux'; 4 | 5 | import { isEmpty } from '../../helper/util/empty'; 6 | import { selectors, actionCreators } from '../../ducks'; 7 | 8 | const sortList = (fn) => (list) => fn ? fn(list) : list; 9 | 10 | const withSort = (configuration = {}) => (Enhanced) => { 11 | class WithSort extends Component { 12 | componentDidMount() { 13 | const { sort, onTableSort } = this.props; 14 | if (configuration.sortKey && configuration.sortFn && isEmpty(sort)) { 15 | onTableSort(configuration.sortKey, configuration.sortFn); 16 | } 17 | } 18 | 19 | render() { 20 | const { sort, onTableSort, ...props } = this.props; 21 | return ; 22 | } 23 | } 24 | 25 | const mapStateToProps = (state, { list, stateKey }) => { 26 | const sort = selectors.getSort(state, stateKey); 27 | return { 28 | list: sortList(sort.sortFn)(list), 29 | sort, 30 | }; 31 | }; 32 | 33 | const mapDispatchToProps = (dispatch, { stateKey }) => ({ 34 | onTableSort: bindActionCreators((sortKey, sortFn) => 35 | actionCreators.doTableSort(stateKey, sortKey, sortFn), dispatch), 36 | }); 37 | 38 | return connect(mapStateToProps, mapDispatchToProps)(WithSort); 39 | }; 40 | 41 | export default withSort; 42 | -------------------------------------------------------------------------------- /src/enhancements/withUnselectables/index.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React, { Component } from 'react'; 3 | 4 | const withUnselectables = (configuration = {}) => (Enhanced) => { 5 | class WithUnselectables extends Component { 6 | getChildContext() { 7 | return { 8 | unselectables: configuration.ids || [], 9 | }; 10 | } 11 | 12 | render() { 13 | return ; 14 | } 15 | } 16 | 17 | WithUnselectables.childContextTypes = { 18 | unselectables: PropTypes.array, 19 | }; 20 | 21 | return WithUnselectables; 22 | }; 23 | 24 | export default withUnselectables; 25 | -------------------------------------------------------------------------------- /src/helper/components/SortCaret/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const takeSuffix = (suffix, isReverse) => 4 | isReverse ? suffix['DESC'] : suffix['ASC']; 5 | 6 | const SortCaret = ({ suffix, isActive, isReverse }) => 7 | 8 | {(suffix && isActive) 9 | ? takeSuffix(suffix, isReverse) 10 | : null 11 | } 12 | ; 13 | 14 | export default SortCaret; 15 | -------------------------------------------------------------------------------- /src/helper/services/index.js: -------------------------------------------------------------------------------- 1 | import * as select from './select'; 2 | import * as sort from './sort'; 3 | 4 | export { 5 | select, 6 | sort, 7 | }; 8 | -------------------------------------------------------------------------------- /src/helper/services/select/index.js: -------------------------------------------------------------------------------- 1 | export const SELECT_STATES = { 2 | selected: 'SELECTED', 3 | notSelected: 'NOT_SELECTED', 4 | preSelected: 'PRE_SELECTED', 5 | unselectable: 'UNSELECTABLE', 6 | }; 7 | 8 | export function getSelectState(id, isSelected, preselected, unselectables) { 9 | const { selected, notSelected, preSelected, unselectable } = SELECT_STATES; 10 | if (preselected.indexOf(id) !== -1) { return preSelected; } 11 | if (unselectables.indexOf(id) !== -1) { return unselectable; } 12 | return isSelected ? selected : notSelected; 13 | } 14 | -------------------------------------------------------------------------------- /src/helper/services/sort/index.js: -------------------------------------------------------------------------------- 1 | export function getAriaSort(isActive, isReverse) { 2 | if (!isActive) { 3 | return 'none'; 4 | } 5 | return isReverse ? 'descending' : 'ascending'; 6 | } 7 | 8 | export const callIfActionKey = callbackFn => event => { 9 | const isActionKey = event && ['Enter', ' '].indexOf(event.key) !== -1; 10 | return isActionKey ? callbackFn(event) : null; 11 | }; 12 | -------------------------------------------------------------------------------- /src/helper/util/empty.js: -------------------------------------------------------------------------------- 1 | const isEmpty = (obj) => Object.keys(obj).length === 0; 2 | 3 | export { 4 | isEmpty 5 | }; 6 | -------------------------------------------------------------------------------- /src/helper/util/empty.spec.js: -------------------------------------------------------------------------------- 1 | import { isEmpty } from './empty'; 2 | 3 | describe('empty', () => { 4 | it('return true when object is empty', () => { 5 | const r = isEmpty({}); 6 | expect(r).to.be.true; 7 | }); 8 | 9 | it('return false when object is not empty', () => { 10 | const r = isEmpty({ a: 1 }); 11 | expect(r).to.be.false; 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /src/helper/util/every.js: -------------------------------------------------------------------------------- 1 | const every = (array, fn) => { 2 | let every = true; 3 | array.forEach(v => { 4 | if (!fn(v)) { 5 | every = false; 6 | } 7 | }); 8 | return every; 9 | }; 10 | 11 | export { 12 | every 13 | }; 14 | -------------------------------------------------------------------------------- /src/helper/util/every.spec.js: -------------------------------------------------------------------------------- 1 | import { every } from './every'; 2 | 3 | const DATA_ONE = [ 4 | true, 5 | false, 6 | ]; 7 | 8 | const DATA_TWO = [ 9 | true, 10 | true, 11 | ]; 12 | 13 | describe('every', () => { 14 | it('return true if all items are true', () => { 15 | const r = every(DATA_TWO, boolean => boolean); 16 | expect(r).to.be.true; 17 | }); 18 | 19 | it('return false if at least one item is false', () => { 20 | const r = every(DATA_ONE, boolean => boolean); 21 | expect(r).to.be.false; 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/helper/util/filter.js: -------------------------------------------------------------------------------- 1 | const filter = (array, fn) => { 2 | let filterArray = []; 3 | array.forEach(v => { 4 | if (fn(v)) { 5 | filterArray.push(v); 6 | } 7 | }); 8 | return filterArray; 9 | }; 10 | 11 | export { 12 | filter 13 | }; 14 | -------------------------------------------------------------------------------- /src/helper/util/filter.spec.js: -------------------------------------------------------------------------------- 1 | import { filter } from './filter'; 2 | 3 | const DATA_ONE = [ 4 | true, 5 | false, 6 | ]; 7 | 8 | describe('filter', () => { 9 | it('return a filtered list', () => { 10 | const r = filter(DATA_ONE, boolean => boolean); 11 | expect(r).to.eql([true]); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /src/helper/util/find.js: -------------------------------------------------------------------------------- 1 | const find = (a, fn) => { 2 | let v; 3 | a.forEach(i => { 4 | if (fn(i)) { 5 | v = i; 6 | } 7 | }); 8 | return v; 9 | }; 10 | 11 | export { 12 | find 13 | }; 14 | -------------------------------------------------------------------------------- /src/helper/util/find.spec.js: -------------------------------------------------------------------------------- 1 | import { find } from './find'; 2 | 3 | const DATA_ONE = [ 4 | 'foo', 5 | 'bar', 6 | ]; 7 | 8 | describe('find', () => { 9 | it('returns a matching item', () => { 10 | const r = find(DATA_ONE, name => name === 'foo'); 11 | expect(r).to.eql('foo'); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /src/helper/util/getContext.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const createEagerElementUtil = ( 4 | hasKey, 5 | isReferentiallyTransparent, 6 | type, 7 | props, 8 | children 9 | ) => { 10 | if (!hasKey && isReferentiallyTransparent) { 11 | if (children) { 12 | return type({ ...props, children }) 13 | } 14 | return type(props) 15 | } 16 | 17 | const Component = type 18 | 19 | if (children) { 20 | return {children} 21 | } 22 | 23 | return 24 | } 25 | 26 | const isClassComponent = Component => Boolean( 27 | Component && 28 | Component.prototype && 29 | typeof Component.prototype.isReactComponent === 'object' 30 | ) 31 | 32 | const isReferentiallyTransparentFunctionComponent = Component => Boolean( 33 | typeof Component === 'function' && 34 | !isClassComponent(Component) && 35 | !Component.defaultProps && 36 | !Component.contextTypes && 37 | (process.env.NODE_ENV === 'production' || !Component.propTypes) 38 | ) 39 | 40 | const createEagerFactory = type => { 41 | const isReferentiallyTransparent = 42 | isReferentiallyTransparentFunctionComponent(type) 43 | return (p, c) => 44 | createEagerElementUtil(false, isReferentiallyTransparent, type, p, c) 45 | } 46 | 47 | const getDisplayName = Component => { 48 | if (typeof Component === 'string') { 49 | return Component 50 | } 51 | 52 | if (!Component) { 53 | return undefined 54 | } 55 | 56 | return Component.displayName || Component.name || 'Component' 57 | } 58 | 59 | const wrapDisplayName = (BaseComponent, hocName) => 60 | `${hocName}(${getDisplayName(BaseComponent)})` 61 | 62 | const createHelper = ( 63 | func, 64 | helperName, 65 | setDisplayName = true, 66 | noArgs = false 67 | ) => { 68 | if (process.env.NODE_ENV !== 'production' && setDisplayName) { 69 | if (noArgs) { 70 | return BaseComponent => { 71 | const Component = func(BaseComponent) 72 | Component.displayName = wrapDisplayName(BaseComponent, helperName) 73 | return Component 74 | } 75 | } 76 | 77 | return (...args) => 78 | BaseComponent => { 79 | const Component = func(...args)(BaseComponent) 80 | Component.displayName = wrapDisplayName(BaseComponent, helperName) 81 | return Component 82 | } 83 | } 84 | 85 | return func 86 | } 87 | 88 | const getContextBase = contextTypes => BaseComponent => { 89 | const factory = createEagerFactory(BaseComponent) 90 | const GetContext = (ownerProps, context) => ( 91 | factory({ 92 | ...ownerProps, 93 | ...context 94 | }) 95 | ) 96 | 97 | GetContext.contextTypes = contextTypes 98 | 99 | return GetContext 100 | } 101 | 102 | const getContext = createHelper(getContextBase, 'getContext'); 103 | 104 | export { 105 | getContext 106 | }; 107 | -------------------------------------------------------------------------------- /src/helper/util/noop.js: -------------------------------------------------------------------------------- 1 | const noop = () => {}; 2 | 3 | export { 4 | noop, 5 | }; 6 | -------------------------------------------------------------------------------- /src/helper/util/noop.spec.js: -------------------------------------------------------------------------------- 1 | import { noop } from './noop'; 2 | 3 | describe('noop', () => { 4 | it('returns an undefined because empty function', () => { 5 | const r = noop(); 6 | expect(r).to.eql(undefined); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /src/helper/util/omit.js: -------------------------------------------------------------------------------- 1 | const omit = (object, key) => { 2 | const { [key]: toOmit, ...rest } = object; 3 | return rest; 4 | }; 5 | 6 | export { 7 | omit 8 | }; 9 | -------------------------------------------------------------------------------- /src/helper/util/omit.spec.js: -------------------------------------------------------------------------------- 1 | import { omit } from './omit'; 2 | 3 | const DATA_ONE = { 4 | foo: 'foo', 5 | bar: 'bar', 6 | }; 7 | 8 | describe('omit', () => { 9 | it('return the object with an omitted key', () => { 10 | const r = omit(DATA_ONE, 'foo'); 11 | expect(r).to.eql({ bar: 'bar' }); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /src/helper/util/partition.js: -------------------------------------------------------------------------------- 1 | const partition = (array, fn) => { 2 | let predictedArray = []; 3 | let restArray = []; 4 | array.forEach(v => { 5 | if (fn(v)) { 6 | predictedArray.push(v); 7 | } else { 8 | restArray.push(v); 9 | } 10 | }); 11 | return [ predictedArray, restArray ]; 12 | }; 13 | 14 | export { 15 | partition 16 | }; 17 | -------------------------------------------------------------------------------- /src/helper/util/partition.spec.js: -------------------------------------------------------------------------------- 1 | import { partition } from './partition'; 2 | 3 | const DATA_ONE = [ 4 | 'foo', 5 | 'bar', 6 | 'zoo', 7 | ]; 8 | 9 | describe('partition', () => { 10 | it('returns a partition', () => { 11 | const [ r0, r1 ] = partition(DATA_ONE, name => name === 'foo'); 12 | expect(r0).to.eql(['foo']); 13 | expect(r1).to.eql(['bar', 'zoo']); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /src/helper/util/some.js: -------------------------------------------------------------------------------- 1 | const some = (array, fn) => { 2 | let some = false; 3 | array.forEach(v => { 4 | if (fn(v)) { 5 | some = true; 6 | } 7 | }); 8 | return some; 9 | }; 10 | 11 | export { 12 | some 13 | }; 14 | -------------------------------------------------------------------------------- /src/helper/util/some.spec.js: -------------------------------------------------------------------------------- 1 | import { some } from './some'; 2 | 3 | const DATA_ONE = [ 4 | true, 5 | false, 6 | ]; 7 | 8 | const DATA_TWO = [ 9 | false, 10 | false, 11 | ]; 12 | 13 | describe('some', () => { 14 | it('return true if at least one item is true', () => { 15 | const r = some(DATA_ONE, boolean => boolean); 16 | expect(r).to.be.true; 17 | }); 18 | 19 | it('return false if no item is true', () => { 20 | const r = some(DATA_TWO, boolean => boolean); 21 | expect(r).to.be.false; 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/helper/util/uniq.js: -------------------------------------------------------------------------------- 1 | const uniq = (array) => { 2 | let uniqArray = []; 3 | array.forEach(v => { 4 | if (!(uniqArray.indexOf(v) !== -1)) { 5 | uniqArray.push(v); 6 | } 7 | }); 8 | return uniqArray; 9 | }; 10 | 11 | export { 12 | uniq 13 | }; 14 | -------------------------------------------------------------------------------- /src/helper/util/uniq.spec.js: -------------------------------------------------------------------------------- 1 | import { uniq } from './uniq'; 2 | 3 | const DATA_ONE = [ 4 | 'foo', 5 | 'bar', 6 | 'bar', 7 | ]; 8 | 9 | describe('uniq', () => { 10 | it('returns a list of unique items', () => { 11 | const r = uniq(DATA_ONE); 12 | expect(r).to.eql(['foo', 'bar']); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import * as components from './components'; 2 | import * as enhancements from './enhancements'; 3 | 4 | import { 5 | reducers, 6 | actionCreators, 7 | selectors, 8 | } from './ducks'; 9 | 10 | export { 11 | components, 12 | enhancements, 13 | 14 | actionCreators, 15 | selectors, 16 | }; 17 | 18 | export default reducers; 19 | -------------------------------------------------------------------------------- /webpack.config.dev.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | entry: './src/index.js', 5 | output: { 6 | path: __dirname + '/dist', 7 | filename: 'bundle.js', 8 | library: 'react-redux-composeable-list', 9 | libraryTarget: 'commonjs2' 10 | }, 11 | resolve: { 12 | root: path.resolve(__dirname), 13 | modulesDirectories: ['src', 'node_modules'] 14 | }, 15 | module: { 16 | loaders: [ 17 | { 18 | test: /\.jsx?$/, 19 | exclude: /node_modules/, 20 | loader: 'babel-loader', 21 | query: { 22 | presets: ['es2015', 'react', 'stage-2'] 23 | } 24 | }, 25 | { 26 | test: /\.js$/, 27 | exclude: /node_modules/, 28 | loaders: ['babel-loader', 'eslint-loader'] 29 | }, 30 | { 31 | test: /\.less$/, 32 | loader: 'style-loader!css-loader!less-loader' 33 | } 34 | ] 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /webpack.config.prod.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const path = require('path'); 3 | var Visualizer = require('webpack-visualizer-plugin'); 4 | 5 | module.exports = { 6 | entry: './src/index.js', 7 | output: { 8 | path: __dirname + '/dist', 9 | filename: 'bundle.js', 10 | library: 'react-redux-composeable-list', 11 | libraryTarget: 'commonjs2' 12 | }, 13 | resolve: { 14 | root: path.resolve(__dirname), 15 | modulesDirectories: ['src', 'node_modules'] 16 | }, 17 | module: { 18 | loaders: [ 19 | { 20 | test: /\.jsx?$/, 21 | exclude: /node_modules/, 22 | loader: 'babel-loader', 23 | query: { 24 | presets: ['es2015', 'react', 'stage-2'] 25 | } 26 | }, 27 | { 28 | test: /\.js$/, 29 | exclude: /node_modules/, 30 | loaders: ['babel-loader', 'eslint-loader'] 31 | }, 32 | { 33 | test: /\.less$/, 34 | loader: 'style-loader!css-loader!less-loader' 35 | } 36 | ] 37 | }, 38 | externals: { 39 | react: { 40 | root: 'React', 41 | commonjs: 'react', 42 | commonjs2: 'react', 43 | amd: 'react', 44 | }, 45 | 'redux': { 46 | root: 'Redux', 47 | commonjs: 'redux', 48 | commonjs2: 'redux', 49 | amd: 'redux', 50 | }, 51 | 'react-redux': { 52 | root: 'ReactRedux', 53 | commonjs: 'react-redux', 54 | commonjs2: 'react-redux', 55 | amd: 'react-redux', 56 | }, 57 | 'prop-types': { 58 | root: 'PropTypes', 59 | commonjs: 'prop-types', 60 | commonjs2: 'prop-types', 61 | amd: 'prop-types', 62 | } 63 | }, 64 | node: { 65 | Buffer: false 66 | }, 67 | plugins:[ 68 | new webpack.DefinePlugin({ 69 | 'process.env.NODE_ENV': JSON.stringify('production'), 70 | }), 71 | new Visualizer({ 72 | filename: './statistics.html' 73 | }) 74 | ] 75 | }; 76 | --------------------------------------------------------------------------------