├── .babelrc ├── .editorconfig ├── .gitignore ├── CONTRIBUTING.md ├── README.md ├── dist └── index.js ├── example ├── config │ ├── loaders.js │ ├── pages.js │ ├── plugins.js │ ├── webpack.config.dev.js │ └── webpack.config.prod.js ├── server │ ├── index.js │ └── middleware │ │ ├── single-page-middleware.js │ │ └── webpack-middleware.js └── src │ ├── index.handlebars │ ├── js │ ├── App.js │ └── InfiniteScrollExample.js │ ├── main.js │ ├── routes.js │ └── styles │ ├── 3rdparty │ └── _bootstrap.scss │ └── screen.scss ├── index.js └── package.json /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["es2015", { "modules": false, "loose" : true }], 4 | "react", 5 | "stage-2" 6 | ], 7 | "plugins": ["react-hot-loader/babel"] 8 | } -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain 2 | # consistent coding styles between different editors and IDEs. 3 | 4 | root = true 5 | 6 | [*.js] 7 | charset = utf-8 8 | indent_style = space 9 | indent_size = 2 10 | end_of_line = lf 11 | insert_final_newline = true 12 | trim_trailing_whitespace = true 13 | 14 | [*.md] 15 | insert_final_newline = false 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea 3 | *.iml 4 | node_modules/ 5 | *.log* 6 | npm-debug.log 7 | mobile/common/ 8 | *.pyc 9 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | We're always looking to improve this project, open source contribution is encouraged so long as they adhere to our guidelines. 3 | 4 | # Pull Requests 5 | 6 | The Solid State team will be monitoring for pull requests. When we get one, a member of team will test the work against our internal uses and sign off on the changes. From here, we'll either merge the pull request or provide feedback suggesting the next steps. 7 | 8 | **A couple things to keep in mind:** 9 | 10 | - If you've changed APIs, update the documentation. 11 | - Keep the code style (indents, wrapping) consistent. 12 | - If your PR involves a lot of commits, squash them using ```git rebase -i``` as this makes it easier for us to review. 13 | - Keep lines under 80 characters. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Virtualized Infinite Scroll Component 2 | 3 | A React component that provides you with an infinite scrolling list that be used in either direction. 4 | 5 | ## Install 6 | ``` 7 | $ npm install react-virtualized-infinite-scroll --save 8 | ``` 9 | 10 | ## Usage 11 | 12 | ``` 13 | import InfiniteScroll from 'react-virtualized-infinite-scroll'; 14 | 15 | ... 16 | 17 | loadMore = () => { 18 | // Set state to loading data 19 | this.setState({ 20 | isLoading: true, 21 | }); 22 | 23 | // @TODO Perform asychronous load of data 24 | } 25 | 26 | // Optional function to return dynamic row height 27 | rowHeight = ({ index }) => { 28 | return index < this.state.data.length ? this.state.data[index].height : 40; 29 | } 30 | 31 | // Example function for adding data to the bottom of the list in reverse mode 32 | addToBottom = () => { 33 | let data = this.state.data.slice(0); 34 | data.unshift({ key: data.length, height: this.getRandomHeight() }); 35 | this.setState({ data }); 36 | if (this.infiniteScroll) { 37 | this.infiniteScroll.adjustScrollPos(this.state.randomRowHeights ? data.height * -1 : -40); 38 | } 39 | } 40 | 41 | renderRow (row) { 42 | return ( 43 |
44 | Row {row.key + 1} 45 |
46 | ); 47 | } 48 | 49 | render () { 50 | return ( 51 |
52 | 61 | Loading... 62 |
63 | )} 64 | containerHeight={200} 65 | ref={(infiniteScroll) => this.infiniteScroll = infiniteScroll} 66 | scrollRef={(virtualScroll) => this.virtualScroll = virtualScroll} 67 | reverse={this.props.reverse} 68 | /> 69 | 70 | ); 71 | } 72 | 73 | ``` 74 | 75 | ### Prop Types 76 | | Property | Type | Required? | Description | 77 | |:---|:---|:---:|:---| 78 | | loadMore | Function | ✓ | Callback used for loading more data | 79 | | renderRow | Function | ✓ | Used to render each row | 80 | | rowHeight | Number or Function | ✓ | Either a fixed row height (number) or a function that returns the height of a row given its index: `({ index: number }): number` | 81 | | threshold | Number | ✓ | How many rows before the bottom (or top in reverse mode) to request more data | 82 | | isLoading | Bool | | While true a loading item is shown at the bottom (or top in reverse mode). Useful while loading more data | 83 | | scrollToRow | Number | | Row index to ensure visible (by forcefully scrolling if necessary) | 84 | | renderLoading | Object | | Render a custom loading item | 85 | | data | Array | | Data array | 86 | | containerHeight | Number | | Force a height on the entire list component. Default is to auto fill available space | 87 | | reverse | Bool | | Reverse scroll direction. Defaults to `false` | 88 | | scrollRef | Function | | Callback used to give back reference to underlying virtual scroll component for finer control | 89 | 90 | ## Development 91 | Should you wish to develop this module further start by cloning this repository 92 | 93 | ### Run Dev - Run hot reloading node server 94 | ``` 95 | $ npm start 96 | ``` 97 | 98 | ### Run Prod - Build, deploy, minify npm module 99 | ``` 100 | $ npm run prod 101 | ``` 102 | 103 | ### Testing the module 104 | See ```InfiniteScrollExample.js```, this component imports your developed module, if you wish to point to production then uncomment the other import line for InfiniteScroll 105 | 106 | # Getting Help 107 | If you encounter a bug or feature request we would like to hear about it. Before you submit an issue please search existing issues in order to prevent duplicates. 108 | 109 | # Contributing 110 | For more information about contributing PRs, please see our Contribution Guidelines. 111 | 112 | 113 | # Get in touch 114 | If you have any questions about our projects you can email projects@solidstategroup.com. 115 | -------------------------------------------------------------------------------- /example/config/loaders.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | { 3 | test: /\.css$/, 4 | loaders: ['style', 'css'] 5 | }, 6 | { 7 | test: /\.js?/, 8 | exclude: /node_modules/, 9 | loaders: ['babel'] 10 | }, 11 | { 12 | test: /\.html$/, 13 | loader: 'html-loader?attrs[]=source:src&attrs[]=img:src' 14 | }, 15 | { 16 | test: /\.(jpe?g|png|gif|svg|mp4|webm)$/i, 17 | loaders: [ 18 | 'file?hash=sha512&digest=hex&name=[hash].[ext]', 19 | 'image-webpack' 20 | ] 21 | } 22 | ]; -------------------------------------------------------------------------------- /example/config/pages.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by kylejohnson on 14/09/2016. 3 | */ 4 | module.exports = ['index']; -------------------------------------------------------------------------------- /example/config/plugins.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const pages = require('./pages'); 3 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 4 | 5 | module.exports = [ 6 | //Copy static content 7 | new CopyWebpackPlugin([ 8 | { from: './src/images', to: './build/images' }, 9 | { from: './src/fonts', to: path.join(__dirname, '../build/fonts') } 10 | ], { copyUnmodified: true }) 11 | ]; -------------------------------------------------------------------------------- /example/config/webpack.config.dev.js: -------------------------------------------------------------------------------- 1 | // webpack.config.dev.js 2 | var path = require('path') 3 | var src = path.join(__dirname, '../src') + '/'; 4 | var webpack = require('webpack'); 5 | 6 | module.exports = { 7 | devtool: 'eval', 8 | entry: [ 9 | 'webpack-hot-middleware/client', 10 | 'react-hot-loader/patch', 11 | './example/src/main.js', 12 | ], 13 | output: { 14 | path: '/', 15 | publicPath: 'http://localhost:3000/example/build/', 16 | filename: '[name].js' 17 | }, 18 | plugins: [ 19 | new webpack.HotModuleReplacementPlugin(), 20 | new webpack.NoErrorsPlugin() 21 | ], 22 | module: { 23 | loaders: require('./loaders') 24 | .concat([ 25 | { 26 | test: /\.scss$/, 27 | loaders: ['style', 'css', 'sass'] 28 | } 29 | ]), 30 | } 31 | }; -------------------------------------------------------------------------------- /example/config/webpack.config.prod.js: -------------------------------------------------------------------------------- 1 | // webpack.config.prod.js 2 | // Watches + deploys files minified + cachebusted 3 | var path = require('path'); 4 | var webpack = require('webpack'); 5 | const CleanWebpackPlugin = require('clean-webpack-plugin'); 6 | var libName = 'index'; 7 | var outputFile = libName + '.js'; 8 | 9 | module.exports = { 10 | devtool: 'source-map', 11 | 12 | entry: [ 13 | './index.js', 14 | ], 15 | 16 | module: { 17 | loaders: require('./loaders') 18 | }, 19 | 20 | output: { 21 | publicPath: '/', 22 | path: './dist', 23 | filename: outputFile, 24 | library: libName, 25 | libraryTarget: 'umd', 26 | umdNamedDefine: true 27 | }, 28 | 29 | plugins: [ 30 | //Clear out build folder 31 | new CleanWebpackPlugin(['dist'], { root: '../' }), 32 | 33 | //Ensure NODE_ENV is set to production 34 | new webpack.DefinePlugin({ 35 | 'process.env': { 36 | 'NODE_ENV': JSON.stringify('production') 37 | }, 38 | __DEV__: JSON.stringify(JSON.parse(process.env.DEBUG || 'false')) 39 | }), 40 | 41 | //remove duplicate files 42 | new webpack.optimize.DedupePlugin(), 43 | 44 | new webpack.LoaderOptionsPlugin({ 45 | minimize: true, 46 | debug: false 47 | }), 48 | 49 | //Uglify 50 | new webpack.optimize.UglifyJsPlugin({ 51 | compress: { 52 | warnings: false, 53 | 'screw_ie8': true 54 | }, 55 | output: { 56 | comments: false 57 | }, 58 | sourceMap: false 59 | }), 60 | 61 | ] 62 | } 63 | ; -------------------------------------------------------------------------------- /example/server/index.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import exphbs from 'express-handlebars'; 3 | import spm from './middleware/single-page-middleware'; 4 | import webpackMiddleware from './middleware/webpack-middleware'; 5 | const isDev = process.env.NODE_ENV !== 'production'; 6 | const app = express(); 7 | 8 | console.log('Enabled Webpack Hot Reloading'); 9 | webpackMiddleware(app); 10 | 11 | app.set('views', 'example/src/'); 12 | app.use(express.static('example/src')); 13 | 14 | app.use(spm); 15 | app.engine('handlebars', exphbs()); 16 | app.set('view engine', 'handlebars'); 17 | 18 | app.get('/', function (req, res) { 19 | res.render('index', { 20 | isDev, 21 | layout: false 22 | }); 23 | }); 24 | 25 | app.listen(3000, function () { 26 | console.log('express-handlebars example server listening on: 3000'); 27 | }); -------------------------------------------------------------------------------- /example/server/middleware/single-page-middleware.js: -------------------------------------------------------------------------------- 1 | module.exports = function(req, res, next) { 2 | 3 | const headers = req.headers; 4 | let rewriteTarget = '/index.html'; 5 | 6 | if (req.method !== 'GET') { 7 | console.log( 8 | 'Not rewriting', 9 | req.method, 10 | req.url, 11 | 'because the method is not GET.' 12 | ); 13 | req.url = rewriteTarget.split('/')[rewriteTarget.split('/').length-1]; 14 | return next(); 15 | } else if (!headers || typeof headers.accept !== 'string') { 16 | console.log( 17 | 'Not rewriting', 18 | req.method, 19 | req.url, 20 | 'because the client did not send an HTTP accept header.' 21 | ); 22 | req.url = rewriteTarget.split('/')[rewriteTarget.split('/').length-1]; 23 | return next(); 24 | } else if (headers.accept.indexOf('application/json') === 0) { 25 | console.log( 26 | 'Not rewriting', 27 | req.method, 28 | req.url, 29 | 'because the client prefers JSON.' 30 | ); 31 | req.url = rewriteTarget.split('/')[rewriteTarget.split('/').length-1]; 32 | return next(); 33 | } else if (headers.accept.indexOf('html') == -1) { 34 | console.log( 35 | 'Not rewriting', 36 | req.method, 37 | req.url, 38 | 'because the client does not accept HTML.' 39 | ); 40 | req.url = '/' + req.url.split('/')[req.url.split('/').length-1]; 41 | return next(); 42 | }; 43 | 44 | var parsedUrl = req.url; 45 | 46 | if (parsedUrl.indexOf('.') !== -1 && parsedUrl.indexOf('t/') == -1) { 47 | console.log( 48 | 'Not rewriting', 49 | req.method, 50 | req.url, 51 | 'because the path includes a dot (.) character.' 52 | ); 53 | return next(); 54 | } 55 | 56 | rewriteTarget = '/'; 57 | req.url = rewriteTarget; 58 | next(); 59 | }; -------------------------------------------------------------------------------- /example/server/middleware/webpack-middleware.js: -------------------------------------------------------------------------------- 1 | //Uses webpack dev + hot middleware 2 | import webpack from 'webpack'; 3 | import config from '../../config/webpack.config.dev'; 4 | import webpackDevMiddleware from 'webpack-dev-middleware'; 5 | import webpackHotMiddleware from 'webpack-hot-middleware'; 6 | 7 | const compiler = webpack(config); 8 | 9 | module.exports = function (app) { 10 | const middleware = webpackDevMiddleware(compiler, { 11 | publicPath: config.output.publicPath, 12 | contentBase: 'example/src', 13 | stats: { colors: true }, 14 | }); 15 | app.use(middleware); 16 | 17 | app.use(webpackHotMiddleware(compiler, { 18 | log: console.log, 19 | path: '/__webpack_hmr', 20 | heartbeat: 10 * 1000 21 | })); 22 | return middleware; 23 | }; -------------------------------------------------------------------------------- /example/src/index.handlebars: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | React Virtualized Infinite Scroll 8 | 9 | 10 | 11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /example/src/js/App.js: -------------------------------------------------------------------------------- 1 | import React, {Component, PropTypes} from 'react'; 2 | import DocumentTitle from 'react-document-title'; 3 | import InfiniteScrollExample from './InfiniteScrollExample'; 4 | 5 | const App = class extends Component { 6 | displayName: 'App'; 7 | 8 | render () { 9 | return ( 10 |
11 |

12 | Async Infinite Scroll 13 |

14 | 15 |

16 | Async Infinite Reverse Scroll 17 |

18 | 19 |
20 | ); 21 | } 22 | }; 23 | 24 | App.propTypes = {}; 25 | 26 | module.exports = App; -------------------------------------------------------------------------------- /example/src/js/InfiniteScrollExample.js: -------------------------------------------------------------------------------- 1 | import React, {Component, PropTypes} from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import InfiniteScroll from '../../../'; 4 | const range = require('lodash.range'); 5 | //import InfiniteScroll from '../../../dist/'; 6 | 7 | class InfiniteScrollExample extends Component { 8 | constructor (props) { 9 | super(props); 10 | this.state = { 11 | data: range(0, 100).map((num) => ({ key: num, height: this.getRandomHeight() })), 12 | randomRowHeights: false, 13 | }; 14 | } 15 | 16 | getRandomHeight () { 17 | return Math.round(Math.random() * 60 + 40); 18 | } 19 | 20 | loadMore = () => { 21 | this.setState({ 22 | isLoading: true, 23 | }); 24 | setTimeout(() => { 25 | const length = this.state.data.length; 26 | this.setState({ 27 | isLoading: false, 28 | data: this.state.data.concat(range(length, length + 100) 29 | .map((num) => ({ key: num, height: this.getRandomHeight() }))) 30 | }); 31 | }, 2000); 32 | } 33 | 34 | addToBottom = () => { 35 | let data = this.state.data.slice(0); 36 | data.unshift({ key: data.length, height: this.getRandomHeight() }); 37 | this.setState({ data }); 38 | if (this.infiniteScroll) { 39 | this.infiniteScroll.adjustScrollPos(this.state.randomRowHeights ? data.height * -1 : -40); 40 | } 41 | } 42 | 43 | rowHeight = ({ index }) => { 44 | if (!this.state.randomRowHeights) { 45 | return 40; 46 | } 47 | return index < this.state.data.length ? this.state.data[index].height : 40; 48 | } 49 | 50 | onRandomRowHeightsChanged = (randomRowHeights) => { 51 | this.setState({ randomRowHeights }); 52 | this.virtualScroll.recomputeRowHeights(); 53 | } 54 | 55 | renderRow (data) { 56 | return ( 57 |
58 | Row {data.key + 1} 59 |
60 | ); 61 | } 62 | 63 | render () { 64 | return ( 65 |
66 | 69 | Loading... 70 |
71 | )} 72 | rowHeight={this.rowHeight} 73 | containerHeight={200} 74 | threshold={50} 75 | data={this.state.data} 76 | isLoading={this.state.isLoading} 77 | loadMore={this.loadMore} 78 | renderRow={this.renderRow} 79 | ref={(infiniteScroll) => this.infiniteScroll = infiniteScroll} 80 | scrollRef={(virtualScroll) => this.virtualScroll = virtualScroll} 81 | reverse={this.props.reverse} 82 | /> 83 |
84 | Use random row heights ? 85 | 86 |
87 | {this.props.reverse &&
} 88 | 89 | ); 90 | } 91 | }; 92 | 93 | InfiniteScrollExample.propTypes = { 94 | reverse: PropTypes.bool 95 | } 96 | 97 | InfiniteScrollExample.defaultProps = { 98 | reverse: false 99 | } 100 | 101 | export default InfiniteScrollExample; 102 | -------------------------------------------------------------------------------- /example/src/main.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by kylejohnson on 16/09/2016. 3 | */ 4 | import './styles/screen.scss'; 5 | import {render} from 'react-dom'; 6 | import {Router, browserHistory} from 'react-router'; 7 | import routes from './routes'; 8 | import React from 'react'; 9 | import {AppContainer} from 'react-hot-loader'; 10 | 11 | // Render the React application to the DOM 12 | 13 | const el = ; 14 | const renderEl = () => { 15 | render( 16 | 17 | {el} 18 | , 19 | document.getElementById('react') 20 | ); 21 | }; 22 | 23 | renderEl(); 24 | if (module.hot) { 25 | module.hot.accept('./routes', renderEl); 26 | } -------------------------------------------------------------------------------- /example/src/routes.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {render} from 'react-dom' 3 | import { Route, IndexRoute } from 'react-router' 4 | import App from './js/App'; 5 | 6 | 7 | module.exports = ( 8 | 9 | ); -------------------------------------------------------------------------------- /example/src/styles/3rdparty/_bootstrap.scss: -------------------------------------------------------------------------------- 1 | $enable-flex: true; 2 | 3 | // Core variables and mixins 4 | //@import "~bootstrap/scss/custom"; 5 | @import "../../../node_modules/bootstrap/scss/variables"; 6 | @import "../../../node_modules/bootstrap/scss/mixins"; 7 | 8 | // Reset and dependencies 9 | @import "../../../node_modules/bootstrap/scss/normalize"; 10 | @import "../../../node_modules/bootstrap/scss/print"; 11 | 12 | // Core CSS 13 | @import "../../../node_modules/bootstrap/scss/reboot"; 14 | //@import "~bootstrap/scss/type"; 15 | //@import "~bootstrap/scss/images"; 16 | //@import "~bootstrap/scss/code"; 17 | @import "../../../node_modules/bootstrap/scss/tables"; 18 | @import "../../../node_modules/bootstrap/scss/grid"; 19 | //@import "~bootstrap/scss/forms"; 20 | @import "../../../node_modules/bootstrap/scss/buttons"; 21 | 22 | // Components 23 | @import "../../../node_modules/bootstrap/scss/animation"; 24 | //@import "~bootstrap/scss/dropdown"; 25 | //@import "~bootstrap/scss/button-group"; 26 | //@import "~bootstrap/scss/input-group"; 27 | //@import "~bootstrap/scss/custom-forms"; 28 | @import "../../../node_modules/bootstrap/scss/nav"; 29 | @import "../../../node_modules/bootstrap/scss/navbar"; 30 | //@import "~bootstrap/scss/card"; 31 | //@import "~bootstrap/scss/breadcrumb"; 32 | //@import "~bootstrap/scss/pagination"; 33 | //@import "~bootstrap/scss/tags"; 34 | //@import "~bootstrap/scss/jumbotron"; 35 | //@import "~bootstrap/scss/alert"; 36 | //@import "~bootstrap/scss/progress"; 37 | //@import "~bootstrap/scss/media"; 38 | //@import "~bootstrap/scss/list-group"; 39 | @import "../../../node_modules/bootstrap/scss/responsive-embed"; 40 | //@import "~bootstrap/scss/close"; 41 | 42 | // Components w/ JavaScript 43 | @import "../../../node_modules/bootstrap/scss/modal"; 44 | //@import "~bootstrap/scss/tooltip"; 45 | //@import "popover"; 46 | //@import "~bootstrap/scss/carousel"; 47 | 48 | // Utility classes 49 | @import "../../../node_modules/bootstrap/scss/utilities"; 50 | -------------------------------------------------------------------------------- /example/src/styles/screen.scss: -------------------------------------------------------------------------------- 1 | @import "~compass-mixins"; 2 | // 3rd party 3 | @import "3rdparty/bootstrap"; -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import 'react-virtualized/styles.css'; 4 | import { AutoSizer, InfiniteLoader, VirtualScroll } from 'react-virtualized'; 5 | 6 | class InfiniteScroll extends Component { 7 | 8 | constructor(props) { 9 | super(props); 10 | this.state = { 11 | scrollToBottom: true 12 | }; 13 | this.scrollHeight = 0; 14 | } 15 | 16 | loadMore = () => { 17 | if (!this.props.isLoading) { 18 | this.props.loadMore(); 19 | } 20 | } 21 | 22 | isRowLoaded = ({ index }) => ( 23 | (this.props.reverse ? this.props.data.length - 1 - index : index) < this.props.data.length - this.props.threshold 24 | ) 25 | 26 | onScroll = ({ clientHeight, scrollHeight, scrollTop }) => { 27 | if (!this.props.reverse) { 28 | return; 29 | } 30 | 31 | // Keep track of scroll height 32 | this.scrollHeight = scrollHeight; 33 | 34 | // Check whether initial scroll to bottom in reverse mode has completed 35 | if (this.state.scrollToBottom && scrollTop === scrollHeight - clientHeight) { 36 | this.setState({ scrollToBottom: false }); 37 | } 38 | } 39 | 40 | adjustScrollPos = (adj) => { 41 | const virtualScroll = ReactDOM.findDOMNode(this.virtualScroll); 42 | if (virtualScroll.scrollTop !== virtualScroll.scrollHeight - virtualScroll.clientHeight) { 43 | virtualScroll.scrollTop += adj; 44 | } 45 | } 46 | 47 | componentWillReceiveProps(newProps) { 48 | // Scroll to bottom on initial data in reverse mode only 49 | if (this.props.reverse && 50 | (!this.props.data || this.props.data.length === 0) && 51 | newProps.data && newProps.data.length) { 52 | this.setState({ scrollToBottom: true }); 53 | } 54 | } 55 | 56 | componentDidUpdate(prevProps, prevState) { 57 | if (!this.props.reverse) { 58 | return; 59 | } 60 | 61 | // Re-measure all estimated rows if data has changed or loading is displayed 62 | if (prevProps.data && this.props.data && 63 | (prevProps.data.length !== this.props.data.length || 64 | prevProps.isLoading !== this.props.isLoading)) { 65 | this.virtualScroll.measureAllRows(); 66 | } 67 | 68 | // Get total size directly from the grid (which is updated by measureAllRows()) 69 | const totalSize = this.virtualScroll.Grid._rowSizeAndPositionManager.getTotalSize(); 70 | 71 | // Get the DOM node for the virtual scroll 72 | const virtualScroll = ReactDOM.findDOMNode(this.virtualScroll); 73 | 74 | // With a valid scroll height and as long as we do not need to scroll to bottom 75 | if (virtualScroll && totalSize && !prevState.scrollToBottom) { 76 | // If the scroll height has changed, adjust the scroll position accordingly 77 | if (this.scrollHeight !== totalSize) { 78 | virtualScroll.scrollTop += totalSize - this.scrollHeight; 79 | this.scrollHeight = totalSize; 80 | } 81 | } 82 | } 83 | 84 | rowRenderer = ({ index }) => { 85 | if (this.props.reverse) { 86 | // Data needs to be rendered in reverse order, check for loading 87 | if (this.props.isLoading) { 88 | // Data is shifted down by 1 while loading 89 | if (index >= 1 && index < this.props.data.length + 1) { 90 | return this.props.renderRow(this.props.data[this.props.data.length - index]); 91 | } 92 | 93 | return this.props.renderLoading; 94 | } 95 | 96 | return this.props.renderRow(this.props.data[this.props.data.length - 1 - index]); 97 | } 98 | 99 | if (index < this.props.data.length) { 100 | return this.props.renderRow(this.props.data[index]); 101 | } 102 | 103 | return this.props.renderLoading; 104 | } 105 | 106 | render () { 107 | const { isLoading, data, containerHeight, rowHeight, scrollToRow, reverse, scrollRef } = this.props; 108 | const rowCount = isLoading ? data.length + 1 : data.length; 109 | 110 | return ( 111 | 112 | {({ height, width }) => ( 113 | 118 | {({ onRowsRendered, registerChild }) => ( 119 | { 127 | this.virtualScroll = virtualScroll; 128 | scrollRef && scrollRef(virtualScroll); 129 | registerChild(virtualScroll); 130 | }} 131 | onRowsRendered={onRowsRendered} 132 | rowRenderer={this.rowRenderer} 133 | onScroll={this.onScroll} 134 | /> 135 | )} 136 | 137 | )} 138 | 139 | ); 140 | } 141 | }; 142 | 143 | InfiniteScroll.propTypes = { 144 | loadMore: PropTypes.func.isRequired, 145 | renderRow: PropTypes.func.isRequired, 146 | rowHeight: PropTypes.oneOfType([PropTypes.number, PropTypes.func]).isRequired, 147 | threshold: PropTypes.number.isRequired, 148 | isLoading: PropTypes.bool, 149 | scrollToRow: PropTypes.number, 150 | renderLoading: PropTypes.object, 151 | data: PropTypes.array, 152 | containerHeight: PropTypes.number, 153 | reverse: PropTypes.bool, 154 | scrollRef: PropTypes.func, 155 | }; 156 | 157 | InfiniteScroll.defaultProps = { 158 | isLoading: false, 159 | renderLoading: ( 160 |
161 | Loading... 162 |
163 | ), 164 | reverse: false 165 | } 166 | 167 | module.exports = InfiniteScroll; 168 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-virtualized-infinite-scroll", 3 | "version": "0.0.4", 4 | "description": "Infinite scroll list for React that also works in reverse", 5 | "license": "MIT", 6 | "author": "kyle-ssg", 7 | "scripts": { 8 | "prod": "webpack --config ./example/config/webpack.config.prod.js", 9 | "server": "babel-node example/server --presets=es2015-node5,stage-2,react", 10 | "start": "nodemon --watch example/server --watch example/config --exec npm run server" 11 | }, 12 | "devDependencies": { 13 | "babel-cli": "6.14.0", 14 | "babel-core": "6.14.0", 15 | "babel-eslint": "6.1.2", 16 | "babel-loader": "6.2.5", 17 | "babel-preset-es2015": "6.14.0", 18 | "babel-preset-es2015-node5": "^1.2.0", 19 | "babel-preset-react": "6.11.1", 20 | "babel-preset-stage-2": "6.13.0", 21 | "bootstrap": "4.0.0-alpha.3", 22 | "clean-webpack-plugin": "0.1.10", 23 | "compass-mixins": "0.12.10", 24 | "copy-webpack-plugin": "3.0.1", 25 | "css-loader": "0.23.1", 26 | "express": "4.14.0", 27 | "express-handlebars": "3.0.0", 28 | "extract-text-webpack-plugin": "2.0.0-beta.4", 29 | "html-loader": "0.4.3", 30 | "html-webpack-plugin": "2.22.0", 31 | "image-webpack-loader": "2.0.0", 32 | "lodash.range": "^3.2.0", 33 | "ngrok": "2.2.2", 34 | "node-sass": "3.8.0", 35 | "postcss-loader": "0.9.1", 36 | "react": "15.3.1", 37 | "react-addons-shallow-compare": "15.3.1", 38 | "react-click-outside": "2.1.0", 39 | "react-document-title": "2.0.2", 40 | "react-dom": "15.3.1", 41 | "react-hot-loader": "3.0.0-beta.2", 42 | "react-router": "2.8.1", 43 | "react-virtualized": "^7.0.0", 44 | "sass-loader": "4.0.0", 45 | "style-loader": "0.13.1", 46 | "webpack": "^2.1.0-beta.4", 47 | "webpack-dashboard": "git+https://github.com/kyle-ssg/webpack-dashboard.git#master", 48 | "webpack-dev-middleware": "1.7.0", 49 | "webpack-hot-middleware": "2.12.2" 50 | }, 51 | "main": "dist/index", 52 | "directories": { 53 | "example": "example" 54 | }, 55 | "peerDependencies": { 56 | "react": ">=0.14.0", 57 | "react-dom": ">=0.14.0", 58 | "react-addons-shallow-compare": ">=0.14.0", 59 | "react-virtualized": "^7.0.0" 60 | }, 61 | "keywords": [ 62 | "react-component", 63 | "scrollview", 64 | "infinite", 65 | "virtualized", 66 | "scroll", 67 | "react" 68 | ], 69 | "dependencies": {}, 70 | "repository": { 71 | "type": "git", 72 | "url": "git+https://github.com/SolidStateGroup/react-virtualized-infinite-scroll.git" 73 | }, 74 | "bugs": { 75 | "url": "https://github.com/SolidStateGroup/react-virtualized-infinite-scroll/issues" 76 | }, 77 | "homepage": "https://github.com/SolidStateGroup/react-virtualized-infinite-scroll#readme" 78 | } 79 | --------------------------------------------------------------------------------