Skip to content

Instantly share code, notes, and snippets.

@gryzzly
Last active December 9, 2016 09:59
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save gryzzly/5550460952817414b948 to your computer and use it in GitHub Desktop.
Save gryzzly/5550460952817414b948 to your computer and use it in GitHub Desktop.
React Pagination (similar to Github’s pagination)
'use strict';
import React, {PropTypes} from 'react';
import classNames from 'classnames';
function last(list) {
return list[list.length - 1];
}
function areAdjacent(prev, curr) {
return last(prev).index + 1 === curr[0].index;
}
function isLeftClickEvent(event) {
return event.button === 0;
}
function isModifiedEvent(event) {
return !!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey);
}
function getQueryParamPattern(queryParam) {
// /(\&|\?)page=(\d)/
return new RegExp('(\\&|\\?)' + queryParam + '=(?:\\d)');
}
export default React.createClass({
displayName: 'Pagination',
propTypes: {
paddingSize: PropTypes.number.isRequired,
total: PropTypes.number.isRequired,
pageSize: PropTypes.number.isRequired,
page: PropTypes.number.isRequired,
baseLink: PropTypes.string.isRequired,
queryParam: PropTypes.string.isRequired
},
componentWillMount() {
this.queryParamPattern = getQueryParamPattern(this.props.queryParam);
},
componentWillReceiveProps(nextProps) {
if (this.props.queryParam !== nextProps.queryParam) {
this.queryParamPattern = getQueryParamPattern(nextProps.queryParam);
}
},
componentDidUpdate(prevProps) {
// update scroll position when changing pages in the paginated list
//
// React Router’s provides a way to trigger window.scrollTo(0, 0) via
// `scrollBehavior` option to `Router.create` but that fails because
// of the way CSS layout of the page is currently built –
// user scrolls a container div and not the document.
//
// We could provide custom `updateScrollPosition` function but Router
// doesn't call this function on query change (only route param changes).
//
// https://github.com/rackt/react-router/issues/690
// https://github.com/rackt/react-router/issues/439
if (prevProps.page !== this.props.page) {
document.querySelector('#app').scrollTop = 0;
}
},
getDefaultProps() {
return {
// the padding around current item
// | |
// [1, 2, 3 … 8, 9, 10, … 98, 99, 100]
// | | | |
// edge padding edge padding
paddingSize: 3,
queryParam: 'page'
};
},
onPaginate(index, e) {
// links have hrefs and should be possible to open in new tab/window
// when pressing mousewheel, triple tap, cmd+click etc.
if (!isModifiedEvent(event) && isLeftClickEvent(event)) {
e.preventDefault();
if (this.props.onPaginate) {
this.props.onPaginate(index);
}
}
},
buildHref(i) {
return this.props.baseLink.replace(this.queryParamPattern, (match, separator) => {
return separator + this.props.queryParam + '=' + i;
});
},
buildPaginationLink(currentPage, i) {
const className = classNames('pagination-link pagination-page', {
current: currentPage === i
});
return (
<a className={className} href={this.buildHref(i)} key={i} onClick={this.onPaginate.bind(this, i)}>{i}</a>
);
},
buildEllipsis(index) {
const className = 'pagination-link pagination-page pagination-ellipsis disabled';
return (<a key={index} className={className}>…</a>);
},
buildPageLinks() {
const padding = this.props.paddingSize;
const total = this.props.total;
const pageSize = this.props.pageSize;
const current = this.props.page;
const pages = Math.ceil(total / pageSize);
let left = [];
let middle = [];
let right = [];
const saveItem = (list, i) => {
list.push({
link: this.buildPaginationLink(current, i),
// we save indices to be able to compare adjacency of the edges
index: i
});
};
for (var i = 1; i <= pages; i++) {
// first "padding" items
// [1, 2, 3]
if (i < padding + 1) {
saveItem(left, i);
continue;
}
// last "padding" items
// [98, 99, 100]
if (i > pages - padding) {
saveItem(right, i);
continue;
}
// items in between
if (i > current - (padding - 1) && i < current + (padding - 1)) {
saveItem(middle, i);
continue;
}
}
// find non-adjacent parts to insert the ellipsis and concat everything
return [left, middle, right]
.filter(list => list.length)
.reduce((prev, curr) => {
if (!curr.length) {
return prev;
}
if (!prev.length) {
return curr;
}
const nextPart = areAdjacent(prev, curr)
? [curr]
: [{link: this.buildEllipsis(last(prev).index + 1)}, curr];
return prev.concat(...nextPart);
}, [])
// prepare array for React render (expose key’d React elements)
.map(item => item.link);
},
render() {
const current = this.props.page;
const pages = Math.ceil(this.props.total / this.props.pageSize);
const prevClassName = classNames('pagination-link pagination-prev', {
disabled: current === 1
});
const nextClassName = classNames('pagination-link pagination-next', {
disabled: current === pages
});
return (
<div className="pagination">
<a className={prevClassName} onClick={this.onPaginate.bind(this, current - 1)}>Prev</a>
{this.buildPageLinks()}
<a className={nextClassName} onClick={this.onPaginate.bind(this, current + 1)}>Next</a>
</div>
);
}
});
'use strict';
jest.dontMock('../Pagination');
describe('Pagination', function() {
it('builds a list of links (current page in the middle)', function() {
var React = require('react/addons');
var TestUtils = React.addons.TestUtils;
var Pagination = require('../Pagination');
var onPaginate = jest.genMockFunction();
var pagination = TestUtils.renderIntoDocument(
<Pagination total={100} pageSize={5} page={10} onPaginate={onPaginate}/>
// PREV [1, 2, 3, …, 9, 10, 11, …, 18, 19, 20] NEXT
// 0 1 2 3 4 5 6 7 8 9 10 11 12
);
var paginationDOMNode = React.findDOMNode(pagination);
expect(paginationDOMNode.nodeName).toEqual('DIV');
var links = paginationDOMNode.querySelectorAll('a');
expect(links.length).toEqual(13);
expect(links[0].textContent).toEqual('Prev');
expect(links[4].textContent).toEqual('…');
expect(links[5].textContent).toEqual('9');
expect(links[8].textContent).toEqual('…');
expect(links[12].textContent).toEqual('Next');
// click "previous"
TestUtils.Simulate.click(links[0]);
// clicked once
expect(onPaginate.mock.calls.length).toEqual(1);
// next page would be 9
expect(onPaginate.mock.calls[0][0] === 9);
// click "next"
TestUtils.Simulate.click(links[12]);
// second click
expect(onPaginate.mock.calls.length).toEqual(2);
// next page after 10 is 11!
expect(onPaginate.mock.calls[1][0]).toEqual(11);
// click <19>
TestUtils.Simulate.click(links[10]);
// third click
expect(onPaginate.mock.calls.length).toEqual(3);
// next page would be 19
expect(onPaginate.mock.calls[2][0]).toEqual(19);
});
it('builds a list of links (current page in the start)', function() {
var React = require('react/addons');
var TestUtils = React.addons.TestUtils;
var Pagination = require('../Pagination');
var pagination = TestUtils.renderIntoDocument(
<Pagination total={100} pageSize={5} page={2} onPaginate=""/>
// PREV [1, 2, 3, …, 18, 19, 20] NEXT
// 0 1 2 3 4 5 6 7 8
);
var paginationDOMNode = React.findDOMNode(pagination);
expect(paginationDOMNode.nodeName).toEqual('DIV');
var links = paginationDOMNode.querySelectorAll('a');
expect(links.length).toEqual(9);
expect(links[4].textContent).toEqual('…');
expect(links[5].textContent).toEqual('18');
expect(links[7].textContent).toEqual('20');
expect(links[8].textContent).toEqual('Next');
});
it('builds a list of links (current page before last 3 items)', function() {
var React = require('react/addons');
var TestUtils = React.addons.TestUtils;
var Pagination = require('../Pagination');
var pagination = TestUtils.renderIntoDocument(
<Pagination total={100} pageSize={5} page={16} onPaginate=""/>
// PREV [1, 2, 3, …, 15, 16, 17, 18, 19, 20] NEXT
// 0 1 2 3 4 5 6 7 8 9 10 11
);
var paginationDOMNode = React.findDOMNode(pagination);
expect(paginationDOMNode.nodeName).toEqual('DIV');
var links = paginationDOMNode.querySelectorAll('a');
expect(links.length).toEqual(12);
expect(links[3].textContent).toEqual('3');
expect(links[4].textContent).toEqual('…');
expect(links[5].textContent).toEqual('15');
expect(links[6].textContent).toEqual('16');
expect(links[7].textContent).toEqual('17');
expect(links[8].textContent).toEqual('18');
expect(links[10].textContent).toEqual('20');
expect(links[11].textContent).toEqual('Next');
});
it('builds a list of links (current page right after first 3 items)', function() {
var React = require('react/addons');
var TestUtils = React.addons.TestUtils;
var Pagination = require('../Pagination');
var pagination = TestUtils.renderIntoDocument(
<Pagination total={100} pageSize={5} page={5} onPaginate=""/>
// PREV [1, 2, 3, 4, 5, 6, …, 18, 19, 20] NEXT
// 0 1 2 3 4 5 6 7 8 9 10 11
);
var paginationDOMNode = React.findDOMNode(pagination);
expect(paginationDOMNode.nodeName).toEqual('DIV');
var links = paginationDOMNode.querySelectorAll('a');
expect(links.length).toEqual(12);
expect(links[3].textContent).toEqual('3');
expect(links[4].textContent).toEqual('4');
expect(links[7].textContent).toEqual('…');
expect(links[11].textContent).toEqual('Next');
});
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment