Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
223 changes: 223 additions & 0 deletions src/components/PaginationRow/PaginationRow.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
import React from 'react';
import PropTypes from 'prop-types';
import ClassNames from 'classnames';

import { MenuItem, DropdownButton, Icon } from '../../index';

const ArrowIcon = props => {
const name = `angle-${props.name}`;
return <Icon type="fa" name={name} className="i" />;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you move the ArrowIcon into src/components/PaginationRow/InnerComponents/ArrowIcon.js as described here:
https://github.com/patternfly/patternfly-react/blob/master/CONTRIBUTING.md#code-consistency

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think we can expose it ;)

};

ArrowIcon.propTypes = {
name: PropTypes.oneOf(['left', 'double-left', 'right', 'double-right'])
};

class PaginationRow extends React.Component {
constructor(props) {
super(props);

this.initPagination(props);
this.state = {
pageChangeValue: Number(props.currentPage)
};
}

componentWillReceiveProps(nextProps) {
if (this.props.currentPage !== nextProps.currentPage) {
this.setState({ pageChangeValue: Number(nextProps.currentPage) });
}

this.initPagination(nextProps);
}

initPagination(props) {
this.perPage = Number(props.perPage);
this.totalCount = Number(props.totalCount);
this.currentPage = Number(props.currentPage);
}

msg(key) {
return this.props.messages[key] || PaginationRow.defaultMessages[key];
}

totalPages() {
return Math.ceil(this.props.totalCount / this.perPage);
}

setPageRelative(diff) {
this.setPage(Number(this.props.currentPage) + diff);
}

setPage(page) {
if (page !== '') {
this.props.onPageSet(Number(page));
} else {
console.error("Page can't be blank");
}
}

handlePageChange(e) {
this.setState({ pageChangeValue: e.target.value });
}

handleFormSubmit(e) {
this.setPage(this.state.pageChangeValue);
e.preventDefault();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dont think it matters much (or at all), but I would make sure the browser doesn't submit first :)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also, shouldn't we allow to execute a custom action? e.g. actually go fetch new data or something?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ya i don't think we need to handle form submit... just handle the callback when the values change...and have the server go fetch it...and the send the props back down.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It actually prevents the form from being submitted. I didn't find any way how to use onChange only, because it's triggered on every key-up -> you can't enter more than one-digit numbers.

this.setPage executes onPageSet callback that can be injected via props and used to trigger new data fetch.

}

renderPerPageDropdown() {
const { perPageOptions, onPerPageSet } = this.props;

return (
<DropdownButton
dropup
bsStyle="default"
title={this.perPage}
id="per-page"
>
{perPageOptions.map(opt => {
return (
<MenuItem
active={opt === this.perPage}
onSelect={() => onPerPageSet(opt)}
key={opt}
>
{opt}
</MenuItem>
);
})}
</DropdownButton>
);
}

render() {
const perPageDropdown = this.renderPerPageDropdown();

const displayedRangeStart = (this.currentPage - 1) * this.perPage + 1;
const displayedRangeEnd = Math.min(
displayedRangeStart + this.perPage - 1,
this.totalCount
);
const displayedRange = `${displayedRangeStart}-${displayedRangeEnd}`;

const backButtonsClass = this.currentPage === 1 ? 'disabled' : '';
const nextButtonsClass =
this.currentPage * this.perPage >= this.totalCount ? 'disabled' : '';

const totalPages = this.totalPages();

const classes = ClassNames(this.props.className, 'clearfix');

return (
<div>
<form className={classes} onSubmit={e => this.handleFormSubmit(e)}>
<div className="form-group">
<div>{perPageDropdown}</div>
&nbsp;
<span>{this.msg('perPage')}</span>
</div>
<div className="form-group">
<span>
<span className="pagination-pf-items-current">
{displayedRange}
</span>
&nbsp;{this.msg('of')}&nbsp;
<span className="pagination-pf-items-total">
{this.totalCount}
</span>
</span>

<ul className="pagination pagination-pf-back">
<li className={backButtonsClass}>
<a
title={this.msg('firstPage')}
onClick={() => this.setPage(1)}
>
<ArrowIcon name="double-left" />
</a>
</li>
<li className={backButtonsClass}>
<a
title={this.msg('previousPage')}
onClick={() => this.setPageRelative(-1)}
>
<ArrowIcon name="left" />
</a>
</li>
</ul>

<label className="sr-only">Current Page</label>
<input
className="pagination-pf-page"
value={this.state.pageChangeValue}
onChange={e => this.handlePageChange(e)}
type="text"
/>
<span>
{this.msg('of')}&nbsp;
<span className="pagination-pf-pages">{totalPages}</span>
</span>

<ul className="pagination pagination-pf-forward">
<li className={nextButtonsClass}>
<a
title={this.msg('nextPage')}
onClick={() => this.setPageRelative(1)}
>
<ArrowIcon name="right" />
</a>
</li>
<li className={nextButtonsClass}>
<a
title={this.msg('lastPage')}
onClick={() => this.setPage(totalPages)}
>
<ArrowIcon name="double-right" />
</a>
</li>
</ul>
</div>
</form>
</div>
);
}
}

PaginationRow.propTypes = {
/** Options for the per page dropdown */
perPageOptions: PropTypes.array,
/** Current per page setting */
perPage: PropTypes.number.isRequired, // eslint-disable-line react/no-unused-prop-types
/** Total number of items to paginate */
totalCount: PropTypes.number.isRequired,
/** Index of page that is currently shown, starting from 1 */
currentPage: PropTypes.number.isRequired,
/** A callback triggered when the per page dropdown value is selected */
onPerPageSet: PropTypes.func,
/** A callback triggered when a page is switched */
onPageSet: PropTypes.func,
/** Strings in the component, see PaginationRow.defaultMessages for details */
messages: PropTypes.object,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you use PropTypes.shape here?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

/** Class name for the form element */
className: PropTypes.string
};

PaginationRow.defaultProps = {
perPageOptions: [],
onPageSet: p => {},
onPerPageSet: pp => {},
messages: {},
className: 'content-view-pf-pagination'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Passing a className will override the content-view-pf-pagination, shouldn't it always be there?
Checkout this pr #163

You can add another prop called baseClassName that can have content-view-pf-pagination as default value.
So consumers can add more classes when using className and override the base class when passing baseClassName.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

};

PaginationRow.defaultMessages = {
firstPage: 'First Page',
previousPage: 'Previous Page',
nextPage: 'Next Page',
lastPage: 'Last Page',
perPage: 'per page',
of: 'of'
};

export default PaginationRow;
51 changes: 51 additions & 0 deletions src/components/PaginationRow/PaginationRow.stories.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import React from 'react';
import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import { withKnobs, select, text } from '@storybook/addon-knobs';
import { defaultTemplate } from '../../../storybook/decorators/storyTemplates';
import { PaginationRow } from './index';

const stories = storiesOf('PaginationRow', module);

stories.addDecorator(
defaultTemplate({
title: 'Pagination Row',
documentationLink:
'http://www.patternfly.org/pattern-library/navigation/pagination/'
})
);
stories.addDecorator(withKnobs);
stories.addWithInfo('Basic example', '', () => {
const page = select('Page', [1, 3, 8], 1);
const totalCount = select('Total items', [75, 80, 81], 75);

return (
<PaginationRow
totalCount={Number(totalCount)}
perPage={10}
currentPage={Number(page)}
perPageOptions={[5, 10, 15]}
onPageSet={action('page set')}
onPerPageSet={action('per page value set')}
/>
);
});

stories.addWithInfo('With translations', '', () => {
var messages = {};
for (var key in PaginationRow.defaultMessages) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In today javascript, we should never use for...in syntax again, it is actually danger.
Please use for...of which is an es2015 syntax.
https://www.eventbrite.com/engineering/learning-es6-for-of-loop/

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

messages[key] = text(key, PaginationRow.defaultMessages[key]);
}

return (
<PaginationRow
totalCount={75}
perPage={10}
currentPage={1}
perPageOptions={[5, 10, 15]}
onPageSet={action('page set')}
onPerPageSet={action('per page value set')}
messages={messages}
/>
);
});
48 changes: 48 additions & 0 deletions src/components/PaginationRow/PaginationRow.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/* eslint-env jest */

import React from 'react';
import renderer from 'react-test-renderer';

import PaginationRow from './PaginationRow';

test('PaginationRow renders properly the first page', () => {
const component = renderer.create(
<PaginationRow
totalCount={75}
perPage={10}
currentPage={1}
perPageOptions={[5, 10, 15]}
/>
);

let tree = component.toJSON();
expect(tree).toMatchSnapshot();
});

test('PaginationRow renders properly a middle page', () => {
const component = renderer.create(
<PaginationRow
totalCount={75}
perPage={10}
currentPage={4}
perPageOptions={[5, 10, 15]}
/>
);

let tree = component.toJSON();
expect(tree).toMatchSnapshot();
});

test('PaginationRow renders properly the last page', () => {
const component = renderer.create(
<PaginationRow
totalCount={75}
perPage={10}
currentPage={8}
perPageOptions={[5, 10, 15]}
/>
);

let tree = component.toJSON();
expect(tree).toMatchSnapshot();
});
Loading