├── .gitignore ├── .npmignore ├── README.md ├── examples ├── multiple.js ├── simple.js └── two-dimensions.js ├── package.json ├── src ├── index.js └── translate.js └── webpack.examples.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | lib/ 3 | 4 | .DS_Store 5 | npm-debug.log 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src/ 2 | examples/ 3 | 4 | 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React List View 2 | 3 | Infinite list view component with support for vertical and horizontal scrolling. Inspired by [Ember.ListView](https://github.com/emberjs/list-view). 4 | 5 | Only visible items are rendered. 6 | 7 | ## Installation 8 | 9 | ```sh 10 | npm install react-list-view --save 11 | ``` 12 | 13 | ## Usage 14 | 15 | ### Props 16 | 17 | `renderItem(x: Number, y: Number, style: Object): ReactElement` 18 | **Required**. Maps an item's coordinates to a React element. The style object contains CSS positioning properties that should be applied to the element returned from `renderItem`. 19 | 20 | #### Vertical scrolling 21 | 22 | `rowHeight: Number` 23 | Height of every row, in pixels. 24 | 25 | `rowCount: Number` 26 | Number of rows in the list. 27 | 28 | `clientHeight: Number` 29 | Height of the scrollable area, in pixels. 30 | 31 | `scrollTop: Number` 32 | Vertical scroll offset of the scrollable area, in pixels. 33 | 34 | #### Horizontal scrolling 35 | 36 | `columnWidth: Number` 37 | Width of every column, in pixels. 38 | 39 | `columnCount: Number` 40 | Number of columns in the list. 41 | 42 | `clientWidth: Number` 43 | Width of the scrollable area, in pixels. 44 | 45 | `scrollLeft: Number` 46 | Horizontal scroll offset of the scrollable area, in pixels. 47 | 48 | ### Notes 49 | 50 | If neither `clientWidth` nor `clientHeight` are provided, the `ReactListView` component will control its own `clientWidth`, `clientHeight`, `scrollTop` and `scrollLeft` properties. 51 | 52 | Otherwise, the `ReactListView` expects to be provided with either: 53 | * `clientHeight` and `scrollTop` 54 | * `clientWidth` and `scrollLeft` 55 | * all four properties 56 | 57 | ### Example 58 | ```js 59 |
Item #{x}
} 63 | /> 64 | ``` 65 | 66 | See also [the examples directory](examples/). 67 | 68 | To run the examples, simply clone the repo and then `npm install` and `npm start` at the root of the repo. 69 | 70 | ## Performance 71 | 72 | For even better performances, you should ignore pointer events inside the list with `pointer-events: none;`. 73 | 74 | When the `client{Height,Width}` and `scroll{Top,Left}` props of the component are controlled by a parent component, the scrollable container should have a CSS transform applied. See #2. 75 | -------------------------------------------------------------------------------- /examples/multiple.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import ReactListView from '../src'; 4 | import translate from '../src/translate'; 5 | 6 | const listStyle = { 7 | width: '33.3%', 8 | display: 'inline-block', 9 | }; 10 | 11 | const MultipleExample = React.createClass({ 12 | 13 | displayName: 'MultipleExample', 14 | 15 | getInitialState() { 16 | return { 17 | clientHeight: 0, 18 | clientWidth: 0, 19 | scrollTop: 0 20 | }; 21 | }, 22 | 23 | componentDidMount() { 24 | this.setState({ 25 | clientHeight: ReactDOM.findDOMNode(this).clientHeight, 26 | clientWidth: ReactDOM.findDOMNode(this).clientWidth, 27 | scrollTop: ReactDOM.findDOMNode(this).scrollTop, 28 | }); 29 | }, 30 | 31 | _handleScroll(e) { 32 | this.setState({ 33 | scrollTop: e.target.scrollTop, 34 | }); 35 | }, 36 | 37 | _renderItem(x, y, style) { 38 | return
Item #{x},#{y}
; 39 | }, 40 | 41 | render() { 42 | let { scrollTop, clientHeight } = this.state; 43 | 44 | return ( 45 |
53 |
54 | 62 |
63 |
64 | 72 |
73 |
74 | 82 |
83 |
84 | ); 85 | }, 86 | 87 | }); 88 | 89 | ReactDOM.render(, document.body); 90 | -------------------------------------------------------------------------------- /examples/simple.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import ReactListView from '../src'; 4 | 5 | const SimpleExample = React.createClass({ 6 | 7 | displayName: 'SimpleExample', 8 | 9 | render() { 10 | return ( 11 | 18 |
19 | Item #{x},#{y} 20 |
21 | } 22 | /> 23 | ); 24 | }, 25 | 26 | }); 27 | 28 | ReactDOM.render(, document.body); 29 | -------------------------------------------------------------------------------- /examples/two-dimensions.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import ReactListView from '../src'; 4 | 5 | const TwoDimensionsExample = React.createClass({ 6 | 7 | displayName: 'TwoDimensionsExample', 8 | 9 | render() { 10 | return ( 11 | 21 |
22 | Item #{x},#{y} 23 |
24 | } 25 | /> 26 | ); 27 | }, 28 | 29 | }); 30 | 31 | ReactDOM.render(, document.body); 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-list-view", 3 | "version": "1.0.0", 4 | "description": "Infinite list view component", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "start": "webpack-simple-server -d examples/ -p $PORT -c webpack.examples.config.js", 8 | "prepublish": "rm -rf lib/ && babel --stage=0 -d lib/ src/" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/morhaus/react-list-view.git" 13 | }, 14 | "keywords": [ 15 | "react", 16 | "infinite", 17 | "list", 18 | "list-view", 19 | "scroll", 20 | "react-component" 21 | ], 22 | "author": "Alexandre Kirszenberg ", 23 | "license": "MIT", 24 | "devDependencies": { 25 | "babel": "^5.4.7", 26 | "babel-core": "^5.4.7", 27 | "babel-loader": "^5.1.3", 28 | "express": "^4.12.4", 29 | "node-libs-browser": "^0.5.2", 30 | "react": "^15.0.1", 31 | "react-dom": "^15.0.1", 32 | "react-hot-loader": "^1.2.7", 33 | "webpack": "^1.9.10", 34 | "webpack-simple-server": "^0.2.0" 35 | }, 36 | "peerDependencies": { 37 | "react": "^15.0.1", 38 | "react-dom": "^15.0.1" 39 | }, 40 | "dependencies": { 41 | "react-addons-create-fragment": "^15.0.1" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React, {PropTypes} from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import createFragment from 'react-addons-create-fragment'; 4 | 5 | import translate from './translate'; 6 | 7 | export default class ReactListView extends React.Component { 8 | 9 | static propTypes = { 10 | className: PropTypes.string, 11 | style: PropTypes.object, 12 | 13 | renderItem: PropTypes.func.isRequired, 14 | 15 | rowCount: PropTypes.number, 16 | columnCount: PropTypes.number, 17 | rowHeight: PropTypes.number, 18 | columnWidth: PropTypes.number, 19 | 20 | // Controllables 21 | clientHeight: PropTypes.number, 22 | clientWidth: PropTypes.number, 23 | scrollTop: PropTypes.number, 24 | scrollLeft: PropTypes.number, 25 | }; 26 | 27 | static defaultProps = { 28 | className: null, 29 | style: {}, 30 | 31 | rowCount: 1, 32 | columnCount: 1, 33 | rowHeight: 0, 34 | columnWidth: 0, 35 | 36 | clientHeight: -1, 37 | clientWidth: -1, 38 | scrollTop: -1, 39 | scrollLeft: -1, 40 | }; 41 | 42 | constructor(props, context) { 43 | super(props, context); 44 | 45 | this._isControlled = ( 46 | props.clientHeight !== -1 || 47 | props.clientWidth !== -1 48 | ); 49 | if (!this._isControlled) { 50 | this.state = { 51 | clientHeight: -1, 52 | clientWidth: -1, 53 | scrollTop: -1, 54 | scrollLeft: -1, 55 | }; 56 | } 57 | 58 | this._handleScroll = this._handleScroll.bind(this); 59 | } 60 | 61 | componentDidMount() { 62 | if (!this._isControlled) { 63 | let { 64 | clientHeight, 65 | clientWidth, 66 | scrollTop, 67 | scrollLeft, 68 | } = ReactDOM.findDOMNode(this); 69 | this.setState({ clientHeight, clientWidth, scrollTop, scrollLeft }); 70 | } 71 | } 72 | 73 | _handleScroll(e) { 74 | this.setState({ 75 | scrollTop: e.target.scrollTop, 76 | scrollLeft: e.target.scrollLeft, 77 | }); 78 | } 79 | 80 | _getBoundaries(scroll, itemDimension, clientDimension, maxDimension) { 81 | let min = Math.floor(scroll / itemDimension); 82 | let max = Math.min( 83 | maxDimension, 84 | min + Math.ceil(clientDimension / itemDimension) 85 | ); 86 | return [min, max]; 87 | } 88 | 89 | render() { 90 | let { 91 | style, 92 | className, 93 | renderItem, 94 | rowCount, 95 | columnCount, 96 | rowHeight, 97 | columnWidth, 98 | } = this.props; 99 | 100 | let { 101 | scrollTop, 102 | scrollLeft, 103 | clientHeight, 104 | clientWidth, 105 | } = this._isControlled ? this.props : this.state; 106 | 107 | let minY, maxY; 108 | if (clientHeight === -1) { 109 | [minX, maxX] = [0, -1]; 110 | } else if (rowHeight > 0) { 111 | [minY, maxY] = this._getBoundaries( 112 | scrollTop, 113 | rowHeight, 114 | clientHeight, 115 | rowCount - 1 116 | ); 117 | } else { 118 | [minY, maxY] = [0, 0]; 119 | } 120 | 121 | let minX, maxX; 122 | if (clientWidth === -1) { 123 | [minX, maxX] = [0, -1]; 124 | } else if (columnWidth > 0) { 125 | [minX, maxX] = this._getBoundaries( 126 | scrollLeft, 127 | columnWidth, 128 | clientWidth, 129 | columnCount - 1 130 | ); 131 | } else { 132 | [minX, maxX] = [0, 0]; 133 | } 134 | 135 | let items = {}; 136 | for (let y = minY; y <= maxY; y++) { 137 | for (let x = minX; x <= maxX; x++) { 138 | items[`${x},${y}`] = renderItem( 139 | x, 140 | y, 141 | translate(x * columnWidth, y * rowHeight, true) 142 | ); 143 | } 144 | } 145 | 146 | let listViewClassName = 'ReactListView'; 147 | if (className) { 148 | listViewClassName += ' ' + className; 149 | } 150 | 151 | return ( 152 |
167 |
175 | {createFragment(items)} 176 |
177 |
178 | ); 179 | } 180 | 181 | } 182 | -------------------------------------------------------------------------------- /src/translate.js: -------------------------------------------------------------------------------- 1 | // Extracted from emberjs/list-view 2 | 3 | let { style } = document.createElement('div'); 4 | 5 | let propPrefixes = ['Webkit', 'Moz', 'O', 'ms']; 6 | 7 | function testProp(prop) { 8 | if (prop in style) { 9 | return prop; 10 | } 11 | 12 | let capitalizedProp = prop.charAt(0).toUpperCase() + prop.slice(1); 13 | 14 | let prefixedProp; 15 | for (let i = 0; i < propPrefixes.length; i++) { 16 | prefixedProp = propPrefixes[i] + capitalizedProp; 17 | if (prefixedProp in style) { 18 | return prefixedProp; 19 | } 20 | } 21 | return false; 22 | } 23 | 24 | let transformProp = testProp('transform'); 25 | let perspectiveProp = testProp('perspective'); 26 | 27 | let supports2D = !!transformProp; 28 | let supports3D = !!perspectiveProp; 29 | 30 | export default function translate(x, y, addPosition = false) { 31 | if (supports3D) { 32 | return { 33 | [transformProp]: `translate3d(${x}px, ${y}px, 0)`, 34 | position: addPosition ? 'fixed' : null, 35 | }; 36 | } 37 | if (supports2D) { 38 | return { 39 | [transformProp]: `translate(${x}px, ${y}px)`, 40 | position: addPosition ? 'fixed' : null, 41 | }; 42 | } 43 | return { 44 | top: `${y}px`, 45 | left: `${x}px`, 46 | position: addPosition ? 'absolute' : null, 47 | }; 48 | } 49 | -------------------------------------------------------------------------------- /webpack.examples.config.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import webpack from 'webpack'; 3 | 4 | export default { 5 | plugins: [ 6 | new webpack.HotModuleReplacementPlugin(), 7 | new webpack.DefinePlugin({ 8 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV), 9 | }), 10 | ], 11 | module: { 12 | loaders: [ 13 | { 14 | test: /\.jsx?$/, 15 | loader: 'react-hot!babel?stage=0', 16 | include: [path.resolve('./examples'), path.resolve('./src')], 17 | }, 18 | ], 19 | }, 20 | }; 21 | --------------------------------------------------------------------------------