virtual-scroller 1.14.0 → 1.15.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/CHANGELOG.md +23 -0
- package/README.md +309 -250
- package/bundle/index-dom-bypass.html +198 -0
- package/bundle/index-dom-grid.html +204 -0
- package/bundle/index-dom-scrollableContainer.html +215 -0
- package/bundle/index-dom-tbody-scrollableContainer.html +81 -0
- package/bundle/index-dom-tbody.html +65 -0
- package/bundle/index-dom.html +115 -84
- package/bundle/{index-bypass.html → index-react-bypass.html} +83 -78
- package/bundle/{index-grid.html → index-react-grid.html} +78 -91
- package/bundle/{index-scrollableContainer.html → index-react-scrollableContainer.html} +96 -91
- package/bundle/index-react-strictMode.html +199 -0
- package/bundle/index-react-tbody-scrollableContainer.html +96 -0
- package/bundle/index-react-tbody.html +80 -0
- package/bundle/{messages.js → items.js} +1 -1
- package/bundle/lib/babel.min.js +25 -0
- package/bundle/lib/prop-types.min.js +1 -0
- package/bundle/lib/react-dom.development.js +29924 -0
- package/bundle/lib/react-dom.production.min.js +267 -0
- package/bundle/lib/react.development.js +3343 -0
- package/bundle/lib/react.production.min.js +31 -0
- package/bundle/style.base.css +33 -0
- package/{website/style.css → bundle/style.list.css} +10 -43
- package/bundle/style.list.grid.css +23 -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 +1 -2
- package/commonjs/BeforeResize.js.map +1 -1
- package/commonjs/DOM/VirtualScroller.js +7 -13
- package/commonjs/DOM/VirtualScroller.js.map +1 -1
- package/commonjs/DOM/tbody.js +6 -6
- package/commonjs/DOM/tbody.js.map +1 -1
- package/commonjs/ItemHeights.js +10 -13
- package/commonjs/ItemHeights.js.map +1 -1
- package/commonjs/Layout.defaults.js +21 -0
- package/commonjs/Layout.defaults.js.map +1 -0
- package/commonjs/Layout.js +75 -64
- package/commonjs/Layout.js.map +1 -1
- package/commonjs/Layout.test.js +8 -4
- package/commonjs/Layout.test.js.map +1 -1
- package/commonjs/VirtualScroller.constructor.js +38 -4
- package/commonjs/VirtualScroller.constructor.js.map +1 -1
- package/commonjs/VirtualScroller.items.js +50 -4
- package/commonjs/VirtualScroller.items.js.map +1 -1
- package/commonjs/VirtualScroller.js +23 -14
- package/commonjs/VirtualScroller.js.map +1 -1
- package/commonjs/VirtualScroller.layout.js +40 -29
- package/commonjs/VirtualScroller.layout.js.map +1 -1
- package/commonjs/VirtualScroller.onContainerResize.js +1 -2
- package/commonjs/VirtualScroller.onContainerResize.js.map +1 -1
- package/commonjs/VirtualScroller.state.js +10 -9
- package/commonjs/VirtualScroller.state.js.map +1 -1
- package/commonjs/VirtualScroller.verticalSpacing.js +39 -6
- package/commonjs/VirtualScroller.verticalSpacing.js.map +1 -1
- package/commonjs/react/VirtualScroller.js +85 -34
- package/commonjs/react/VirtualScroller.js.map +1 -1
- package/commonjs/react/useClassName.js +2 -2
- package/commonjs/react/useClassName.js.map +1 -1
- package/commonjs/react/useForwardedRef.js +50 -0
- package/commonjs/react/useForwardedRef.js.map +1 -0
- package/commonjs/react/useInstanceMethods.js +4 -4
- package/commonjs/react/useInstanceMethods.js.map +1 -1
- package/commonjs/react/useItemKeys.js +28 -5
- package/commonjs/react/useItemKeys.js.map +1 -1
- package/commonjs/react/useOnItemHeightDidChange.js +28 -12
- package/commonjs/react/useOnItemHeightDidChange.js.map +1 -1
- package/commonjs/react/useSetItemState.js +31 -12
- package/commonjs/react/useSetItemState.js.map +1 -1
- package/commonjs/react/useState.js +9 -9
- package/commonjs/react/useState.js.map +1 -1
- package/commonjs/react/{useStateNoStaleBug.js → useStateWithRepeatableRead.js} +3 -3
- package/commonjs/react/useStateWithRepeatableRead.js.map +1 -0
- package/commonjs/react/useStyle.js +10 -4
- package/commonjs/react/useStyle.js.map +1 -1
- package/commonjs/react/useValidateTableBodyItemsContainer.js +24 -0
- package/commonjs/react/useValidateTableBodyItemsContainer.js.map +1 -0
- package/commonjs/react/useVirtualScroller.js +4 -3
- package/commonjs/react/useVirtualScroller.js.map +1 -1
- package/commonjs/test/ItemsContainer.js +10 -10
- package/commonjs/test/ItemsContainer.js.map +1 -1
- package/commonjs/test/VirtualScroller.js +25 -10
- package/commonjs/test/VirtualScroller.js.map +1 -1
- package/dom/index.d.ts +6 -5
- package/index.d.ts +19 -8
- package/modules/BeforeResize.js +1 -2
- package/modules/BeforeResize.js.map +1 -1
- package/modules/DOM/VirtualScroller.js +7 -13
- package/modules/DOM/VirtualScroller.js.map +1 -1
- package/modules/DOM/tbody.js +4 -4
- package/modules/DOM/tbody.js.map +1 -1
- package/modules/ItemHeights.js +11 -14
- package/modules/ItemHeights.js.map +1 -1
- package/modules/Layout.defaults.js +11 -0
- package/modules/Layout.defaults.js.map +1 -0
- package/modules/Layout.js +74 -64
- package/modules/Layout.js.map +1 -1
- package/modules/Layout.test.js +8 -4
- package/modules/Layout.test.js.map +1 -1
- package/modules/VirtualScroller.constructor.js +37 -4
- package/modules/VirtualScroller.constructor.js.map +1 -1
- package/modules/VirtualScroller.items.js +51 -5
- package/modules/VirtualScroller.items.js.map +1 -1
- package/modules/VirtualScroller.js +23 -14
- package/modules/VirtualScroller.js.map +1 -1
- package/modules/VirtualScroller.layout.js +40 -29
- package/modules/VirtualScroller.layout.js.map +1 -1
- package/modules/VirtualScroller.onContainerResize.js +1 -2
- package/modules/VirtualScroller.onContainerResize.js.map +1 -1
- package/modules/VirtualScroller.state.js +10 -9
- package/modules/VirtualScroller.state.js.map +1 -1
- package/modules/VirtualScroller.verticalSpacing.js +38 -6
- package/modules/VirtualScroller.verticalSpacing.js.map +1 -1
- package/modules/react/VirtualScroller.js +84 -35
- package/modules/react/VirtualScroller.js.map +1 -1
- package/modules/react/useClassName.js +3 -3
- package/modules/react/useClassName.js.map +1 -1
- package/modules/react/useForwardedRef.js +42 -0
- package/modules/react/useForwardedRef.js.map +1 -0
- package/modules/react/useInstanceMethods.js +4 -4
- package/modules/react/useInstanceMethods.js.map +1 -1
- package/modules/react/useItemKeys.js +28 -5
- package/modules/react/useItemKeys.js.map +1 -1
- package/modules/react/useOnItemHeightDidChange.js +28 -12
- package/modules/react/useOnItemHeightDidChange.js.map +1 -1
- package/modules/react/useSetItemState.js +31 -12
- package/modules/react/useSetItemState.js.map +1 -1
- package/modules/react/useState.js +9 -9
- package/modules/react/useState.js.map +1 -1
- package/modules/react/{useStateNoStaleBug.js → useStateWithRepeatableRead.js} +2 -2
- package/modules/react/useStateWithRepeatableRead.js.map +1 -0
- package/modules/react/useStyle.js +10 -4
- package/modules/react/useStyle.js.map +1 -1
- package/modules/react/useValidateTableBodyItemsContainer.js +16 -0
- package/modules/react/useValidateTableBodyItemsContainer.js.map +1 -0
- package/modules/react/useVirtualScroller.js +4 -3
- package/modules/react/useVirtualScroller.js.map +1 -1
- package/modules/test/ItemsContainer.js +10 -10
- package/modules/test/ItemsContainer.js.map +1 -1
- package/modules/test/VirtualScroller.js +25 -10
- package/modules/test/VirtualScroller.js.map +1 -1
- package/package.json +1 -1
- package/react/as.d.ts +42 -0
- package/react/index.d.ts +204 -63
- package/source/BeforeResize.js +1 -2
- package/source/DOM/VirtualScroller.js +7 -13
- package/source/DOM/tbody.js +5 -5
- package/source/ItemHeights.js +11 -12
- package/source/Layout.defaults.js +8 -0
- package/source/Layout.js +66 -53
- package/source/Layout.test.js +7 -2
- package/source/VirtualScroller.constructor.js +27 -4
- package/source/VirtualScroller.items.js +47 -2
- package/source/VirtualScroller.js +23 -14
- package/source/VirtualScroller.layout.js +41 -28
- package/source/VirtualScroller.onContainerResize.js +1 -2
- package/source/VirtualScroller.state.js +10 -11
- package/source/VirtualScroller.verticalSpacing.js +32 -6
- package/source/react/VirtualScroller.js +96 -33
- package/source/react/useClassName.js +3 -3
- package/source/react/useForwardedRef.js +39 -0
- package/source/react/useInstanceMethods.js +12 -4
- package/source/react/useItemKeys.js +22 -4
- package/source/react/useOnItemHeightDidChange.js +29 -10
- package/source/react/useSetItemState.js +32 -10
- package/source/react/useState.js +6 -6
- package/source/react/{useStateNoStaleBug.js → useStateWithRepeatableRead.js} +1 -1
- package/source/react/useStyle.js +3 -2
- package/source/react/useValidateTableBodyItemsContainer.js +16 -0
- package/source/react/useVirtualScroller.js +4 -3
- package/source/test/ItemsContainer.js +10 -10
- package/source/test/VirtualScroller.js +16 -10
- package/website/index-dom-bypass.html +198 -0
- package/website/index-dom-grid.html +204 -0
- package/website/index-dom-scrollableContainer.html +215 -0
- package/website/index-dom-tbody-scrollableContainer.html +81 -0
- package/website/index-dom-tbody.html +65 -0
- package/website/index-dom.html +117 -84
- package/website/index-react-bypass.html +200 -0
- package/website/{index-grid.html → index-react-grid.html} +79 -92
- package/website/index-react-scrollableContainer.html +213 -0
- package/website/index-react-strictMode.html +199 -0
- package/website/index-react-tbody-scrollableContainer.html +96 -0
- package/website/index-react-tbody.html +80 -0
- package/website/{index-bypass.html → index-react.html} +84 -70
- package/website/index.html +84 -69
- package/website/{messages.js → items.js} +1 -1
- package/website/lib/babel.min.js +25 -0
- package/website/lib/prop-types.min.js +1 -0
- package/website/lib/react-dom.development.js +29924 -0
- package/website/lib/react-dom.production.min.js +267 -0
- package/website/lib/react.development.js +3343 -0
- package/website/lib/react.production.min.js +31 -0
- package/website/style.base.css +33 -0
- package/website/style.list.css +92 -0
- package/website/style.list.grid.css +23 -0
- package/bundle/index-tbody-scrollableContainer.html +0 -70
- package/bundle/index-tbody.html +0 -57
- package/bundle/on-scroll-to-dom.js +0 -2
- package/bundle/on-scroll-to-dom.js.map +0 -1
- package/bundle/on-scroll-to-react.js +0 -2
- package/bundle/on-scroll-to-react.js.map +0 -1
- package/bundle/on-scroll-to.js +0 -2
- package/bundle/on-scroll-to.js.map +0 -1
- package/commonjs/react/useStateNoStaleBug.js.map +0 -1
- package/modules/react/useStateNoStaleBug.js.map +0 -1
- package/website/index-scrollableContainer.html +0 -208
- package/website/index-tbody-scrollableContainer.html +0 -70
- package/website/index-tbody.html +0 -57
- package/website/lib/on-scroll-to-dom.js +0 -2
- package/website/lib/on-scroll-to-dom.js.map +0 -1
- package/website/lib/on-scroll-to-react.js +0 -2
- package/website/lib/on-scroll-to-react.js.map +0 -1
package/README.md
CHANGED
|
@@ -6,24 +6,29 @@ A universal open-source implementation of Twitter's [`VirtualScroller`](https://
|
|
|
6
6
|
|
|
7
7
|
* For React users, it exports a [React](#react) component from `virtual-scroller/react`.
|
|
8
8
|
* For those who prefer "vanilla" DOM, it exports a [DOM](#dom) component from `virtual-scroller/dom`.
|
|
9
|
-
* For everyone else, it exports a
|
|
9
|
+
* For everyone else, it exports a ["core"](#core) component from `virtual-scroller`. The "core" component supports any type of UI "framework", or even any type of [rendering engine](#rendering-engine), not just DOM. Use it to create your own implementation for any UI "framework" or non-browser environment.
|
|
10
10
|
|
|
11
11
|
## Demo
|
|
12
12
|
|
|
13
13
|
[DOM](#dom) component
|
|
14
14
|
|
|
15
15
|
* [List](https://catamphetamine.gitlab.io/virtual-scroller/index-dom.html)
|
|
16
|
-
* [Paginated List](https://catamphetamine.gitlab.io/virtual-scroller/index-dom.html?
|
|
17
|
-
* [
|
|
18
|
-
* [Table
|
|
16
|
+
* [Paginated List](https://catamphetamine.gitlab.io/virtual-scroller/index-dom.html?pagination=✓)
|
|
17
|
+
* [List in a scrollable container](https://catamphetamine.gitlab.io/virtual-scroller/index-dom-scrollableContainer.html)
|
|
18
|
+
* [Table](https://catamphetamine.gitlab.io/virtual-scroller/index-dom-tbody.html)
|
|
19
|
+
* [Table in a scrollable container](https://catamphetamine.gitlab.io/virtual-scroller/index-dom-tbody-scrollableContainer.html)
|
|
20
|
+
* [Grid](https://catamphetamine.gitlab.io/virtual-scroller/index-dom-grid.html)
|
|
21
|
+
* [Paginated Grid](https://catamphetamine.gitlab.io/virtual-scroller/index-dom-grid.html?pagination=✓)
|
|
19
22
|
|
|
20
23
|
[React](#react) component
|
|
21
24
|
|
|
22
|
-
* [List](https://catamphetamine.gitlab.io/virtual-scroller/)
|
|
23
|
-
* [Paginated List](https://catamphetamine.gitlab.io/virtual-scroller
|
|
24
|
-
* [List in a scrollable container](https://catamphetamine.gitlab.io/virtual-scroller/index-scrollableContainer.html)
|
|
25
|
-
* [
|
|
26
|
-
* [
|
|
25
|
+
* [List](https://catamphetamine.gitlab.io/virtual-scroller/index-react.html)
|
|
26
|
+
* [Paginated List](https://catamphetamine.gitlab.io/virtual-scroller/index-react.html?pagination=✓)
|
|
27
|
+
* [List in a scrollable container](https://catamphetamine.gitlab.io/virtual-scroller/index-react-scrollableContainer.html)
|
|
28
|
+
* [Table](https://catamphetamine.gitlab.io/virtual-scroller/index-react-tbody.html)
|
|
29
|
+
* [Table in a scrollable container](https://catamphetamine.gitlab.io/virtual-scroller/index-react-tbody-scrollableContainer.html)
|
|
30
|
+
* [Grid](https://catamphetamine.gitlab.io/virtual-scroller/index-react-grid.html)
|
|
31
|
+
* [Paginated Grid](https://catamphetamine.gitlab.io/virtual-scroller/index-react-grid.html?pagination=✓)
|
|
27
32
|
|
|
28
33
|
## Rationale
|
|
29
34
|
|
|
@@ -59,9 +64,9 @@ Alternatively, one could include it on a web page [directly](#cdn) via a `<scrip
|
|
|
59
64
|
|
|
60
65
|
As it has been mentioned, this package exports three different components:
|
|
61
66
|
|
|
62
|
-
* For React framework — `virtual-scroller/react`
|
|
63
|
-
* For "vanilla" DOM — `virtual-scroller/dom`
|
|
64
|
-
* For any other case ("core") — `virtual-scroller`
|
|
67
|
+
* For React framework — [`virtual-scroller/react`](#react)
|
|
68
|
+
* For "vanilla" DOM — [`virtual-scroller/dom`](#dom)
|
|
69
|
+
* For any other case ("core") — [`virtual-scroller`](#core)
|
|
65
70
|
|
|
66
71
|
Below is a description of each component.
|
|
67
72
|
|
|
@@ -71,9 +76,9 @@ Below is a description of each component.
|
|
|
71
76
|
|
|
72
77
|
The React component is based on the ["core"](#core) component, and it requires the following properties:
|
|
73
78
|
|
|
74
|
-
* `items` —
|
|
79
|
+
* `items` — an array of items.
|
|
75
80
|
|
|
76
|
-
* `itemComponent` —
|
|
81
|
+
* `itemComponent` — a React component that renders an item.
|
|
77
82
|
|
|
78
83
|
* The `itemComponent` will receive properties:
|
|
79
84
|
* `item` — The item object (an element of the `items` array). Use it to render the item.
|
|
@@ -82,12 +87,20 @@ The React component is based on the ["core"](#core) component, and it requires t
|
|
|
82
87
|
<!-- * `state` — The item component's "state". -->
|
|
83
88
|
<!-- * Curious readers may see the description of `itemStates` property of the `state` object in the ["core"](#core) component section. -->
|
|
84
89
|
<!-- * `setState(newState)` — Sets the item component's "state". -->
|
|
85
|
-
<!-- * Curious readers may see the description of `setItemState(
|
|
90
|
+
<!-- * Curious readers may see the description of `setItemState(item, newState)` function in the ["core"](#core) component section. -->
|
|
86
91
|
* `onHeightDidChange()` — Call this function whenever the item's height changes, if it ever does. For example, if the item could be "expanded" and the user clicks that button. The reason for manually calling this function is because `<VirtualScroller/>` only bothers measuring the item's height when the items is initially rendered. After that, it just assumes that the item's height always stays the same and doesn't track it in any way. Hence, a developer is responsible for manually telling it to re-measure the item's height if it has changed for whatever reason.
|
|
87
92
|
* When calling this function, do it immediately after the item's height has changed on the screen, i.e. do it in `useLayoutEffect()` hook.
|
|
88
93
|
|
|
89
94
|
* As an optional performance optimization, it is advised to wrap the `itemComponent` with a [`React.memo()`](https://react.dev/reference/react/memo) function. It will prevent needless re-renders of the component when its props haven't changed (and they never do). The rationale is that all visible items get frequently re-rendered during scroll.
|
|
90
95
|
|
|
96
|
+
* `itemComponentProps: object` — (optional) any additional props for the `itemComponent`.
|
|
97
|
+
|
|
98
|
+
* `itemsContainerComponent` — a React component that will be used as a container for the items.
|
|
99
|
+
* Must be either a simple string like `"div"` or a React component that "forwards" `ref` to the resulting `Element`.
|
|
100
|
+
* Edge case: when list items are rendered as `<tr/>`s and the items container is a `<tbody/>`, the `itemsContainerComponent` must be `"tbody"`, otherwise it won't work correctly.
|
|
101
|
+
|
|
102
|
+
* `itemsContainerComponentProps: object` — (optional) any additional props for the `itemsContainerComponent`.
|
|
103
|
+
|
|
91
104
|
Code example:
|
|
92
105
|
|
|
93
106
|
#####
|
|
@@ -101,6 +114,7 @@ function List({ items }) {
|
|
|
101
114
|
<VirtualScroller
|
|
102
115
|
items={items}
|
|
103
116
|
itemComponent={ListItem}
|
|
117
|
+
itemsContainerComponent="div"
|
|
104
118
|
/>
|
|
105
119
|
)
|
|
106
120
|
}
|
|
@@ -152,110 +166,28 @@ function App() {
|
|
|
152
166
|
-->
|
|
153
167
|
|
|
154
168
|
<details>
|
|
155
|
-
<summary>
|
|
156
|
-
|
|
157
|
-
#####
|
|
158
|
-
|
|
159
|
-
If the `itemComponent` has any internal state, it should be stored in the "virtual scroller" `state` rather than in the usual React state. This is because an item component gets unmounted as soon as it goes off screen, and when it does, all its React state is lost. If the user then scrolls back, the item will be re-rendered "from scratch", without any previous state, which could cause a "jump of content" if the item was somehow "expanded" before it got unmounted.
|
|
160
|
-
|
|
161
|
-
For example, consider a social network feed where the feed items (posts) can be expanded or collapsed via a "Show more"/"Show less" button. Suppose a user clicks a "Show more" button in 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 in a default non-expanded state, resulting in a perceived "jump" of page content by the difference in height between the expanded and non-expanded post state.
|
|
162
|
-
|
|
163
|
-
To fix that, `itemComponent` receives the following state management properties:
|
|
164
|
-
|
|
165
|
-
* `state` — The state of the item component. It is persisted throughout the entire lifecycle of the list.
|
|
166
|
-
|
|
167
|
-
* In the example described above, `state` might look like `{ expanded: true }`.
|
|
168
|
-
|
|
169
|
-
* This is simply a proxy for the ["core"](#core) component's `.getState().itemStates[i]`.
|
|
170
|
-
|
|
171
|
-
* `setState(newState)` — Use this function to save the item component state whenever it changes.
|
|
172
|
-
|
|
173
|
-
* In the example described above, `setState({ expanded: true/false })` would be called whenever a user clicks a "Show more"/"Show less" button.
|
|
174
|
-
|
|
175
|
-
* This is simply a proxy for the ["core"](#core) component's `.setItemState(i, newState)`.
|
|
176
|
-
|
|
177
|
-
* `onHeightDidChange()` — Call this function immediately after (if ever) the item element height has changed.
|
|
178
|
-
|
|
179
|
-
* In the example described above, `onHeightDidChange()` would be called immediately after a user has clicked a "Show more"/"Show less" button and the component has re-rendered itself. Because that sequence of events has resulted in a change of the item element's height, `VirtualScroller` should re-measure the item's height in order for its internal calculations to stay in sync.
|
|
180
|
-
|
|
181
|
-
* This is simply a proxy for the ["core"](#core) component's `.onItemHeightDidChange(i)`.
|
|
182
|
-
|
|
183
|
-
Example of using `state`/`setState()`/`onHeightDidChange()`:
|
|
184
|
-
|
|
185
|
-
```js
|
|
186
|
-
function ItemComponent({
|
|
187
|
-
item,
|
|
188
|
-
state,
|
|
189
|
-
setState,
|
|
190
|
-
onHeightDidChange
|
|
191
|
-
}) {
|
|
192
|
-
const [internalState, setInternalState] = useState(state)
|
|
193
|
-
|
|
194
|
-
const hasMounted = useRef()
|
|
195
|
-
|
|
196
|
-
useLayoutEffect(() => {
|
|
197
|
-
if (hasMounted.current) {
|
|
198
|
-
setState(internalState)
|
|
199
|
-
onHeightDidChange()
|
|
200
|
-
} else {
|
|
201
|
-
// Skip the initial mount.
|
|
202
|
-
// Only handle the changes of the `internalState`.
|
|
203
|
-
hasMounted.current = true
|
|
204
|
-
}
|
|
205
|
-
}, [internalState])
|
|
206
|
-
|
|
207
|
-
return (
|
|
208
|
-
<section>
|
|
209
|
-
<h1>
|
|
210
|
-
{item.title}
|
|
211
|
-
</h1>
|
|
212
|
-
{internalState && internalState.expanded &&
|
|
213
|
-
<p>{item.text}</p>
|
|
214
|
-
}
|
|
215
|
-
<button onClick={() => {
|
|
216
|
-
setInternalState({
|
|
217
|
-
...internalState,
|
|
218
|
-
expanded: !expanded
|
|
219
|
-
})
|
|
220
|
-
}}>
|
|
221
|
-
{internalState && internalState.expanded ? 'Show less' : 'Show more'}
|
|
222
|
-
</button>
|
|
223
|
-
</section>
|
|
224
|
-
)
|
|
225
|
-
}
|
|
226
|
-
```
|
|
227
|
-
</details>
|
|
228
|
-
|
|
229
|
-
#####
|
|
230
|
-
|
|
231
|
-
<details>
|
|
232
|
-
<summary><code><VirtualScroller/></code> optional properties</summary>
|
|
169
|
+
<summary>Available options (properties)</summary>
|
|
233
170
|
|
|
234
171
|
#####
|
|
235
172
|
|
|
236
173
|
<!-- 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. -->
|
|
237
174
|
|
|
238
|
-
* `as` — a React component that will be used as a container for the list items. Is `"div"` by default.
|
|
239
|
-
* Edge case: when list items are rendered as `<tr/>`s and the items container is a `<tbody/>`, `as` property value must be `"tbody"`, otherwise it won't work correctly.
|
|
240
|
-
|
|
241
|
-
* `itemComponentProps: object` — These props will be passed through to the `itemComponent`.
|
|
242
|
-
|
|
243
175
|
* `getColumnsCount(): number` — Returns the count of the columns.
|
|
244
|
-
* This is simply a proxy for the ["core"](#core) component's `getColumnsCount` option.
|
|
176
|
+
* This is simply a proxy for the ["core"](#core) component's `getColumnsCount` [option](#options).
|
|
245
177
|
* Only the initial value of this property is used, and any changes to it will be ignored.
|
|
246
178
|
|
|
247
|
-
* `getInitialItemState
|
|
248
|
-
* This is simply a proxy for the ["core"](#core) component's `getInitialItemState` option.
|
|
179
|
+
* `getInitialItemState(item): any?` — If you're using `state`/`setState()` properties, this function could be used to define the initial `state` for every item in the list. By default, the initial state of an item is `undefined`.
|
|
180
|
+
* This is simply a proxy for the ["core"](#core) component's `getInitialItemState` [option](#options).
|
|
249
181
|
* Only the initial value of this property is used, and any changes to it will be ignored.
|
|
250
182
|
|
|
251
183
|
* `initialState: object` — The initial state of the entire list, including the initial state of each item. For example, one could snapshot this state right before the list is unmounted and then pass it back in the form of the `initialState` property when the list is re-mounted, effectively preserving the list's state. This could be used, for example, to instantly restore the list and its scroll position when the user navigates "Back" to the list's page in a web browser. P.S. In that specific case of using `initialState` property for "Back" restoration, a developer might need to pass `readyToStart: false` property until the "Back" page's scroll position has been restored.
|
|
252
|
-
* This is simply a proxy for the ["core"](#core) component's `state` option.
|
|
184
|
+
* This is simply a proxy for the ["core"](#core) component's `state` [option](#options).
|
|
253
185
|
* Only the initial value of this property is used, and any changes to it will be ignored.
|
|
254
186
|
|
|
255
187
|
<!-- * `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. -->
|
|
256
188
|
|
|
257
189
|
* `onStateChange(newState: object, previousState: object?)` — When this function is passed, it will be called every time the list's state is changed. Use it together with `initialState` property to preserve the list's state while it is unmounted.
|
|
258
|
-
* This is simply a proxy for the ["core"](#core) component's `onStateChange` option.
|
|
190
|
+
* This is simply a proxy for the ["core"](#core) component's `onStateChange` [option](#options).
|
|
259
191
|
* Only the initial value of this property is used, and any changes to it will be ignored.
|
|
260
192
|
|
|
261
193
|
Example of using `initialState`/`onStateChange()`:
|
|
@@ -287,33 +219,31 @@ function ListWithPreservedState() {
|
|
|
287
219
|
}
|
|
288
220
|
```
|
|
289
221
|
|
|
290
|
-
* `getItemId(item):
|
|
291
|
-
* This is simply a proxy for the ["core"](#core) component's `getItemId` option.
|
|
222
|
+
* `getItemId(item): number | string`
|
|
223
|
+
* This is simply a proxy for the ["core"](#core) component's `getItemId` [option](#options).
|
|
292
224
|
* Only the initial value of this property is used, and any changes to it will be ignored.
|
|
293
225
|
* `<VirtualScroller/>` also uses it to create a React `key` for every item's element. When `getItemId()` property is not passed, an item element's `key` will consist of the item's index in the `items` array plus a random-generated prefix that changes every time when `items` property value changes. This means that when the application frequently changes the `items` property, a developer could optimize it a little bit by supplying a custom `getItemId()` function whose result doesn't change when new `items` are supplied, preventing `<VirtualScroller/>` from needlessly re-rendering all visible items every time the `items` property is updated.
|
|
294
226
|
|
|
295
227
|
* `preserveScrollPositionOnPrependItems: boolean` — By default, when prepending new items to the list, the existing items will be pushed downwards on screen. For a user, it would look as if the scroll position has suddenly "jumped", even though technically the scroll position has stayed the same — it's just that the content itself has "jumped". But the user's perception is still that the scroll position has "jumped", as if the application was "buggy". In order to fix such inconvenience, one could pass `true` value here to automatically adjust the scroll position every time when prepending new items to the list. To the end user it would look as if the scroll position is correctly "preserved" when prepending new items to the list, i.e. the application works correctly.
|
|
296
|
-
* This is simply a proxy for the ["core"](#core) component's `.setItems()` method's `preserveScrollPositionOnPrependItems` option.
|
|
228
|
+
* This is simply a proxy for the ["core"](#core) component's `.setItems()` method's `preserveScrollPositionOnPrependItems` [option](#options).
|
|
297
229
|
* Only the initial value of this property is used, and any changes to it will be ignored.
|
|
298
230
|
|
|
299
231
|
* `readyToStart: boolean` — One could initially pass `false` here in order to just initially render the `<VirtualScroller/>` with the provided `initialState` and then hold off calling the `.start()` method of the ["core"](#core) component, effectively "freezing" the `<VirtualScroller/>` until the `false` value is changed to `true`. While in "frozen" state, the `<VirtualScroller/>` will not attempt to re-render itself according to the current scroll position, postponing any such re-renders until `readyToStart` property `false` value is changed to `true`.
|
|
300
232
|
* An example when this could be required is when a user navigates "Back" to the list's page in a web browser. In that case, the application may use the `initialState` property in an attempt to instantly restore the state of the entire list from a previously-saved snapshot, so that it immediately shows the same items that it was showing before the user navigated away from the list's page. But even if the application passes the previously-snapshotted `initialState`, by default the list will still re-render itself according to the current scroll position. And there wouldn't be any issue with that if the page's scroll position has already been restored to what it was before the user navigated away from the list's page. But if by the time the list is mounted, the page's scroll position hasn't been restored yet, the list will re-render itself with an "incorrect" scroll position, and it will "jump" to completely different items, very unexpectedly to the user, as if the application was "buggy". How could scroll position restoration possibly lag behind? In React it's actually very simple: `<VirtualScroller/>` re-renders itself in a `useLayoutEffect()` hook, which, by React's design, runs before any `useLayoutEffect()` hook in any of the parent components, including the top-level "router" component that handles scroll position restoration on page mount. So it becomes a ["chicken-and-egg"](https://en.wikipedia.org/wiki/Chicken_or_the_egg) problem. And `readyToStart: false` property is the only viable workaround for this dilemma: as soon as the top-level "router" component has finished restoring the scroll position, it could somehow signal that to the rest of the application, and then the application would pass `readyToStart: true` property to the `<VirtualScroller/>` component, unblocking it from re-rendering itself.
|
|
301
233
|
|
|
302
234
|
* `getScrollableContainer(): Element`
|
|
303
|
-
* This is simply a proxy for the ["core"](#core) component's `getScrollableContainer` option.
|
|
235
|
+
* This is simply a proxy for the ["core"](#core) component's `getScrollableContainer` [option](#options).
|
|
304
236
|
* Only the initial value of this property is used, and any changes to it will be ignored.
|
|
305
|
-
* This function will
|
|
237
|
+
* This function will be initially called right after `<VirtualScroller/>` component is mounted. However, even though all ancestor DOM Elements already exist in the DOM tree by that time, the corresponding ancestor React Elements haven't "mounted" yet, so their `ref`s are still `null`. This means that `getScrollableContainer()` shouldn't use any `ref`s and should instead get the DOM Element of the scrollable container directly from the `document`.
|
|
306
238
|
|
|
307
|
-
Example of `getScrollableContainer()
|
|
239
|
+
Example of an incorrect `getScrollableContainer()` that won't work:
|
|
308
240
|
|
|
309
241
|
```js
|
|
310
242
|
function ListContainer() {
|
|
311
243
|
const scrollableContainer = useRef()
|
|
312
244
|
|
|
313
|
-
// This function returns `undefined` until the `<ListContainer/>` is mounted, but it's fine,
|
|
314
|
-
// because the `<VirtualScroller/>` will only call it when it itself has mounted, and by that time
|
|
315
|
-
// `scrollableContainer.current` will already have been set to a non-`undefined` `HTMLDivElement`.
|
|
316
245
|
const getScrollableContainer = useCallback(() => {
|
|
246
|
+
// This won't work: it will return `null` because `<ListContainer/>` hasn't "mounted" yet.
|
|
317
247
|
return scrollableContainer.current
|
|
318
248
|
}, [])
|
|
319
249
|
|
|
@@ -328,6 +258,25 @@ function ListContainer() {
|
|
|
328
258
|
}
|
|
329
259
|
```
|
|
330
260
|
|
|
261
|
+
Example of a correct `getScrollableContainer()` that would work:
|
|
262
|
+
|
|
263
|
+
```js
|
|
264
|
+
function ListContainer() {
|
|
265
|
+
const getScrollableContainer = useCallback(() => {
|
|
266
|
+
return document.getElementById("scrollable-container")
|
|
267
|
+
}, [])
|
|
268
|
+
|
|
269
|
+
return (
|
|
270
|
+
<div id="scrollable-container" style={{ height: "400px", overflow: "scroll" }}>
|
|
271
|
+
<VirtualScroller
|
|
272
|
+
{...}
|
|
273
|
+
getScrollableContainer={getScrollableContainer}
|
|
274
|
+
/>
|
|
275
|
+
</div>
|
|
276
|
+
)
|
|
277
|
+
}
|
|
278
|
+
```
|
|
279
|
+
|
|
331
280
|
<!--
|
|
332
281
|
(this property has been ignored for a long time and was eventually removed)
|
|
333
282
|
|
|
@@ -336,26 +285,34 @@ function ListContainer() {
|
|
|
336
285
|
* Only the initial value of this property is used, and any changes to it will be ignored.
|
|
337
286
|
-->
|
|
338
287
|
|
|
288
|
+
* `itemsContainerComponentRef: object` — Could be used to get access to the `itemsContainerComponent` instance.
|
|
289
|
+
* For example, if `itemsContainerComponent` is `"ul"` then `itemsContainerComponentRef.current` will be set to the `<ul/>` `Element`.
|
|
290
|
+
|
|
339
291
|
* `onItemInitialRender(item)` — When passed, this function will be called for each item when it's rendered for the first time. It could be used to somehow "initialize" an item, if required.
|
|
340
|
-
* This is simply a proxy for the ["core"](#core) component's `onItemInitialRender` option.
|
|
292
|
+
* This is simply a proxy for the ["core"](#core) component's `onItemInitialRender` [option](#options).
|
|
341
293
|
* Only the initial value of this property is used, and any changes to it will be ignored.
|
|
342
294
|
|
|
343
295
|
* `bypass: boolean` — Disables the "virtual" aspect of the list, effectively making it a regular "dumb" list that just renders all items.
|
|
344
|
-
* This is simply a proxy for the ["core"](#core) component's `bypass` option.
|
|
296
|
+
* This is simply a proxy for the ["core"](#core) component's `bypass` [option](#options).
|
|
345
297
|
* Only the initial value of this property is used, and any changes to it will be ignored.
|
|
346
298
|
|
|
347
|
-
* `
|
|
348
|
-
* This is simply a proxy for the ["core"](#core) component's `
|
|
299
|
+
* `getEstimatedVisibleItemRowsCount(): number` — Should be specified if server-side rendering is used. Can be omitted if server-side rendering is not used.
|
|
300
|
+
* This is simply a proxy for the ["core"](#core) component's `getEstimatedVisibleItemRowsCount` [option](#options).
|
|
349
301
|
* Only the initial value of this property is used, and any changes to it will be ignored.
|
|
350
302
|
|
|
351
|
-
* `
|
|
352
|
-
* This is simply a proxy for the ["core"](#core) component's `
|
|
303
|
+
* `getEstimatedItemHeight(): number` — Should be specified if server-side rendering is used. Can be omitted if server-side rendering is not used.
|
|
304
|
+
* This is simply a proxy for the ["core"](#core) component's `getEstimatedItemHeight` [option](#options).
|
|
353
305
|
* Only the initial value of this property is used, and any changes to it will be ignored.
|
|
354
306
|
|
|
355
|
-
* `
|
|
356
|
-
* This is simply a proxy for the ["core"](#core) component's `
|
|
307
|
+
* `getEstimatedInterItemVerticalSpacing(): number` — Should be specified if server-side rendering is used. Can be omitted if server-side rendering is not used.
|
|
308
|
+
* This is simply a proxy for the ["core"](#core) component's `getEstimatedInterItemVerticalSpacing` [option](#options).
|
|
357
309
|
* Only the initial value of this property is used, and any changes to it will be ignored.
|
|
358
310
|
|
|
311
|
+
* Any other ["core"](#core) component [options](#options) could be passed here.
|
|
312
|
+
* Such as:
|
|
313
|
+
* `measureItemsBatchSize`
|
|
314
|
+
* Only the initial values of those options will be used, any any changes to those will be ignored.
|
|
315
|
+
|
|
359
316
|
<!-- * `onMount()` — Is called after `<VirtualScroller/>` component has been mounted and before `VirtualScroller.onMount()` is called. -->
|
|
360
317
|
|
|
361
318
|
<!-- * `shouldUpdateLayoutOnScreenResize(event)` — The `shouldUpdateLayoutOnScreenResize` option of `VirtualScroller` class. -->
|
|
@@ -412,7 +369,7 @@ function getScrollableContainer() {
|
|
|
412
369
|
#####
|
|
413
370
|
|
|
414
371
|
<details>
|
|
415
|
-
<summary
|
|
372
|
+
<summary>Instance methods</summary>
|
|
416
373
|
|
|
417
374
|
#####
|
|
418
375
|
|
|
@@ -422,11 +379,102 @@ function getScrollableContainer() {
|
|
|
422
379
|
|
|
423
380
|
<!-- * `getItemCoordinates(i)` — A proxy for the corresponding `VirtualScroller` method. -->
|
|
424
381
|
|
|
382
|
+
<!-- * `getElement()` — Returns the items container `Element`. -->
|
|
383
|
+
|
|
425
384
|
* `updateLayout()` — Forces a re-calculation and re-render of the list.
|
|
426
385
|
* This is simply a proxy for the ["core"](#core) component's `.updateLayout()` method.
|
|
427
386
|
|
|
428
387
|
</details>
|
|
429
388
|
|
|
389
|
+
#####
|
|
390
|
+
|
|
391
|
+
<details>
|
|
392
|
+
<summary>More on <code>state</code>, <code>setState</code> and <code>onHeightChange()</code></summary>
|
|
393
|
+
|
|
394
|
+
#####
|
|
395
|
+
|
|
396
|
+
If the `itemComponent` has any internal state, it should be stored in the "virtual scroller" `state` rather than in the usual React state. This is because an item component gets unmounted as soon as it goes off screen, and when it does, all its React state is lost. If the user then scrolls back, the item will be re-rendered "from scratch", without any previous state, which could cause a "jump of content" if the item was somehow "expanded" before it got unmounted.
|
|
397
|
+
|
|
398
|
+
For example, consider a social network feed where the feed items (posts) can be expanded or collapsed via a "Show more"/"Show less" button. Suppose a user clicks a "Show more" button in 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 in a default non-expanded state, resulting in a perceived "jump" of page content by the difference in height between the expanded and non-expanded post state.
|
|
399
|
+
|
|
400
|
+
To fix that, `itemComponent` receives the following state management properties:
|
|
401
|
+
|
|
402
|
+
* `state` — The state of the item component. It is persisted throughout the entire lifecycle of the list.
|
|
403
|
+
|
|
404
|
+
* In the example described above, `state` might look like `{ expanded: true }`.
|
|
405
|
+
|
|
406
|
+
* This is simply a proxy for the ["core"](#core) component's `.getState().itemStates[i]`.
|
|
407
|
+
|
|
408
|
+
* `setState(newState)` — Use this function to save the item component state whenever it changes.
|
|
409
|
+
|
|
410
|
+
* In the example described above, `setState({ expanded: true/false })` would be called whenever a user clicks a "Show more"/"Show less" button.
|
|
411
|
+
|
|
412
|
+
* This is simply a proxy for the ["core"](#core) component's `.setItemState(item, newState)`.
|
|
413
|
+
|
|
414
|
+
* `onHeightDidChange()` — Call this function immediately after (if ever) the item element height has changed.
|
|
415
|
+
|
|
416
|
+
* In the example described above, `onHeightDidChange()` would be called immediately after a user has clicked a "Show more"/"Show less" button and the component has re-rendered itself. Because that sequence of events has resulted in a change of the item element's height, `VirtualScroller` should re-measure the item's height in order for its internal calculations to stay in sync.
|
|
417
|
+
|
|
418
|
+
* This is simply a proxy for the ["core"](#core) component's `.onItemHeightDidChange(item)`.
|
|
419
|
+
|
|
420
|
+
Example of using `state`/`setState()`/`onHeightDidChange()`:
|
|
421
|
+
|
|
422
|
+
```js
|
|
423
|
+
function ItemComponent({
|
|
424
|
+
item,
|
|
425
|
+
state,
|
|
426
|
+
setState,
|
|
427
|
+
onHeightDidChange
|
|
428
|
+
}) {
|
|
429
|
+
const [internalState, setInternalState] = useState(state)
|
|
430
|
+
|
|
431
|
+
const hasMounted = useRef()
|
|
432
|
+
|
|
433
|
+
useLayoutEffect(() => {
|
|
434
|
+
if (hasMounted.current) {
|
|
435
|
+
setState(internalState)
|
|
436
|
+
onHeightDidChange()
|
|
437
|
+
} else {
|
|
438
|
+
// Skip the initial mount.
|
|
439
|
+
// Only handle the changes of the `internalState`.
|
|
440
|
+
hasMounted.current = true
|
|
441
|
+
}
|
|
442
|
+
}, [internalState])
|
|
443
|
+
|
|
444
|
+
return (
|
|
445
|
+
<section>
|
|
446
|
+
<h1>
|
|
447
|
+
{item.title}
|
|
448
|
+
</h1>
|
|
449
|
+
{internalState && internalState.expanded &&
|
|
450
|
+
<p>{item.text}</p>
|
|
451
|
+
}
|
|
452
|
+
<button onClick={() => {
|
|
453
|
+
setInternalState({
|
|
454
|
+
...internalState,
|
|
455
|
+
expanded: !expanded
|
|
456
|
+
})
|
|
457
|
+
}}>
|
|
458
|
+
{internalState && internalState.expanded ? 'Show less' : 'Show more'}
|
|
459
|
+
</button>
|
|
460
|
+
</section>
|
|
461
|
+
)
|
|
462
|
+
}
|
|
463
|
+
```
|
|
464
|
+
</details>
|
|
465
|
+
|
|
466
|
+
#####
|
|
467
|
+
|
|
468
|
+
<details>
|
|
469
|
+
<summary>Server-Side Render</summary>
|
|
470
|
+
|
|
471
|
+
#####
|
|
472
|
+
|
|
473
|
+
By default, on server side, it will just render the first item, as if the list only had one item. This is because on server side it doesn't know how many items it should render because it doesn't know neither the item height nor the screen height.
|
|
474
|
+
|
|
475
|
+
To fix that, a developer should specify certain properties — `getEstimatedVisibleItemRowsCount(): number` and `getEstimatedItemHeight(): number` and `getEstimatedInterItemVerticalSpacing(): number` — so that it could calculate how many items it should render and how much space it should leave for scrolling. For more technical details, see the description of these parameters in the ["core"](#core) component's [options](#options).
|
|
476
|
+
</details>
|
|
477
|
+
|
|
430
478
|
## DOM
|
|
431
479
|
|
|
432
480
|
`virtual-scroller/dom` exports a `VirtualScroller` class that implements a "virtual scroller" in a standard [Document Object Model](https://en.wikipedia.org/wiki/Document_Object_Model) environment such as a web browser.
|
|
@@ -511,7 +559,7 @@ const virtualScroller = new VirtualScroller(
|
|
|
511
559
|
|
|
512
560
|
* `readyToRender: boolean` — The `readyToStart: false` option described above "freezes" the list for any updates but it still performs the initial render of it. If even the initial render of the list should be postponed, pass `readyToRender: false` option, and it will not only prevent the automatic "start" of the `VirtualScroller` at creation time, but it will also prevent the automatic initial render of it until the developer manually calls `.start()` instance method.
|
|
513
561
|
|
|
514
|
-
* Any other options are simply passed through to the ["core"](#core) component.
|
|
562
|
+
* Any other [options](#options) are simply passed through to the ["core"](#core) component.
|
|
515
563
|
</details>
|
|
516
564
|
|
|
517
565
|
#####
|
|
@@ -526,15 +574,15 @@ The following instance methods are just proxies for the corresponding methods of
|
|
|
526
574
|
* `start()`
|
|
527
575
|
* `stop()`
|
|
528
576
|
* `setItems(items, options)`
|
|
529
|
-
* `setItemState(
|
|
530
|
-
* `onItemHeightDidChange(
|
|
577
|
+
* `setItemState(item, itemState)`
|
|
578
|
+
* `onItemHeightDidChange(item)`
|
|
531
579
|
|
|
532
|
-
<!-- * `getItemCoordinates(
|
|
580
|
+
<!-- * `getItemCoordinates(item)` -->
|
|
533
581
|
</details>
|
|
534
582
|
|
|
535
583
|
## Core
|
|
536
584
|
|
|
537
|
-
The default export is a
|
|
585
|
+
The default export is a "core" `VirtualScroller` class: it implements the core logic of a "virtual scroller" component and can be used to build a "virtual scroller" 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/react`](#react) or [`virtual-scroller/dom`](#dom). Or implement your own: see `source/test` folder for an example of using the core component to build an "imaginary" renderer implementation.
|
|
538
586
|
|
|
539
587
|
### State
|
|
540
588
|
|
|
@@ -553,7 +601,7 @@ Sometimes though, by design, re-rendering could only be done "asynchronously" (i
|
|
|
553
601
|
|
|
554
602
|
The main `state` properties are:
|
|
555
603
|
|
|
556
|
-
* `items: any[]` — The list of items (can be updated via [`.setItems()`](#
|
|
604
|
+
* `items: any[]` — The list of items (can be updated via [`.setItems()`](#updating-items)).
|
|
557
605
|
|
|
558
606
|
* `firstShownItemIndex: number` — The index of the first item that should be rendered.
|
|
559
607
|
|
|
@@ -567,19 +615,19 @@ The following `state` properties are only used for saving and restoring `Virtual
|
|
|
567
615
|
|
|
568
616
|
* `itemStates: any[]` — The "states" of all items. If an item's appearance is not "static" and could change, then every aspect of the item's appearance that could change should be represented in the item's "state", and that "state" must be preserved somewhere. That's because of the nature of how `VirtualScroller` works: no-longer-visible items get un-rendered, and when they later become visible again, they should precisely restore their latest-rendered appearance by re-rendering from a previously preserved "state".
|
|
569
617
|
|
|
570
|
-
* The item "state" could be preserved anywhere in the application, or the developer could use `VirtualScroller`'s built-in item "state" storage. To preserve an item's state in the built-in storage, call `.setItemState(
|
|
618
|
+
* The item "state" could be preserved anywhere in the application, or the developer could use `VirtualScroller`'s built-in item "state" storage. To preserve an item's state in the built-in storage, call `.setItemState(item, itemState)` instance method (described below) immediately after an item's state has changed.
|
|
571
619
|
|
|
572
|
-
* An example would be an item representing a social media comment, with a "Show more"/"Show less" button that shows or hides the full text of the comment. Immediately after the full text of a comment has been shown or hidden, it should call `.setItemState(
|
|
620
|
+
* An example would be an item representing a social media comment, with a "Show more"/"Show less" button that shows or hides the full text of the comment. Immediately after the full text of a comment has been shown or hidden, it should call `.setItemState(item, { showMore: true/false })` instance method along with `.onItemHeightDidChange(item)` instance method (described below), so that next time when the item is rendered, it could restore its appearance from `virtualScroller.getState().itemStates[i]`.
|
|
573
621
|
|
|
574
622
|
* For another similar example, consider a social network feed, where each post optionally has 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 wasn't preserved, then the following "glitch" would be observed: the user expands the video, then scrolls down so that the post with the video is no longer visible, the post gets unmounted due to going off screen, then the user scrolls back up so that the post with the video is visible again, the post gets mounted again, but the video is not expanded and instead a small thumbnail is shown because there's no previous "state" to restore from.
|
|
575
623
|
|
|
576
|
-
* In this example, besides preserving the item state itself, one should also call `.onItemHeightDidChange(
|
|
624
|
+
* In this example, besides preserving the item state itself, one should also call `.onItemHeightDidChange(item)` instance method (described below) right after the YouTube video has been expanded/collapsed.
|
|
577
625
|
|
|
578
626
|
* `itemHeights: number[]` — The measured heights of all items. If an item's height hasn't been measured yet then it's `undefined`.
|
|
579
627
|
|
|
580
|
-
* By default, items are only measured once: when they're initially rendered. If an item's height changes afterwards, then `.onItemHeightDidChange(
|
|
628
|
+
* By default, items are only measured once: when they're initially rendered. If an item's height changes afterwards, then `.onItemHeightDidChange(item)` instance method must be called right after it happens (described later in the document), otherwise `VirtualScroller`'s calculations will be off. For example, if an item is a social media comment, and there's a "Show more"/"Show less" button that shows the full text of the comment, then it must call `.onItemHeightDidChange(item)` immediately after the comment text has been expanded or collapsed.
|
|
581
629
|
|
|
582
|
-
* Besides the requirement of calling `.onItemHeightDidChange(
|
|
630
|
+
* Besides the requirement of calling `.onItemHeightDidChange(item)`, every change in an item's height must also be reflected in the actual data: the change in height must be either a result of the item's internal properties changing or it could be a result of changing the item's "state". The reason is that when an item gets hidden, it's no longer rendered, so when it becomes visible again, it should precisely restore its last-rendered appearance based on the item's properties and any persisted "state".
|
|
583
631
|
|
|
584
632
|
* `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.
|
|
585
633
|
|
|
@@ -630,112 +678,18 @@ virtualScroller.start()
|
|
|
630
678
|
virtualScroller.stop()
|
|
631
679
|
```
|
|
632
680
|
|
|
633
|
-
|
|
634
|
-
* `items` — The list of items.
|
|
635
|
-
* `render(state, prevState)` — "re-renders" the list.
|
|
636
|
-
|
|
637
|
-
#####
|
|
638
|
-
|
|
639
|
-
<details>
|
|
640
|
-
<summary>An example of implementing a high-level <code>virtual-scroller/dom</code> component on top of the core <code>VirtualScroller</code> component.
|
|
641
|
-
</summary>
|
|
681
|
+
`VirtualScroller` class constructor arguments:
|
|
642
682
|
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
const items = [
|
|
649
|
-
{ title: 'Apple' },
|
|
650
|
-
{ title: 'Banana' },
|
|
651
|
-
{ title: 'Cranberry' }
|
|
652
|
-
]
|
|
653
|
-
|
|
654
|
-
function renderItem(item) {
|
|
655
|
-
const div = document.createElement('div')
|
|
656
|
-
div.innerText = item.title
|
|
657
|
-
return div
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
const container = document.getElementById('list')
|
|
661
|
-
|
|
662
|
-
function render(state, prevState) {
|
|
663
|
-
const {
|
|
664
|
-
items,
|
|
665
|
-
beforeItemsHeight,
|
|
666
|
-
afterItemsHeight,
|
|
667
|
-
firstShownItemIndex,
|
|
668
|
-
lastShownItemIndex
|
|
669
|
-
} = state
|
|
670
|
-
|
|
671
|
-
// Set `paddingTop` and `paddingBottom` on the container element:
|
|
672
|
-
// it emulates the non-visible items as if they were rendered.
|
|
673
|
-
container.style.paddingTop = Math.round(beforeItemsHeight) + 'px'
|
|
674
|
-
container.style.paddingBottom = Math.round(afterItemsHeight) + 'px'
|
|
675
|
-
|
|
676
|
-
// Perform an intelligent "diff" re-render as the user scrolls the page.
|
|
677
|
-
// This also requires that the list of `items` hasn't been changed.
|
|
678
|
-
// On initial render, `prevState` is `undefined`.
|
|
679
|
-
if (prevState && items === prevState.items) {
|
|
680
|
-
|
|
681
|
-
// Remove no longer visible items.
|
|
682
|
-
let i = prevState.lastShownItemIndex
|
|
683
|
-
while (i >= prevState.firstShownItemIndex) {
|
|
684
|
-
if (i >= firstShownItemIndex && i <= lastShownItemIndex) {
|
|
685
|
-
// The item is still visible.
|
|
686
|
-
} else {
|
|
687
|
-
// The item is no longer visible. Remove it.
|
|
688
|
-
container.removeChild(container.childNodes[i - prevState.firstShownItemIndex])
|
|
689
|
-
}
|
|
690
|
-
i--
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
// Add newly visible items.
|
|
694
|
-
let prependBefore = container.firstChild
|
|
695
|
-
let i = firstShownItemIndex
|
|
696
|
-
while (i <= lastShownItemIndex) {
|
|
697
|
-
if (i >= prevState.firstShownItemIndex && i <= prevState.lastShownItemIndex) {
|
|
698
|
-
// The item is already being rendered.
|
|
699
|
-
// Next items will be appended rather than prepended.
|
|
700
|
-
prependBefore = undefined
|
|
701
|
-
} else {
|
|
702
|
-
if (prependBefore) {
|
|
703
|
-
container.insertBefore(renderItem(items[i]), prependBefore)
|
|
704
|
-
} else {
|
|
705
|
-
container.appendChild(renderItem(items[i]))
|
|
706
|
-
}
|
|
707
|
-
}
|
|
708
|
-
i++
|
|
709
|
-
}
|
|
710
|
-
}
|
|
711
|
-
else {
|
|
712
|
-
// Re-render the list from scratch.
|
|
713
|
-
while (container.firstChild) {
|
|
714
|
-
container.removeChild(container.firstChild)
|
|
715
|
-
}
|
|
716
|
-
let i = firstShownItemIndex
|
|
717
|
-
while (i <= lastShownItemIndex) {
|
|
718
|
-
container.appendChild(renderItem(items[i]))
|
|
719
|
-
i++
|
|
720
|
-
}
|
|
721
|
-
}
|
|
722
|
-
}
|
|
723
|
-
|
|
724
|
-
const virtualScroller = new VirtualScroller(() => element, items, { render })
|
|
725
|
-
|
|
726
|
-
// Start VirtualScroller listening for scroll events.
|
|
727
|
-
virtualScroller.start()
|
|
728
|
-
|
|
729
|
-
// Stop VirtualScroller listening for scroll events
|
|
730
|
-
// when the user navigates to another page:
|
|
731
|
-
// router.onPageUnload(virtualScroller.stop)
|
|
732
|
-
```
|
|
733
|
-
</details>
|
|
683
|
+
* `getContainerElement()` — returns the container "element" for the list item "elements".
|
|
684
|
+
* `items` — an array of items.
|
|
685
|
+
* `options` — (optional)
|
|
686
|
+
* `render(state, prevState)` — "re-renders" the list according to the new `state`.
|
|
687
|
+
* The `render()` function can only be specified when it immediately re-renders the list. Sometimes, an immediate re-render is not possible. For example, in React framework, re-render is done "asynchronously", i.e. with a short delay. In such case, instead of specifying a `render` parameter when creating a `virtualScroller` instance, one should omit it and then call an instance method — `virtualScroller.useState({ getState, setState/updateState })` — where `getState` function returns the currently-rendered state and `setState/updateState` function is responsible for triggerring an eventual "re-render" of the list according to the new `state`.
|
|
734
688
|
|
|
735
689
|
#### Options
|
|
736
690
|
|
|
737
691
|
<details>
|
|
738
|
-
<summary
|
|
692
|
+
<summary>Available <code>options</code></summary>
|
|
739
693
|
|
|
740
694
|
#####
|
|
741
695
|
|
|
@@ -759,11 +713,20 @@ virtualScroller.start()
|
|
|
759
713
|
|
|
760
714
|
* `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.
|
|
761
715
|
|
|
762
|
-
|
|
716
|
+
* `getEstimatedVisibleItemRowsCount(): number` and/or `getEstimatedItemHeight(): number` and/or `getEstimatedInterItemVerticalSpacing(): number` — These functions are only used during the initial render of the list, i.e. when `VirtualScroller` doesn't know anything about the item dimensions.
|
|
717
|
+
* `getEstimatedVisibleItemRowsCount()` is used to guess how many rows of items should be rendered in order to cover the screen area. Sidenote: It will actually render more items than that, with a "prerender margin" on top and bottom, just to account for future scrolling.
|
|
718
|
+
* `getEstimatedItemHeight()` is used to guess the average item height before any of the items have been rendered yet. This average item height is then used to calculate the size of the scrollbar, i.e. how much the user can scroll. It can also be used to calculate the count of visible rows of items if the screen size is known and `getEstimatedVisibleItemRowsCount()` function is not specified.
|
|
719
|
+
* `getEstimatedInterItemVerticalSpacing()` is used to guess the vertical spacing between the items. It is used to calculate the size of the scrollbar, i.e. how much the user can scroll.
|
|
720
|
+
* After the initial render has finished, the list will measure the heights of the rendered items and will use those values to calculate the average item height, the vertical spacing between the items and the count of visible rows of items, and with these new values it will re-render itself.
|
|
721
|
+
* This means that on client side, `getEstimatedVisibleItemRowsCount()` and `getEstimatedItemHeight()` and `getEstimatedInterItemVerticalSpacing()` don't really matter because the list will immediately re-render itself with the correct measured values anyway, and the user will not even observe the results of the initial render because a follow-up render happens immediately.
|
|
722
|
+
* On server side though, `getEstimatedVisibleItemRowsCount()` and `getEstimatedItemHeight()` and `getEstimatedInterItemVerticalSpacing()` completely determine the output of a "server-side render".
|
|
723
|
+
* When these parameters aren't specified, the list will render just the first item during the initial render.
|
|
724
|
+
|
|
725
|
+
#### "Advanced" (rarely-used) options
|
|
763
726
|
|
|
764
727
|
* `bypass: boolean` — Pass `true` to disable the "virtualization" behavior and just render the entire list of items.
|
|
765
728
|
|
|
766
|
-
* `getInitialItemState
|
|
729
|
+
* `getInitialItemState(item): any?` — Creates the initial state for an item. It can be used to populate the default initial states for list items. By default, an item's state is `undefined`.
|
|
767
730
|
|
|
768
731
|
* `initialScrollPosition: number` — If passed, the page will be scrolled to this `scrollY` position.
|
|
769
732
|
|
|
@@ -771,7 +734,7 @@ virtualScroller.start()
|
|
|
771
734
|
|
|
772
735
|
<!-- * `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. -->
|
|
773
736
|
|
|
774
|
-
* `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.
|
|
737
|
+
* `getItemId(item): number | string` — (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.
|
|
775
738
|
|
|
776
739
|
* `onItemInitialRender(item)` — (advanced) Will be called for each `item` when it's about to be rendered for the first time. This function could be used to somehow "initialize" an item before it gets 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, the application could "preprocess" only the items that're actually visible, preventing the unnecessary work and reducing the "time to first render".
|
|
777
740
|
* The function is guaranteed to be called at least once for each item that ever gets rendered.
|
|
@@ -784,17 +747,15 @@ virtualScroller.start()
|
|
|
784
747
|
|
|
785
748
|
* `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.
|
|
786
749
|
|
|
787
|
-
<!-- * (alternative description) `getEstimatedItemHeight
|
|
788
|
-
|
|
789
|
-
* `getEstimatedItemHeight: () => number` or `getEstimatedVisibleItemRowsCount: () => number` — These functions are only used during the initial render of the list to estimate how many of the list items should be rendered initially to cover the screen space plus some extra vertical margin (called "prerender margin") for future scrolling. The list will then immediately re-render itself the second time anyway, just to make sure that the layout has been calculated correctly, so the values returned by these functions doesn't have to be 100% accurate. But the more accurate they are, the more accurate is the initial render. The only purpose for the existence of these parameters is eliminating any potential initial "flash" of empty space on screen caused by the list of items being only half-rendered. These two parameters are mutually exclusive: one may only provide one of them. If none of these parameters is provided, then the list initially renders with just the first item being shown, then measures the first item's size, and then rerenders itself again using the measured item height as a substitute for `getEstimatedItemHeight()`.
|
|
750
|
+
<!-- * (alternative description) `getEstimatedItemHeight(): number` — By default, `<VirtualScroller/>` uses an average measured item height as an estimate for the height of any item that hasn't been rendered yet. This way, it is able to guess what will be the total height of the items below the current scroll position, which is required in order to display a correct scrollbar. However, if the application thinks that it has a better idea of what the average item height is gonna be, it could force `<VirtualScroller/>` to use that value instead of the average measured one. -->
|
|
790
751
|
|
|
791
|
-
* `
|
|
752
|
+
* `prerenderMarginRatio` — (currently unused) The list component renders not only the items that're currently visible but also the items that lie within some additional vertical distance (called "prerender margin") on top and bottom to account for future scrolling. This way, it doesn't have to recalculate the layout on each scroll event and is only forced to recalculate the layout if the user scrolls past the "prerender margin". Therefore, "prerender margin" is an optimization that "throttles" layout recalculation. By default, the "prerender margin" is equal to scrollable container height: this seems to be the most optimal value to account for "Page Up" / "Page Down" scrolling. This parameter is currently not customizable because the default value of `1` seems to work fine in all possible use cases.
|
|
792
753
|
</details>
|
|
793
754
|
|
|
794
755
|
#####
|
|
795
756
|
|
|
796
757
|
<details>
|
|
797
|
-
<summary
|
|
758
|
+
<summary>Instance methods</summary>
|
|
798
759
|
|
|
799
760
|
#####
|
|
800
761
|
|
|
@@ -804,7 +765,7 @@ virtualScroller.start()
|
|
|
804
765
|
|
|
805
766
|
* `getState(): object` — Returns `VirtualScroller` state.
|
|
806
767
|
|
|
807
|
-
* `setItems(newItems: any[], options: object?)` — Updates `VirtualScroller` `items`. For example, it can be used to prepend or append new items to the list. See [
|
|
768
|
+
* `setItems(newItems: any[], options: object?)` — Updates `VirtualScroller` `items`. For example, it can be used to prepend or append new items to the list. See [Updating Items](#updating-items) section for more details. Available options:
|
|
808
769
|
* `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).
|
|
809
770
|
|
|
810
771
|
#### Custom (External) State Management
|
|
@@ -841,25 +802,123 @@ When using custom (external) state management, contrary to the default (internal
|
|
|
841
802
|
|
|
842
803
|
#### "Advanced" (rarely used) instance methods
|
|
843
804
|
|
|
844
|
-
* `onItemHeightDidChange(
|
|
805
|
+
* `onItemHeightDidChange(item)` — (advanced) If an item's height could've changed, this function should be called immediately after the item's height has potentially changed. The function re-measures the item's height (the item must still be rendered) and re-calculates `VirtualScroller` layout. An example for using this function would be having an "Expand"/"Collapse" button in a list item.
|
|
845
806
|
|
|
846
807
|
* There's also a convention that every change in an item's height must come as a result of changing the item's "state". See the descripton of `itemStates` and `itemHeights` properties of the `VirtualScroller` [state](#state) for more details.
|
|
847
808
|
|
|
848
|
-
* Implementation-wise, calling `onItemHeightDidChange()` manually could be replaced with detecting item height changes automatically via [Resize Observer](https://caniuse.com/#search=Resize%20Observer) in some future version.
|
|
809
|
+
* Implementation-wise, calling `onItemHeightDidChange(item)` manually could be replaced with detecting item height changes automatically via [Resize Observer](https://caniuse.com/#search=Resize%20Observer) in some future version.
|
|
849
810
|
|
|
850
|
-
* `setItemState(
|
|
811
|
+
* `setItemState(item, itemState: any?)` — (advanced) Preserves a list item's "state" inside `VirtualScroller`'s built-in item "state" storage. See the descripton of `itemStates` property of the `VirtualScroller` [state](#state) for more details.
|
|
851
812
|
|
|
852
813
|
* A developer could use it to preserve an item's "state" if it could change. The reason is that offscreen items get unmounted and any unsaved state is lost in the process. If an item's state is correctly preserved, the item's latest-rendered appearance could be restored from that state when the item gets mounted again due to becoming visible again.
|
|
853
814
|
|
|
854
|
-
* Calling `setItemState()` 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, one should call `onItemHeightDidChange(
|
|
815
|
+
* Calling `setItemState()` 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, one should call `onItemHeightDidChange(item)` right after the change in the item's state has been reflected on screen.
|
|
855
816
|
|
|
856
|
-
* `getItemScrollPosition(
|
|
817
|
+
* `getItemScrollPosition(item): 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.
|
|
857
818
|
|
|
858
|
-
<!-- * `getItemCoordinates(
|
|
819
|
+
<!-- * `getItemCoordinates(item): 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. -->
|
|
859
820
|
|
|
860
821
|
* `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.
|
|
861
822
|
</details>
|
|
862
823
|
|
|
824
|
+
#####
|
|
825
|
+
|
|
826
|
+
<details>
|
|
827
|
+
<summary>Example: implement <code>virtual-scroller/dom</code> component using the "core" <code>VirtualScroller</code> component
|
|
828
|
+
</summary>
|
|
829
|
+
|
|
830
|
+
#####
|
|
831
|
+
|
|
832
|
+
```js
|
|
833
|
+
import VirtualScroller from 'virtual-scroller'
|
|
834
|
+
|
|
835
|
+
const items = [
|
|
836
|
+
{ title: 'Apple' },
|
|
837
|
+
{ title: 'Banana' },
|
|
838
|
+
{ title: 'Cranberry' }
|
|
839
|
+
]
|
|
840
|
+
|
|
841
|
+
function renderItem(item) {
|
|
842
|
+
const div = document.createElement('div')
|
|
843
|
+
div.innerText = item.title
|
|
844
|
+
return div
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
const container = document.getElementById('list')
|
|
848
|
+
|
|
849
|
+
function render(state, prevState) {
|
|
850
|
+
const {
|
|
851
|
+
items,
|
|
852
|
+
beforeItemsHeight,
|
|
853
|
+
afterItemsHeight,
|
|
854
|
+
firstShownItemIndex,
|
|
855
|
+
lastShownItemIndex
|
|
856
|
+
} = state
|
|
857
|
+
|
|
858
|
+
// Set `paddingTop` and `paddingBottom` on the container element:
|
|
859
|
+
// it emulates the non-visible items as if they were rendered.
|
|
860
|
+
container.style.paddingTop = Math.round(beforeItemsHeight) + 'px'
|
|
861
|
+
container.style.paddingBottom = Math.round(afterItemsHeight) + 'px'
|
|
862
|
+
|
|
863
|
+
// Perform an intelligent "diff" re-render as the user scrolls the page.
|
|
864
|
+
// This also requires that the list of `items` hasn't been changed.
|
|
865
|
+
// On initial render, `prevState` is `undefined`.
|
|
866
|
+
if (prevState && items === prevState.items) {
|
|
867
|
+
|
|
868
|
+
// Remove no longer visible items.
|
|
869
|
+
let i = prevState.lastShownItemIndex
|
|
870
|
+
while (i >= prevState.firstShownItemIndex) {
|
|
871
|
+
if (i >= firstShownItemIndex && i <= lastShownItemIndex) {
|
|
872
|
+
// The item is still visible.
|
|
873
|
+
} else {
|
|
874
|
+
// The item is no longer visible. Remove it.
|
|
875
|
+
container.removeChild(container.childNodes[i - prevState.firstShownItemIndex])
|
|
876
|
+
}
|
|
877
|
+
i--
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
// Add newly visible items.
|
|
881
|
+
let prependBefore = container.firstChild
|
|
882
|
+
let i = firstShownItemIndex
|
|
883
|
+
while (i <= lastShownItemIndex) {
|
|
884
|
+
if (i >= prevState.firstShownItemIndex && i <= prevState.lastShownItemIndex) {
|
|
885
|
+
// The item is already being rendered.
|
|
886
|
+
// Next items will be appended rather than prepended.
|
|
887
|
+
prependBefore = undefined
|
|
888
|
+
} else {
|
|
889
|
+
if (prependBefore) {
|
|
890
|
+
container.insertBefore(renderItem(items[i]), prependBefore)
|
|
891
|
+
} else {
|
|
892
|
+
container.appendChild(renderItem(items[i]))
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
i++
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
else {
|
|
899
|
+
// Re-render the list from scratch.
|
|
900
|
+
while (container.firstChild) {
|
|
901
|
+
container.removeChild(container.firstChild)
|
|
902
|
+
}
|
|
903
|
+
let i = firstShownItemIndex
|
|
904
|
+
while (i <= lastShownItemIndex) {
|
|
905
|
+
container.appendChild(renderItem(items[i]))
|
|
906
|
+
i++
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
const virtualScroller = new VirtualScroller(() => element, items, { render })
|
|
912
|
+
|
|
913
|
+
// Start VirtualScroller listening for scroll events.
|
|
914
|
+
virtualScroller.start()
|
|
915
|
+
|
|
916
|
+
// Stop VirtualScroller listening for scroll events
|
|
917
|
+
// when the user navigates to another page:
|
|
918
|
+
// router.onPageUnload(virtualScroller.stop)
|
|
919
|
+
```
|
|
920
|
+
</details>
|
|
921
|
+
|
|
863
922
|
## Updating Items
|
|
864
923
|
|
|
865
924
|
If the list represents a social media feed, it has to be updated periodically as new posts get published.
|
|
@@ -1129,7 +1188,7 @@ Due to the [inherent limitations](https://gitlab.com/catamphetamine/virtual-scro
|
|
|
1129
1188
|
|
|
1130
1189
|
### "Item index N height changed unexpectedly" warning on page load in development mode
|
|
1131
1190
|
|
|
1132
|
-
`VirtualScroller` assumes there'd be no "unexpected" (unannounced) changes in items' heights. If an item's height changes for whatever reason, a developer must announce it immediately by calling `.onItemHeightDidChange(
|
|
1191
|
+
`VirtualScroller` assumes there'd be no "unexpected" (unannounced) changes in items' heights. If an item's height changes for whatever reason, a developer must announce it immediately by calling `.onItemHeightDidChange(item)` instance method.
|
|
1133
1192
|
|
|
1134
1193
|
There might still be cases outside of a developer's control when items' heights do change "unexpectedly". One such case is when running an application in "development" mode in a bundler such as Webpack, and the CSS styles or custom fonts haven't loaded yet, resulting in different item height measurements "before" and "after" the page has fully loaded. Note that this is not a bug of `VirtualScroller`. It's just an inconvenience introduced by a bundler such as Webpack, and only in "development" mode, i.e. it won't happen in production.
|
|
1135
1194
|
|
|
@@ -1270,7 +1329,7 @@ To include this library directly via a `<script/>` tag on a page, one can use an
|
|
|
1270
1329
|
<!--
|
|
1271
1330
|
## Possible enhancements
|
|
1272
1331
|
|
|
1273
|
-
* Use [Resize Observer](https://caniuse.com/#search=Resize%20Observer) instead of calling `.onItemHeightDidChange(
|
|
1332
|
+
* Use [Resize Observer](https://caniuse.com/#search=Resize%20Observer) instead of calling `.onItemHeightDidChange(item)` manually.
|
|
1274
1333
|
|
|
1275
1334
|
* Currently React `<VirtualScroller/>` passes `onHeightDidChange()` 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.
|
|
1276
1335
|
-->
|