Last active
October 23, 2016 17:55
-
-
Save froger/7d75f5bc126e156d218f348414ba2bf0 to your computer and use it in GitHub Desktop.
Make react-infinite-scroll async.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/*! | |
* React v15.0.0 | |
* Compile with babel-presets: ['babel-preset-es2015', 'react']. @see https://babeljs.io/docs/plugins/#presets | |
* | |
* Refactoring from react-infinite-scroll - v 0.1.3 - guillaumervls 2014-04-07 | |
* @see https://www.npmjs.com/package/react-infinite-scroll | |
* | |
* Changes from react-infinite-scroll | |
* ==== | |
* | |
* - Put a debounce timeout, too not send too much loadMore (as the scroll event is very fast) | |
* - Put a finished callback, to easely put some informations at the bottom of the infinite list | |
* when she is finished (if she is once) | |
* - Allow async loadMore, with Promise. (look https://github.com/calvinmetcalf/lie for better cross-browser compatibilty) | |
* - Refactor for React v15.0.0, in ES2015 | |
* | |
* Required Props | |
* === | |
* | |
* @param {number} pageStart Where should the pagination index start | |
* @param {bool} hasMore If the Scroller need to continue asking for new resources | |
* @param {func} loadMore A function that return a Promise. Will stop loading on error. | |
* @param {} | |
* | |
* Optional Props | |
* === | |
* | |
* @param {func} finished Callback when there are no more items to load | |
* @param {func} onError Callback when the loadMore promise is rejected. | |
* @param {number} threshold Set it when you want to put your list after the top of the page. | |
* Will try to guess a OK value if not provided. | |
* @param {any} children The loaded components to show in the list | |
* @param {string} className Class names to add to the infinite scroll list. | |
* Will add : | |
* - `is-finished` when there is no more to load (or there were an error) | |
* - `has-more` when there are still things to load (from this.props.hasMore) | |
* - `js-page-#{pagination}` the number of this.loadMore call we have done yet | |
* - `js-infinite_scroll` Just a class to be referenced by js. | |
* | |
* I personally use `js-` prefix to manipulate classes in JS | |
* to stay independant from my styles. | |
*/ | |
var React = require("react"); | |
/** | |
* Bind the `this` of a given instance to the given methods | |
* | |
* Example : | |
* `bind(my_instance, "toString", "callMe");` | |
* | |
* @param {Object} Instance to bind | |
* @param {...String} Methods to bind | |
* @return {void} | |
*/ | |
const bind = function(obj) { | |
var methods = [].slice.call(arguments, 1); | |
for(var i = methods.length - 1; i >= 0; i--){ | |
var method = methods[i]; | |
if(typeof obj[method] === "undefined") | |
throw new Error(method + " not defined"); | |
else if(typeof obj[method] !== "function") | |
throw new Error(method + " is not a function"); | |
obj[method] = obj[method].bind(obj); | |
} | |
}; | |
/** | |
* Calculate the position from the top of the page. | |
* Will recursively add the offset top of the parent element. | |
* | |
* @param {DOMNode} dom_element the node we want to know the top position | |
* @return {number} the top position in px. | |
*/ | |
const topPosition = function(dom_element) { | |
if (!dom_element) { | |
return 0; | |
} | |
return dom_element.offsetTop + topPosition(dom_element.offsetParent); | |
} | |
export default class extends React.Component { | |
static displayName = "InfiniteScroll"; | |
static defaultProps = { | |
className: "", | |
finished: function(){}, | |
onError: function(){}, | |
loader: (<span className="loader">Loading…</span>) | |
}; | |
/** | |
* @see the file comment for props docs. | |
* @type {obj} | |
*/ | |
static propTypes = { | |
pageStart: React.PropTypes.number.isRequired, | |
hasMore: React.PropTypes.bool.isRequired, | |
loadMore: React.PropTypes.func.isRequired, | |
finished: React.PropTypes.func, | |
onError: React.PropTypes.func, | |
threshold: React.PropTypes.number, | |
children: React.PropTypes.any, | |
loader: React.PropTypes.object, | |
className: React.PropTypes.string | |
}; | |
constructor(){ | |
super(); | |
this.state = { | |
isLoading: false, | |
threshold: this.props.threshold, | |
pagination: this.props.pageStart, | |
finished: false | |
}; | |
bind(this, "attachScrollListener", | |
"detachScrollListener", | |
"scrollListener") | |
this.reference = null; | |
this.debounceTimeout = null; | |
this.debounceDelay = 250; | |
} | |
componentDidUpdate(prevProps){ | |
// If we had no more item before and now we have. | |
if(!prevProps.hasMore && this.props.hasMore){ | |
this.setState({ finished: false }); | |
this.attachScrollListener(); | |
} | |
} | |
componentDidMount(){ | |
if(typeof this.props.threshold !== "undefined"){ | |
this.setState({ threshold: nextProps.threshold }); | |
}else{ | |
// Guess some good threshold value for our list. | |
this.setState({ threshold: topPosition(this.reference) }); | |
} | |
// Start listening to scroll | |
this.attachScrollListener(); | |
} | |
/** | |
* IF the page we want has increased, we want to go back listen to scroll. | |
* ELSE we still are fetching async data with loadmore. | |
* @param {object} nextProps The new props the component receive | |
* @return {void} | |
*/ | |
componentWillReceiveProps(nextProps){ | |
if(this.props.hasMore && !nextProps.hasMore){ | |
this.setState({ finished: true }); | |
nextProps.finished(); | |
return; | |
} | |
if(typeof nextProps.threshold !== "undefined" && | |
nextProps.threshold !== this.state.threshold) | |
this.setState({ threshold: nextProps.threshold }); | |
if(nextProps.hasMore) | |
this.attachScrollListener(); | |
} | |
attachScrollListener() { | |
if(!this.props.hasMore) | |
return; | |
window.addEventListener("scroll", this.scrollListener); | |
window.addEventListener("resize", this.scrollListener); | |
this.scrollListener(); | |
} | |
detachScrollListener() { | |
window.removeEventListener("scroll", this.scrollListener); | |
window.removeEventListener("resize", this.scrollListener); | |
} | |
scrollListener() { | |
if(this.debounceTimeout !== null && | |
typeof debounceTimeout !== "undefined" && | |
!this.state.finished) | |
return; // We can not accomplish the scroll callback action for now. | |
this.debounceTimeout = setTimeout(() => { | |
clearTimeout(this.debounceTimeout); | |
this.debounceTimeout = null; | |
var el = this.reference; | |
var scrollTop = | |
(typeof window.pageYOffset !== "undefined") ? window.pageYOffset | |
: (document.documentElement || | |
document.body.parentNode || | |
document.body).scrollTop; | |
if(!this.state.isLoading && this.props.hasMore){ | |
if(topPosition(el) | |
+ el.offsetHeight | |
- scrollTop | |
- window.innerHeight < Number(this.state.threshold)) { | |
this.detachScrollListener(); | |
this.setState({ isLoading: true }); | |
// Loading always return a promise, to be able to do | |
// sync process or async process | |
this.props.loadMore(this.state.pagination + 1).then(() => { | |
this.setState({ | |
isLoading: false, | |
pagination: this.state.pagination + 1 | |
}) | |
this.attachScrollListener(); | |
}).catch((error) => { | |
this.props.onError(error); | |
this.setState({ | |
isLoading: false, | |
pagination: this.state.pagination + 1, | |
finished: true | |
}) | |
}); | |
} | |
} | |
}, this.debounceDelay); | |
} | |
componentWillUnmount() { | |
// The component will disapear, stop listening to scroll | |
this.detachScrollListener(); | |
} | |
render(){ | |
var additionalClassName = _.isUndefined(this.props.className) ? "" : " " + this.props.className; | |
return <section | |
className={"js-infinite_scroll" + additionalClassName + | |
" js-page-" + this.props.pageStart + | |
(this.props.hasMore ? " has-more" : " is-finished") | |
} | |
ref={(reference) => { | |
if(reference !== null && | |
typeof reference !== "undefined" && | |
this.reference === null) | |
this.reference = reference; | |
}}> | |
{ this.props.children } | |
{ this.state.isLoading && this.props.loader } | |
</section> | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment