├── .gitignore
├── LICENSE
├── README.md
├── dist
├── grid.js
└── item.js
├── example
├── index.html
└── main.js
├── package.json
├── scripts
└── postinstall.js
├── src
├── grid.js
└── item.js
└── webpack.config.js
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | example/*bundle.js
3 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 Gordan Grasarevic
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # react-infinite-grid
2 |
3 | _react infinite grid_ is a React component which renders a grid of React elements. It's different because it only renders the elements that the user can see (and a small buffer) meaning that it is well suited for displaying a large number of elements.
4 |
5 | # Installation
6 |
7 | ```
8 | npm install react-infinite-grid
9 | ```
10 |
11 | # Example
12 |
13 | The example below renders a grid with 100,000 items.
14 |
15 | ```jsx
16 | import React from 'react';
17 | import ReactDOM from 'react-dom';
18 | import InfiniteGrid from '../src/grid';
19 |
20 | class ExampleItem extends React.Component {
21 |
22 | static get propTypes() {
23 | return {
24 | index: React.PropTypes.number
25 | };
26 | }
27 |
28 | render() {
29 | return(
30 |
31 | This is {this.props.index}
32 |
33 | );
34 | }
35 |
36 | }
37 |
38 | // Create 100,000 Example items
39 | let items = [];
40 | for (let i = 0; i <= 100000; i++) {
41 | items.push();
42 | }
43 |
44 | ReactDOM.render(, document.getElementById('grid'));
45 | ```
46 |
47 | ## Required props
48 |
49 | - **entries** `React.PropTypes.arrayOf(React.PropTypes.element)` - The only required property is an array of React elements that you want to render.
50 |
51 | ## Optional props
52 |
53 | - **height** `React.PropTypes.number` - The height of the grid item
54 | - **width** `React.PropTypes.number` - The width of the grid item
55 | - **padding** `React.PropTypes.number` - The padding around your items
56 | - **wrapperHeight** `React.PropTypes.number` - The height of the grid.
57 | - **lazyCallback** `React.PropTypes.func` - A function that takes no arguments which is called when a user reaches the end of the grid. Useful if you want to lazy load your data.
58 |
59 | # Demo
60 |
61 | You can find a demo [here](http://ggordan.com/post/react-infinite-grid.html).
62 |
--------------------------------------------------------------------------------
/dist/grid.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })();
4 |
5 | Object.defineProperty(exports, "__esModule", {
6 | value: true
7 | });
8 |
9 | var _react = require('react');
10 |
11 | var _react2 = _interopRequireDefault(_react);
12 |
13 | var _lodash = require('lodash');
14 |
15 | var _item = require('./item');
16 |
17 | var _item2 = _interopRequireDefault(_item);
18 |
19 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
20 |
21 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
22 |
23 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }
24 |
25 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
26 |
27 | var InfiniteGrid = (function (_React$Component) {
28 | _inherits(InfiniteGrid, _React$Component);
29 |
30 | _createClass(InfiniteGrid, [{
31 | key: 'initialState',
32 | value: function initialState() {
33 | return {
34 | initiatedLazyload: false,
35 | minHeight: window.innerHeight * 2,
36 | minItemIndex: 0,
37 | maxItemIndex: 100,
38 | itemDimensions: {
39 | height: this._itemHeight(),
40 | width: this._itemHeight(),
41 | gridWidth: 0,
42 | itemsPerRow: 2
43 | }
44 | };
45 | }
46 | }], [{
47 | key: 'propTypes',
48 | get: function get() {
49 | return {
50 | itemClassName: _react2.default.PropTypes.string,
51 | entries: _react2.default.PropTypes.arrayOf(_react2.default.PropTypes.object).isRequired,
52 | height: _react2.default.PropTypes.number,
53 | width: _react2.default.PropTypes.number,
54 | padding: _react2.default.PropTypes.number,
55 | wrapperHeight: _react2.default.PropTypes.number,
56 | lazyCallback: _react2.default.PropTypes.func,
57 | renderRangeCallback: _react2.default.PropTypes.func,
58 | buffer: _react2.default.PropTypes.number
59 | };
60 | }
61 | }]);
62 |
63 | function InfiniteGrid(props) {
64 | _classCallCheck(this, InfiniteGrid);
65 |
66 | var _this2 = _possibleConstructorReturn(this, Object.getPrototypeOf(InfiniteGrid).call(this, props));
67 |
68 | _this2.state = _this2.initialState();
69 | // bind the functions
70 | _this2._scrollListener = _this2._scrollListener.bind(_this2);
71 | _this2._updateItemDimensions = _this2._updateItemDimensions.bind(_this2);
72 | _this2._resizeListener = _this2._resizeListener.bind(_this2);
73 | _this2._visibleIndexes = _this2._visibleIndexes.bind(_this2);
74 | return _this2;
75 | }
76 |
77 | // METHODS
78 |
79 | _createClass(InfiniteGrid, [{
80 | key: '_wrapperStyle',
81 | value: function _wrapperStyle() {
82 | return {
83 | maxHeight: this._getGridHeight(),
84 | overflowY: 'scroll',
85 | width: '100%',
86 | height: this.props.wrapperHeight,
87 | WebkitOverflowScrolling: true
88 | };
89 | }
90 | }, {
91 | key: '_gridStyle',
92 | value: function _gridStyle() {
93 | return {
94 | position: 'relative',
95 | marginTop: this.props.padding,
96 | marginLeft: this.props.padding,
97 | minHeight: this._getGridHeight()
98 | };
99 | }
100 | }, {
101 | key: '_getGridRect',
102 | value: function _getGridRect() {
103 | return this.refs.grid.getBoundingClientRect();
104 | }
105 | }, {
106 | key: '_getGridHeight',
107 | value: function _getGridHeight() {
108 | return Math.floor(this.props.entries.length / this.state.itemDimensions.itemsPerRow) * this.state.itemDimensions.height;
109 | }
110 | }, {
111 | key: '_getWrapperRect',
112 | value: function _getWrapperRect() {
113 | return this.refs.wrapper.getBoundingClientRect();
114 | }
115 | }, {
116 | key: '_visibleIndexes',
117 | value: function _visibleIndexes() {
118 | var itemsPerRow = this._itemsPerRow();
119 |
120 | // The number of rows that the user has scrolled past
121 | var scrolledPast = this._scrolledPastRows() * itemsPerRow;
122 | if (scrolledPast < 0) scrolledPast = 0;
123 |
124 | // If i have scrolled past 20 items, but 60 are visible on screen,
125 | // we do not want to change the minimum
126 | var min = scrolledPast - itemsPerRow;
127 | if (min < 0) min = 0;
128 |
129 | // the maximum should be the number of items scrolled past, plus some
130 | // buffer
131 | var bufferRows = this._numVisibleRows() + this.props.buffer;
132 | var max = scrolledPast + itemsPerRow * bufferRows;
133 | if (max > this.props.entries.length) max = this.props.entries.length;
134 |
135 | this.setState({
136 | minItemIndex: min,
137 | maxItemIndex: max
138 | }, function () {
139 | this._lazyCallback();
140 | });
141 | }
142 | }, {
143 | key: '_updateItemDimensions',
144 | value: function _updateItemDimensions() {
145 | this.setState({
146 | itemDimensions: {
147 | height: this._itemHeight(),
148 | width: this._itemHeight(),
149 | gridWidth: this._getGridRect().width,
150 | itemsPerRow: this._itemsPerRow()
151 | },
152 | minHeight: this._totalRows()
153 | });
154 | }
155 | }, {
156 | key: '_itemsPerRow',
157 | value: function _itemsPerRow() {
158 | return Math.floor(this._getGridRect().width / this._itemWidth());
159 | }
160 | }, {
161 | key: '_totalRows',
162 | value: function _totalRows() {
163 | var scrolledPastHeight = this.props.entries.length / this._itemsPerRow() * this._itemHeight();
164 | if (scrolledPastHeight < 0) return 0;
165 | return scrolledPastHeight;
166 | }
167 | }, {
168 | key: '_scrolledPastRows',
169 | value: function _scrolledPastRows() {
170 | var rect = this._getGridRect();
171 | var topScrollOffset = rect.height - rect.bottom;
172 | return Math.floor(topScrollOffset / this._itemHeight());
173 | }
174 | }, {
175 | key: '_itemHeight',
176 | value: function _itemHeight() {
177 | return this.props.height + 2 * this.props.padding;
178 | }
179 | }, {
180 | key: '_itemWidth',
181 | value: function _itemWidth() {
182 | return this.props.width + 2 * this.props.padding;
183 | }
184 | }, {
185 | key: '_numVisibleRows',
186 | value: function _numVisibleRows() {
187 | return Math.ceil(this._getWrapperRect().height / this._itemHeight());
188 | }
189 | }, {
190 | key: '_lazyCallback',
191 | value: function _lazyCallback() {
192 | if (!this.state.initiatedLazyload && this.state.maxItemIndex === this.props.entries.length && this.props.lazyCallback) {
193 | this.setState({ initiatedLazyload: true });
194 | this.props.lazyCallback(this.state.maxItemIndex);
195 | }
196 | }
197 |
198 | // LIFECYCLE
199 |
200 | }, {
201 | key: 'componentWillMount',
202 | value: function componentWillMount() {
203 | window.addEventListener('resize', this._resizeListener);
204 | }
205 | }, {
206 | key: 'componentDidMount',
207 | value: function componentDidMount() {
208 | this._updateItemDimensions();
209 | this._visibleIndexes();
210 | }
211 | }, {
212 | key: 'componentWillReceiveProps',
213 | value: function componentWillReceiveProps(nextProps) {
214 | if (nextProps.entries.length > this.props.entries.length) {
215 | this.setState({
216 | initiatedLazyload: false
217 | });
218 | }
219 | // Update these all the time because entries may change on the fly.
220 | // this._updateItemDimensions();
221 | this._visibleIndexes();
222 | }
223 | }, {
224 | key: 'componentDidUpdate',
225 | value: function componentDidUpdate(prevProps, prevState) {
226 | if (typeof this.props.renderRangeCallback === 'function') {
227 | this.props.renderRangeCallback(this.state.minItemIndex, this.state.maxItemIndex);
228 | }
229 | }
230 | }, {
231 | key: 'shouldComponentUpdate',
232 | value: function shouldComponentUpdate(nextProps, nextState) {
233 | return !(0, _lodash.isEqual)(this.state, nextState);
234 | }
235 | }, {
236 | key: 'componentWillUnmount',
237 | value: function componentWillUnmount() {
238 | window.removeEventListener('resize', this._resizeListener);
239 | }
240 |
241 | // LISTENERS
242 |
243 | }, {
244 | key: '_scrollListener',
245 | value: function _scrollListener(event) {
246 | var _this = this;
247 |
248 | clearTimeout(this.scrollOffset);
249 | this.scrollOffset = setTimeout(function () {
250 | _this._visibleIndexes();
251 | }, 10);
252 | }
253 | }, {
254 | key: '_resizeListener',
255 | value: function _resizeListener(event) {
256 | if (!this.props.wrapperHeight) {
257 | this.setState({
258 | wrapperHeight: window.innerHeight
259 | });
260 | }
261 | this._updateItemDimensions();
262 | this._visibleIndexes();
263 | }
264 |
265 | // RENDER
266 |
267 | }, {
268 | key: 'render',
269 | value: function render() {
270 | var entries = [];
271 |
272 | // if no entries exist, there's nothing left to do
273 | if (!this.props.entries.length) {
274 | return null;
275 | }
276 |
277 | for (var i = this.state.minItemIndex; i <= this.state.maxItemIndex; i++) {
278 | var entry = this.props.entries[i];
279 | if (!entry) {
280 | continue;
281 | }
282 | var itemProps = {
283 | key: 'item-' + i,
284 | index: i,
285 | padding: this.props.padding,
286 | dimensions: this.state.itemDimensions,
287 | data: entry
288 | };
289 | entries.push(_react2.default.createElement(_item2.default, itemProps));
290 | }
291 | return _react2.default.createElement(
292 | 'div',
293 | { className: 'infinite-grid-wrapper', ref: 'wrapper', onScroll: this._scrollListener, style: this._wrapperStyle() },
294 | _react2.default.createElement(
295 | 'div',
296 | { ref: 'grid', className: 'infinite-grid', style: this._gridStyle() },
297 | entries
298 | )
299 | );
300 | }
301 | }]);
302 |
303 | return InfiniteGrid;
304 | })(_react2.default.Component);
305 |
306 | exports.default = InfiniteGrid;
307 | ;
308 |
309 | InfiniteGrid.defaultProps = {
310 | buffer: 10,
311 | padding: 10,
312 | entries: [],
313 | height: 250,
314 | width: 250
315 | };
--------------------------------------------------------------------------------
/dist/item.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })();
4 |
5 | Object.defineProperty(exports, "__esModule", {
6 | value: true
7 | });
8 |
9 | var _react = require('react');
10 |
11 | var _react2 = _interopRequireDefault(_react);
12 |
13 | var _lodash = require('lodash');
14 |
15 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
16 |
17 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
18 |
19 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }
20 |
21 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
22 |
23 | var Item = (function (_React$Component) {
24 | _inherits(Item, _React$Component);
25 |
26 | function Item(props) {
27 | _classCallCheck(this, Item);
28 |
29 | return _possibleConstructorReturn(this, Object.getPrototypeOf(Item).call(this, props));
30 | }
31 |
32 | _createClass(Item, [{
33 | key: '_itemWidth',
34 | value: function _itemWidth() {
35 | return this.props.dimensions.gridWidth / this.props.dimensions.itemsPerRow;
36 | }
37 | }, {
38 | key: '_itemLeft',
39 | value: function _itemLeft() {
40 | var column = this.props.index % this.props.dimensions.itemsPerRow;
41 | return column * (this.props.dimensions.gridWidth / this.props.dimensions.itemsPerRow);
42 | }
43 | }, {
44 | key: '_itemTop',
45 | value: function _itemTop() {
46 | return Math.floor(this.props.index / this.props.dimensions.itemsPerRow) * this.props.dimensions.height;
47 | }
48 |
49 | // LIFECYCLE
50 |
51 | }, {
52 | key: 'shouldComponentUpdate',
53 | value: function shouldComponentUpdate(nextProps, nextState) {
54 | return !(0, _lodash.isEqual)(this.props, nextProps);
55 | }
56 |
57 | // RENDER
58 |
59 | }, {
60 | key: 'render',
61 | value: function render() {
62 | var _style = {
63 | width: this._itemWidth() - this.props.padding,
64 | height: this.props.dimensions.height - this.props.padding,
65 | left: this._itemLeft(),
66 | top: this._itemTop(),
67 | position: 'absolute'
68 | };
69 |
70 | var props = {
71 | className: 'item',
72 | style: _style
73 | };
74 |
75 | return _react2.default.createElement(
76 | 'div',
77 | props,
78 | _react2.default.createElement(
79 | 'div',
80 | null,
81 | this.props.data
82 | )
83 | );
84 | }
85 | }]);
86 |
87 | return Item;
88 | })(_react2.default.Component);
89 |
90 | exports.default = Item;
--------------------------------------------------------------------------------
/example/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Example
5 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/example/main.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import InfiniteGrid from '../src/grid';
4 |
5 | class ExampleItem extends React.Component {
6 |
7 | static get propTypes() {
8 | return {
9 | index: React.PropTypes.number
10 | };
11 | }
12 |
13 | render() {
14 | return(
15 |
16 | This is {this.props.index}
17 |
18 | );
19 | }
20 |
21 | }
22 |
23 | // Create 100,000 Example items
24 | let items = [];
25 | for (let i = 0; i <= 1000; i++) {
26 | items.push();
27 | }
28 |
29 | const lazyCallback = (index) => {
30 | console.log(index);
31 | }
32 |
33 | ReactDOM.render(, document.getElementById('grid'));
34 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-infinite-grid",
3 | "version": "0.4.0",
4 | "description": "An React grid component which can handle rendering large amounts of data.",
5 | "author": "Gordan Grasarevic ",
6 | "main": "dist/grid.js",
7 | "repository": {
8 | "type": "git",
9 | "url": "https://github.com/ggordan/react-infinite-grid"
10 | },
11 | "keywords": [
12 | "react-component",
13 | "react",
14 | "grid",
15 | "infinite"
16 | ],
17 | "license": "MIT",
18 | "dependencies": {
19 | "lodash": "^3.10.1",
20 | "react": "^0.14.2",
21 | "react-dom": "^0.14.2"
22 | },
23 | "devDependencies": {
24 | "babel": "^6.1.18",
25 | "babel-cli": "^6.1.18",
26 | "babel-core": "^6.1.20",
27 | "babel-eslint": "^4.1.3",
28 | "babel-loader": "^6.1.0",
29 | "babel-preset-es2015": "^6.1.18",
30 | "babel-preset-react": "^6.1.18",
31 | "rimraf": "^2.4.3",
32 | "webpack": "^1.7.3"
33 | },
34 | "scripts": {
35 | "build": "rimraf dist && babel ./src -d dist --presets=react,es2015"
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/scripts/postinstall.js:
--------------------------------------------------------------------------------
1 | var execSync = require('child_process').execSync
2 | var stat = require('fs').stat
3 |
4 | function exec(command) {
5 | execSync(command, { stdio: [0, 1, 2] })
6 | }
7 |
8 | stat('dist', function (error, stat) {
9 | if (error || !stat.isDirectory())
10 | exec('npm run build');
11 | });
12 |
--------------------------------------------------------------------------------
/src/grid.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {isEqual} from 'lodash';
3 | import Item from './item';
4 |
5 | export default class InfiniteGrid extends React.Component {
6 |
7 | static get propTypes() {
8 | return {
9 | itemClassName: React.PropTypes.string,
10 | entries: React.PropTypes.arrayOf(React.PropTypes.object).isRequired,
11 | height: React.PropTypes.number,
12 | width: React.PropTypes.number,
13 | padding: React.PropTypes.number,
14 | wrapperHeight: React.PropTypes.number,
15 | lazyCallback: React.PropTypes.func,
16 | renderRangeCallback: React.PropTypes.func,
17 | buffer: React.PropTypes.number
18 | }
19 | }
20 |
21 | initialState() {
22 | return {
23 | initiatedLazyload: false,
24 | minHeight: window.innerHeight * 2,
25 | minItemIndex: 0,
26 | maxItemIndex: 100,
27 | itemDimensions: {
28 | height: this._itemHeight(),
29 | width: this._itemHeight(),
30 | gridWidth: 0,
31 | itemsPerRow: 2,
32 | },
33 | };
34 | }
35 |
36 | constructor(props) {
37 | super(props);
38 | this.state = this.initialState();
39 | // bind the functions
40 | this._scrollListener = this._scrollListener.bind(this);
41 | this._updateItemDimensions = this._updateItemDimensions.bind(this);
42 | this._resizeListener = this._resizeListener.bind(this);
43 | this._visibleIndexes = this._visibleIndexes.bind(this);
44 | }
45 |
46 | // METHODS
47 |
48 | _wrapperStyle() {
49 | return {
50 | maxHeight: this._getGridHeight(),
51 | overflowY: 'scroll',
52 | width: '100%',
53 | height: this.props.wrapperHeight,
54 | WebkitOverflowScrolling: true,
55 | };
56 | }
57 |
58 | _gridStyle() {
59 | return {
60 | position: 'relative',
61 | marginTop: this.props.padding,
62 | marginLeft: this.props.padding,
63 | minHeight: this._getGridHeight(),
64 | };
65 | }
66 |
67 | _getGridRect() {
68 | return this.refs.grid.getBoundingClientRect();
69 | }
70 |
71 | _getGridHeight() {
72 | return (this.props.entries.length < this.state.itemDimensions.itemsPerRow) ?
73 | this.state.itemDimensions.height :
74 | Math.floor(this.props.entries.length / this.state.itemDimensions.itemsPerRow) * this.state.itemDimensions.height;
75 | }
76 |
77 | _getWrapperRect() {
78 | return this.refs.wrapper.getBoundingClientRect();
79 | }
80 |
81 | _visibleIndexes() {
82 | var itemsPerRow = this._itemsPerRow();
83 |
84 | // The number of rows that the user has scrolled past
85 | var scrolledPast = (this._scrolledPastRows() * itemsPerRow);
86 | if (scrolledPast < 0) scrolledPast = 0;
87 |
88 | // If i have scrolled past 20 items, but 60 are visible on screen,
89 | // we do not want to change the minimum
90 | var min = scrolledPast - itemsPerRow;
91 | if (min < 0) min = 0;
92 |
93 | // the maximum should be the number of items scrolled past, plus some
94 | // buffer
95 | var bufferRows = this._numVisibleRows() + this.props.buffer;
96 | var max = scrolledPast + (itemsPerRow * bufferRows);
97 | if (max > this.props.entries.length) max = this.props.entries.length;
98 |
99 | this.setState({
100 | minItemIndex: min,
101 | maxItemIndex: max,
102 | }, function() {
103 | this._lazyCallback();
104 | });
105 | }
106 |
107 | _updateItemDimensions() {
108 | this.setState({
109 | itemDimensions: {
110 | height: this._itemHeight(),
111 | width: this._itemHeight(),
112 | gridWidth: this._getGridRect().width,
113 | itemsPerRow: this._itemsPerRow(),
114 | },
115 | minHeight: this._totalRows(),
116 | });
117 | }
118 |
119 | _itemsPerRow() {
120 | return Math.floor(this._getGridRect().width / this._itemWidth());
121 | }
122 |
123 | _totalRows() {
124 | const scrolledPastHeight = (this.props.entries.length / this._itemsPerRow()) * this._itemHeight();
125 | if (scrolledPastHeight < 0) return 0;
126 | return scrolledPastHeight;
127 | }
128 |
129 | _scrolledPastRows() {
130 | const rect = this._getGridRect();
131 | const topScrollOffset = rect.height - rect.bottom;
132 | return Math.floor(topScrollOffset / this._itemHeight());
133 | }
134 |
135 | _itemHeight() {
136 | return this.props.height + (2 * this.props.padding);
137 | }
138 |
139 | _itemWidth() {
140 | return this.props.width + (2 * this.props.padding);
141 | }
142 |
143 | _numVisibleRows() {
144 | return Math.ceil(this._getWrapperRect().height / this._itemHeight());
145 | }
146 |
147 | _lazyCallback() {
148 | if (!this.state.initiatedLazyload && (this.state.maxItemIndex === this.props.entries.length) && this.props.lazyCallback) {
149 | this.setState({initiatedLazyload: true });
150 | this.props.lazyCallback(this.state.maxItemIndex);
151 | }
152 | }
153 |
154 | // LIFECYCLE
155 |
156 | componentWillMount() {
157 | window.addEventListener('resize', this._resizeListener);
158 | }
159 |
160 | componentDidMount() {
161 | this._updateItemDimensions();
162 | this._visibleIndexes();
163 | }
164 |
165 | componentWillReceiveProps(nextProps) {
166 | if (nextProps.entries.length > this.props.entries.length) {
167 | this.setState({
168 | initiatedLazyload: false,
169 | });
170 | }
171 | // Update these all the time because entries may change on the fly.
172 | // this._updateItemDimensions();
173 | this._visibleIndexes();
174 | }
175 |
176 | componentDidUpdate(prevProps, prevState) {
177 | if (typeof this.props.renderRangeCallback === 'function') {
178 | this.props.renderRangeCallback(this.state.minItemIndex, this.state.maxItemIndex);
179 | }
180 | }
181 |
182 | shouldComponentUpdate(nextProps, nextState) {
183 | return !isEqual(this.state, nextState);
184 | }
185 |
186 | componentWillUnmount() {
187 | window.removeEventListener('resize', this._resizeListener);
188 | }
189 |
190 | // LISTENERS
191 |
192 | _scrollListener(event) {
193 | clearTimeout(this.scrollOffset);
194 | this.scrollOffset = setTimeout(() => {
195 | this._visibleIndexes();
196 | }, 10);
197 | }
198 |
199 | _resizeListener(event) {
200 | if (!this.props.wrapperHeight) {
201 | this.setState({
202 | wrapperHeight: window.innerHeight,
203 | });
204 | }
205 | this._updateItemDimensions();
206 | this._visibleIndexes();
207 | }
208 |
209 | // RENDER
210 |
211 | render() {
212 | var entries = [];
213 |
214 | // if no entries exist, there's nothing left to do
215 | if (!this.props.entries.length) {
216 | return null;
217 | }
218 |
219 | for (let i = this.state.minItemIndex; i <= this.state.maxItemIndex; i++) {
220 | let entry = this.props.entries[i];
221 | if (!entry) {
222 | continue;
223 | }
224 | const itemProps = {
225 | key: 'item-' + i,
226 | index: i,
227 | padding: this.props.padding,
228 | dimensions: this.state.itemDimensions,
229 | data: entry
230 | };
231 | entries.push( );
232 | }
233 | return (
234 |
235 |
236 | {entries}
237 |
238 |
239 | );
240 |
241 | }
242 |
243 | };
244 |
245 | InfiniteGrid.defaultProps = {
246 | buffer: 10,
247 | padding: 10,
248 | entries: [],
249 | height: 250,
250 | width: 250
251 | }
252 |
--------------------------------------------------------------------------------
/src/item.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {isEqual} from 'lodash';
3 |
4 | export default class Item extends React.Component {
5 |
6 | constructor(props) {
7 | super(props);
8 | }
9 |
10 | _itemWidth() {
11 | return this.props.dimensions.gridWidth / this.props.dimensions.itemsPerRow;
12 | }
13 |
14 | _itemLeft() {
15 | var column = this.props.index % this.props.dimensions.itemsPerRow;
16 | return column * (this.props.dimensions.gridWidth / this.props.dimensions.itemsPerRow);
17 | }
18 |
19 | _itemTop() {
20 | return Math.floor(this.props.index / this.props.dimensions.itemsPerRow) * this.props.dimensions.height;
21 | }
22 |
23 | // LIFECYCLE
24 |
25 | shouldComponentUpdate(nextProps, nextState) {
26 | return !isEqual(this.props, nextProps);
27 | }
28 |
29 | // RENDER
30 |
31 | render() {
32 | const _style = {
33 | width: this._itemWidth() - this.props.padding,
34 | height: this.props.dimensions.height - this.props.padding,
35 | left: this._itemLeft(),
36 | top: this._itemTop(),
37 | position: 'absolute'
38 | };
39 |
40 | var props = {
41 | className: 'item',
42 | style: _style
43 | };
44 |
45 | return (
46 |
47 |
{this.props.data}
48 |
49 | );
50 |
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | var webpack = require('webpack');
2 | var path = require('path');
3 |
4 | module.exports = {
5 | entry: "./example/main.js",
6 | output: {
7 | path: './example',
8 | filename: "example.bundle.js"
9 | },
10 | module: {
11 | loaders: [{
12 | test: /\.jsx?$/,
13 | loaders: ['babel?presets[]=es2015,presets[]=react']
14 | }]
15 | }
16 | };
17 |
--------------------------------------------------------------------------------