violet-paginator
v3.0.0-beta-6
Published
Display, paginate, sort, filter, and update items from the server. violet-paginator is a complete list management library for react/redux applications.
Downloads
134
Maintainers
Readme
violet-paginator
VioletPaginator is a react-redux package allowing users to manage arbitrarily many filtered, paginated lists of records. We provide a set of premade components including both simple and robust pagination controls, sort links, and data tables. We also make it ridiculously easy to write your own components and configure and extend VioletPaginator's default behavior by composing actions.
Demo
https://sslotsky.github.io/violet-paginator/
Extended Documentation
https://sslotsky.gitbooks.io/violet-paginator/content/
Installation
npm i --save violet-paginator
Dependencies
The current version of this package includes the following peer dependencies:
"peerDependencies": {
"immutable": "^3.7.6",
"react": "^0.14.8 || ^15.1.0",
"react-redux": "^4.4.4 || 5.x",
"redux": "^3.4.0"
},
Additionally, it is assumed that you are running some middleware that allows action creators to return promises, such as redux-thunk.
Finally, if you wish to use the premade VioletPaginator
components, it is recommended that you include the violet
and font-awesome
stylesheets as described later in this document.
Usage
VioletPaginator
is intended to be flexible so that it can be used in many ways without much fuss. We provide premade components, but our library is broken down into small, exposed pieces that allow you to easily override default settings, abstract core functionality, and create your own components.
Creating a reducer
Rather than exposing a single reducer, violet-paginator
uses a
higher order reducer function
that creates a reducer and ties it to a listId
and a fetch
function (this has changed since version 1, see the upgrade guide for details).
import { createPaginator } from 'violet-paginator'
import { combineReducers } from 'redux'
import users from './users/reducer'
import { fetch } from './recipes/actions'
export default combineReducers({
users,
recipes: createPaginator({
listId: 'recipes',
fetch
})
})
Configuration
VioletPaginator aims to make client-server communication painless. For us, usability means:
- We know how to read data from your server.
- We will provide you with the correctly formatted parameters that you need to send to your server.
Because different backends will use different property names for pagination and sorting, we make this fully configurable. Example config:
import { configurePageParams } from 'violet-paginator'
configurePageParams({
perPage: 'results_per_page',
sortOrder: 'sort_reverse',
sortReverse: true // Means that a boolean will be used to indicate sort direction.
})
An example URL with this configuration:
https://brewed-dev.herokuapp.com/v1/recipes?page=6&results_per_page=15&sort=name&sort_reverse=true
Another example config:
configurePageParams({
perPage: 'page_size',
sortOrder: 'direction'
})
And a corresponding example URL:
https://www.example.com/v1/users?page=6&page_size=15&sort=name&direction=asc
The complete list of configuration options and their defaults can be found in the pageInfoTranslator:
Property Name | Default Value | Description
---|:---:|:---
page | 'page'
| The page number being requested
perPage | 'pageSize'
| The page size being requested
sort | 'sort'
| The field to sort by when requesting a page
sortOrder | 'sortOrder'
| The sort direction for the requested page
sortReverse | false
| Use a boolean to indicate sort direction
totalCount | 'total_count'
| The name of the property on the server response that indicates total record count
results | 'results'
| The name of the property on the server that contains the page of results
id | 'id'
| The name of the property on the record to be used as the unique identifer
Using Premade VioletPaginator Components
The following will display a 3 column data table with full pagination controls above and below the table.
All pagination components require the listId
prop, and they will use the fetch
function that was supplied
in the createPaginator
call to retrieve the results at the appropriate times. You never actually call fetch
yourself.
The VioletDataTable
component also takes an array of headers.
import React, { PropTypes } from 'react'
import { VioletDataTable, VioletPaginator } from 'violet-paginator'
export default function RecipeList() {
const headers = [{
field: 'name',
text: 'Name'
}, {
field: 'created_at',
text: 'Date Created'
}, {
field: 'boil_time',
sortable: false,
text: 'Boil Time'
}]
const paginator = (
<VioletPaginator listId="recipes" />
)
return (
<section>
{paginator}
<VioletDataTable listId="recipes" headers={this.headers()} />
{paginator}
</section>
)
}
The fetch
function that you supply to the paginator is an action creator that returns a promise. Therefore,
while redux-thunk isn't explicitly required as a peer dependency, you will need to have some such middleware
hooked up that allows action creators to return promises. Below is an example fetch function.
export default function fetchRecipes(pageInfo) {
return () => api.recipes.index(pageInfo.query);
}
Unlike most asynchronous action creators, notice that ours has no success and error handlers. VioletPaginator
has its own
handlers, so supplying your own is not necessary. However, if you wish to handle the response before passing it along to
VioletPaginator
, this isn't a problem as long as your success handler returns the response and your failure handler re-throws
for us to catch, like below.
export default function fetchRecipes(pageInfo) {
return dispatch => {
dispatch({ type: actionTypes.FETCH_RECIPES })
return api.recipes.index(pageInfo.query).then(resp => {
dispatch({ type: actionTypes.FETCH_RECIPES_SUCCESS, ...resp.data })
return resp
}).catch(error => {
dispatch({ type: actionTypes.FETCH_RECIPES_ERROR, error })
throw error
})
}
}
Styling
Our premade components were built to be dispalyed using the Violet CSS framework and Font Awesome. We don't expose these stylesheets from our package. We leave it to you to include those in your project however you see fit. The easiest way is with CDN links:
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Work+Sans:400,500">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/violet/0.0.1/violet.min.css">
If Violet isn't for you but you still want to use our components, just write your own CSS. Our components use very few CSS classess, since Violet CSS rules are mostly structural in nature. However, we do recommend keeping the font-awesome link for displaying the icons.
Customizing VioletDataTable
By default, the VioletDataTable
will simply display the raw values from the data that correspond to the headers that
are specified. However, each header can be supplied with a format
function, which can return a simple value, some markup,
or a full-fledged react component. Example:
const activeColumn = recipe => {
const icon = recipe.get('active') ? 'check' : 'ban'
return (
<FontAwesome name={icon} />
)
}
const headers = [{
field: 'active',
sortable: false,
text: 'Active',
format: activeColumn
}, {
...
}]
Composing Actions
violet-paginator
is a plugin for redux apps, and as such, it dispatches its own actions and stores state in its own reducer. To give you complete control of the pagination state, the API provides access to all of these actions via the composables and simpleComposables functions. This allows you the flexibility to call them directly as part of a more complex operation. The most common use case for this would be updating an item within the list.
As an example, consider a datatable where one column has a checkbox that's supposed to mark an item as active or inactive.
Assuming that you have a listId
of 'recipes'
, you could write an action creator like this to update the record on the server
and then toggle the active state of the corresponding recipe within the list:
import api from 'ROOT/api'
import { composables } from 'violet-paginator'
const pageActions = composables({ listId: 'recipes' })
export function toggleActive(recipe) {
const data = {
active: !recipe.get('active')
}
return pageActions.updateAsync(
recipe.get('id'),
data,
api.recipes.update(data)
)
}
Now you can bring this action creator into your connected component using connect
and mapDispatchToProps
:
import { toggleActive } from './actions'
export function Recipes({ toggle }) {
...
}
export default connect(
undefined,
{ toggle: toggleActive }
)(Recipes)
Finally, the format
function for the active
column in your data table might look like this:
const activeColumn = recipe => (
<input
type="checkbox"
checked={recipe.get('active')}
onClick={toggle}
/>
)
Building Custom Components
We understand that every product team could potentially want something different, and our premade components sometimes just won't fit that mold. We want to make it painless to write your own components, so to accomplish that, we made sure that it was every bit as painless to write ours. The best way to see how to build a custom component is to look at some of the simpler premade components. For example, here's a link that retrieves the next page of records:
import React from 'react'
import FontAwesome from 'react-fontawesome'
import { flip } from './decorators'
export function Next({ pageActions, hasNextPage }) {
const next = <FontAwesome name="chevron-right" />
const link = hasNextPage ? (
<a onClick={pageActions.next}>{next}</a>
) : next
return link
}
export default flip(Next)
And here's a link that can sort our list in either direction by a given field name:
import React, { PropTypes } from 'react'
import FontAwesome from 'react-fontawesome'
import { sort as decorate } from './decorators'
export function SortLink({ pageActions, field, text, sort, sortReverse, sortable=true }) {
if (!sortable) {
return <span>{text}</span>
}
const sortByField = () =>
pageActions.sort(field, !sortReverse)
const arrow = sort === field && (
sortReverse ? 'angle-up' : 'angle-down'
)
return (
<a onClick={sortByField}>
{text} <FontAwesome name={arrow || ''} />
</a>
)
}
SortLink.propTypes = {
sort: PropTypes.string,
sortReverse: PropTypes.bool,
pageActions: PropTypes.object,
field: PropTypes.string.isRequired,
text: PropTypes.string.isRequired,
sortable: PropTypes.bool
}
export default decorate(SortLink)
These components are simple and small enough to be written as pure functions rather than classes, and you should be able
to accomplish the same. As you might have guessed, we expose the flip
and sorter
functions that are being called as the default export
for our components, and those functions decorate your components with props that allow you to read and update the pagination state.
The only prop that callers need to supply to these components is
a listId
, and one or two additional props in some cases. Simply import our decorators into your custom component:
import { decorators } from 'violet-paginator'
and you are ready to roll your own:
// Supports 'previous' and 'next' links
export defaut decorators.flip(MyFlipperComponent)
// Supports full pagination controls
export default decorators.paginate(MyPaginationComponent)
// Supports grids/datatables
export default decorators.tabulate(MyDataGridComponent)
// Supprts controls for changing the page size
export default decorators.stretch(MyPageSizeDropdown)
// Supports a control for sorting the list by the field name
export default decorators.sort(MySortLink)
// The kitchen sink! Injects properties from all decorators
export default decorators.violetPaginator(MyPaginatedGridComponent)
For more on using decorators or creating your own, check the docs on decorators.
Contributing
If you wish to contribute, please create a fork and submit a pull request, which will be reviewed as soon as humanly possible. A couple of key points:
- Don't check in any changes to the
lib
folder. When we are ready to publish a new version, we will do a build and commit thelib
changes and the new version number. - Add tests for your feature, and make sure all existing tests still pass and that the code passes lint (described further below).
Testing
This package is tested with mocha. The project uses CI through Travis which includes running tests, linting, and code coverage. Please make sure to write tests for any new pull requests. Code coverage will block the PR if your code is not sufficiently covered.