diff --git a/.gitignore b/.gitignore index f011788..e0d4623 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ lib/ react-table.js react-table.css *.log + +dist/ diff --git a/README.md b/README.md index f143bd5..593c83b 100644 --- a/README.md +++ b/README.md @@ -83,11 +83,14 @@ These are the default props for the main react component `` { // General loading: false, // Whether to show the loading overlay or not - pageSize: 20, + pageSize: 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 + showPageSizeOptions: true, // Enables the user to change the page size + pageSizeOptions: [5, 10, 20, 25, 50, 100], // The available page size options // Callbacks - onChange: () => null, + onChange: (state, instance) => null, // Anytime the internal state of the table changes, this will fire // Text previousText: 'Previous', diff --git a/example/package.json b/example/package.json index 7e71bf0..822985d 100644 --- a/example/package.json +++ b/example/package.json @@ -10,7 +10,8 @@ }, "scripts": { "watch": "jumpsuit watch", - "build": "jumpsuit build" + "build": "jumpsuit build", + "deploy": "jumpsuit build && zab deploy" }, "devDependencies": { "nib": "^1.1.0", @@ -21,6 +22,7 @@ "jumpsuit": "^0.7.5", "lodash": "^4.16.4", "namor": "^0.3.0", + "react-syntax-highlighter": "^3.0.0", "react-table": "^2.0.0" } } diff --git a/example/src/app.js b/example/src/app.js index fde4949..70b469b 100644 --- a/example/src/app.js +++ b/example/src/app.js @@ -1,5 +1,15 @@ -import { Render } from 'jumpsuit' -// import App from 'screens/async' -import App from 'screens/index' +import { Render, Router, Route, IndexRoute } from 'jumpsuit' +// +import Layout from 'components/layout' +import Simple from 'screens/simple' +import ServerSide from 'screens/serverSide' -Render(null, ) +Render(null, ( + + + + + + + +)) diff --git a/example/src/app.styl b/example/src/app.styl index 9c4fd59..5ee629a 100644 --- a/example/src/app.styl +++ b/example/src/app.styl @@ -11,7 +11,7 @@ global-reset() // vendor styles // ----------------------------------------------------------------------------- -@import '../node_modules/react-table/react-table.css' +@import '../../src/index' // ----------------------------------------------------------------------------- // variables @@ -34,7 +34,6 @@ body background: white font-family: $fnt-open-sans font-weight: 300 - padding-bottom: 50px h1 font-size: 2.5em @@ -50,6 +49,7 @@ strong .logo width: 400px + max-width: 100% .container display: flex @@ -63,11 +63,34 @@ strong font-size: 20px padding: 10px - .table-wrap - width: 700px +.viewport + width:100% + +.table-wrap + width: 90% + margin: auto + padding: 10px + border-radius: 5px + box-shadow: 0 0 20px 0 alpha(black, .2) + +.menu + display:block + margin: 0 10px 20px + ul + display:block + li + display:inline-block + a + display:block padding: 10px - border-radius: 5px - box-shadow: 0 0 20px 0 alpha(black, .2) + margin: 5px + background: alpha(black, .7) + color: white + border-radius: 3px + transition: all .2s ease-out + &.active + &:hover + background: alpha(black, .9) .ReactTable thead @@ -77,3 +100,13 @@ strong box-shadow:inset 0 3px 0 0 alpha(black, .6) &.-sort-desc box-shadow:inset 0 -3px 0 0 alpha(black, .6) + +pre + display:block + font-family: monospace + font-size: 15px + line-height: 20px + border-radius: 5px + margin: 20px auto + max-width: 90% + padding: 0 20px !important diff --git a/example/src/assets/index.html b/example/src/assets/index.html index 90fe657..fdd6aa2 100644 --- a/example/src/assets/index.html +++ b/example/src/assets/index.html @@ -2,6 +2,7 @@ + React-Table Demo diff --git a/example/src/components/codeHighlight.js b/example/src/components/codeHighlight.js new file mode 100644 index 0000000..e8c1fe0 --- /dev/null +++ b/example/src/components/codeHighlight.js @@ -0,0 +1,11 @@ +import { Component } from 'jumpsuit' +import SyntaxHighlighter from 'react-syntax-highlighter' +import atomOneDark from '../../node_modules/react-syntax-highlighter/dist/styles/atom-one-dark' + +export default Component({ + render () { + return ( + {this.props.children} + ) + } +}) diff --git a/example/src/screens/index.js b/example/src/components/layout.js similarity index 54% rename from example/src/screens/index.js rename to example/src/components/layout.js index 1c3f93f..53e08bb 100644 --- a/example/src/screens/index.js +++ b/example/src/components/layout.js @@ -1,43 +1,13 @@ -import { Component } from 'jumpsuit' -import _ from 'lodash' -import namor from 'namor' - -import ReactTable from 'react-table' +import { Component, Link } from 'jumpsuit' export default Component({ render () { - const data = _.map(_.range(5000), d => { - return { - firstName: namor.generate({ words: 1, numLen: 0 }), - lastName: namor.generate({ words: 1, numLen: 0 }), - age: Math.floor(Math.random() * 30) - } - }) - - const columns = [{ - header: 'Name', - columns: [{ - header: 'First Name', - accessor: 'firstName' - }, { - header: 'Last Name', - id: 'lastName', - accessor: d => d.lastName - }] - }, { - header: 'Info', - columns: [{ - header: 'Age', - accessor: 'age' - }] - }] - return (

- react-table demo + react-table

@@ -66,15 +36,22 @@ export default Component({

-
- +
+
    +
  • + + Simple + +
  • +
  • + + Server-Side + +
  • +
-
-
- Tip: Hold shift when sorting to multi-sort! +
+ {this.props.children}
) diff --git a/example/src/screens/async.js b/example/src/screens/async.js deleted file mode 100644 index c04a90d..0000000 --- a/example/src/screens/async.js +++ /dev/null @@ -1,96 +0,0 @@ -import { Component } from 'jumpsuit' -import _ from 'lodash' -import namor from 'namor' - -import ReactTable from 'react-table' - -// Let's mock some data to play around with -const rawData = _.map(_.range(1000), d => { - return { - firstName: namor.generate({ words: 1, numLen: 0 }), - lastName: namor.generate({ words: 1, numLen: 0 }), - age: Math.floor(Math.random() * 30) - } -}) - -// Now let's mock the server. It's job is simple: use the table model to sort and return the page data -const requestData = (pageSize, page, sorting) => { - return new Promise((resolve, reject) => { - // On the server, you'll likely use SQL or noSQL or some other query language to do this. - // For this mock, we'll just use lodash - const sortedData = _.orderBy(rawData, 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')) - - // Be sure to send back the rows to be displayed and any other pertinent information, like how many pages there are total. - const res = { - rows: sortedData.slice(pageSize * page, (pageSize * page) + pageSize), - pages: Math.ceil(rawData.length / pageSize) - } - - // Here we'll simulate a server response with 500ms of delay. - setTimeout(() => resolve(res), 500) - }) -} - -export default Component({ - getInitialState () { - // To handle our data server-side, we need a few things in the state to help us out: - return { - data: [], - pages: null, - loading: true - } - }, - fetchData (state, instance) { - // Whenever the table model changes, or the user sorts or changes pages, this method gets called and passed the current table model. - // You can set the `loading` prop of the table to true to use the built-in one or show you're own loading bar if you want. - this.setState({loading: true}) - // Request the data however you want. Here, we'll use our mocked service we created earlier - requestData(state.pageSize, state.page, state.sorting) - .then((res) => { - // Now just get the rows of data to your React Table (and update anything else like total pages or loading) - this.setState({ - data: res.rows, - pages: res.pages, - loading: false - }) - }) - }, - render () { - return ( -
- d.lastName - }] - }, { - header: 'Info', - columns: [{ - header: 'Age', - accessor: 'age' - }] - }]} - manual // Forces table not to paginate or sort automatically, so we can handle it server-side - pageSize={5} - 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 - onChange={this.fetchData} // Request new data when things change - /> -
- ) - } -}) diff --git a/example/src/screens/serverSide.js b/example/src/screens/serverSide.js new file mode 100644 index 0000000..2eae3a0 --- /dev/null +++ b/example/src/screens/serverSide.js @@ -0,0 +1,170 @@ +import { Component } from 'jumpsuit' +import _ from 'lodash' +import namor from 'namor' + +import CodeHighlight from 'components/codeHighlight' +import ReactTable from '../../../lib/index.js' + +// Let's mock some data to play around with +const rawData = _.map(_.range(3424), d => { + return { + firstName: namor.generate({ words: 1, numLen: 0 }), + lastName: namor.generate({ words: 1, numLen: 0 }), + age: Math.floor(Math.random() * 30) + } +}) + +// Now let's mock the server. It's job is simple: use the table model to sort and return the page data +const requestData = (pageSize, page, sorting) => { + return new Promise((resolve, reject) => { + // On the server, you'll likely use SQL or noSQL or some other query language to do this. + // For this mock, we'll just use lodash + const sortedData = _.orderBy(rawData, 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')) + + // Be sure to send back the rows to be displayed and any other pertinent information, like how many pages there are total. + const res = { + rows: sortedData.slice(pageSize * page, (pageSize * page) + pageSize), + pages: Math.ceil(rawData.length / pageSize) + } + + // Here we'll simulate a server response with 500ms of delay. + setTimeout(() => resolve(res), 500) + }) +} + +export default Component({ + getInitialState () { + // To handle our data server-side, we need a few things in the state to help us out: + return { + data: [], + pages: null, + loading: true + } + }, + fetchData (state, instance) { + // Whenever the table model changes, or the user sorts or changes pages, this method gets called and passed the current table model. + // You can set the `loading` prop of the table to true to use the built-in one or show you're own loading bar if you want. + this.setState({loading: true}) + // Request the data however you want. Here, we'll use our mocked service we created earlier + requestData(state.pageSize, state.page, state.sorting) + .then((res) => { + // Now just get the rows of data to your React Table (and update anything else like total pages or loading) + this.setState({ + data: res.rows, + pages: res.pages, + loading: false + }) + }) + }, + render () { + return ( +
+
+ d.lastName + }] + }, { + header: 'Info', + columns: [{ + header: 'Age', + accessor: 'age' + }] + }]} + manual // Forces table not to paginate or sort automatically, so we can handle it server-side + pageSize={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 + onChange={this.fetchData} // Request new data when things change + /> +
+
+
+ Tip: Hold shift when sorting to multi-sort! +
+ {getCode()} +
+ ) + } +}) + +function getCode () { + return ` +import ReactTable from 'react-table' + +export default React.creatClass({ + getInitialState () { + // To handle our data server-side, we need to keep track of our table state + return { + data: [], + pages: null, + loading: true + } + }, + fetchData (state, instance) { + // Whenever the table model changes (sorting, pagination, etc), this method gets called and passed the current table model. + // You can set the 'loading' prop of the table to true to use the built-in loading notice, or show you're own loading bar if you want. + this.setState({loading: true}) + // Request the data from a server however you want! Be sure to send the bits of the table model that it may neeed. + Axios.post('mysite.com/data', { + pageSize: state.pageSize, + page: state.page, + sorting: state.sorting + }) + .then((res) => { + // Now update your state! + this.setState({ + data: res.rows, + pages: res.pages, + loading: false + }) + }) + }, + render () { + const columns = [{ + header: 'Name', + columns: [{ + header: 'First Name', + accessor: 'firstName' + }, { + header: 'Last Name', + id: 'lastName', + accessor: d => d.lastName + }] + }, { + header: 'Info', + columns: [{ + header: 'Age', + accessor: 'age' + }] + }] + + return ( + + ) + } +}) + ` +} diff --git a/example/src/screens/simple.js b/example/src/screens/simple.js new file mode 100644 index 0000000..c93de30 --- /dev/null +++ b/example/src/screens/simple.js @@ -0,0 +1,103 @@ +import { Component } from 'jumpsuit' +import _ from 'lodash' +import namor from 'namor' + +import CodeHighlight from 'components/codeHighlight' +import ReactTable from '../../../lib/index.js' + +export default Component({ + render () { + const data = _.map(_.range(5553), d => { + return { + firstName: namor.generate({ words: 1, numLen: 0 }), + lastName: namor.generate({ words: 1, numLen: 0 }), + age: Math.floor(Math.random() * 30) + } + }) + + const columns = [{ + header: 'Name', + columns: [{ + header: 'First Name', + accessor: 'firstName' + }, { + header: 'Last Name', + id: 'lastName', + accessor: d => d.lastName + }] + }, { + header: 'Info', + columns: [{ + header: 'Age', + accessor: 'age' + }] + }] + + return ( +
+
+ +
+
+
+ Tip: Hold shift when sorting to multi-sort! +
+ {getCode()} +
+ ) + } +}) + +function getCode () { + return ` +import ReactTable from 'react-table' + +// To help us mock some data +import namor from 'namor' +import _ from 'lodash' + +export default () => { + + // Mock some data + const data = _.map(_.range(5553), d => { + return { + firstName: namor.generate({ words: 1, numLen: 0 }), + lastName: namor.generate({ words: 1, numLen: 0 }), + age: Math.floor(Math.random() * 30) + } + }) + + // Create some column definitions + const columns = [{ + header: 'Name', + columns: [{ + header: 'First Name', + accessor: 'firstName' + }, { + header: 'Last Name', + id: 'lastName', + accessor: d => d.lastName + }] + }, { + header: 'Info', + columns: [{ + header: 'Age', + accessor: 'age' + }] + }] + + // Display your table! + return ( + + ) +}) + ` +} diff --git a/src/index.js b/src/index.js index c4e1494..e60648d 100644 --- a/src/index.js +++ b/src/index.js @@ -3,16 +3,17 @@ import classnames from 'classnames' // import _ from './utils' -const defaultButton = (props) => ( - -) +import Pagination from './pagination' export const ReactTableDefaults = { - // State + // General data: [], loading: false, pageSize: 20, - minRows: 0, + showPagination: true, + showPageSizeOptions: true, + pageSizeOptions: [5, 10, 20, 25, 50, 100], + showPageJump: true, // Callbacks onChange: () => null, // Classes @@ -58,10 +59,24 @@ export const ReactTableDefaults = { theadComponent: (props) => {props.children}, tbodyComponent: (props) => {props.children}, trComponent: (props) => {props.children}, - thComponent: (props) => {props.children}, + thComponent: (props) => { + const {toggleSort, ...rest} = props + return ( + { + toggleSort && toggleSort(e) + }}>{props.children} + ) + }, tdComponent: (props) => {props.children}, previousComponent: null, - nextComponent: null + nextComponent: null, + loadingComponent: props => ( +
+
+ {props.loadingText} +
+
+ ) } export default React.createClass({ @@ -71,7 +86,6 @@ export default React.createClass({ getInitialState () { return { page: 0, - pages: -1, sorting: false } }, @@ -80,12 +94,18 @@ export default React.createClass({ }, fireOnChange () { this.props.onChange({ - page: _.getFirstDefined(this.props.page, this.state.page), - pageSize: this.props.pageSize, - pages: this.props.pages, + 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 [] @@ -138,6 +158,13 @@ export default React.createClass({ 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.props.pageSize) + }, render () { // Build Columns const decoratedColumns = [] @@ -198,17 +225,22 @@ export default React.createClass({ }) const data = this.props.manual ? accessedData : this.sortData(accessedData, sorting) + // Normalize state + const currentPage = this.getPropOrState('page') + const pageSize = this.getStateOrProp('pageSize') + const pagesLength = this.getPagesLength() + // Pagination - const pagesLength = this.props.manual ? this.props.pages : Math.ceil(data.length / this.props.pageSize) - const startRow = this.props.pageSize * this.state.page - const endRow = startRow + this.props.pageSize + const startRow = pageSize * currentPage + const endRow = startRow + pageSize const pageRows = this.props.manual ? data : data.slice(startRow, endRow) - const padRows = pagesLength > 1 ? _.range(this.props.pageSize - pageRows.length) - : this.props.minRows ? _.range(Math.max(this.props.minRows - pageRows.length, 0)) + const minRows = this.getMinRows() + const padRows = pagesLength > 1 ? _.range(pageSize - pageRows.length) + : minRows ? _.range(Math.max(minRows - pageRows.length, 0)) : [] - const canPrevious = this.state.page > 0 - const canNext = this.state.page + 1 < pagesLength + const canPrevious = currentPage > 0 + const canNext = currentPage + 1 < pagesLength const TableComponent = this.props.tableComponent const TheadComponent = this.props.theadComponent @@ -216,9 +248,9 @@ export default React.createClass({ const TrComponent = this.props.trComponent const ThComponent = this.props.thComponent const TdComponent = this.props.tdComponent - - const PreviousComponent = this.props.previousComponent || defaultButton - const NextComponent = this.props.nextComponent || defaultButton + const PreviousComponent = this.props.previousComponent + const NextComponent = this.props.nextComponent + const LoadingComponent = this.props.loadingComponent return (
{ + toggleSort={(e) => { column.sortable && this.sortColumn(column, e.shiftKey) }} > @@ -384,37 +416,26 @@ export default React.createClass({ })} - {pagesLength > 1 && ( -
-
- this.previousPage(e))} - disabled={!canPrevious} - > - {this.props.previousText} - -
-
- Page {this.state.page + 1} of {pagesLength} -
-
- this.nextPage(e))} - disabled={!canNext} - > - {this.props.nextText} - -
-
+ {this.props.showPagination && pagesLength > 1 && ( + )} -
-
- {this.props.loadingText} -
-
+
) }, @@ -426,13 +447,17 @@ export default React.createClass({ this.fireOnChange() }) }, - nextPage (e) { - e.preventDefault() - this.setPage(this.state.page + 1) - }, - previousPage (e) { - e.preventDefault() - this.setPage(this.state.page - 1) + setPageSize (pageSize) { + const currentPageSize = this.getStateOrProp('pageSize') + const currentPage = this.getPropOrState('page') + const currentRow = currentPageSize * currentPage + const page = Math.floor(currentRow / pageSize) + this.setState({ + pageSize, + page + }, () => { + this.fireOnChange() + }) }, sortColumn (column, additive) { const existingSorting = this.getSorting() diff --git a/src/index.styl b/src/index.styl index 1714191..37013ac 100644 --- a/src/index.styl +++ b/src/index.styl @@ -120,10 +120,10 @@ $easeOutBack = cubic-bezier(0.175, 0.885, 0.320, 1.275) .-pagination width:100% display:flex - margin-top:5px justify-content: space-between align-items: center flex-wrap: wrap + padding-top:3px .-btn appearance:none @@ -147,21 +147,33 @@ $easeOutBack = cubic-bezier(0.175, 0.885, 0.320, 1.275) background: alpha(black, .3) color: white - .-left + .-previous .-center - .-right - flex: 1 - // min-width:150px - margin-bottom:5px + .-next + margin:3px 0 - .-left - .-right + .-previous + .-next + flex: 1 text-align: center - .-center + flex: 1.5 text-align:center - padding: 0 5px + + .-pageInfo + display: inline-block + margin: 0 10px 3px + white-space: nowrap + + .-pageJump + display:inline-block + input + width: 70px + text-align:center + + .-pageSizeOptions + margin-left:12px .-loading @@ -194,3 +206,29 @@ $easeOutBack = cubic-bezier(0.175, 0.885, 0.320, 1.275) pointer-events: all > div transform: translateY(50%) + + input + select + appearance: none + border: 1px solid rgba(0,0,0,0.1) + padding: 5px 7px + font-size: inherit + border-radius: 3px + font-weight: normal + outline:none + + .select-wrap + position:relative + display:inline-block + select + padding: 5px 15px 5px 7px + min-width:100px + &:after + content: '' + position: absolute + right: 8px + top: 50% + transform: translate(0, -50%) + border-color: #999 transparent transparent + border-style: solid + border-width: 5px 5px 2.5px diff --git a/src/pagination.js b/src/pagination.js new file mode 100644 index 0000000..4cbd7b7 --- /dev/null +++ b/src/pagination.js @@ -0,0 +1,121 @@ +import React from 'react' +import classnames from 'classnames' +// +// import _ from './utils' + +const defaultButton = (props) => ( + +) + +export default React.createClass({ + getInitialState () { + return { + page: this.props.currentPage + } + }, + componentWillReceiveProps (nextProps) { + this.setState({page: nextProps.currentPage}) + }, + getSafePage (page) { + return Math.min(Math.max(page, 0), this.props.pagesLength - 1) + }, + changePage (page) { + page = this.getSafePage(page) + this.setState({page}) + this.props.onChange(page) + }, + applyPage (e) { + e && e.preventDefault() + const page = this.state.page + this.changePage(page === '' ? this.props.currentPage : page) + }, + render () { + const { + currentPage, + pagesLength, + showPageSizeOptions, + pageSizeOptions, + pageSize, + showPageJump, + canPrevious, + canNext, + onPageSizeChange + } = this.props + + const PreviousComponent = this.props.PreviousComponent || defaultButton + const NextComponent = this.props.NextComponent || defaultButton + + return ( +
+
+ { + if (!canPrevious) return + this.changePage(currentPage - 1) + }} + disabled={!canPrevious} + > + {this.props.previousText} + +
+
+ + Page {showPageJump ? ( +
+ { + const val = e.target.value + const page = val - 1 + if (val === '') { + return this.setState({page: val}) + } + this.setState({page: this.getSafePage(page)}) + }} + value={this.state.page === '' ? '' : this.state.page + 1} + onBlur={this.applyPage} + /> +
+ ) : ( + {currentPage + 1} + )} of {pagesLength} +
+ {showPageSizeOptions && ( + + + + )} +
+
+ { + if (!canNext) return + this.changePage(currentPage + 1) + }} + disabled={!canNext} + > + {this.props.nextText} + +
+
+ ) + } +})