virtual-scroller 1.7.9 → 1.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.gitlab-ci.yml +1 -1
- package/CHANGELOG.md +71 -1
- package/README.md +434 -151
- package/bundle/index-bypass.html +1 -1
- package/bundle/index-dom.html +1 -1
- package/bundle/index-grid.html +1 -2
- package/bundle/index-scrollableContainer.html +1 -1
- package/bundle/index-tbody-scrollableContainer.html +2 -0
- package/bundle/index-tbody.html +2 -0
- package/bundle/virtual-scroller-dom.js +1 -1
- package/bundle/virtual-scroller-dom.js.map +1 -1
- package/bundle/virtual-scroller-react.js +1 -1
- package/bundle/virtual-scroller-react.js.map +1 -1
- package/bundle/virtual-scroller.js +1 -1
- package/bundle/virtual-scroller.js.map +1 -1
- package/commonjs/BeforeResize.js +315 -0
- package/commonjs/BeforeResize.js.map +1 -0
- package/commonjs/DOM/Engine.js +46 -0
- package/commonjs/DOM/Engine.js.map +1 -0
- package/commonjs/DOM/ItemsContainer.js +78 -0
- package/commonjs/DOM/ItemsContainer.js.map +1 -0
- package/commonjs/DOM/{WaitForStylesToLoad.js → ListTopOffsetWatcher.js} +71 -44
- package/commonjs/DOM/ListTopOffsetWatcher.js.map +1 -0
- package/commonjs/DOM/ScrollableContainer.js +69 -101
- package/commonjs/DOM/ScrollableContainer.js.map +1 -1
- package/commonjs/DOM/VirtualScroller.js +37 -29
- package/commonjs/DOM/VirtualScroller.js.map +1 -1
- package/commonjs/DOM/tbody.js +17 -11
- package/commonjs/DOM/tbody.js.map +1 -1
- package/commonjs/ItemHeights.js +33 -34
- package/commonjs/ItemHeights.js.map +1 -1
- package/commonjs/Layout.js +591 -216
- package/commonjs/Layout.js.map +1 -1
- package/commonjs/Layout.test.js +196 -0
- package/commonjs/Layout.test.js.map +1 -0
- package/commonjs/ListHeightMeasurement.js +124 -0
- package/commonjs/ListHeightMeasurement.js.map +1 -0
- package/commonjs/Resize.js +50 -39
- package/commonjs/Resize.js.map +1 -1
- package/commonjs/Scroll.js +139 -95
- package/commonjs/Scroll.js.map +1 -1
- package/commonjs/VirtualScroller.columns.js +43 -0
- package/commonjs/VirtualScroller.columns.js.map +1 -0
- package/commonjs/VirtualScroller.constructor.js +408 -0
- package/commonjs/VirtualScroller.constructor.js.map +1 -0
- package/commonjs/VirtualScroller.items.js +305 -0
- package/commonjs/VirtualScroller.items.js.map +1 -0
- package/commonjs/VirtualScroller.js +160 -1021
- package/commonjs/VirtualScroller.js.map +1 -1
- package/commonjs/VirtualScroller.layout.js +562 -0
- package/commonjs/VirtualScroller.layout.js.map +1 -0
- package/commonjs/VirtualScroller.onRender.js +357 -0
- package/commonjs/VirtualScroller.onRender.js.map +1 -0
- package/commonjs/VirtualScroller.resize.js +186 -0
- package/commonjs/VirtualScroller.resize.js.map +1 -0
- package/commonjs/VirtualScroller.state.js +301 -0
- package/commonjs/VirtualScroller.state.js.map +1 -0
- package/commonjs/VirtualScroller.verticalSpacing.js +65 -0
- package/commonjs/VirtualScroller.verticalSpacing.js.map +1 -0
- package/commonjs/getItemCoordinates.js.map +1 -1
- package/commonjs/getItemsDiff.js.map +1 -1
- package/commonjs/getVerticalSpacing.js +8 -8
- package/commonjs/getVerticalSpacing.js.map +1 -1
- package/commonjs/package.json +5 -0
- package/commonjs/react/VirtualScroller.js +182 -628
- package/commonjs/react/VirtualScroller.js.map +1 -1
- package/commonjs/react/useClassName.js +26 -0
- package/commonjs/react/useClassName.js.map +1 -0
- package/commonjs/react/useHandleItemsChange.js +116 -0
- package/commonjs/react/useHandleItemsChange.js.map +1 -0
- package/commonjs/react/useInstanceMethods.js +37 -0
- package/commonjs/react/useInstanceMethods.js.map +1 -0
- package/commonjs/react/useItemKeys.js +60 -0
- package/commonjs/react/useItemKeys.js.map +1 -0
- package/commonjs/react/useOnItemHeightChange.js +32 -0
- package/commonjs/react/useOnItemHeightChange.js.map +1 -0
- package/commonjs/react/useOnItemStateChange.js +32 -0
- package/commonjs/react/useOnItemStateChange.js.map +1 -0
- package/commonjs/react/useState.js +140 -0
- package/commonjs/react/useState.js.map +1 -0
- package/commonjs/react/useStyle.js +29 -0
- package/commonjs/react/useStyle.js.map +1 -0
- package/commonjs/react/useVirtualScroller.js +62 -0
- package/commonjs/react/useVirtualScroller.js.map +1 -0
- package/commonjs/react/useVirtualScrollerStartStop.js +20 -0
- package/commonjs/react/useVirtualScrollerStartStop.js.map +1 -0
- package/commonjs/test/Engine.js +23 -0
- package/commonjs/test/Engine.js.map +1 -0
- package/commonjs/test/ItemsContainer.js +127 -0
- package/commonjs/test/ItemsContainer.js.map +1 -0
- package/commonjs/test/ScrollableContainer.js +130 -0
- package/commonjs/test/ScrollableContainer.js.map +1 -0
- package/commonjs/test/VirtualScroller.js +281 -0
- package/commonjs/test/VirtualScroller.js.map +1 -0
- package/commonjs/utility/debounce.js +28 -6
- package/commonjs/utility/debounce.js.map +1 -1
- package/commonjs/utility/debug.js +51 -12
- package/commonjs/utility/debug.js.map +1 -1
- package/commonjs/utility/getStateSnapshot.js +50 -0
- package/commonjs/utility/getStateSnapshot.js.map +1 -0
- package/commonjs/utility/px.js +1 -1
- package/commonjs/utility/px.js.map +1 -1
- package/commonjs/utility/px.test.js +14 -0
- package/commonjs/utility/px.test.js.map +1 -0
- package/commonjs/utility/shallowEqual.js +1 -1
- package/commonjs/utility/shallowEqual.js.map +1 -1
- package/commonjs/utility/throttle.js.map +1 -1
- package/dom/index.cjs +4 -0
- package/dom/index.cjs.js +9 -0
- package/dom/index.d.ts +25 -0
- package/dom/index.js +1 -1
- package/dom/package.json +10 -4
- package/index.cjs +4 -0
- package/index.cjs.js +9 -0
- package/index.d.ts +99 -0
- package/index.js +1 -1
- package/modules/BeforeResize.js +305 -0
- package/modules/BeforeResize.js.map +1 -0
- package/modules/DOM/Engine.js +27 -0
- package/modules/DOM/Engine.js.map +1 -0
- package/modules/DOM/ItemsContainer.js +71 -0
- package/modules/DOM/ItemsContainer.js.map +1 -0
- package/modules/DOM/{WaitForStylesToLoad.js → ListTopOffsetWatcher.js} +72 -44
- package/modules/DOM/ListTopOffsetWatcher.js.map +1 -0
- package/modules/DOM/ScrollableContainer.js +68 -100
- package/modules/DOM/ScrollableContainer.js.map +1 -1
- package/modules/DOM/VirtualScroller.js +32 -28
- package/modules/DOM/VirtualScroller.js.map +1 -1
- package/modules/DOM/tbody.js +11 -9
- package/modules/DOM/tbody.js.map +1 -1
- package/modules/ItemHeights.js +28 -33
- package/modules/ItemHeights.js.map +1 -1
- package/modules/Layout.js +585 -214
- package/modules/Layout.js.map +1 -1
- package/modules/Layout.test.js +190 -0
- package/modules/Layout.test.js.map +1 -0
- package/modules/ListHeightMeasurement.js +117 -0
- package/modules/ListHeightMeasurement.js.map +1 -0
- package/modules/Resize.js +50 -39
- package/modules/Resize.js.map +1 -1
- package/modules/Scroll.js +139 -94
- package/modules/Scroll.js.map +1 -1
- package/modules/VirtualScroller.columns.js +36 -0
- package/modules/VirtualScroller.columns.js.map +1 -0
- package/modules/VirtualScroller.constructor.js +371 -0
- package/modules/VirtualScroller.constructor.js.map +1 -0
- package/modules/VirtualScroller.items.js +288 -0
- package/modules/VirtualScroller.items.js.map +1 -0
- package/modules/VirtualScroller.js +159 -1014
- package/modules/VirtualScroller.js.map +1 -1
- package/modules/VirtualScroller.layout.js +549 -0
- package/modules/VirtualScroller.layout.js.map +1 -0
- package/modules/VirtualScroller.onRender.js +337 -0
- package/modules/VirtualScroller.onRender.js.map +1 -0
- package/modules/VirtualScroller.resize.js +176 -0
- package/modules/VirtualScroller.resize.js.map +1 -0
- package/modules/VirtualScroller.state.js +283 -0
- package/modules/VirtualScroller.state.js.map +1 -0
- package/modules/VirtualScroller.verticalSpacing.js +54 -0
- package/modules/VirtualScroller.verticalSpacing.js.map +1 -0
- package/modules/getItemCoordinates.js.map +1 -1
- package/modules/getItemsDiff.js.map +1 -1
- package/modules/getVerticalSpacing.js +8 -8
- package/modules/getVerticalSpacing.js.map +1 -1
- package/modules/react/VirtualScroller.js +179 -634
- package/modules/react/VirtualScroller.js.map +1 -1
- package/modules/react/useClassName.js +18 -0
- package/modules/react/useClassName.js.map +1 -0
- package/modules/react/useHandleItemsChange.js +108 -0
- package/modules/react/useHandleItemsChange.js.map +1 -0
- package/modules/react/useInstanceMethods.js +28 -0
- package/modules/react/useInstanceMethods.js.map +1 -0
- package/modules/react/useItemKeys.js +52 -0
- package/modules/react/useItemKeys.js.map +1 -0
- package/modules/react/useOnItemHeightChange.js +24 -0
- package/modules/react/useOnItemHeightChange.js.map +1 -0
- package/modules/react/useOnItemStateChange.js +24 -0
- package/modules/react/useOnItemStateChange.js.map +1 -0
- package/modules/react/useState.js +132 -0
- package/modules/react/useState.js.map +1 -0
- package/modules/react/useStyle.js +19 -0
- package/modules/react/useStyle.js.map +1 -0
- package/modules/react/useVirtualScroller.js +51 -0
- package/modules/react/useVirtualScroller.js.map +1 -0
- package/modules/react/useVirtualScrollerStartStop.js +12 -0
- package/modules/react/useVirtualScrollerStartStop.js.map +1 -0
- package/modules/test/Engine.js +11 -0
- package/modules/test/Engine.js.map +1 -0
- package/modules/test/ItemsContainer.js +120 -0
- package/modules/test/ItemsContainer.js.map +1 -0
- package/modules/test/ScrollableContainer.js +123 -0
- package/modules/test/ScrollableContainer.js.map +1 -0
- package/modules/test/VirtualScroller.js +270 -0
- package/modules/test/VirtualScroller.js.map +1 -0
- package/modules/utility/debounce.js +28 -6
- package/modules/utility/debounce.js.map +1 -1
- package/modules/utility/debug.js +47 -10
- package/modules/utility/debug.js.map +1 -1
- package/modules/utility/getStateSnapshot.js +43 -0
- package/modules/utility/getStateSnapshot.js.map +1 -0
- package/modules/utility/px.js +1 -1
- package/modules/utility/px.js.map +1 -1
- package/modules/utility/px.test.js +9 -0
- package/modules/utility/px.test.js.map +1 -0
- package/modules/utility/shallowEqual.js +1 -1
- package/modules/utility/shallowEqual.js.map +1 -1
- package/modules/utility/throttle.js.map +1 -1
- package/package.json +54 -29
- package/react/index.cjs +4 -0
- package/react/index.cjs.js +9 -0
- package/react/index.d.ts +28 -0
- package/react/index.js +1 -1
- package/react/package.json +10 -4
- package/rollup.config.mjs +62 -0
- package/runnable/create-commonjs-package-json.js +11 -0
- package/source/BeforeResize.js +312 -0
- package/source/DOM/Engine.js +30 -0
- package/source/DOM/ItemsContainer.js +48 -0
- package/source/DOM/{WaitForStylesToLoad.js → ListTopOffsetWatcher.js} +61 -30
- package/source/DOM/ScrollableContainer.js +51 -73
- package/source/DOM/VirtualScroller.js +33 -18
- package/source/DOM/tbody.js +30 -21
- package/source/ItemHeights.js +27 -27
- package/source/Layout.js +629 -252
- package/source/Layout.test.js +176 -0
- package/source/ListHeightMeasurement.js +95 -0
- package/source/Resize.js +56 -32
- package/source/Scroll.js +135 -82
- package/source/VirtualScroller.columns.js +26 -0
- package/source/VirtualScroller.constructor.js +336 -0
- package/source/VirtualScroller.items.js +302 -0
- package/source/VirtualScroller.js +162 -936
- package/source/VirtualScroller.layout.js +539 -0
- package/source/VirtualScroller.onRender.js +345 -0
- package/source/VirtualScroller.resize.js +189 -0
- package/source/VirtualScroller.state.js +284 -0
- package/source/VirtualScroller.verticalSpacing.js +51 -0
- package/source/getVerticalSpacing.js +7 -7
- package/source/react/VirtualScroller.js +243 -603
- package/source/react/useClassName.js +14 -0
- package/source/react/useHandleItemsChange.js +115 -0
- package/source/react/useInstanceMethods.js +25 -0
- package/source/react/useItemKeys.js +59 -0
- package/source/react/useOnItemHeightChange.js +28 -0
- package/source/react/useOnItemStateChange.js +28 -0
- package/source/react/useState.js +114 -0
- package/source/react/useStyle.js +20 -0
- package/source/react/useVirtualScroller.js +59 -0
- package/source/react/useVirtualScrollerStartStop.js +12 -0
- package/source/test/Engine.js +11 -0
- package/source/test/ItemsContainer.js +87 -0
- package/source/test/ScrollableContainer.js +88 -0
- package/source/test/VirtualScroller.js +232 -0
- package/source/utility/debounce.js +22 -5
- package/source/utility/debug.js +34 -3
- package/source/utility/getStateSnapshot.js +36 -0
- package/source/utility/px.js +1 -1
- package/source/utility/px.test.js +9 -0
- package/website/index-bypass.html +195 -0
- package/website/index-grid.html +0 -1
- package/website/index-scrollableContainer.html +208 -0
- package/website/index-tbody-scrollableContainer.html +68 -0
- package/website/index-tbody.html +55 -0
- package/commonjs/DOM/RenderingEngine.js +0 -33
- package/commonjs/DOM/RenderingEngine.js.map +0 -1
- package/commonjs/DOM/Screen.js +0 -87
- package/commonjs/DOM/Screen.js.map +0 -1
- package/commonjs/DOM/WaitForStylesToLoad.js.map +0 -1
- package/commonjs/RestoreScroll.js +0 -118
- package/commonjs/RestoreScroll.js.map +0 -1
- package/dom/index.commonjs.js +0 -4
- package/index.commonjs.js +0 -4
- package/modules/DOM/RenderingEngine.js +0 -19
- package/modules/DOM/RenderingEngine.js.map +0 -1
- package/modules/DOM/Screen.js +0 -80
- package/modules/DOM/Screen.js.map +0 -1
- package/modules/DOM/WaitForStylesToLoad.js.map +0 -1
- package/modules/RestoreScroll.js +0 -111
- package/modules/RestoreScroll.js.map +0 -1
- package/react/index.commonjs.js +0 -4
- package/source/DOM/RenderingEngine.js +0 -22
- package/source/DOM/Screen.js +0 -51
- package/source/RestoreScroll.js +0 -86
package/README.md
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
# VirtualScroller
|
|
2
2
|
|
|
3
|
-
A universal open-source implementation of Twitter's [`VirtualScroller`](https://medium.com/@paularmstrong/twitter-lite-and-high-performance-react-progressive-web-apps-at-scale-d28a00e780a3) component: a component for efficiently rendering large lists of *variable height* items.
|
|
3
|
+
A universal open-source implementation of Twitter's [`VirtualScroller`](https://medium.com/@paularmstrong/twitter-lite-and-high-performance-react-progressive-web-apps-at-scale-d28a00e780a3) component: a component for efficiently rendering large lists of *variable height* items. Supports grid layout.
|
|
4
|
+
|
|
5
|
+
<!-- Automatically measures items as they're rendered and supports items of variable/dynamic height. -->
|
|
6
|
+
|
|
7
|
+
* For React users, includes a [React](#react) component.
|
|
8
|
+
* For those who prefer "vanilla" DOM, there's a [DOM](#dom) component.
|
|
9
|
+
* For everyone else, there's a low-level [core](#core) component that supports any type of [rendering engine](#rendering-engine), not just DOM. Use it to create your own implementation for any framework or environment.
|
|
4
10
|
|
|
5
11
|
## Demo
|
|
6
12
|
|
|
@@ -53,19 +59,19 @@ If you're not using a bundler then use a [standalone version from a CDN](#cdn).
|
|
|
53
59
|
|
|
54
60
|
## Core
|
|
55
61
|
|
|
56
|
-
The default export is a low-level `VirtualScroller` class: it implements the core logic of a `VirtualScroller` component and can be used for building a `VirtualScroller` component for any UI framework or even any [rendering engine](#rendering-engine) other than DOM.
|
|
62
|
+
The default export is a low-level `VirtualScroller` class: it implements the core logic of a `VirtualScroller` component and can be used for building a `VirtualScroller` component for any UI framework or even any [rendering engine](#rendering-engine) other than DOM. This core class is not meant to be used in applications directly. Instead, prefer using one of the high-level components provided by this library: [`virtual-scroller/dom`](#dom) or [`virtual-scroller/react`](#react) packages. Or implement your own: see `source/test` folder for an example of using the core class to build an "imaginary" renderer implementation.
|
|
57
63
|
|
|
58
64
|
#### State
|
|
59
65
|
|
|
60
|
-
The core `VirtualScroller` component works by dynamically updating its `state` as the user scrolls the page. The `state` provides the calculations on which items should be rendered (and which should not) depending on the current scroll position.
|
|
66
|
+
The core `VirtualScroller` component works by dynamically updating its `state` as the user scrolls the page. The `state` provides the calculations on which items should be rendered (and which should not be) depending on the current scroll position.
|
|
67
|
+
|
|
68
|
+
A higher-level wrapper around the core `VirtualScroller` component must manage the rendering of the items using the information from the `state`. At any given time, `state` should correspond exactly to what's rendered on the screen: whenever `state` gets updated, the corresponding changes should be immediately (without any "timeout" or "delay") rendered on the screen.
|
|
61
69
|
|
|
62
70
|
<details>
|
|
63
71
|
<summary>Show the list of all <code>state</code> properties</summary>
|
|
64
72
|
|
|
65
73
|
#####
|
|
66
74
|
|
|
67
|
-
A high-level wrapper should supply either `getState`/`setState` functions, or `onStateChange` function (or both of them), and those functions are gonna be responsible for rendering the actual list using the information from `state`.
|
|
68
|
-
|
|
69
75
|
The main `state` properties are:
|
|
70
76
|
|
|
71
77
|
* `items: any[]` — The list of items (can be updated via [`.setItems()`](#dynamically-loaded-lists)).
|
|
@@ -86,34 +92,58 @@ The following `state` properties are only used for saving and restoring `Virtual
|
|
|
86
92
|
|
|
87
93
|
* `verticalSpacing: number?` — Vertical item spacing. Is `undefined` until it has been measured. Is only measured once, when at least two rows of items have been rendered.
|
|
88
94
|
|
|
89
|
-
* `columnsCount: number?` — The count of items in a row. Is `undefined` if no `getColumnsCount()` parameter has been passed to `VirtualScroller`.
|
|
95
|
+
* `columnsCount: number?` — The count of items in a row. Is `undefined` if no `getColumnsCount()` parameter has been passed to `VirtualScroller`, or if the columns count is `1`.
|
|
90
96
|
|
|
91
|
-
* `
|
|
97
|
+
* `scrollableContainerWidth: number?` — The width of the scrollable container. For DOM implementations, that's gonna be either the browser window width or some scrollable parent element width. Is `undefined` until it has been measured after the `VirtualScroller` has been `start()`-ed.
|
|
92
98
|
</details>
|
|
93
99
|
|
|
94
100
|
#### Example
|
|
95
101
|
|
|
96
|
-
<
|
|
97
|
-
<summary>A general idea of using the low-level <code>VirtualScroller</code> class.</summary>
|
|
102
|
+
A general idea of using the low-level <code>VirtualScroller</code> class:
|
|
98
103
|
|
|
99
104
|
#####
|
|
100
105
|
|
|
101
106
|
```js
|
|
102
107
|
import VirtualScroller from 'virtual-scroller'
|
|
103
108
|
|
|
104
|
-
const
|
|
109
|
+
const items = [
|
|
110
|
+
{ name: 'Apple' },
|
|
111
|
+
{ name: 'Banana' },
|
|
112
|
+
...
|
|
113
|
+
]
|
|
114
|
+
|
|
115
|
+
const getContainerElement = () => document.getElementById(...)
|
|
116
|
+
|
|
117
|
+
const virtualScroller = new VirtualScroller(getContainerElement, items, {
|
|
118
|
+
render(state) {
|
|
119
|
+
// Re-renders the list based on its `state`.
|
|
120
|
+
const {
|
|
121
|
+
items,
|
|
122
|
+
firstShownItemIndex,
|
|
123
|
+
lastShownItemIndex,
|
|
124
|
+
beforeItemsHeight,
|
|
125
|
+
afterItemsHeight
|
|
126
|
+
} = state
|
|
127
|
+
|
|
128
|
+
container.paddingTop = beforeItemsHeight
|
|
129
|
+
container.paddingBottom = afterItemsHeight
|
|
130
|
+
|
|
131
|
+
container.children = items
|
|
132
|
+
.slice(firstShownItemIndex, lastShownItemIndex + 1)
|
|
133
|
+
.map(createItemElement)
|
|
134
|
+
}
|
|
135
|
+
})
|
|
105
136
|
|
|
106
137
|
// Start listening to scroll events.
|
|
107
|
-
virtualScroller.
|
|
138
|
+
virtualScroller.start()
|
|
108
139
|
|
|
109
140
|
// Stop listening to scroll events.
|
|
110
141
|
virtualScroller.stop()
|
|
111
142
|
```
|
|
112
143
|
|
|
113
|
-
* `getContainerElement()`
|
|
114
|
-
* `items`
|
|
115
|
-
* `
|
|
116
|
-
</details>
|
|
144
|
+
* `getContainerElement()` — Returns the list "element" that is gonna contain all list item "elements".
|
|
145
|
+
* `items` — The list of items.
|
|
146
|
+
* `render(state, prevState)` — "Renders" the list.
|
|
117
147
|
|
|
118
148
|
#####
|
|
119
149
|
|
|
@@ -140,22 +170,25 @@ function renderItem(item) {
|
|
|
140
170
|
|
|
141
171
|
const container = document.getElementById('list')
|
|
142
172
|
|
|
143
|
-
function
|
|
173
|
+
function render(state, prevState) {
|
|
144
174
|
const {
|
|
145
175
|
items,
|
|
146
176
|
beforeItemsHeight,
|
|
147
177
|
afterItemsHeight,
|
|
148
178
|
firstShownItemIndex,
|
|
149
179
|
lastShownItemIndex
|
|
150
|
-
} =
|
|
180
|
+
} = state
|
|
181
|
+
|
|
151
182
|
// Set `paddingTop` and `paddingBottom` on the container element:
|
|
152
183
|
// it emulates the non-visible items as if they were rendered.
|
|
153
184
|
container.style.paddingTop = Math.round(beforeItemsHeight) + 'px'
|
|
154
185
|
container.style.paddingBottom = Math.round(afterItemsHeight) + 'px'
|
|
186
|
+
|
|
155
187
|
// Perform an intelligent "diff" re-render as the user scrolls the page.
|
|
156
188
|
// This also requires that the list of `items` hasn't been changed.
|
|
157
189
|
// On initial render, `prevState` is `undefined`.
|
|
158
190
|
if (prevState && items === prevState.items) {
|
|
191
|
+
|
|
159
192
|
// Remove no longer visible items.
|
|
160
193
|
let i = prevState.lastShownItemIndex
|
|
161
194
|
while (i >= prevState.firstShownItemIndex) {
|
|
@@ -167,6 +200,7 @@ function onStateChange(newState, prevState) {
|
|
|
167
200
|
}
|
|
168
201
|
i--
|
|
169
202
|
}
|
|
203
|
+
|
|
170
204
|
// Add newly visible items.
|
|
171
205
|
let prependBefore = container.firstChild
|
|
172
206
|
let i = firstShownItemIndex
|
|
@@ -184,7 +218,8 @@ function onStateChange(newState, prevState) {
|
|
|
184
218
|
}
|
|
185
219
|
i++
|
|
186
220
|
}
|
|
187
|
-
}
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
188
223
|
// Re-render the list from scratch.
|
|
189
224
|
while (container.firstChild) {
|
|
190
225
|
container.removeChild(container.firstChild)
|
|
@@ -197,12 +232,10 @@ function onStateChange(newState, prevState) {
|
|
|
197
232
|
}
|
|
198
233
|
}
|
|
199
234
|
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
const virtualScroller = new VirtualScroller(() => element, items, options)
|
|
235
|
+
const virtualScroller = new VirtualScroller(() => element, items, { render })
|
|
203
236
|
|
|
204
237
|
// Start VirtualScroller listening for scroll events.
|
|
205
|
-
virtualScroller.
|
|
238
|
+
virtualScroller.start()
|
|
206
239
|
|
|
207
240
|
// Stop VirtualScroller listening for scroll events
|
|
208
241
|
// when the user navigates to another page:
|
|
@@ -217,37 +250,47 @@ virtualScroller.listen()
|
|
|
217
250
|
|
|
218
251
|
#####
|
|
219
252
|
|
|
220
|
-
* `
|
|
253
|
+
* `state: object` — The initial state for `VirtualScroller`. Can be used, for example, to quicky restore the list when it's re-rendered on "Back" navigation.
|
|
221
254
|
|
|
222
|
-
|
|
223
|
-
* `margin` — Renders items which are outside of the screen by the amount of this "margin". Is the screen height by default: seems to be the optimal value for "Page Up" / "Page Down" navigation and optimized mouse wheel scrolling.
|
|
224
|
-
-->
|
|
255
|
+
* `render(state: object, previousState: object?)` — When a developer doesn't pass custom `getState()`/`updateState()` parameters (more on that later), `VirtualScroller` uses the default ones. The default `updateState()` function relies on a developer-supplied `render()` function that must "render" the current `state` of the `VirtualScroller` on the screen. See DOM `VirtualScroller` implementation for an example of such a `render()` function.
|
|
225
256
|
|
|
226
|
-
* `
|
|
257
|
+
* `onStateChange(newState: object, previousState: object?)` — An "on change" listener for the `VirtualScroller` `state` that gets called whenever `state` gets updated, including when setting the initial `state`.
|
|
258
|
+
|
|
259
|
+
* Is not called when individual item heights (including "before resize" ones) or individual item states are updated: instead, individual item heights or states are updated in-place, as `state.itemHeights[i] = newItemHeight` or `state.itemStates[i] = newItemState`. That's because those `state` properties are the ones that don’t affect the presentation, so there's no need to re-render the list when those properties do change — updates to those properties are just an effect of a re-render rather than a cause for a new re-render.
|
|
260
|
+
|
|
261
|
+
* `onStateChange()` parameter could be used to keep a copy of `VirtualScroller` `state` so that it could be quickly restored in case the `VirtualScroller` component gets unmounted and then re-mounted back again — for example, when the user navigates away by clicking on a list item and then navigates "Back" to the list.
|
|
227
262
|
|
|
228
|
-
*
|
|
263
|
+
* (advanced) If state updates are done "asynchronously" via a custom (external) `updateState()` function, then `onStateChange()` gets called after such state updates get "rendered" (after `virtualScroller.onRender()` gets called).
|
|
229
264
|
|
|
230
|
-
* `
|
|
265
|
+
* `getScrollableContainer(): Element` — (advanced) If the list is being rendered in a "scrollable container" (for example, if one of the parent elements of the list is styled with `max-height` and `overflow: auto`), then passing the "scrollable container" DOM Element is required for correct operation. "Gotchas":
|
|
231
266
|
|
|
232
|
-
*
|
|
267
|
+
* If `getColumnsCount()` parameter depends on the "scrollable container" argument for getting the available area width, then the "scrollable container" element must already exist when creating a `VirtualScroller` class instance, because the initial `state` is calculated at construction time.
|
|
233
268
|
|
|
234
|
-
*
|
|
269
|
+
* When used with one of the DOM environment `VirtualScroller` implementations, the width and height of a "scrollable container" should only change when the browser window is resized, i.e. not manually via `scrollableContainerElement.width = 720`, because `VirtualScroller` only listens to browser window resize events, and any other changes in "scrollable container" width won't be detected.
|
|
235
270
|
|
|
236
|
-
* `
|
|
271
|
+
* `getColumnsCount(container: ScrollableContainer): number` — (advanced) Provides support for ["grid"](#grid-layout) layout. Should return the columns count. The `container` argument provides a `.getWidth()` method for getting the available area width.
|
|
272
|
+
|
|
273
|
+
#### "Advanced" (rarely used) options
|
|
237
274
|
|
|
238
275
|
* `bypass: boolean` — Pass `true` to turn off `VirtualScroller` behavior and just render the full list of items.
|
|
239
276
|
|
|
240
|
-
* `
|
|
277
|
+
* `initialScrollPosition: number` — If passed, the page will be scrolled to this `scrollY` position.
|
|
278
|
+
|
|
279
|
+
* `onScrollPositionChange(scrollY: number)` — Is called whenever a user scrolls the page.
|
|
241
280
|
|
|
242
|
-
* `
|
|
281
|
+
<!-- * `customState: object` — (advanced) A developer might want to store some "custom" (additional) state along with the `VirtualScroller` state, for whatever reason. To do that, pass the initial value of such "custom" state as the `customState` option when creating a `VirtualScroller` instance. -->
|
|
243
282
|
|
|
244
|
-
* `
|
|
283
|
+
* `getItemId(item)` — (advanced) When `items` are dynamically updated via `.setItems()`, `VirtualScroller` detects an "incremental" update by comparing "new" and "old" item ["references"](https://codeburst.io/explaining-value-vs-reference-in-javascript-647a975e12a0): this way, `VirtualScroller` can understand that the "new" `items` are (mostly) the same as the "old" `items` when some items get prepended or appended to the list, in which case it doesn't re-render the whole list from scratch, but rather just renders the "new" items that got prepended or appended. Sometimes though, some of the "old" items might get updated: for example, if `items` is a list of comments, then some of those comments might get edited in-between the refreshes. In that case, the edited comment object reference should change in order to indicate that the comment's content has changed and that the comment should be re-rendered (at least that's how it has to be done in React world). At the same time, changing the edited comment object reference would break `VirtualScroller`'s "incremental" update detection, and it would re-render the whole list of comments from scratch, which is not what it should be doing in such cases. So, in cases like this, `VirtualScroller` should have some way to understand that the updated item, even if its object reference has changed, is still the same as the old one, so that it doesn't break "incremental" update detection. For that, `getItemId(item)` parameter could be passed, which `VirtualScroller` would use to compare "old" and "new" items (instead of the default "reference equality" check), and that would fix the "re-rendering the whole list from scratch" issue. It can also be used when `items` are fetched from an external API, in which case all item object references change on every such fetch.
|
|
284
|
+
|
|
285
|
+
* `onItemInitialRender(item)` — (advanced) Is called for each `item` when it's about to be rendered for the first time. Is guaranteed to be called at least once for each item rendered, though, in "asynchronous" rendering systems like React, it could be called multiple times for a given item, because "an item is calculated to be rendered" doesn't necessarily mean that the actual rendering will take place before a later calculation supercedes the former one. This function can be used to somehow "initialize" items before they're rendered for the first time. For example, consider a list of items that must be somehow "preprocessed" (parsed, enhanced, etc) before being rendered, and such "preprocessing" puts some load on the CPU (and therefore takes some time). In such case, instead of "preprocessing" the whole list of items up front, a developer could "preprocess" the items as they're being rendered, thereby eliminating any associated lag or freezing that would be inevitable have all the items been "preprocessed" up front. If a user only wants to see a few of the items, "preprocessing" all the items up front would simply be a waste.
|
|
245
286
|
|
|
246
|
-
* `shouldUpdateLayoutOnScreenResize(event: Event): boolean` — By default, `VirtualScroller` always performs a re-layout on window `resize` event. The `resize` event is not only triggered when a user resizes the window itself: it's also [triggered](https://developer.mozilla.org/en-US/docs/Web/API/Window/fullScreen#Notes) when the user switches into (and out of) fullscreen mode. By default, `VirtualScroller` performs a re-layout on all window `resize` events, except for ones that don't result in actual window width or height change, and except for cases when, for example, a video somewhere in a list is maximized into fullscreen. There still can be other "custom" cases: for example, when an application uses a custom "slideshow" component (rendered outside of the list DOM element) that goes into fullscreen when a user clicks a picture or a video in the list. For such "custom" cases `shouldUpdateLayoutOnScreenResize(event)` option / property can be specified.
|
|
287
|
+
<!-- * `shouldUpdateLayoutOnScreenResize(event: Event): boolean` — By default, `VirtualScroller` always performs a re-layout on window `resize` event. The `resize` event is not only triggered when a user resizes the window itself: it's also [triggered](https://developer.mozilla.org/en-US/docs/Web/API/Window/fullScreen#Notes) when the user switches into (and out of) fullscreen mode. By default, `VirtualScroller` performs a re-layout on all window `resize` events, except for ones that don't result in actual window width or height change, and except for cases when, for example, a video somewhere in a list is maximized into fullscreen. There still can be other "custom" cases: for example, when an application uses a custom "slideshow" component (rendered outside of the list DOM element) that goes into fullscreen when a user clicks a picture or a video in the list. For such "custom" cases `shouldUpdateLayoutOnScreenResize(event)` option / property can be specified. -->
|
|
247
288
|
|
|
248
289
|
* `measureItemsBatchSize: number` — (advanced) (experimental) Imagine a situation when a user doesn't gradually scroll through a huge list but instead hits an End key to scroll right to the end of such huge list: this will result in the whole list rendering at once (because an item needs to know the height of all previous items in order to render at correct scroll position) which could be CPU-intensive in some cases (for example, when using React due to its slow performance when initially rendering components on a page). To prevent freezing the UI in the process, a `measureItemsBatchSize` could be configured, that would limit the maximum count of items that're being rendered in a single pass for measuring their height: if `measureItemsBatchSize` is configured, then such items will be rendered and measured in batches. By default it's set to `100`. This is an experimental feature and could be removed in future non-major versions of this library. For example, the future React 17 will come with [Fiber](https://www.youtube.com/watch?v=ZCuYPiUIONs) rendering engine that is said to resolve such freezing issues internally. In that case, introducing this option may be reconsidered.
|
|
249
290
|
|
|
250
|
-
* `
|
|
291
|
+
* `estimatedItemHeight: number` — Is used for the initial render of the list: determines how many list items are rendered initially to cover the screen height plus some extra vertical margin (called "prerender margin") for future scrolling. If not set then the list first renders just the first item, measures it, and then assumes it to be the `estimatedItemHeight` from which it calculates how many items to show on the second render pass to fill the screen height plus the "prerender margin". Therefore, this setting is only for the initial render minor optimization and is not required.
|
|
292
|
+
|
|
293
|
+
* `prerenderMargin` — The list component renders not only the items that're currently visible but also the items that lie within some extra vertical margin (called "prerender margin") on top and bottom for future scrolling: this way, there'll be significantly less layout recalculations as the user scrolls, because now it doesn't have to recalculate layout on each scroll event. By default, the "prerender margin" is equal to the screen height: this seems to be the optimal value for "Page Up" / "Page Down" navigation and optimized mouse wheel scrolling. This parameter is currently ignored because the default value seems to fit all possible use cases.
|
|
251
294
|
</details>
|
|
252
295
|
|
|
253
296
|
#####
|
|
@@ -257,40 +300,79 @@ virtualScroller.listen()
|
|
|
257
300
|
|
|
258
301
|
#####
|
|
259
302
|
|
|
260
|
-
* `
|
|
303
|
+
* `start()` — `VirtualScroller` starts listening for scroll events. Should be called after the list has been rendered initially.
|
|
304
|
+
|
|
305
|
+
* `stop()` — `VirtualScroller` stops listening for scroll events. Should be called when the list is about to be removed from the page. To re-start the `VirtualScroller`, call `.start()` method again.
|
|
306
|
+
|
|
307
|
+
* `getState(): object` — Returns `VirtualScroller` state.
|
|
308
|
+
|
|
309
|
+
* `setItems(newItems: any[], options: object?)` — Updates `VirtualScroller` `items`. For example, it can be used to prepend or append new items to the list. See [Dynamically Loaded Lists](#dynamically-loaded-lists) section for more details. Available options:
|
|
310
|
+
* `preserveScrollPositionOnPrependItems: boolean` — Set to `true` to enable "restore scroll position after prepending new items" feature (should be used when implementing a "Show previous items" button).
|
|
311
|
+
|
|
312
|
+
#### Custom (External) State Management
|
|
313
|
+
|
|
314
|
+
A developer might prefer to use custom (external) state management rather than the default one. That might be the case when a certain high-order `VirtualScroller` implementation comes with a specific state management paradigm, like in React. In such case, `VirtualScroller` provides the following instance methods:
|
|
315
|
+
|
|
316
|
+
* `onRender()` — When using custom (external) state management, the `.onRender()` function must be called every time right after the list has been "rendered" (including the initial render).
|
|
317
|
+
|
|
318
|
+
* `getInitialState(): object` — Returns the initial `VirtualScroller` state for the cases when a developer configures `VirtualScroller` for custom (external) state management.
|
|
319
|
+
|
|
320
|
+
* `useState({ getState, updateState })` — Enables custom (external) state management.
|
|
321
|
+
|
|
322
|
+
* `getState(): object` — Returns the externally managed `VirtualScroller` `state`.
|
|
323
|
+
|
|
324
|
+
* `updateState(stateUpdate: object)` — Updates the externally managed `VirtualScroller` `state`. Must call `.onRender()` right after the updated `state` gets "rendered". A higher-order `VirtualScroller` implementation could either "render" the list immediately in its `updateState()` function, like the DOM implementation does, or the `updateState()` function could "schedule" a "re-render", like the React implementation does, in which case such `updateState()` function would be called an ["asynchronous"](https://reactjs.org/docs/state-and-lifecycle.html#state-updates-may-be-asynchronous) one, meaning that state updates aren't "rendered" immediately and are instead queued and then "rendered" in a single compound state update for better performance.
|
|
325
|
+
|
|
326
|
+
For a usage example, see `./source/react/VirtualScroller.js`. The steps are:
|
|
261
327
|
|
|
262
|
-
*
|
|
328
|
+
* Create a `VirtualScroller` instance.
|
|
263
329
|
|
|
264
|
-
*
|
|
330
|
+
* Get the initial state value via `virtualScroller.getInitialState()`.
|
|
265
331
|
|
|
266
|
-
|
|
267
|
-
<!-- * `didUpdateState(prevState: object?)` — If custom `setState` is defined, then it must call `VirtualScroller`'s `.didUpdateState()` instance method right after updating the `state`. The `prevState` argument should be `undefined` when (and only when) setting initial `state`. -->
|
|
332
|
+
* Initialize the externally managed state with the initial state value.
|
|
268
333
|
|
|
269
|
-
* `
|
|
334
|
+
* Define `getState()` and `updateState()` functions for reading or updating the externally managed state.
|
|
270
335
|
|
|
271
|
-
*
|
|
336
|
+
* Call `virtualScroller.useState({ getState, updateState })`.
|
|
272
337
|
|
|
273
|
-
*
|
|
338
|
+
* "Render" the list and call `virtualScroller.start()`.
|
|
339
|
+
|
|
340
|
+
When using custom (external) state management, contrary to the default (internal) state management approach, the `render()` function parameter can't be passed to the `VirtualScroller` constructor. The reason is that `VirtualScroller` wouldn't know when exactly should it call such `render()` function because by design it can only be called right after the state has been updated, and `VirtualScroller` doesn't know when exactly does the state get updated, because state updates are done via an "external" `updateState()` function that could as well apply state updates "asynchronously" (after a short delay), like in React, rather than "synchronously" (immediately). That's why the `updateState()` function must re-render the list by itself, at any time it finds appropriate, and right after the list has been re-rendered, it must call `virtualScroller.onRender()`.
|
|
341
|
+
|
|
342
|
+
#### "Advanced" (rarely used) instance methods
|
|
343
|
+
|
|
344
|
+
* `onItemHeightChange(i: number)` — (advanced) Must be called whenever a list item's height changes (for example, when a user clicks an "Expand"/"Collapse" button of a list item): it re-measures the item's height and updates `VirtualScroller` layout. Every change in an item's height must come as a result of changing some kind of a state, be it the item's state in `VirtualScroller` via `.onItemStateChange()`, or some other state managed by the application. Implementation-wise, calling `onItemHeightChange()` manually could be replaced with detecting item height changes automatically via [Resize Observer](https://caniuse.com/#search=Resize%20Observer).
|
|
345
|
+
|
|
346
|
+
* `onItemStateChange(i: number, itemState: object?)` — (advanced) Updates a list item's state inside `VirtualScroller` state. Must be called whenever an item's "state" changes: this way, the item's state is preserved when the item is unmounted due to going off screen, and then restored when the item is on screen again. Calling `onItemStateChange()` doesn't trigger a re-layout of `VirtualScroller` because changing a list item's state doesn't necessarily mean a change of its height, so a re-layout might not be required. If an item's height did change as a result of changing its state, then `VirtualScroller` layout must be updated, and to do that, call `onItemHeightChange(i)` after calling `onItemStateChange()`. For example, consider a social network feed, each post optionally having an attachment. Suppose there's a post in the feed having a YouTube video attachment. The attachment is initially shown as a small thumbnail that expands into a full-sized embedded YouTube video player when a user clicks on it. If the expanded/collapsed state of such attachment isn't been managed in `VirtualScroller`, then, when the user expands the video, then scrolls down so that the post with the video is no longer visible and is unmounted as a result, then scrolls back up so that the post with the video is visible again, the video's expanded state would be lost, and it would be rendered as a small thumbnail as if the user didn't click on it. And don't forget about calling `onItemHeightChange(i)` in such cases: if `onItemHeightChange(i)` isn't called after expanding the thumbnail into a video player, then the scroll position would "jump" when such item goes off screen, because `VirtualScroller` would have based its calculations on the initially measured item height, not the "expanded" one.
|
|
347
|
+
|
|
348
|
+
* `getItemScrollPosition(i: number): number?` — (advanced) Returns an item's scroll position inside the scrollable container. Returns `undefined` if any of the items before this item haven't been rendered yet.
|
|
274
349
|
|
|
275
350
|
<!-- * `getItemCoordinates(i: number): object` — Returns coordinates of item with index `i` relative to the "scrollable container": `top` is the top offset of the item relative to the start of the "scrollable container", `bottom` is the top offset of the item's bottom edge relative to the start of the "scrollable container", `height` is the item's height. -->
|
|
276
351
|
|
|
277
|
-
* `updateLayout()` — (advanced) Triggers a re-layout of `VirtualScroller`. It's what's called every time on page scroll or window resize. You most likely won't ever need to call this method manually. Still,
|
|
352
|
+
* `updateLayout()` — (advanced) Triggers a re-layout of `VirtualScroller`. It's what's called every time on page scroll or window resize. You most likely won't ever need to call this method manually. Still, one could imagine a hypothetical case when a developer might want to call this method. For example, when the list's top position changes not as a result of scrolling the page or resizing the window, but rather because of some unrelated "dynamic" changes of the page's content. For example, if some DOM elements above the list are removed (like a closeable "info" notification element) or collapsed (like an "accordion" panel), then the list's top position changes, which means that now some of the previoulsy shown items might go off screen, revealing an unrendered blank area to the user. The area would be blank because the "shift" of the list's vertical position happened not as a result of the user scrolling the page or resizing the window, and, therefore, it won't be registered by the `VirtualScroller` component. To fix that, a developer might want to trigger a re-layout manually.
|
|
278
353
|
</details>
|
|
279
354
|
|
|
280
355
|
## DOM
|
|
281
356
|
|
|
282
357
|
`virtual-scroller/dom` component implements a `VirtualScroller` in a standard [Document Object Model](https://en.wikipedia.org/wiki/Document_Object_Model) environment (a web browser).
|
|
283
358
|
|
|
284
|
-
|
|
359
|
+
The DOM `VirtualScroller` component constructor accepts arguments:
|
|
360
|
+
|
|
361
|
+
* `container` — Container DOM `Element`.
|
|
362
|
+
* `items` — The list of items.
|
|
363
|
+
* `renderItem(item)` — A function that "renders" an `item` as a DOM `Element`.
|
|
364
|
+
* `options` — (optional) Core `VirtualScroller` options.
|
|
365
|
+
|
|
366
|
+
It `.start()`s automatically upon being created, so there's no need to call `.start()` after creating it.
|
|
285
367
|
|
|
286
368
|
```js
|
|
287
369
|
import VirtualScroller from 'virtual-scroller/dom'
|
|
288
370
|
|
|
289
371
|
const messages = [
|
|
290
372
|
{
|
|
291
|
-
username:
|
|
292
|
-
date:
|
|
293
|
-
text:
|
|
373
|
+
username: 'john.smith',
|
|
374
|
+
date: new Date(),
|
|
375
|
+
text: 'I woke up today'
|
|
294
376
|
},
|
|
295
377
|
...
|
|
296
378
|
]
|
|
@@ -298,20 +380,24 @@ const messages = [
|
|
|
298
380
|
function renderMessage(message) {
|
|
299
381
|
// Message element.
|
|
300
382
|
const root = document.createElement('article')
|
|
383
|
+
|
|
301
384
|
// Message author.
|
|
302
385
|
const author = document.createElement('a')
|
|
303
386
|
author.setAttribute('href', `/users/${message.username}`)
|
|
304
387
|
author.textContent = `@${message.username}`
|
|
305
388
|
root.appendChild(author)
|
|
389
|
+
|
|
306
390
|
// Message date.
|
|
307
391
|
const time = document.createElement('time')
|
|
308
392
|
time.setAttribute('datetime', message.date.toISOString())
|
|
309
393
|
time.textContent = message.date.toString()
|
|
310
394
|
root.appendChild(time)
|
|
395
|
+
|
|
311
396
|
// Message text.
|
|
312
397
|
const text = document.createElement('p')
|
|
313
398
|
text.textContent = message.text
|
|
314
399
|
root.appendChild(text)
|
|
400
|
+
|
|
315
401
|
// Return message element.
|
|
316
402
|
return root
|
|
317
403
|
}
|
|
@@ -322,23 +408,14 @@ const virtualScroller = new VirtualScroller(
|
|
|
322
408
|
renderMessage
|
|
323
409
|
)
|
|
324
410
|
|
|
325
|
-
//
|
|
326
|
-
//
|
|
411
|
+
// When the `VirtualScroller` component is no longer needed on the page:
|
|
412
|
+
// virtualScroller.stop()
|
|
327
413
|
```
|
|
328
414
|
<details>
|
|
329
|
-
<summary>Show the list of DOM <code>VirtualScroller</code>
|
|
415
|
+
<summary>Show the list of additional DOM <code>VirtualScroller</code> options.</summary>
|
|
330
416
|
|
|
331
417
|
#####
|
|
332
418
|
|
|
333
|
-
DOM `VirtualScroller` constructor takes arguments:
|
|
334
|
-
|
|
335
|
-
* `container` — Items list container DOM `Element`.
|
|
336
|
-
* `items` — The items list.
|
|
337
|
-
* `renderItem(item)` — Renders an `item` as a DOM `Element`.
|
|
338
|
-
* `options` — `VirtualScroller` options.
|
|
339
|
-
|
|
340
|
-
Additional `options`:
|
|
341
|
-
|
|
342
419
|
<!-- * `onMount()` — Is called before `VirtualScroller.onMount()` is called. -->
|
|
343
420
|
|
|
344
421
|
* `onItemUnmount(itemElement)` — Is called after a `VirtualScroller` item DOM `Element` is unmounted. Can be used to add DOM `Element` ["pooling"](https://github.com/ChrisAntaki/dom-pool#what-performance-gains-can-i-expect).
|
|
@@ -351,6 +428,10 @@ Additional `options`:
|
|
|
351
428
|
|
|
352
429
|
#####
|
|
353
430
|
|
|
431
|
+
* `start()` — A proxy for the corresponding `VirtualScroller` method.
|
|
432
|
+
|
|
433
|
+
* `stop()` — A proxy for the corresponding `VirtualScroller` method.
|
|
434
|
+
|
|
354
435
|
* `setItems(items, options)` — A proxy for the corresponding `VirtualScroller` method.
|
|
355
436
|
|
|
356
437
|
* `onItemHeightChange(i)` — A proxy for the corresponding `VirtualScroller` method.
|
|
@@ -358,15 +439,23 @@ Additional `options`:
|
|
|
358
439
|
* `onItemStateChange(i, itemState)` — A proxy for the corresponding `VirtualScroller` method.
|
|
359
440
|
|
|
360
441
|
<!-- * `getItemCoordinates(i)` — A proxy for the corresponding `VirtualScroller` method. -->
|
|
361
|
-
|
|
362
|
-
* `stop()` — A proxy for the corresponding `VirtualScroller` method.
|
|
363
442
|
</details>
|
|
364
443
|
|
|
365
444
|
## React
|
|
366
445
|
|
|
367
446
|
`virtual-scroller/react` component implements a `VirtualScroller` in a [React](https://reactjs.org/) environment.
|
|
368
447
|
|
|
369
|
-
|
|
448
|
+
The required properties are:
|
|
449
|
+
|
|
450
|
+
* `items` — The list of items.
|
|
451
|
+
|
|
452
|
+
* `itemComponent` — List item React component.
|
|
453
|
+
|
|
454
|
+
* The `itemComponent` will receive a `children` property which is gonna be the item object itself (an element of the `items` array).
|
|
455
|
+
|
|
456
|
+
* For best performance, make sure that `itemComponent` is a `React.memo()` component or a `React.PureComponent`. Otherwise, list items will keep re-rendering themselves as the user scrolls because the containing `<VirtualScroller/>` component gets re-rendered on scroll.
|
|
457
|
+
|
|
458
|
+
#####
|
|
370
459
|
|
|
371
460
|
```js
|
|
372
461
|
import React from 'react'
|
|
@@ -378,19 +467,10 @@ function Messages({ messages }) {
|
|
|
378
467
|
<VirtualScroller
|
|
379
468
|
items={messages}
|
|
380
469
|
itemComponent={Message}
|
|
470
|
+
/>
|
|
381
471
|
)
|
|
382
472
|
}
|
|
383
473
|
|
|
384
|
-
const message = PropTypes.shape({
|
|
385
|
-
username: PropTypes.string.isRequired,
|
|
386
|
-
date: PropTypes.instanceOf(Date).isRequired,
|
|
387
|
-
text: PropTypes.string.isRequired
|
|
388
|
-
})
|
|
389
|
-
|
|
390
|
-
Messages.propTypes = {
|
|
391
|
-
messages: PropTypes.arrayOf(message).isRequired
|
|
392
|
-
}
|
|
393
|
-
|
|
394
474
|
function Message({ children: message }) {
|
|
395
475
|
const {
|
|
396
476
|
username,
|
|
@@ -412,87 +492,151 @@ function Message({ children: message }) {
|
|
|
412
492
|
)
|
|
413
493
|
}
|
|
414
494
|
|
|
495
|
+
const message = PropTypes.shape({
|
|
496
|
+
username: PropTypes.string.isRequired,
|
|
497
|
+
date: PropTypes.instanceOf(Date).isRequired,
|
|
498
|
+
text: PropTypes.string.isRequired
|
|
499
|
+
})
|
|
500
|
+
|
|
501
|
+
Messages.propTypes = {
|
|
502
|
+
messages: PropTypes.arrayOf(message).isRequired
|
|
503
|
+
}
|
|
504
|
+
|
|
415
505
|
Message.propTypes = {
|
|
416
506
|
children: message.isRequired
|
|
417
507
|
}
|
|
418
508
|
```
|
|
419
509
|
|
|
420
510
|
<details>
|
|
421
|
-
<summary>
|
|
511
|
+
<summary>Managing <code>itemComponent</code> state.</summary>
|
|
422
512
|
|
|
423
513
|
#####
|
|
424
514
|
|
|
425
|
-
|
|
515
|
+
If the `itemComponent` has any internal state, it should be stored in the `VirtualScroller` `state`. The need for saving and restoring list item component state arises because item components get unmounted as they go off screen. If the item component's state is not persested somehow, it would be lost when the item goes off screen. If the user then decides to scroll back up, that item would get re-rendered "from scratch", potentually causing a "jump of content" if it was somehow "expanded" prior to being hidden.
|
|
426
516
|
|
|
427
|
-
|
|
517
|
+
For example, consider a social network feed where feed items (posts) can be expanded or collapsed via a "Show more"/"Show less" button. Suppose a user clicks a "Show more" button on a post resulting in that post expanding in height. Then the user scrolls down and since the post is no longer visible it gets unmounted. Since no state is preserved by default, when the user scrolls back up and the post gets mounted again, its previous state will be lost and it will render as a collapsed post instead of an expanded one, resulting in a perceived "jump" of page content by the difference in height of the post being expanded and collapsed.
|
|
428
518
|
|
|
429
|
-
|
|
519
|
+
To fix that, `itemComponent` receives the following state management properties:
|
|
430
520
|
|
|
431
|
-
* `
|
|
521
|
+
* `state` — Saved state of the item component. Use this property as the initial value for the item component state.
|
|
432
522
|
|
|
433
|
-
*
|
|
523
|
+
* In the example described above, `state` might look like `{ expanded: true }`.
|
|
434
524
|
|
|
435
|
-
*
|
|
525
|
+
* This is simply a proxy for `virtualScroller.getState().itemStates[i]`.
|
|
436
526
|
|
|
437
|
-
* `
|
|
527
|
+
* `onStateChange(newItemState)` — Use this function to save the item component state whenever it changes.
|
|
438
528
|
|
|
439
|
-
*
|
|
529
|
+
* In the example described above, `onStateChange()` would be called whenever a user clicks a "Show more"/"Show less" button.
|
|
440
530
|
|
|
441
|
-
*
|
|
531
|
+
* This is simply a proxy for `virtualScroller.onItemStateChange(i, itemState)`.
|
|
442
532
|
|
|
443
|
-
* `
|
|
533
|
+
* `onHeightChange()` — Call this function whenever the item element height changes.
|
|
444
534
|
|
|
445
|
-
|
|
535
|
+
* In the example described above, `onHeightChange()` would be called whenever a user clicks a "Show more"/"Show less" button, because that results in a change of the item element's height, so `VirtualScroller` should re-measure it in order for its internal calculations to stay correct.
|
|
446
536
|
|
|
447
|
-
*
|
|
537
|
+
* This is simply a proxy for `virtualScroller.onItemHeightChange(i)`.
|
|
448
538
|
|
|
449
|
-
|
|
539
|
+
```js
|
|
540
|
+
function ItemComponent({
|
|
541
|
+
state: savedState,
|
|
542
|
+
onStateChange,
|
|
543
|
+
onHeightChange,
|
|
544
|
+
children: item
|
|
545
|
+
}) {
|
|
546
|
+
const [state, setState] = useState(savedState)
|
|
547
|
+
|
|
548
|
+
useLayoutEffect(() => {
|
|
549
|
+
onStateChange(state)
|
|
550
|
+
onHeightChange()
|
|
551
|
+
}, [state])
|
|
450
552
|
|
|
451
|
-
|
|
553
|
+
return (
|
|
554
|
+
<section>
|
|
555
|
+
<h1>
|
|
556
|
+
{item.title}
|
|
557
|
+
</h1>
|
|
558
|
+
{state.expanded &&
|
|
559
|
+
<p>{item.text}</p>
|
|
560
|
+
}
|
|
561
|
+
<button onClick={() => {
|
|
562
|
+
setState({
|
|
563
|
+
...state,
|
|
564
|
+
expanded: !expanded
|
|
565
|
+
})
|
|
566
|
+
}}>
|
|
567
|
+
{state.expanded ? 'Show less' : 'Show more'}
|
|
568
|
+
</button>
|
|
569
|
+
</section>
|
|
570
|
+
)
|
|
571
|
+
}
|
|
572
|
+
```
|
|
573
|
+
</details>
|
|
452
574
|
|
|
453
|
-
|
|
575
|
+
#####
|
|
576
|
+
|
|
577
|
+
<details>
|
|
578
|
+
<summary>Show the list of React <code><VirtualScroller/></code> optional properties.</summary>
|
|
454
579
|
|
|
455
|
-
|
|
580
|
+
#####
|
|
456
581
|
|
|
457
|
-
|
|
582
|
+
Note: When passing any core `VirtualScroller` class options, only the initial values of those options will be applied, and any updates to those options will be ignored. That's because those options are only passed to the `VirtualScroller` base class constructor at initialization time. That means that none of those options should depend on any variable state or props. For example, if `getColumnsCount()` parameter was defined as `() => props.columnsCount`, then, if the `columnsCount` property changes, the underlying `VirtualScroller` instance won't see that change.
|
|
583
|
+
|
|
584
|
+
* `itemComponentProps: object` — The props passed to `itemComponent`.
|
|
585
|
+
|
|
586
|
+
* `getColumnsCount(): number` — The `getColumnsCount()` option of `VirtualScroller`.
|
|
587
|
+
|
|
588
|
+
* `as` — A component used as a container for the list items. Is `"div"` by default.
|
|
589
|
+
|
|
590
|
+
* `initialState: object` — The initial state for `VirtualScroller`: the `state` option of `VirtualScroller`. For example, can be used to quicky restore the list on "Back" navigation.
|
|
591
|
+
|
|
592
|
+
<!-- * `initialCustomState: object` — (advanced) The initial "custom" state for `VirtualScroller`: the `initialCustomState` option of `VirtualScroller`. It can be used to initialize the "custom" part of `VirtualScroller` state in cases when `VirtualScroller` state is used to store some "custom" list state. -->
|
|
593
|
+
|
|
594
|
+
* `onStateChange(newState: object, previousState: object?)` — The `onStateChange` option of `VirtualScroller`. Could be used to restore a `VirtualScroller` state on "Back" navigation:
|
|
458
595
|
|
|
459
596
|
```js
|
|
460
597
|
import {
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
} from './
|
|
598
|
+
readVirtualScrollerState,
|
|
599
|
+
saveVirtualScrollerState
|
|
600
|
+
} from './globalStorage'
|
|
464
601
|
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
602
|
+
function Example() {
|
|
603
|
+
const virtualScrollerState = useRef()
|
|
604
|
+
|
|
605
|
+
useEffect(() => {
|
|
606
|
+
return () => {
|
|
607
|
+
// Save `VirtualScroller` state before the page unmounts.
|
|
608
|
+
saveVirtualScrollerState(virtualScrollerState.current)
|
|
609
|
+
}
|
|
610
|
+
})
|
|
611
|
+
|
|
612
|
+
return (
|
|
613
|
+
<VirtualScroller
|
|
614
|
+
items={...}
|
|
615
|
+
itemComponent={...}
|
|
616
|
+
state={hasUserNavigatedBack ? readVirtualScrollerState() : undefined}
|
|
617
|
+
onStateChange={state => virtualScrollerState.current = state}
|
|
618
|
+
/>
|
|
619
|
+
)
|
|
478
620
|
}
|
|
479
621
|
```
|
|
480
|
-
</details>
|
|
481
622
|
|
|
482
|
-
|
|
623
|
+
* `preserveScrollPositionOnPrependItems: boolean` — The `preserveScrollPositionOnPrependItems` option of `VirtualScroller.setItems()` method.
|
|
483
624
|
|
|
484
|
-
|
|
485
|
-
<summary>Show the list of properties passed to <code>itemComponent</code>.</summary>
|
|
625
|
+
* `getItemId(item): any` — The `getItemId` option of `VirtualScroller` class. The React component also uses it as a source for a React `key` for rendering an `item`. If `getItemId()` is not supplied, then item `key`s are autogenerated from a random-generated prefix (that changes every time `items` are updated) and an `item` index. Can be used to prevent `<VirtualScroller/>` from re-rendering all visible items every time `items` property is updated.
|
|
486
626
|
|
|
487
|
-
|
|
627
|
+
* `bypass: boolean` — The `bypass` option of `VirtualScroller` class.
|
|
628
|
+
|
|
629
|
+
* `tbody: boolean` — The `tbody` option of `VirtualScroller` class.
|
|
488
630
|
|
|
489
|
-
* `
|
|
631
|
+
* `estimatedItemHeight: number` — The `estimatedItemHeight` option of `VirtualScroller` class.
|
|
490
632
|
|
|
491
|
-
* `
|
|
633
|
+
* `measureItemsBatchSize: number` — The `measureItemsBatchSize` option of `VirtualScroller`.
|
|
492
634
|
|
|
493
|
-
* `
|
|
635
|
+
<!-- * `onMount()` — Is called after `<VirtualScroller/>` component has been mounted and before `VirtualScroller.onMount()` is called. -->
|
|
494
636
|
|
|
495
|
-
* `
|
|
637
|
+
* `onItemInitialRender(item)` — The `onItemInitialRender` option of `VirtualScroller` class.
|
|
638
|
+
|
|
639
|
+
<!-- * `shouldUpdateLayoutOnScreenResize(event)` — The `shouldUpdateLayoutOnScreenResize` option of `VirtualScroller` class. -->
|
|
496
640
|
</details>
|
|
497
641
|
|
|
498
642
|
#####
|
|
@@ -502,43 +646,20 @@ class Example extends React.Component {
|
|
|
502
646
|
|
|
503
647
|
#####
|
|
504
648
|
|
|
505
|
-
|
|
649
|
+
<!--
|
|
650
|
+
* `renderItem(i)` — Calls `.forceUpdate()` on the `itemComponent` instance for the item with index `i`. Does nothing if the item isn't currently rendered. Is only supported for `itemComponent`s that are `React.Component`s. The `i` item index argument could be replaced with the item object itself, in which case `<VirtualScroller/>` will get `i` as `items.indexOf(item)`.
|
|
651
|
+
-->
|
|
506
652
|
|
|
507
653
|
<!-- * `getItemCoordinates(i)` — A proxy for the corresponding `VirtualScroller` method. -->
|
|
508
654
|
|
|
509
655
|
* `updateLayout()` — A proxy for the corresponding `VirtualScroller` method.
|
|
510
656
|
</details>
|
|
511
657
|
|
|
512
|
-
## Rendering Engine
|
|
513
|
-
|
|
514
|
-
`VirtualScroller` is written in such a way that it supports any type of a rendering engine, not just DOM. For example, it could support something like React Native or `<canvas/>`: for that, someone would have to write custom versions of [`Screen.js`](https://gitlab.com/catamphetamine/virtual-scroller/-/blob/master/source/DOM/Screen.js) and [`ScrollableContainer.js`](https://gitlab.com/catamphetamine/virtual-scroller/-/blob/master/source/DOM/ScrollableContainer.js), and then instruct `VirtualScroller` to use those instead of the default ones by passing custom `renderingEngine` configuration when constructing a `VirtualScroller` instance:
|
|
515
|
-
|
|
516
|
-
```js
|
|
517
|
-
import VirtualScroller from 'virtual-scroller'
|
|
518
|
-
|
|
519
|
-
import Screen from './Screen'
|
|
520
|
-
import ScrollableContainer from './ScrollableContainer'
|
|
521
|
-
|
|
522
|
-
new VirtualScroller(getContainerElement, {
|
|
523
|
-
scrollableContainer,
|
|
524
|
-
renderingEngine: {
|
|
525
|
-
name: 'Non-DOM Rendering Engine',
|
|
526
|
-
createScreen() {
|
|
527
|
-
return new Screen()
|
|
528
|
-
},
|
|
529
|
-
createScrollableContainer(scrollableContainer) {
|
|
530
|
-
return new ScrollableContainer(scrollableContainer)
|
|
531
|
-
}
|
|
532
|
-
},
|
|
533
|
-
...
|
|
534
|
-
})
|
|
535
|
-
```
|
|
536
|
-
|
|
537
|
-
`getContainerElement()` function would simply return a list "element", whatever that could mean. The concept of an "element" is "something, that can be rendered", so it could be anything, not just a DOM Element. Any operations with "elements" are done either in `Screen.js` or in `ScrollableContainer.js`: `Screen.js` defines the operations that could be applied to an "element", such as getting its height or getting its child elements' heights, and `ScrollableContainer.js` defines the operations that could be applied to a "scrollable container", such as getting its dimensions, listening for "resize" and "scroll" events, controlling scroll position, etc.
|
|
538
|
-
|
|
539
658
|
## Dynamically Loaded Lists
|
|
540
659
|
|
|
541
|
-
All previous examples described cases with
|
|
660
|
+
All previous examples described cases with static `items` list. When there's a need to update the `items` list dynamically, one can use `virtualScroller.setItems(newItems)` instance method. For example:
|
|
661
|
+
* When the user clicks "Show previous items" button, the `newItems` argument should be `previousItems.concat(currentlyShownItems)`.
|
|
662
|
+
* When the user clicks "Show next items" button, the `newItems` argument should be `currentlyShownItems.concat(nextItems)`.
|
|
542
663
|
|
|
543
664
|
<details>
|
|
544
665
|
<summary>Find out what are "incremental" and "non-incremental" items updates, and why "incremental" updates are better.</summary>
|
|
@@ -608,7 +729,7 @@ function getColumnsCount(container) {
|
|
|
608
729
|
|
|
609
730
|
### Margin collapse
|
|
610
731
|
|
|
611
|
-
If any vertical `margin` is set on the list items, then this may lead to page content "jumping" by the value of that margin while scrolling. The reason is that when the top of the list is visible on screen, no `padding-top` gets applied to the list element, and the CSS spec states that having `padding` on an element disables its ["margin collapse"](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Box_Model/Mastering_margin_collapsing), so, while there's no `padding-top` on the list element, its margins do "collapse" with outer margins, but when the first item is no longer visible (and no longer rendered), `padding-top` gets applied to the list element to compensate for the non-rendered items, and that `padding-top` prevents the list's margins from "collapsing" with outer margins. So that results in the page content "jumping" when the first item in the list becomes invisible or becomes visible again. To fix that, don't set any `margin-top` on the first item of the list, and don't set any `margin-bottom` on the last item of the list. An example of fixing `margin` for the first and the last items of the list:
|
|
732
|
+
If any vertical CSS `margin` is set on the list items, then this may lead to page content "jumping" by the value of that margin while scrolling. The reason is that when the top of the list is visible on screen, no `padding-top` gets applied to the list element, and the CSS spec states that having `padding` on an element disables its ["margin collapse"](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Box_Model/Mastering_margin_collapsing), so, while there's no `padding-top` on the list element, its margins do "collapse" with outer margins, but when the first item is no longer visible (and no longer rendered), `padding-top` gets applied to the list element to compensate for the non-rendered items, and that `padding-top` prevents the list's margins from "collapsing" with outer margins. So that results in the page content "jumping" when the first item in the list becomes invisible or becomes visible again. To fix that, don't set any `margin-top` on the first item of the list, and don't set any `margin-bottom` on the last item of the list. An example of fixing `margin` for the first and the last items of the list:
|
|
612
733
|
|
|
613
734
|
```css
|
|
614
735
|
/* This margin is supposed to "collapse" with the outer ones
|
|
@@ -628,7 +749,7 @@ If any vertical `margin` is set on the list items, then this may lead to page co
|
|
|
628
749
|
|
|
629
750
|
### Styling `:first-child` and `:last-child`
|
|
630
751
|
|
|
631
|
-
When styling the first and the last items of the list via `:first-child` and `:last-child
|
|
752
|
+
When styling the first and the last items of the list via `:first-child` and `:last-child`, one should also check that such styles don't change the item's height, which means that one should not add any `border` or `padding` styles to `:first-child` and `:last-child`, otherwise the list items will jump by that extra height during scrolling.
|
|
632
753
|
|
|
633
754
|
An example of a `:first-child`/`:last-child` style that will not work correctly with `VirtualScroller`:
|
|
634
755
|
|
|
@@ -641,9 +762,87 @@ An example of a `:first-child`/`:last-child` style that will not work correctly
|
|
|
641
762
|
}
|
|
642
763
|
```
|
|
643
764
|
|
|
765
|
+
### Resize
|
|
766
|
+
|
|
767
|
+
When the container width changes, all items' heights must be recalculated because:
|
|
768
|
+
|
|
769
|
+
* If item elements render multi-line text, the lines count might've changed because there's more or less width available now.
|
|
770
|
+
|
|
771
|
+
* Some CSS [`@media()`](https://developer.mozilla.org/en-US/docs/Web/CSS/Media_Queries) rules might have been added or removed, affecting item layout.
|
|
772
|
+
|
|
773
|
+
If the list currently shows items starting from the `N`-th one, then all `N - 1` previous items have to be remeasured. But they can't be remeasured until they're rendered again, so `VirtualScroller` temporarily uses their old heights until those items get re-measured after they become visible again as the user scrolls up.
|
|
774
|
+
|
|
775
|
+
When such upper items get rendered and re-measured, the scroll position is automatically corrected to avoid ["content jumping"](https://css-tricks.com/content-jumping-avoid/).
|
|
776
|
+
|
|
777
|
+
<details>
|
|
778
|
+
<summary>I found a single edge case when the automatic correction of scroll position doesn't seem to work.</summary>
|
|
779
|
+
|
|
780
|
+
#####
|
|
781
|
+
|
|
782
|
+
(was reproduced in Chrome web browser on a desktop)
|
|
783
|
+
|
|
784
|
+
When the user scrolls up past the "prerender margin", which equals to the screen height by default, the list content does "jump" because the web browser doesn't want to apply the scroll position correction while scrolling for some weird reason. Looks like a bug in the web browser.
|
|
785
|
+
|
|
786
|
+
```
|
|
787
|
+
[virtual-scroller] The user is scrolling: perform a re-layout when they stop scrolling
|
|
788
|
+
Current scroll position: 7989
|
|
789
|
+
[virtual-scroller] The user is scrolling: perform a re-layout when they stop scrolling
|
|
790
|
+
Current scroll position: 7972
|
|
791
|
+
[virtual-scroller] The user is scrolling: perform a re-layout when they stop scrolling
|
|
792
|
+
Current scroll position: 7957
|
|
793
|
+
[virtual-scroller] The user has scrolled far enough: perform a re-layout
|
|
794
|
+
[virtual-scroller] ~ Update Layout (on scroll) ~
|
|
795
|
+
...
|
|
796
|
+
[virtual-scroller] ~ Rendered ~
|
|
797
|
+
[virtual-scroller] State ...
|
|
798
|
+
[virtual-scroller] ~ Measure item heights ~
|
|
799
|
+
[virtual-scroller] Item index 27 height 232
|
|
800
|
+
[virtual-scroller] Item index 28 height 178
|
|
801
|
+
[virtual-scroller] ~ Clean up "before resize" item heights and correct scroll position ~
|
|
802
|
+
[virtual-scroller] For item indexes from 27 to 28 — drop "before resize" heights [340, 259]
|
|
803
|
+
[virtual-scroller] Correct scroll position by -189
|
|
804
|
+
Scroll to position: 7768
|
|
805
|
+
[virtual-scroller] Set state ...
|
|
806
|
+
[virtual-scroller] ~ Rendered ~
|
|
807
|
+
[virtual-scroller] State ...
|
|
808
|
+
Current scroll position: 7944
|
|
809
|
+
[virtual-scroller] The user is scrolling: perform a re-layout when they stop scrolling
|
|
810
|
+
Current scroll position: 7933
|
|
811
|
+
[virtual-scroller] The user is scrolling: perform a re-layout when they stop scrolling
|
|
812
|
+
Current scroll position: 7924
|
|
813
|
+
[virtual-scroller] The user is scrolling: perform a re-layout when they stop scrolling
|
|
814
|
+
...
|
|
815
|
+
```
|
|
816
|
+
|
|
817
|
+
```js
|
|
818
|
+
var listener = () => {
|
|
819
|
+
console.log('Current scroll position:', window.pageYOffset)
|
|
820
|
+
}
|
|
821
|
+
document.addEventListener('scroll', listener)
|
|
822
|
+
var unlisten = () => document.removeEventListener('scroll', listener)
|
|
823
|
+
|
|
824
|
+
// Also add `console.log('Scroll to position:', scrollY)` in
|
|
825
|
+
// `scrollToY()` method in `./source/DOM/ScrollableContainer.js`.
|
|
826
|
+
```
|
|
827
|
+
|
|
828
|
+
Also, pressing the "Home" key wouldn't scroll up past the "prerender margin", which is equal to the screen height by default. The reason is the same: applying scroll position correction while the "Home" key is pressed cancels the effect of pressing the "Home" key.
|
|
829
|
+
|
|
830
|
+
A hypothetical workaround for this edge case bug could be rewriting the scroll position automatic correction code to postpone scroll position correction until the user stops scrolling, and instead change `margin-bottom` of some "spacer" element at the top of the list (or maybe even before the list). When the user stops scrolling, the scroll position would get corrected by the value of `margin-bottom` of that "spacer" element, after which the `margin-bottom` value on that "spacer" element would be reset. But this type of a workaround would only work in a DOM environment because it requires the support of "negative" margin.
|
|
831
|
+
|
|
832
|
+
For now, I don't see it as a bug that would be worth fixing. The user could just refresh the page, or not scroll up at all because they've already seen that content.
|
|
833
|
+
</details>
|
|
834
|
+
|
|
835
|
+
#####
|
|
836
|
+
|
|
837
|
+
The "before resize" layout parameters snapshot is stored in `VirtualScroller` state in `beforeResize` object:
|
|
838
|
+
|
|
839
|
+
* `itemHeights: number[]`
|
|
840
|
+
* `verticalSpacing: number`
|
|
841
|
+
* `columnsCount: number`
|
|
842
|
+
|
|
644
843
|
### `<tbody/>`
|
|
645
844
|
|
|
646
|
-
Due to the [inherent limitations](https://gitlab.com/catamphetamine/virtual-scroller/-/issues/1) of the `<tbody/>` HTML tag, when used as a container for the list items, a workaround involving CSS variables
|
|
845
|
+
Due to the [inherent limitations](https://gitlab.com/catamphetamine/virtual-scroller/-/issues/1) of the `<tbody/>` HTML tag, when a `<tbody/>` is used as a container for the list items, the `VirtualScroller` code has to use a workaround involving CSS variables, and CSS variables aren't supported in Internet Explorer, so using a `<tbody/>` as a list items container won't work in Internet Explorer: in such case, `VirtualScroller` renders in "bypass" mode (render all items). In all browsers other than Internet Explorer it works as usual.
|
|
647
846
|
|
|
648
847
|
### Search, focus management.
|
|
649
848
|
|
|
@@ -667,7 +866,7 @@ For example, consider a page:
|
|
|
667
866
|
</div>
|
|
668
867
|
```
|
|
669
868
|
|
|
670
|
-
The sidebar is styled as `position: fixed`, but until the page styles have been applied it's gonna be a regular `<div/>` meaning that `<main/>` will be rendered below the sidebar causing it to be offscreen and so the list will only render the first item. Then, the page styles are loaded and applied and the sidebar is now `position: fixed` so `<main/>` is now rendered at the top of the page but `VirtualScroller`
|
|
869
|
+
The sidebar is styled as `position: fixed`, but until the page styles have been applied it's gonna be a regular `<div/>` meaning that `<main/>` will be rendered below the sidebar causing it to be offscreen and so the list will only render the first item. Then, the page styles are loaded and applied and the sidebar is now `position: fixed` so `<main/>` is now rendered at the top of the page but `VirtualScroller` has already been rendered and it won't re-render until the user scrolls or the window is resized.
|
|
671
870
|
|
|
672
871
|
This type of a bug doesn't occur in production, but it can appear in development mode when using Webpack. The workaround `VirtualScroller` implements for such cases is calling `.getBoundingClientRect()` on the list container DOM element periodically (every second) to check if the `top` coordinate has changed as a result of CSS being applied: if it has then it recalculates the shown item indexes and re-renders.
|
|
673
872
|
</details>
|
|
@@ -676,6 +875,34 @@ This type of a bug doesn't occur in production, but it can appear in development
|
|
|
676
875
|
|
|
677
876
|
Set `window.VirtualScrollerDebug` to `true` to output debug messages to `console`.
|
|
678
877
|
|
|
878
|
+
## Rendering Engine
|
|
879
|
+
|
|
880
|
+
(advanced)
|
|
881
|
+
|
|
882
|
+
`VirtualScroller` is written in such a way that it supports any type of a rendering engine, not just DOM. For example, it could support something like React Native or `<canvas/>`: for that, someone would have to write custom versions of [`Screen.js`](https://gitlab.com/catamphetamine/virtual-scroller/-/blob/master/source/DOM/Screen.js) and [`ScrollableContainer.js`](https://gitlab.com/catamphetamine/virtual-scroller/-/blob/master/source/DOM/ScrollableContainer.js), and then instruct `VirtualScroller` to use those instead of the default ones by passing custom `engine` object when constructing a `VirtualScroller` instance:
|
|
883
|
+
|
|
884
|
+
```js
|
|
885
|
+
import VirtualScroller from 'virtual-scroller'
|
|
886
|
+
|
|
887
|
+
import Container from './Container'
|
|
888
|
+
import ScrollableContainer from './ScrollableContainer'
|
|
889
|
+
|
|
890
|
+
new VirtualScroller(getItemsContainerElement, items, {
|
|
891
|
+
getScrollableContainer,
|
|
892
|
+
engine: {
|
|
893
|
+
createItemsContainer(getItemsContainerElement) {
|
|
894
|
+
return new Container(getItemsContainerElement)
|
|
895
|
+
},
|
|
896
|
+
createScrollableContainer(getScrollableContainer, getItemsContainerElement) {
|
|
897
|
+
return new ScrollableContainer(getScrollableContainer, getItemsContainerElement)
|
|
898
|
+
}
|
|
899
|
+
},
|
|
900
|
+
...
|
|
901
|
+
})
|
|
902
|
+
```
|
|
903
|
+
|
|
904
|
+
`getItemsContainerElement()` function would simply return a list "element", whatever that could mean. The concept of an "element" is "something, that can be rendered", so it could be anything, not just a DOM Element. Any operations with "elements" are done either in `Container.js` or in `ScrollableContainer.js`: `Container.js` defines the operations that could be applied to the list "container", or its items, such as getting its height or getting an items' height, and `ScrollableContainer.js` defines the operations that could be applied to a "scrollable container", such as getting its dimensions, listening for "resize" and "scroll" events, controlling scroll position, etc.
|
|
905
|
+
|
|
679
906
|
## CDN
|
|
680
907
|
|
|
681
908
|
One can use any npm CDN service, e.g. [unpkg.com](https://unpkg.com) or [jsdelivr.net](https://jsdelivr.net)
|
|
@@ -708,6 +935,62 @@ One can use any npm CDN service, e.g. [unpkg.com](https://unpkg.com) or [jsdeliv
|
|
|
708
935
|
* Currently React `<VirtualScroller/>` passes `onHeightChange()` property and provides `.renderItem(i)` instance method. Both these features could be replaced with doing it internally in `VirtualScroller`'s `.setItems(newItems)` method: it could detect the items that have changed (`prevItems[i] !== newItems[i]`) and recalculate heights for such items, while the changed `item` properties would also cause the relevant React elements to be rerendered.
|
|
709
936
|
-->
|
|
710
937
|
|
|
938
|
+
## TypeScript
|
|
939
|
+
|
|
940
|
+
This library comes with TypeScript "typings". If you happen to find any bugs in those, create an issue.
|
|
941
|
+
|
|
942
|
+
## Possible enhancements
|
|
943
|
+
|
|
944
|
+
### Alternative approach in DOM rendering
|
|
945
|
+
|
|
946
|
+
This library's `DOM` and `React` component implementations use `padding-top` and `padding-bottom` on the items container to emulate the items that're not currently visible. In DOM environment, this approach comes with a slight drawback: the web browser has to perform a "reflow" every time shown item indexes change as a result of the user scrolling the page.
|
|
947
|
+
|
|
948
|
+
Twitter seems to use a slightly different approach: they set `position: relative` and `min-height: <all-items-height>` on the items container, and then `position: absolute`, `width: 100%` and `transform: translateY(<item-top-offset>)` on every items. Since `transform`s are only applied at the "compositing" stage of a web browser's rendering cycle, there's no need to recalculate anything, and so scrolling the page comes without any possible performance penalties at all.
|
|
949
|
+
|
|
950
|
+
<details>
|
|
951
|
+
<summary>My thoughts on moving from <code>padding</code>s to <code>transform</code>s</summary>
|
|
952
|
+
|
|
953
|
+
######
|
|
954
|
+
|
|
955
|
+
I've fantasised a bit about moving to `transforms` in this library's `DOM` and `React` component implementations, and it seems to involve a bit more than it initially seems:
|
|
956
|
+
|
|
957
|
+
* Item heights aren't known before the items have been rendered, so it'll have to re-render twice rather than once as the user scrolls: first time to measure the newly-shown items' heights and second time to apply the calculated Y positions of those items.
|
|
958
|
+
|
|
959
|
+
* A bit more complexity is added when one recalls that this library supports multi-column layout: now not only `y` positions but also `x` positions of every item would have to be calculated, and not only vertical spacing but also horizontal spacing between the items in a row.
|
|
960
|
+
|
|
961
|
+
* The `state` would have to include a new property — `itemPositions` — that would include an `x` and `y` position for every item.
|
|
962
|
+
|
|
963
|
+
* Using `x`/`y` positions for every item would mean that the `x`/`y` position of every item would no longer be dynamically calculated by a web browser (in `auto` mode) and instead would have to be pre-calculated by the library meaning that everything would have to be constantly re-calculated and re-rendered as the user resizes the window, not just on window resize end like it currently does. For example, if the user starts shrinking window width, the items' heights would start increasing due to content overflow, which, without constant re-calculation and re-rendering, would result in items being rendered on top of each other. So the fix for that would be re-calculating and re-rendering stuff immediately on every window `resize` event as the user drags the handle rather than waiting for the user to let go of that handle and stop resizing the window, which would obviously come with some performance penalties but maybe a modern device can handle such things without breaking a sweat.
|
|
964
|
+
|
|
965
|
+
The points listed above aren't something difficult to implement, it's just that I don't want to do it unless there're any real observed performance issues related to the "reflows" during scrolling. "If it works, no need to change it".
|
|
966
|
+
</details>
|
|
967
|
+
|
|
968
|
+
## Tests
|
|
969
|
+
|
|
970
|
+
This component comes with about 80% code coverage (for the core `VirtualScroller`).
|
|
971
|
+
|
|
972
|
+
To run tests:
|
|
973
|
+
|
|
974
|
+
```
|
|
975
|
+
npm test
|
|
976
|
+
```
|
|
977
|
+
|
|
978
|
+
To generate a code coverage report:
|
|
979
|
+
|
|
980
|
+
```
|
|
981
|
+
npm run test-coverage
|
|
982
|
+
```
|
|
983
|
+
|
|
984
|
+
The code coverage report can be viewed by opening `./coverage/lcov-report/index.html`.
|
|
985
|
+
|
|
986
|
+
The `handlebars@4.5.3` [work](https://github.com/handlebars-lang/handlebars.js/issues/1646#issuecomment-578306544)[around](https://github.com/facebook/jest/issues/9396#issuecomment-573328488) in `devDependencies` is for the test coverage to not produce empty reports:
|
|
987
|
+
|
|
988
|
+
```
|
|
989
|
+
Handlebars: Access has been denied to resolve the property "statements" because it is not an "own property" of its parent.
|
|
990
|
+
You can add a runtime option to disable the check or this warning:
|
|
991
|
+
See https://handlebarsjs.com/api-reference/runtime-options.html#options-to-control-prototype-access for details
|
|
992
|
+
```
|
|
993
|
+
|
|
711
994
|
## GitHub
|
|
712
995
|
|
|
713
996
|
On March 9th, 2020, GitHub, Inc. silently [banned](https://medium.com/@catamphetamine/how-github-blocked-me-and-all-my-libraries-c32c61f061d3) my account (erasing all my repos, issues and comments) without any notice or explanation. Because of that, all source codes had to be promptly moved to GitLab. The [GitHub repo](https://github.com/catamphetamine/virtual-scroller) is now only used as a backup (you can star the repo there too), and the primary repo is now the [GitLab one](https://gitlab.com/catamphetamine/virtual-scroller). Issues can be reported in any repo.
|