diff --git a/README.md b/README.md index e2df610..8c6c425 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ React Lazy Load Component ========================= -Really simple component that renders children elements when they enter the viewport. +React Lazy Load is easy to use React component which helps you defer loading content in predictable way. It's fast, works in IE8+, 6KB minified and uses debounce function by default. You can also use component inside scrolling container, such as div with scrollbar. It will be found automatically. Check out an example. [![build status](https://img.shields.io/travis/loktar00/react-lazy-load.svg?style=flat-square)](https://travis-ci.org/loktar00/react-lazy-load) [![dependency status](https://david-dm.org/loktar00/react-lazy-load.svg?style=flat-square)](https://david-dm.org/loktar00/react-lazy-load) @@ -20,48 +20,91 @@ npm install --save react-lazy-load ## Usage ```jsx -import React, { Component } from 'react'; +import React from 'react'; import LazyLoad from 'react-lazy-load'; -class MyComponent extends Component { - render() { - return ( - -
some content
-
- ); - } -} +const MyComponent = () => ( +
+ Scroll to load images. +
+ + + +
+ + + +
+ + + +
+ console.log('look ma I have been lazyloaded!')} + > + + +
+
+); ``` ## Props -### height={String|Number} +#### offset +Type: `Number|String` Default: `0` -This is used to set the elements height even when it contains no content. +Aliases: `threshold` -```jsx - -
some content
-
-``` +The `offset` option allows you to specify how far below, above, to the left, and to the right of the viewport you want to _begin_ displaying your content. If you specify `0`, your content will be displayed as soon as it is visible in the viewport, if you want to load _1000px_ below or above the viewport, use `1000`. -### threshold={Number} +#### offsetVertical +Type: `Number|String` Default: `offset`'s value -By default content is loaded when it appears on the screen. If you want content to load earlier use threshold parameter. Setting threshold to 200 causes image to load 200 pixels before it appears on viewport. +The `offsetVertical` option allows you to specify how far above and below the viewport you want to _begin_ displaying your content. -```jsx - -
some content
-
-``` +#### offsetHorizontal +Type: `Number|String` Default: `offset`'s value -### onContentVisible={Function} +The `offsetHorizontal` option allows you to specify how far to the left and right of the viewport you want to _begin_ displaying your contet. -A callback function to execute when the content appears on the screen. +#### offsetTop +Type: `Number|String` Default: `offsetVertical`'s value -```jsx - { console.log('content visible'); }}> -
some content
-
-``` +The `offsetTop` option allows you to specify how far above the viewport you want to _begin_ displaying your content. + +#### offsetBottom +Type: `Number|String` Default: `offsetVertical`'s value + +The `offsetBottom` option allows you to specify how far below the viewport you want to _begin_ displaying your content. + +#### offsetLeft +Type: `Number|String` Default: `offsetVertical`'s value + +The `offsetLeft` option allows you to specify how far to left of the viewport you want to _begin_ displaying your content. + +#### offsetRight +Type: `Number|String` Default: `offsetVertical`'s value + +The `offsetRight` option allows you to specify how far to the right of the viewport you want to _begin_ displaying your content. + +#### throttle +Type: `Number|String` Default: `250` + +The throttle is managed by an internal function that prevents performance issues from continuous firing of `scroll` events. Using a throttle will set a small timeout when the user scrolls and will keep throttling until the user stops. The default is `250` milliseconds. + +#### debounce +Type: `Boolean` Default: `true` + +By default the throttling function is actually a [debounce](https://lodash.com/docs#debounce) function so that the checking function is only triggered after a user stops scrolling. To use traditional throttling where it will only check the loadable content every `throttle` milliseconds, set `debounce` to `false`. + +### height +Type: `String|Number` Default: `100` + +This is used to set the elements height even when it has no content. + +### onContentVisible +Type `Function` + +A callback function to execute when the content appears on the screen. \ No newline at end of file diff --git a/examples/basic/src/components/Application/index.jsx b/examples/basic/src/components/Application/index.jsx index 9304599..7552e3e 100644 --- a/examples/basic/src/components/Application/index.jsx +++ b/examples/basic/src/components/Application/index.jsx @@ -9,19 +9,24 @@ class Application extends Component {
Scroll to load images.
- +
- +
- - - +
+
+
+
+ + + +
- +
diff --git a/examples/basic/src/components/Application/style.css b/examples/basic/src/components/Application/style.css index 4a3db81..7f1bf1f 100644 --- a/examples/basic/src/components/Application/style.css +++ b/examples/basic/src/components/Application/style.css @@ -1,12 +1,18 @@ -.filler { - height: 300px; -} - -.lazy-load { +.LazyLoad { opacity: 0; transition: all 2s ease-in-out; + + &.is-visible { + opacity: 1; + } +} + +.filler { + height: 150px; } -.lazy-load-visible { - opacity: 1; +.ScrollableContainer { + height: 200px; + overflow: scroll; + background-color: grey; } \ No newline at end of file diff --git a/package.json b/package.json index 1badcde..90d0f45 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,8 @@ { "name": "react-lazy-load", - - "version": "2.0.2", - + "version": "3.0.0", "description": "Simple lazy loading component built with react", - "main": "./lib/LazyLoad.js", - "scripts": { "build": "npm run build:lib && npm run build:umd && npm run build:umd:min", "build:lib": "babel src --out-dir lib", @@ -17,17 +13,14 @@ "prepublish": "npm run clean && npm run build", "test": "echo \"Error: no test specified\" && exit 1" }, - "repository": { "type": "git", "url": "https://github.com/loktar00/react-lazy-load.git" }, - "files": [ "dist", "lib" ], - "keywords": [ "react", "reactjs", @@ -35,15 +28,11 @@ "load", "lazy" ], - "author": "Jason Brown (https://twitter.com/loktar00)", - "contributors": [ "Sergey Laptev (https://twitter.com/iamsergeylaptev)" ], - "license": "MIT", - "devDependencies": { "babel-cli": "^6.3.17", "babel-core": "^6.2.1", @@ -60,13 +49,12 @@ "rimraf": "^2.4.4", "webpack": "^1.12.2" }, - "dependencies": { - "classnames": "^2.2.0" + "eventlistener": "0.0.1", + "lodash.debounce": "^4.0.0" }, - "peerDependencies": { "react": "^0.14.0", "react-dom": "^0.14.0" } -} \ No newline at end of file +} diff --git a/src/LazyLoad.jsx b/src/LazyLoad.jsx index 69b5d20..42835f7 100644 --- a/src/LazyLoad.jsx +++ b/src/LazyLoad.jsx @@ -1,64 +1,93 @@ -import React, { Component, PropTypes } from 'react'; -import { findDOMNode } from 'react-dom'; -import classNames from 'classnames'; +const React = require('react'); +const { findDOMNode } = require('react-dom'); +const { Children, Component, PropTypes } = React; -export default class LazyLoad extends Component { +const { add, remove } = require('eventlistener'); +const debounce = require('lodash.debounce'); + +const parentScroll = require('./utils/parentScroll'); +const inViewport = require('./utils/inViewport'); + +class LazyLoad extends Component { constructor(props) { super(props); - this.onWindowScroll = this.onWindowScroll.bind(this); + + if (props.debounce) { + this.lazyLoadHandler = debounce(this.lazyLoadHandler, props.throttle).bind(this); + } else { + this.lazyLoadHandler = this.lazyLoadHandler.bind(this); + } + + this.state = { + visible: false, + }; } - state = { - visible: false, - }; componentDidMount() { - window.addEventListener('scroll', this.onWindowScroll); - window.addEventListener('resize', this.onWindowScroll); - this.onWindowScroll(); - } - componentDidUpdate() { - if (!this.state.visible) { - this.onWindowScroll(); - } else { - const { onContentVisible } = this.props; + const eventNode = this.getEventNode(); - if (onContentVisible) { - onContentVisible(); - } - } + this.lazyLoadHandler(); + + add(window, 'resize', this.lazyLoadHandler); + add(eventNode, 'scroll', this.lazyLoadHandler); + } + shouldComponentUpdate(_nextProps, nextState) { + return nextState.visible; } componentWillUnmount() { - this.onVisible(); + this.detachListeners(); + } + getEventNode() { + return parentScroll(findDOMNode(this)); } - onVisible() { - window.removeEventListener('scroll', this.onWindowScroll); - window.removeEventListener('resize', this.onWindowScroll); + getOffset() { + const { + offset, offsetVertical, offsetHorizontal, + offsetTop, offsetBottom, offsetLeft, offsetRight, threshold, + } = this.props; + + const _offsetAll = threshold || offset; + const _offsetVertical = offsetVertical || _offsetAll; + const _offsetHorizontal = offsetHorizontal || _offsetAll; + + return { + top: offsetTop || _offsetVertical, + bottom: offsetBottom || _offsetVertical, + left: offsetLeft || _offsetHorizontal, + right: offsetRight || _offsetHorizontal, + }; } - onWindowScroll() { - const { threshold } = this.props; + lazyLoadHandler() { + const offset = this.getOffset(); + const node = findDOMNode(this); + const eventNode = this.getEventNode(); - const bounds = findDOMNode(this).getBoundingClientRect(); - const scrollTop = window.pageYOffset; - const top = bounds.top + scrollTop; - const height = bounds.bottom - bounds.top; + if (inViewport(node, eventNode, offset)) { + const { onContentVisible } = this.props; - if (top === 0 || (top <= (scrollTop + window.innerHeight + threshold) - && (top + height) > (scrollTop - threshold))) { this.setState({ visible: true }); - this.onVisible(); + this.detachListeners(); + + if (onContentVisible) { + onContentVisible(); + } } } + detachListeners() { + const eventNode = this.getEventNode(); + + remove(window, 'resize', this.lazyLoadHandler); + remove(eventNode, 'scroll', this.lazyLoadHandler); + } render() { - const elStyles = { - height: this.props.height, - }; - const elClasses = classNames({ - 'lazy-load': true, - 'lazy-load-visible': this.state.visible, - }); + const { children, height, width } = this.props; + const { visible } = this.state; + + const elStyles = { height, width }; + const elClasses = 'LazyLoad' + (visible ? ' is-visible' : ''); return (
- {this.state.visible && this.props.children} + {visible && Children.only(children)}
); } @@ -66,13 +95,38 @@ export default class LazyLoad extends Component { LazyLoad.propTypes = { children: PropTypes.node.isRequired, + debounce: PropTypes.bool, height: PropTypes.oneOfType([ PropTypes.string, PropTypes.number, ]), + offset: PropTypes.number, + offsetBottom: PropTypes.number, + offsetHorizontal: PropTypes.number, + offsetLeft: PropTypes.number, + offsetRight: PropTypes.number, + offsetTop: PropTypes.number, + offsetVertical: PropTypes.number, threshold: PropTypes.number, + throttle: PropTypes.number, + width: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.number, + ]), onContentVisible: PropTypes.func, }; + LazyLoad.defaultProps = { - threshold: 0, + debounce: true, + height: 100, + offset: 0, + offsetBottom: 0, + offsetHorizontal: 0, + offsetLeft: 0, + offsetRight: 0, + offsetTop: 0, + offsetVertical: 0, + throttle: 250, }; + +module.exports = LazyLoad; diff --git a/src/utils/inViewport.js b/src/utils/inViewport.js new file mode 100644 index 0000000..fe532d2 --- /dev/null +++ b/src/utils/inViewport.js @@ -0,0 +1,44 @@ +const isHidden = (element) => + element.offsetParent === null; + +const offset = (element) => { + const rect = element.getBoundingClientRect(); + + return { + top: rect.top + window.pageYOffset, + left: rect.left + window.pageXOffset, + }; +}; + +const inViewport = (element, container, customOffset) => { + if (isHidden(element)) { + return false; + } + + let top, left, bottom, right; + + if (typeof container === 'undefined' || container === window) { + top = window.pageYOffset; + left = window.pageXOffset; + bottom = top + window.innerHeight; + right = left + window.innerWidth; + } else { + const containerOffset = offset(container); + + top = containerOffset.top; + left = containerOffset.left; + bottom = top + container.offsetHeight; + right = left + container.offsetWidth; + } + + const elementOffset = offset(element); + + return ( + top < elementOffset.top + customOffset.bottom + element.offsetHeight && + bottom > elementOffset.top - customOffset.top && + left < elementOffset.left + customOffset.right + element.offsetWidth && + right > elementOffset.left - customOffset.left + ); +}; + +module.exports = inViewport; \ No newline at end of file diff --git a/src/utils/parentScroll.js b/src/utils/parentScroll.js new file mode 100644 index 0000000..d82bdc3 --- /dev/null +++ b/src/utils/parentScroll.js @@ -0,0 +1,31 @@ +const style = (element, prop) => + typeof getComputedStyle !== 'undefined' + ? getComputedStyle(element, null).getPropertyValue(prop) + : element.style[prop]; + +const overflow = (element) => + style(element, 'overflow') + style(element, 'overflow-y') + style(element, 'overflow-x'); + +const scrollParent = (element) => { + if (!(element instanceof HTMLElement)) { + return window; + } + + let parent = element; + + while(parent) { + if (!parent.parentNode) { + return window; + } + + if (/(scroll|auto)/.test(overflow(parent))) { + return parent; + } + + parent = parent.parentNode; + } + + return window; +}; + +module.exports = scrollParent; \ No newline at end of file