├── .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 | 
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 | 
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 | onSelectShowcase(x.id)}
158 | >
159 | {x.label}
160 |
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 |
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 | onPaginate(page)}
36 | title={tooltip}
37 | type="button"
38 | >
39 |
40 | { dotted ? '' : (page + 1) }
41 |
42 |
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 | ;
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 |
--------------------------------------------------------------------------------