From 3c5ba2843de6f10d6853a17ee33cccee2722638d Mon Sep 17 00:00:00 2001 From: Tanner Linsley Date: Wed, 11 Jan 2017 19:58:52 -0700 Subject: [PATCH] Bug fixes and readme updates --- README.md | 69 +++++----- src/index.js | 280 ++++++++++++++++++++------------------- src/pagination.js | 16 +-- src/utils.js | 17 ++- stories/ServerSide.js | 2 +- stories/Simple.js | 4 +- stories/SubComponents.js | 6 +- 7 files changed, 211 insertions(+), 183 deletions(-) diff --git a/README.md b/README.md index b01a7d8..2504c74 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ - [Installation](#installation) - [Example](#example) - [Data](#data) -- [Default Props](#default-props) +- [Props](#props) - [Columns](#columns) - [Styles](#styles) - [Header Groups](#header-groups) @@ -33,13 +33,11 @@ - [Multi-sort](#multi-sort) - [Component Overrides](#component-overrides) - ## Installation ```bash $ npm install react-table ``` - ## Example ```javascript import ReactTable from 'react-table' @@ -76,19 +74,17 @@ const columns = [{ /> ``` - ## Data Simply pass the `data` prop anything that resembles an array or object. Client-side sorting and pagination are built in, and your table will update gracefully as you change any props. [Server-side data](#server-side-data) is also supported! - -## Default Props -These are the default props for the main react component `` +## Props +These are all of the available props (and their default values) for the main `` component. ```javascript { // General loading: false, // Whether to show the loading overlay or not - pageSize: 20, // The default page size (this can be changed by the user if `showPageSizeOptions` is enabled) + defaultPageSize: 20, // The default page size (this can be changed by the user if `showPageSizeOptions` is enabled) minRows: 0, // Ensure this many rows are always rendered, regardless of rows on page showPagination: true, // Shows or hides the pagination component showPageJump: true, // Shows or hides the pagination number input @@ -115,6 +111,7 @@ These are the default props for the main react component `` trClassName: '', // ClassName for all `tr` elements trClassCallback: row => null, // A call back to dynamically add classes (via the classnames module) to a row element paginationClassName: '' // ClassName for `pagination` element + // Styles style: {}, // Main style object for the component tableStyle: {}, // style object for the `table` component @@ -125,6 +122,12 @@ These are the default props for the main react component `` thStyle: {}, // style object for the `th` component tdStyle: {}, // style object for the `td` component paginationStyle: {}, // style object for the `paginination` component + + // Controlled State (see Using as a Fully Controlled Component below) + page: undefined, + pageSize: undefined, + sorting: undefined, + visibleSubComponents: undefined } ``` @@ -134,7 +137,7 @@ You can easily override the core defaults like so: import { ReactTableDefaults } from 'react-table' Object.assign(ReactTableDefaults, { - pageSize: 10, + defaultPageSize: 10, minRows: 3, // etc... }) @@ -144,15 +147,14 @@ Or just define them on the component per-instance ```javascript ``` - ## Columns -`` requires a `columns` prop, which is an array of objects with the following properties +`` requires a `columns` prop, which is an array of objects containing the following properties ```javascript [{ @@ -162,7 +164,7 @@ Or just define them on the component per-instance sortable: true, sort: 'asc' or 'desc', // used to determine the column sorting on init show: true, // can be used to hide a column - minWidth: 100 // A minimum width for this column. If there is room, columns will flex to fill + minWidth: 100 // A minimum width for this column. If there is room, columns will flex to fill available space // Cell Options className: '', // Set the classname of the `td` element of the column @@ -185,11 +187,9 @@ Or just define them on the component per-instance }] ``` - ## Styles React-table is built to be dropped into existing applications or styled from the ground up, but if you'd like a decent starting point, you can optionally include our default theme `react-table.css`. We think it looks great, honestly :) - ## Header Groups To group columns with another header column, just nest your columns in a header column like so: ```javascript @@ -209,13 +209,13 @@ const columns = [{ ``` ## Sub Tables & Components -By adding a `subComponent` props, you can easily add an expansion level to all root-level rows: +By adding a `SubComponent` props, you can easily add an expansion level to all root-level rows: ```javascript { + defaultPageSize={10} + SubComponent={(row) => { return (
You can put any component you want here, even another React Table! You even have access to the row-level data if you need! Spark-charts, drill-throughs, infographics... the possibilities are endless! @@ -226,7 +226,6 @@ By adding a `subComponent` props, you can easily add an expansion level to all r ``` - ## Server-side Data If you want to handle pagination, and sorting on the server, `react-table` makes it easy on you. @@ -267,38 +266,38 @@ If you want to handle pagination, and sorting on the server, `react-table` makes For a detailed example, take a peek at our [async table mockup](https://github.com/tannerlinsley/react-table/blob/master/example/src/screens/async.js) - ## Multi-Sort When clicking on a column header, hold shift to multi-sort! You can toggle `ascending` `descending` and `none` for multi-sort columns. Clicking on a header without holding shift will clear the multi-sort and replace it with the single sort of that column. It's quite handy! - ## Component Overrides -Though we wouldn't suggest it, `react-table` has the ability to change the core componentry it used to render it's table. You can do so by assigning a react component to it's corresponding global prop, or on a one-off basis like so: +Though we confidently stand by the markup and architecture behind it, `react-table` does offer the ability to change the core componentry it uses to render everything. You can extend or override these internal components by passing a react component to it's corresponding prop on either the global props or on a one-off basis like so: ```javascript // Change the global default import { ReactTableDefaults } from 'react-table' Object.assign(ReactTableDefaults, { - tableComponent: Component, - theadComponent: Component, - tbodyComponent: Component, - trComponent: Component, - thComponent: Component, - tdComponent: Component, - paginationComponent: Component, - previousComponent: Component, - nextComponent: Component, - loadingComponent: Component + TableComponent: Component, + TheadComponent: Component, + TbodyComponent: Component, + TrGroupComponent: Component, + TrComponent: Component, + ThComponent: Component, + TdComponent: Component, + PaginationComponent: Component, + PreviousComponent: Component, + NextComponent: Component, + LoadingComponent: Component, + ExpanderComponent: Component }) // Or change per instance ``` -If you choose to change the core components React-Table uses to render, you must make sure your components support and utilized all of the neeeded features for that component to work properly. For a broad reference on how to do this, investigate [the source](https://github.com/tannerlinsley/react-table/blob/master/src/index.js) for the component you wish to replace. +If you choose to change the core components React-Table uses to render, you must make sure your replacement components consume and utilize all of the supplied and inherited props that are needed for that component to function properly. We would suggest investigating [the source](https://github.com/tannerlinsley/react-table/blob/master/src/index.js) for the component you wish to replace. ## Contributing diff --git a/src/index.js b/src/index.js index 96e9ae2..4a3ac23 100644 --- a/src/index.js +++ b/src/index.js @@ -6,25 +6,23 @@ import _ from './utils' import Pagination from './pagination' -const makeTemplateComponent = (compClass) => ({children, className, ...rest}) => ( -
- {children} -
-) - export const ReactTableDefaults = { // General data: [], loading: false, - pageSize: 20, showPagination: true, showPageSizeOptions: true, pageSizeOptions: [5, 10, 20, 25, 50, 100], + defaultPageSize: 20, showPageJump: true, expanderColumnWidth: 30, + + // State Overrides (for controlled-component style) + // page + // pageSize + // sorting + // visibleSubComponents + // Callbacks onChange: () => null, onTrClick: () => null, @@ -69,12 +67,12 @@ export const ReactTableDefaults = { ofText: 'of', rowsText: 'rows', // Components - tableComponent: makeTemplateComponent('rt-table'), - theadComponent: makeTemplateComponent('rt-thead'), - tbodyComponent: makeTemplateComponent('rt-tbody'), - trGroupComponent: makeTemplateComponent('rt-tr-group'), - trComponent: makeTemplateComponent('rt-tr'), - thComponent: ({toggleSort, className, children, ...rest}) => { + TableComponent: _.makeTemplateComponent('rt-table'), + TheadComponent: _.makeTemplateComponent('rt-thead'), + TbodyComponent: _.makeTemplateComponent('rt-tbody'), + TrGroupComponent: _.makeTemplateComponent('rt-tr-group'), + TrComponent: _.makeTemplateComponent('rt-tr'), + ThComponent: ({toggleSort, className, children, ...rest}) => { return (
) }, - tdComponent: makeTemplateComponent('rt-td'), - expanderComponent: ({isOpen, toggle, ...rest}) => { + TdComponent: _.makeTemplateComponent('rt-td'), + ExpanderComponent: ({isOpen, toggle, ...rest}) => { return (
) }, - paginationComponent: Pagination, - previousComponent: null, - nextComponent: null, - loadingComponent: props => ( -
+ PaginationComponent: Pagination, + PreviousComponent: null, + NextComponent: null, + LoadingComponent: ({loading, loadingText}) => ( +
- {props.loadingText} + {loadingText}
) @@ -112,91 +110,21 @@ export default React.createClass({ getDefaultProps () { return ReactTableDefaults }, + getInitialState () { return { page: 0, + pageSize: this.props.defaultPageSize || 10, sorting: false, visibleSubComponents: {} } }, + componentDidMount () { this.fireOnChange() }, - fireOnChange () { - this.props.onChange({ - page: this.getPropOrState('page'), - pageSize: this.getStateOrProp('pageSize'), - pages: this.getPagesLength(), - sorting: this.getSorting() - }, this) - }, - getPropOrState (key) { - return _.getFirstDefined(this.props[key], this.state[key]) - }, - getStateOrProp (key) { - return _.getFirstDefined(this.state[key], this.props[key]) - }, - getInitSorting (columns) { - if (!columns) { - return [] - } - const initSorting = columns.filter(d => { - return typeof d.sort !== 'undefined' - }).map(d => { - return { - id: d.id, - asc: d.sort === 'asc' - } - }) - return initSorting.length ? initSorting : [{ - id: columns[0].id, - asc: true - }] - }, - sortData (data, sorting) { - return _.orderBy(data, sorting.map(sort => { - return row => { - if (row[sort.id] === null || row[sort.id] === undefined) { - return -Infinity - } - return typeof row[sort.id] === 'string' ? row[sort.id].toLowerCase() : row[sort.id] - } - }), sorting.map(d => d.asc ? 'asc' : 'desc')) - }, - makeDecoratedColumn (column) { - const dcol = Object.assign({}, this.props.column, column) - - if (typeof dcol.accessor === 'string') { - dcol.id = dcol.id || dcol.accessor - const accessorString = dcol.accessor - dcol.accessor = row => _.get(row, accessorString) - return dcol - } - - if (dcol.accessor && !dcol.id) { - console.warn(dcol) - throw new Error('A column id is required if using a non-string accessor for column above.') - } - - if (!dcol.accessor) { - dcol.accessor = d => undefined - } - - return dcol - }, - getSorting (columns) { - return this.props.sorting || (this.state.sorting && this.state.sorting.length ? this.state.sorting : this.getInitSorting(columns)) - }, - getPagesLength () { - return this.props.manual ? this.props.pages - : Math.ceil(this.props.data.length / this.getStateOrProp('pageSize')) - }, - getMinRows () { - return _.getFirstDefined(this.props.minRows, this.getStateOrProp('pageSize')) - }, render () { - const {visibleSubComponents} = this.state const { className, style, @@ -209,6 +137,7 @@ export default React.createClass({ thClassname, thStyle, data, + columns, theadClassName, tbodyClassName, tbodyStyle, @@ -226,8 +155,29 @@ export default React.createClass({ ofText, rowsText, paginationClassName, - expanderColumnWidth - } = this.props + expanderColumnWidth, + manual, + loadingText, + // State + visibleSubComponents, + loading, + pageSize, + page, + // Components + TableComponent, + TheadComponent, + TbodyComponent, + TrGroupComponent, + TrComponent, + ThComponent, + TdComponent, + ExpanderComponent, + PaginationComponent, + PreviousComponent, + NextComponent, + LoadingComponent, + SubComponent + } = this.getResolvedState() // Build Columns const decoratedColumns = [] @@ -236,8 +186,7 @@ export default React.createClass({ // Determine Header Groups let hasHeaderGroups = false - this.props.columns - .forEach(column => { + columns.forEach(column => { if (column.columns) { hasHeaderGroups = true } @@ -252,7 +201,7 @@ export default React.createClass({ } // Build the columns and headers - const visibleColumns = this.props.columns.filter(d => _.getFirstDefined(d.show, true)) + const visibleColumns = columns.filter(d => _.getFirstDefined(d.show, true)) visibleColumns.forEach((column, i) => { if (column.columns) { const nestedColumns = column.columns.filter(d => _.getFirstDefined(d.show, true)) @@ -278,7 +227,7 @@ export default React.createClass({ } const sorting = this.getSorting(decoratedColumns) - const accessedData = this.props.data.map((d, i) => { + const accessedData = data.map((d, i) => { const row = { __original: d, __index: i @@ -288,38 +237,22 @@ export default React.createClass({ }) return row }) - const resolvedData = this.props.manual ? accessedData : this.sortData(accessedData, sorting) + const resolvedData = manual ? accessedData : this.sortData(accessedData, sorting) // Normalize state - const currentPage = this.getPropOrState('page') - const pageSize = this.getStateOrProp('pageSize') const pagesLength = this.getPagesLength() // Pagination - const startRow = pageSize * currentPage + const startRow = pageSize * page const endRow = startRow + pageSize - const pageRows = this.props.manual ? resolvedData : resolvedData.slice(startRow, endRow) + const pageRows = manual ? resolvedData : resolvedData.slice(startRow, endRow) const minRows = this.getMinRows() const padRows = pagesLength > 1 ? _.range(pageSize - pageRows.length) : minRows ? _.range(Math.max(minRows - pageRows.length, 0)) : [] - const canPrevious = currentPage > 0 - const canNext = currentPage + 1 < pagesLength - - const TableComponent = this.props.tableComponent - const TheadComponent = this.props.theadComponent - const TbodyComponent = this.props.tbodyComponent - const TrGroupComponent = this.props.trGroupComponent - const TrComponent = this.props.trComponent - const ThComponent = this.props.thComponent - const TdComponent = this.props.tdComponent - const ExpanderComponent = this.props.expanderComponent - const PaginationComponent = this.props.paginationComponent - const PreviousComponent = this.props.previousComponent - const NextComponent = this.props.nextComponent - const LoadingComponent = this.props.loadingComponent - const SubComponent = this.props.subComponent + const canPrevious = page > 0 + const canNext = page + 1 < pagesLength const rowWidth = (SubComponent ? expanderColumnWidth : 0) + _.sum(decoratedColumns.map(d => d.minWidth)) @@ -535,7 +468,7 @@ export default React.createClass({ {showPagination && ( )} - +
) }, + + // Helpers + getResolvedState () { + return { + ...this.state, + ...this.props, + pages: this.getPagesLength(), + sorting: this.getSorting() + } + }, + fireOnChange () { + this.props.onChange(this.getResolvedState(), this) + }, + getPropOrState (key) { + return _.getFirstDefined(this.props[key], this.state[key]) + }, + getStateOrProp (key) { + return _.getFirstDefined(this.state[key], this.props[key]) + }, + getInitSorting (columns) { + if (!columns) { + return [] + } + const initSorting = columns.filter(d => { + return typeof d.sort !== 'undefined' + }).map(d => { + return { + id: d.id, + asc: d.sort === 'asc' + } + }) + + return initSorting.length ? initSorting : [{ + id: columns[0].id, + asc: true + }] + }, + sortData (data, sorting) { + return _.orderBy(data, sorting.map(sort => { + return row => { + if (row[sort.id] === null || row[sort.id] === undefined) { + return -Infinity + } + return typeof row[sort.id] === 'string' ? row[sort.id].toLowerCase() : row[sort.id] + } + }), sorting.map(d => d.asc ? 'asc' : 'desc')) + }, + makeDecoratedColumn (column) { + const dcol = Object.assign({}, this.props.column, column) + + if (typeof dcol.accessor === 'string') { + dcol.id = dcol.id || dcol.accessor + const accessorString = dcol.accessor + dcol.accessor = row => _.get(row, accessorString) + return dcol + } + + if (dcol.accessor && !dcol.id) { + console.warn(dcol) + throw new Error('A column id is required if using a non-string accessor for column above.') + } + + if (!dcol.accessor) { + dcol.accessor = d => undefined + } + + return dcol + }, + getSorting (columns) { + return this.props.sorting || (this.state.sorting && this.state.sorting.length ? this.state.sorting : this.getInitSorting(columns)) + }, + getPagesLength () { + return this.props.manual ? this.props.pages + : Math.ceil(this.props.data.length / this.getStateOrProp('pageSize')) + }, + getMinRows () { + return _.getFirstDefined(this.props.minRows, this.getStateOrProp('pageSize')) + }, + // User actions setPage (page) { this.setState({ @@ -570,14 +585,13 @@ export default React.createClass({ this.fireOnChange() }) }, - setPageSize (pageSize) { - const currentPageSize = this.getStateOrProp('pageSize') - const currentPage = this.getPropOrState('page') - const currentRow = currentPageSize * currentPage - const page = Math.floor(currentRow / pageSize) + setPageSize (newPageSize) { + const { pageSize, page } = this.getResolvedState() + const currentRow = pageSize * page + const newPage = Math.floor(currentRow / pageSize) this.setState({ - pageSize, - page + pageSize: newPageSize, + page: newPage }, () => { this.fireOnChange() }) diff --git a/src/pagination.js b/src/pagination.js index 4502ae5..7f6d14c 100644 --- a/src/pagination.js +++ b/src/pagination.js @@ -10,11 +10,11 @@ const defaultButton = (props) => ( export default React.createClass({ getInitialState () { return { - page: this.props.currentPage + page: this.props.page } }, componentWillReceiveProps (nextProps) { - this.setState({page: nextProps.currentPage}) + this.setState({page: nextProps.page}) }, getSafePage (page) { return Math.min(Math.max(page, 0), this.props.pagesLength - 1) @@ -27,11 +27,11 @@ export default React.createClass({ applyPage (e) { e && e.preventDefault() const page = this.state.page - this.changePage(page === '' ? this.props.currentPage : page) + this.changePage(page === '' ? this.props.page : page) }, render () { const { - currentPage, + page, pagesLength, showPageSizeOptions, pageSizeOptions, @@ -55,7 +55,7 @@ export default React.createClass({ { if (!canPrevious) return - this.changePage(currentPage - 1) + this.changePage(page - 1) }} disabled={!canPrevious} > @@ -82,8 +82,8 @@ export default React.createClass({ onBlur={this.applyPage} /> - ) : ( - {currentPage + 1} + ) : ( + {page + 1} )} {this.props.ofText} {pagesLength} {showPageSizeOptions && ( @@ -109,7 +109,7 @@ export default React.createClass({ { if (!canNext) return - this.changePage(currentPage + 1) + this.changePage(page + 1) }} disabled={!canNext} > diff --git a/src/utils.js b/src/utils.js index 959df6a..d8eed54 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,3 +1,6 @@ +import React from 'react' +import classnames from 'classnames' +// export default { get, takeRight, @@ -7,7 +10,8 @@ export default { clone, remove, getFirstDefined, - sum + sum, + makeTemplateComponent } function remove (a, b) { @@ -100,3 +104,14 @@ function sum (arr) { return a + b }, 0) } + +function makeTemplateComponent (compClass) { + return ({children, className, ...rest}) => ( +
+ {children} +
+ ) +} diff --git a/stories/ServerSide.js b/stories/ServerSide.js index ad8df0b..477a25d 100644 --- a/stories/ServerSide.js +++ b/stories/ServerSide.js @@ -85,7 +85,7 @@ const ServerSide = React.createClass({ }] }]} manual // Forces table not to paginate or sort automatically, so we can handle it server-side - pageSize={10} + defaultPageSize={10} data={this.state.data} // Set the rows to be displayed pages={this.state.pages} // Display the total number of pages loading={this.state.loading} // Display the loading overlay when we need it diff --git a/stories/Simple.js b/stories/Simple.js index de4b945..9f45cee 100644 --- a/stories/Simple.js +++ b/stories/Simple.js @@ -38,7 +38,7 @@ export default () => {
@@ -78,7 +78,7 @@ return ( ) ` diff --git a/stories/SubComponents.js b/stories/SubComponents.js index 1f90c9b..053f799 100644 --- a/stories/SubComponents.js +++ b/stories/SubComponents.js @@ -38,7 +38,7 @@ export default () => { { return (
@@ -47,7 +47,7 @@ export default () => { { return (
@@ -95,7 +95,7 @@ export default ( { return (
{JSON.stringify(row, null, 2)}