Skip to content

Commit

Permalink
Complete realization
Browse files Browse the repository at this point in the history
  • Loading branch information
MaxSvargal committed Feb 17, 2017
1 parent 8b7eb63 commit f80a457
Show file tree
Hide file tree
Showing 8 changed files with 2,738 additions and 1 deletion.
1 change: 1 addition & 0 deletions .coveralls.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
repo_token: cWHL7w4ZYPnJJUIxELXwfrKHA18BtMEwU
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
logs
*.log
npm-debug.log*
/index.js

# Runtime data
pids
Expand Down
43 changes: 42 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,43 @@
# react-lazy-list
Lazy list react component for render very large Infinitely lists
This is a simple and fast realization of lazy list as the react component that show entities only when user can see them. Thats why it can render very large lists and does not lose performance.

```javascript
import React, { Component } from 'react'
import LazyList from 'react-lazy-list'

export default class App extends Component {
render() {
const onMoreHandle = () => void
return (
<LazyList elementHeight={ 300 } onMore={ this.onMoreHandle } >
{ entities.map(entity => (
<Item key={ entity.id } entity={ entity } />
)) }
<Dummy />
</LazyList>
)
}
}
```

## props

### `children`
`Object` **required** Entities collection to render.

### `elementHeight`
`Number` **required** Pixels number of height of collection entity.

### `windowHeight`
`Number` Custom container height in pixels, i.e. for server side rendering.

### `topScrollOffset`
`Number` Offset from top of window to increase the threshold of the moment trip.
Usable to make a scroll offset for header height.

### `bottomScrollOffset`
`Number` Offset from top of window to decrease the threshold of the moment trip.
Usable for increase value an scroll offset for header height.

### `onMore`
`Function` Handler to call then list seek to end.
45 changes: 45 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
{
"name": "react-lazy-list",
"version": "0.1.0",
"description": "Lazy list react component for render very large Infinitely lists",
"main": "index.js",
"scripts": {
"build": "rollup -c rollup.config.js",
"test": "npm run build && jest",
"coverage": "npm test -- --coverage",
"coveralls": "npm run coverage && cat ./coverage/lcov.info | coveralls",
"prepare": "npm run build"
},
"repository": {
"type": "git",
"url": "git+https://github.com/MaxSvargal/react-lazy-list.git"
},
"keywords": [
"react",
"lazy",
"list",
"scroll",
"infinite",
"fast",
"render"
],
"author": "MaxSvargal",
"license": "MIT",
"bugs": {
"url": "https://github.com/MaxSvargal/react-lazy-list/issues"
},
"homepage": "https://github.com/MaxSvargal/react-lazy-list#readme",
"peerDependencies": {
"react": ">=15"
},
"devDependencies": {
"coveralls": "^2.11.16",
"enzyme": "^2.7.1",
"jest": "^18.1.0",
"react": "^15.4.2",
"react-addons-test-utils": "^15.4.2",
"react-dom": "^15.4.2",
"rollup": "^0.41.4",
"rollup-plugin-buble": "^0.15.0"
}
}
9 changes: 9 additions & 0 deletions rollup.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import buble from 'rollup-plugin-buble'

export default {
entry: 'src/index.js',
dest: 'index.js',
plugins: [ buble() ],
format: 'cjs',
external: [ 'react' ]
}
90 changes: 90 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import React, { Component, PropTypes } from 'react'

export default class LazyList extends Component {
constructor(props) {
super(props)
this.scrollListener = this.scrollListener.bind(this)

const { windowHeight, elementHeight, children } = props
const computedWindowHeight = windowHeight || document && document.body.clientHeight
const computedElementHeight = elementHeight
const showNum = Math.round(computedWindowHeight / computedElementHeight)
const visibleChildren = children.slice(0, showNum)

this.state = {
showNum,
visibleChildren,
scrolledNum: 0,
topOffset: 0,
bottomOffset: 0
}
}

componentDidMount() {
window.addEventListener('scroll', this.scrollListener)
}

componentWillUnmount() {
window.removeEventListener('scroll', this.scrollListener)
}

componentWillReceiveProps(props) {
const { scrolledNum, showNum } = this.state
const { children } = this.props

children.length !== props.children.length &&
this.setState({ visibleChildren: props.children.slice(scrolledNum, scrolledNum + showNum + 1) })
}

shouldComponentUpdate(props, state) {
return this.state.topOffset !== state.topOffset ||
this.props.children.length !== props.children.length
}

scrollListener() {
const { showNum } = this.state
const {
children, elementHeight, topScrollOffset,
onLoad, windowHeight, bottomScrollOffset
} = this.props

const fullHeight = children.length * elementHeight
const scrolled = (window && window.scrollY || 0) - topScrollOffset
const scrolledNum = Math.round((scrolled > 0 ? scrolled : 0) / elementHeight) - 1

const visibleChildren = children.slice(scrolledNum, scrolledNum + showNum + 1)
const topOffset = scrolledNum * elementHeight
const bottomOffset = fullHeight - (topOffset + (showNum * elementHeight))
const breakPoint = fullHeight - windowHeight - bottomScrollOffset

if (this.state.topOffset !== topOffset || this.state.bottomOffset !== bottomOffset) {
this.setState({ topOffset, bottomOffset, scrolledNum, visibleChildren })
scrolled >= breakPoint && onLoad()
}
}

render() {
const { visibleChildren, topOffset, bottomOffset } = this.state
return (
<div>
<div style={ { marginTop: topOffset } } />
<div style={ { position: 'relative', width: '100%' } }>{ visibleChildren }</div>
<div style={ { marginBottom: bottomOffset } } />
</div>
)
}
}

LazyList.propTypes = {
children: PropTypes.array.isRequired,
elementHeight: PropTypes.number.isRequired,
windowHeight: PropTypes.number,
topScrollOffset: PropTypes.number,
bottomScrollOffset: PropTypes.number,
onLoad: PropTypes.func
}

LazyList.defaultProps = {
topScrollOffset: 0,
bottomScrollOffset: 0
}
63 changes: 63 additions & 0 deletions test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
const React = require('react')
const { shallow, mount } = require('enzyme')
const ReactTestUtils = require('react-addons-test-utils')
const LazyList = require('./index')

describe('LazyList', () => {
let lazyListElement
const children = [...Array(4)].map((v, key) => React.createElement('li', { key }))
const props = { elementHeight: 100, windowHeight: 250, onLoad: jest.fn() }

beforeEach(() => {
lazyListElement = React.createElement(LazyList, props, children)
})

test('should render initial number of visible children', () => {
const wrapper = shallow(lazyListElement)
expect(wrapper.find('li').length).toBe(3)
})

test('should render without windowHeight', () => {
Object.defineProperty(document.body, 'clientHeight', { value: 300 })
lazyListElement = React.createElement(LazyList, { elementHeight: 100 }, children)
const wrapper = mount(lazyListElement)
expect(wrapper.find('li').length).toBe(3)
})

test('should rerender when pass a new children', () => {
const wrapper = shallow(lazyListElement)
expect(wrapper.find('li').length).toBe(3)

wrapper.setProps({
elementHeight: 100,
children: Array.prototype.concat(children, React.createElement('li', { key: 4 }))
})
wrapper.render()
expect(wrapper.find('li').length).toBe(4)
})

test('should show next entities on window scroll and call onLoad callback', () => {
window.addEventListener = function(event, handler) {
window.scrollY = 150
handler()
}
const wrapper = mount(lazyListElement)
const els = wrapper.find('li')

expect(els.length).toBe(3)
expect(els.at(0).key()).toBe('1')
expect(els.at(1).key()).toBe('2')
expect(els.at(2).key()).toBe('3')
expect(props.onLoad.mock.calls.length).toBe(1)
})

test('should unsubscribe from scroll listener on unmount', () => {
window.removeEventListener = jest.fn()
const mockCalls = window.removeEventListener.mock.calls
const wrapper = mount(lazyListElement)

expect(mockCalls.length).toBe(0)
wrapper.unmount()
expect(mockCalls.length).toBe(1)
})
})
Loading

0 comments on commit f80a457

Please sign in to comment.