Skip to content

Commit

Permalink
Merge branch 'master' of github.com:roderickhsiao/react-in-viewport
Browse files Browse the repository at this point in the history
  • Loading branch information
roderickhsiao committed Dec 11, 2017
2 parents f3ff0c8 + 0df11d7 commit c85256d
Show file tree
Hide file tree
Showing 4 changed files with 109 additions and 43 deletions.
14 changes: 10 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,18 @@ Use [Intersection Observer API](https://developer.mozilla.org/en-US/docs/Web/API

Dependencies: [Intersection Observer Polyfills](https://www.npmjs.com/package/intersection-observer)


## Usages

Wrap your component with handleViewport HOC, you will receive `inViewport` props indicating the component is in viewport or not.

`handleViewport` HOC accepts two params, the first one is your component and the second param is the option you want to pass to [Intersection Observer API](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API).
`handleViewport` HOC accepts three params

1. Component
1. Options: second param is the option you want to pass to [Intersection Observer API](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API).
1. Config object:
- `disconnectOnLeave { Boolean }` disconnect intersection observer after leave

The HOC preserve `onEnterViewport` and `onLeaveViewport` props as a callback

*NOTE*: Stateless: Need to add `ref={this.props.innerRef}` on your component

Expand All @@ -30,14 +35,15 @@ const Block = (props: { inViewport: boolean }) => {
</div>
);
};
const ViewportBlock = handleViewport(Block, /** options: {} **/);

const ViewportBlock = handleViewport(Block, /** options: {}, config: {} **/);

const Component = (props) => (
<div>
<div style={{ height: '100vh' }}>
<h2>Scroll down to make component in viewport</h2>
</div>
<ViewportBlock />
<ViewportBlock onEnterViewport={() => console.log('enter')} onLeaveViewport={() => console.log('leave')} />
</div>
))
```
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "react-in-viewport",
"version": "0.0.12",
"version": "0.0.14",
"description": "Track React component in viewport",
"author": "Roderick Hsiao <roderickhsiao@gmail.com>",
"repository": {
Expand Down
47 changes: 41 additions & 6 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import React, { PureComponent } from 'react';
import ReactDOM from 'react-dom';
import hoistNonReactStatic from 'hoist-non-react-statics';

function handleViewport(Component, options) {
function handleViewport(Component, options, config = { disconnectOnLeave: false }) {
class InViewport extends PureComponent {
constructor(props) {
super(props);
Expand All @@ -16,6 +16,7 @@ function handleViewport(Component, options) {
this.state = {
inViewport: false
};
this.intersected = false;
this.handleIntersection = this.handleIntersection.bind(this);
this.initIntersectionObserver = this.initIntersectionObserver.bind(this);
}
Expand All @@ -26,6 +27,17 @@ function handleViewport(Component, options) {
this.startObserver(this.node, this.observer);
}

componentDidUpdate(prevProps, prevState) {
// reset observer on update, to fix race condition that when observer init,
// the element is not in viewport, such as in animation
if (!this.intersected && !prevState.inViewport) {
if (this.observer && this.node) {
this.observer.unobserve(this.node);
this.observer.observe(this.node);
}
}
}

initIntersectionObserver() {
if (!this.observer) {
this.observer = new IntersectionObserver(
Expand All @@ -49,30 +61,53 @@ function handleViewport(Component, options) {
if (node && observer) {
observer.unobserve(node);
observer.disconnect();
observer = null;
this.observer = null;
}
}

handleIntersection(entries) {
const { onEnterViewport, onLeaveViewport } = this.props;
const entry = entries[0] || {};
const { intersectionRatio } = entry;
const inViewport = intersectionRatio > 0;

this.setState({
inViewport: intersectionRatio <= 0 ? false : true
});
// enter
if (!this.intersected && inViewport) {
this.intersected = true;
onEnterViewport && onEnterViewport();
this.setState({
inViewport
});
return;
}

// leave
if (this.intersected && !inViewport) {
this.intersected = false;
onLeaveViewport && onLeaveViewport();
if (config.disconnectOnLeave) {
// disconnect obsever on leave
this.observer && this.observer.disconnect();
}
this.setState({
inViewport
});
}
}

render() {
const { onEnterViewport, onLeaveViewport, ...others } = this.props;
return (
<Component
{...this.props}
{...others}
inViewport={this.state.inViewport}
ref={node => {
this.node = ReactDOM.findDOMNode(node);
}}
innerRef={node => {
if (node && !this.node) {
// handle stateless
this.node = ReactDOM.findDOMNode(node);
this.initIntersectionObserver();
this.startObserver(ReactDOM.findDOMNode(node), this.observer);
}
Expand Down
89 changes: 57 additions & 32 deletions src/stories/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,58 +47,83 @@ const Block = props => {
titleText={text}
innerRef={innerRef}
contentNode={
<div style={{ width: '400px', height: '300px', background: color }} />
<div style={{ width: '400px', height: '300px', background: color, transitionDuration: '1s' }} />
}
/>
);
};
const ViewportBlock = handleViewport(Block);
const ViewportBlock = handleViewport(Block, {}, { disconnectOnLeave: false });

const Iframe = ({ innerRef, src, ratio, inViewport }) => {
const Component = inViewport ? 'iframe' : 'div';
const props = inViewport
? {
class Iframe extends PureComponent {
constructor(props) {
super(props);
this.state = {
loaded: false
};
}

componentWillReceiveProps(nextProps) {
if (nextProps.inViewport && !this.loaded) {
this.setState({
loaded: true
});
}
}

render() {
const { src, ratio } = this.props;
const Component = this.state.loaded ? 'iframe' : 'div';
const props = this.state.loaded
? {
src,
frameBorder: 0
frameBorder: 0,
}
: {};
return (
<AspectRatio
ref={innerRef}
ratio={ratio}
style={{ marginBottom: '20px', backgroundColor: 'rgba(0,0,0,.12)' }}
>
<Component {...props} />
</AspectRatio>
);
};
const LazyIframe = handleViewport(Iframe);
class Image extends PureComponent {
: {};

return (
<AspectRatio
ratio={ratio}
style={{ marginBottom: '20px', backgroundColor: 'rgba(0,0,0,.12)' }}
>
<Component {...props} />
</AspectRatio>
);
}
}

const LazyIframe = handleViewport(Iframe, {}, { disconnectOnLeave: true });
class ImageObject extends PureComponent {
constructor(props) {
super(props);
this.state = {
src: DUMMY_IMAGE_SRC
src: DUMMY_IMAGE_SRC,
loaded: false
};
}

componentDidMount() {
if (this.props.inViewport) {
this.setState({
src: this.props.src
});
this.loadImage(this.props.src);
}
}

componentWillReceiveProps(nextProps) {
if (nextProps.inViewport) {
// prefetch image
const image = new Image();
image.src = nextProps.src;
action('Load image')(nextProps.src);
if (nextProps.inViewport && !this.state.loaded) {
this.loadImage(nextProps.src);
}
}

loadImage = (src) => {
const img = new Image();
img.onload = () => {
action('Image loaded')(src);
this.setState({
src: nextProps.src
src,
loaded: true
});
}
img.src = src;
action('Load image')(src);
}

render() {
Expand All @@ -114,15 +139,15 @@ class Image extends PureComponent {
}
}

const LazyImage = handleViewport(Image);
const LazyImage = handleViewport(ImageObject, {}, { disconnectOnLeave: true });
storiesOf('Viewport detection', module)
.add('Callback when in viewport', () => (
<div>
<PageTitle />
<div style={{ height: '100vh', padding: '20px' }}>
<p>Scroll down to make component in viewport 👇 </p>
</div>
<ViewportBlock className='card' />
<ViewportBlock className='card' onEnterViewport={() => console.log('enter')} onLeaveViewport={() => console.log('leave')} />
</div>
))
.add('Lazyload Image', () => {
Expand Down

0 comments on commit c85256d

Please sign in to comment.