Skip to content

Commit 5ddd1d2

Browse files
author
Tasveer Singh
committed
🌅
1 parent 6740068 commit 5ddd1d2

10 files changed

Lines changed: 3561 additions & 0 deletions

.babelrc

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"presets": [
3+
["env", {
4+
"loose": true
5+
}],
6+
"react",
7+
"stage-2"
8+
]
9+
}

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
node_modules
2+
*.log
3+
lib

LICENSE

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
Copyright (c) 2017 Stratiform Limited. All rights reserved.
2+
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
3+
4+
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
5+
6+
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
7+
8+
3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
9+
10+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

package.json

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
{
2+
"name": "react-loadable-visibility",
3+
"version": "1.0.0",
4+
"description": "A wrapper around react-loadable for elements that are visible on the page",
5+
"main": "lib/index.js",
6+
"repository": "https://github.com/stratiformltd/react-loadable-visibility",
7+
"author": "Tasveer Singh",
8+
"license": "BSD-3-Clause",
9+
"scripts": {
10+
"build": "babel src --out-dir lib --ignore __mocks__,__tests__",
11+
"prepublish": "yarn run build",
12+
"test": "jest"
13+
},
14+
"peerDependencies": {
15+
"react": ">=15.4.0",
16+
"react-dom": ">=15.4.0",
17+
"react-loadable": ">=4.0.0"
18+
},
19+
"devDependencies": {
20+
"babel-cli": "^6.24.1",
21+
"babel-preset-env": "^1.6.0",
22+
"babel-preset-react": "^6.24.1",
23+
"babel-preset-stage-2": "^6.24.1",
24+
"enzyme": "^2.9.1",
25+
"jest": "^20.0.4",
26+
"react": "^15.6.1",
27+
"react-dom": "^15.6.1",
28+
"react-loadable": "^4.0.3",
29+
"react-test-renderer": "^15.6.1"
30+
}
31+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
const intersectionObservers = []
2+
3+
module.exports.makeElementsVisible = function makeElementsVisible () {
4+
intersectionObservers.forEach((observer) => {
5+
const entries = observer.trackedElements.map((element) => {
6+
return {
7+
intersectionRatio: 1,
8+
target: element,
9+
}
10+
})
11+
12+
observer.callback(entries, observer)
13+
})
14+
}
15+
16+
module.exports.IntersectionObserver = class IntersectionObserver {
17+
constructor(callback) {
18+
this.callback = callback
19+
20+
this.trackedElements = []
21+
22+
intersectionObservers.push(this)
23+
}
24+
25+
observe(element) {
26+
this.trackedElements.push(element)
27+
}
28+
29+
unobserve(element) {
30+
const elementIndex = this.trackedElements.indexOf(element)
31+
32+
if (elementIndex >= 0) {
33+
this.trackedElements.splice(elementIndex, 1)
34+
}
35+
}
36+
}

src/__mocks__/react-loadable.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
const LoadableReturn = jest.fn()
2+
const LoadableMapReturn = jest.fn()
3+
4+
const LoadableObject = (props) => {
5+
LoadableReturn(props)
6+
7+
return null
8+
}
9+
10+
LoadableObject.preload = jest.fn()
11+
12+
const LoadableMapObject = (props) => {
13+
LoadableMapReturn(props)
14+
15+
return null
16+
}
17+
18+
LoadableMapObject.preload = jest.fn()
19+
20+
function Loadable (opts) {
21+
return LoadableObject
22+
}
23+
24+
Loadable.Map = (opts) => {
25+
return LoadableMapObject
26+
}
27+
28+
Loadable.LoadableReturn = LoadableReturn
29+
Loadable.LoadableMapReturn = LoadableMapReturn
30+
31+
module.exports = Loadable
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
const Loadable = require('react-loadable')
2+
const React = require('react')
3+
const { mount } = require('enzyme')
4+
5+
const { IntersectionObserver, makeElementsVisible } = require('../__mocks__/IntersectionObserver')
6+
7+
global.IntersectionObserver = IntersectionObserver
8+
9+
const LoadableVisibility = require('../')
10+
11+
const opts = {
12+
loading: () => null,
13+
loader: () => Promise.resolve(),
14+
}
15+
16+
const props = {'a': 1, 'b': 2}
17+
18+
beforeEach(() => {
19+
jest.resetAllMocks()
20+
})
21+
22+
describe('Loadable', () => {
23+
test('exports', () => {
24+
expect(typeof LoadableVisibility).toBe('function')
25+
})
26+
27+
test('doesnt return Loadable', () => {
28+
expect(LoadableVisibility(opts)).not.toBe(Loadable(opts))
29+
})
30+
31+
test('calls Loadable when elements are visible', () => {
32+
const Loader = LoadableVisibility(opts)
33+
34+
const wrapper = mount(<Loader {...props} />)
35+
36+
expect(Loadable.LoadableReturn).not.toHaveBeenCalled()
37+
38+
makeElementsVisible()
39+
40+
expect(Loadable.LoadableReturn).toHaveBeenCalledWith(props)
41+
})
42+
43+
test('preload calls Loadable preload', () => {
44+
LoadableVisibility(opts).preload()
45+
46+
expect(Loadable().preload).toHaveBeenCalled()
47+
})
48+
})
49+
50+
describe('Loadable.Map', () => {
51+
test('exports', () => {
52+
expect(typeof LoadableVisibility.Map).toBe('function')
53+
})
54+
55+
test('doesnt return Map', () => {
56+
expect(LoadableVisibility.Map(opts)).not.toBe(Loadable.Map(opts))
57+
})
58+
59+
test('calls Map when elements are visible', () => {
60+
const Loader = LoadableVisibility.Map(opts)
61+
62+
const wrapper = mount(<Loader {...props} />)
63+
64+
expect(Loadable.LoadableMapReturn).not.toHaveBeenCalled()
65+
66+
makeElementsVisible()
67+
68+
expect(Loadable.LoadableMapReturn).toHaveBeenCalledWith(props)
69+
})
70+
71+
test('preload calls Map preload', () => {
72+
LoadableVisibility.Map(opts).preload()
73+
74+
expect(Loadable.Map().preload).toHaveBeenCalled()
75+
})
76+
})
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
const Loadable = require('react-loadable')
2+
3+
const LoadableVisibility = require('../')
4+
5+
const opts = {
6+
loading: () => null,
7+
loader: () => Promise.resolve(),
8+
}
9+
10+
beforeEach(() => {
11+
jest.resetAllMocks()
12+
})
13+
14+
describe('Loadable', () => {
15+
test('exports', () => {
16+
expect(typeof LoadableVisibility).toBe('function')
17+
})
18+
19+
test('returns Loadable', () => {
20+
expect(LoadableVisibility(opts)).toBe(Loadable(opts))
21+
})
22+
})
23+
24+
describe('Loadable.Map', () => {
25+
test('exports', () => {
26+
expect(typeof LoadableVisibility.Map).toBe('function')
27+
})
28+
29+
test('returns Map', () => {
30+
expect(LoadableVisibility.Map(opts)).toBe(Loadable.Map(opts))
31+
})
32+
})

src/index.js

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import React, { Component } from 'react'
2+
import { findDOMNode } from 'react-dom'
3+
4+
import Loadable from 'react-loadable'
5+
6+
let intersectionObserver
7+
let trackedElements = {}
8+
9+
if (window && window.IntersectionObserver) {
10+
intersectionObserver = new window.IntersectionObserver((entries, observer) => {
11+
entries.forEach((entry) => {
12+
if (entry.intersectionRatio > 0 && trackedElements[entry.target]) {
13+
const component = trackedElements[entry.target]
14+
15+
component.visibilityHandler()
16+
}
17+
})
18+
})
19+
}
20+
21+
function createLoadableVisibilityComponent (opts, Loadable) {
22+
let preloaded = false
23+
const visibilityHandlers = []
24+
25+
const LoadableComponent = Loadable(opts)
26+
27+
return class LoadableVisibilityComponent extends Component {
28+
static preload() {
29+
if (!preloaded) {
30+
preloaded = true
31+
visibilityHandlers.forEach((handler) => handler())
32+
}
33+
34+
LoadableComponent.preload()
35+
}
36+
37+
constructor(props) {
38+
super(props)
39+
40+
if (!preloaded) {
41+
visibilityHandlers.push(this.visibilityHandler)
42+
}
43+
44+
this.state = {
45+
visible: preloaded,
46+
}
47+
}
48+
49+
attachRef = (element) => {
50+
this.loadingRef = element
51+
52+
if (element) {
53+
trackedElements[element] = this
54+
intersectionObserver.observe(element)
55+
}
56+
}
57+
58+
componentWillUnmount() {
59+
if (this.loadingRef) {
60+
intersectionObserver.unobserve(this.loadingRef)
61+
delete trackedElements[this.loadingRef]
62+
}
63+
64+
const handlerIndex = visibilityHandlers.indexOf(this.visibilityHandler)
65+
66+
if (handlerIndex >= 0) {
67+
visibilityHandlers.splice(handlerIndex, 1)
68+
}
69+
}
70+
71+
visibilityHandler = () => {
72+
const node = findDOMNode(this)
73+
74+
intersectionObserver.unobserve(node)
75+
delete trackedElements[node]
76+
77+
this.setState({
78+
visible: true
79+
})
80+
}
81+
82+
render() {
83+
if (this.state.visible) {
84+
return <LoadableComponent {...this.props} />
85+
} else if (opts.loading) {
86+
return <span ref={this.attachRef}>
87+
{React.createElement(opts.loading, {
88+
isLoading: true,
89+
})}
90+
</span>
91+
} else {
92+
return <span ref={this.attachRef} />
93+
}
94+
}
95+
}
96+
}
97+
98+
function LoadableVisibility (opts) {
99+
if (intersectionObserver) {
100+
return createLoadableVisibilityComponent(opts, Loadable)
101+
} else {
102+
return Loadable(opts)
103+
}
104+
}
105+
106+
function LoadableVisibilityMap (opts) {
107+
if (intersectionObserver) {
108+
return createLoadableVisibilityComponent(opts, Loadable.Map)
109+
} else {
110+
return Loadable.Map(opts)
111+
}
112+
}
113+
114+
LoadableVisibility.Map = LoadableVisibilityMap
115+
116+
module.exports = LoadableVisibility

0 commit comments

Comments
 (0)