virtual-scroller 1.14.0 → 1.15.1
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 +29 -0
- package/README.md +403 -254
- package/bundle/index-dom-bypass.html +197 -0
- package/bundle/index-dom-grid.html +203 -0
- package/bundle/index-dom-scrollableContainer.html +214 -0
- package/bundle/index-dom-tbody-scrollableContainer.html +81 -0
- package/bundle/index-dom-tbody.html +65 -0
- package/bundle/index-dom.html +114 -84
- package/bundle/index-react-bypass.html +194 -0
- package/bundle/{index-bypass.html → index-react-grid.html} +122 -120
- package/bundle/index-react-hook.html +209 -0
- package/bundle/index-react-scrollableContainer.html +207 -0
- package/bundle/index-react-strictMode.html +193 -0
- package/bundle/index-react-tbody-scrollableContainer.html +94 -0
- package/bundle/index-react-tbody.html +78 -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 +124 -131
- 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/useCreateVirtualScroller.js +64 -0
- package/commonjs/react/useCreateVirtualScroller.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/useMergeRefs.js +52 -0
- package/commonjs/react/useMergeRefs.js.map +1 -0
- 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/{useVirtualScrollerStartStop.js → useStartStopVirtualScroller.js} +1 -1
- package/commonjs/react/{useVirtualScrollerStartStop.js.map → useStartStopVirtualScroller.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 +28 -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 +142 -42
- 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 +122 -124
- 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/useCreateVirtualScroller.js +53 -0
- package/modules/react/useCreateVirtualScroller.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/useMergeRefs.js +44 -0
- package/modules/react/useMergeRefs.js.map +1 -0
- 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/{useVirtualScrollerStartStop.js → useStartStopVirtualScroller.js} +1 -1
- package/modules/react/{useVirtualScrollerStartStop.js.map → useStartStopVirtualScroller.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 +27 -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 +136 -42
- 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 +4 -1
- package/react/as.d.ts +42 -0
- package/react/index.cjs +2 -1
- package/react/index.d.ts +248 -63
- package/react/index.js +1 -0
- package/rollup.config.mjs +15 -1
- 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 +135 -133
- package/source/react/useClassName.js +3 -3
- package/source/react/useCreateVirtualScroller.js +65 -0
- package/source/react/useInstanceMethods.js +12 -4
- package/source/react/useItemKeys.js +22 -4
- package/source/react/useMergeRefs.js +45 -0
- 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 +18 -2
- package/source/react/useValidateTableBodyItemsContainer.js +16 -0
- package/source/react/useVirtualScroller.js +155 -47
- package/source/test/ItemsContainer.js +10 -10
- package/source/test/VirtualScroller.js +16 -10
- package/website/index-dom-bypass.html +197 -0
- package/website/index-dom-grid.html +203 -0
- package/website/index-dom-scrollableContainer.html +214 -0
- package/website/index-dom-tbody-scrollableContainer.html +81 -0
- package/website/index-dom-tbody.html +65 -0
- package/website/index-dom.html +116 -84
- package/website/index-react-bypass.html +194 -0
- package/website/index-react-grid.html +197 -0
- package/website/index-react-hook.html +209 -0
- package/website/index-react-scrollableContainer.html +207 -0
- package/website/index-react-strictMode.html +193 -0
- package/website/index-react-tbody-scrollableContainer.html +94 -0
- package/website/index-react-tbody.html +78 -0
- package/website/index-react.html +193 -0
- package/website/index.html +120 -111
- 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-grid.html +0 -216
- package/bundle/index-scrollableContainer.html +0 -208
- 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-bypass.html +0 -185
- package/website/index-grid.html +0 -216
- 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/source/react/{useVirtualScrollerStartStop.js → useStartStopVirtualScroller.js} +0 -0
package/README.md
CHANGED
|
@@ -6,24 +6,31 @@ 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=✓)
|
|
32
|
+
* [List (using hook)](https://catamphetamine.gitlab.io/virtual-scroller/index-react-hook.html)
|
|
33
|
+
* [Paginated List (using hook)](https://catamphetamine.gitlab.io/virtual-scroller/index-react-hook.html?pagination=✓)
|
|
27
34
|
|
|
28
35
|
## Rationale
|
|
29
36
|
|
|
@@ -59,9 +66,9 @@ Alternatively, one could include it on a web page [directly](#cdn) via a `<scrip
|
|
|
59
66
|
|
|
60
67
|
As it has been mentioned, this package exports three different components:
|
|
61
68
|
|
|
62
|
-
* For React framework — `virtual-scroller/react`
|
|
63
|
-
* For "vanilla" DOM — `virtual-scroller/dom`
|
|
64
|
-
* For any other case ("core") — `virtual-scroller`
|
|
69
|
+
* For React framework — [`virtual-scroller/react`](#react)
|
|
70
|
+
* For "vanilla" DOM — [`virtual-scroller/dom`](#dom)
|
|
71
|
+
* For any other case ("core") — [`virtual-scroller`](#core)
|
|
65
72
|
|
|
66
73
|
Below is a description of each component.
|
|
67
74
|
|
|
@@ -71,9 +78,9 @@ Below is a description of each component.
|
|
|
71
78
|
|
|
72
79
|
The React component is based on the ["core"](#core) component, and it requires the following properties:
|
|
73
80
|
|
|
74
|
-
* `items` —
|
|
81
|
+
* `items` — an array of items.
|
|
75
82
|
|
|
76
|
-
* `itemComponent` —
|
|
83
|
+
* `itemComponent` — a React component that renders an item.
|
|
77
84
|
|
|
78
85
|
* The `itemComponent` will receive properties:
|
|
79
86
|
* `item` — The item object (an element of the `items` array). Use it to render the item.
|
|
@@ -82,12 +89,20 @@ The React component is based on the ["core"](#core) component, and it requires t
|
|
|
82
89
|
<!-- * `state` — The item component's "state". -->
|
|
83
90
|
<!-- * Curious readers may see the description of `itemStates` property of the `state` object in the ["core"](#core) component section. -->
|
|
84
91
|
<!-- * `setState(newState)` — Sets the item component's "state". -->
|
|
85
|
-
<!-- * Curious readers may see the description of `setItemState(
|
|
92
|
+
<!-- * Curious readers may see the description of `setItemState(item, newState)` function in the ["core"](#core) component section. -->
|
|
86
93
|
* `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
94
|
* 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
95
|
|
|
89
96
|
* 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
97
|
|
|
98
|
+
* `itemComponentProps: object` — (optional) any additional props for the `itemComponent`.
|
|
99
|
+
|
|
100
|
+
* `itemsContainerComponent` — a React component that will be used as a container for the items.
|
|
101
|
+
* Must be either a simple string like `"div"` or a React component that "forwards" `ref` to the resulting `Element`.
|
|
102
|
+
* 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.
|
|
103
|
+
|
|
104
|
+
* `itemsContainerComponentProps: object` — (optional) any additional props for the `itemsContainerComponent`.
|
|
105
|
+
|
|
91
106
|
Code example:
|
|
92
107
|
|
|
93
108
|
#####
|
|
@@ -101,6 +116,7 @@ function List({ items }) {
|
|
|
101
116
|
<VirtualScroller
|
|
102
117
|
items={items}
|
|
103
118
|
itemComponent={ListItem}
|
|
119
|
+
itemsContainerComponent="div"
|
|
104
120
|
/>
|
|
105
121
|
)
|
|
106
122
|
}
|
|
@@ -152,110 +168,40 @@ function App() {
|
|
|
152
168
|
-->
|
|
153
169
|
|
|
154
170
|
<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>
|
|
171
|
+
<summary>Available options (properties)</summary>
|
|
233
172
|
|
|
234
173
|
#####
|
|
235
174
|
|
|
236
175
|
<!-- 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
176
|
|
|
238
|
-
|
|
239
|
-
|
|
177
|
+
<!-- I guess that `style` property should be deprecated because it could potentially be dangerous
|
|
178
|
+
due to potential conflicts with `VirtualScroller` styles. It currently isn't present anyway. -->
|
|
179
|
+
<!-- * `style: object` — Custom CSS style, except for `padding-top` or `padding-bottom`. -->
|
|
240
180
|
|
|
241
|
-
|
|
181
|
+
<!-- I guess that `className` property should be deprecated because it could potentially be dangerous
|
|
182
|
+
due to potential conflicts with `VirtualScroller` styles. -->
|
|
183
|
+
<!-- * `className: string` — Custom CSS class name. -->
|
|
184
|
+
|
|
185
|
+
* `tbody: boolean` — When the list items container element is going to be a `<tbody/>`, it will have to use a special workaround in order for the `<VirtualScroller/>` to work correctly. To enable this special workaround, a developer could pass a `tbody: true` property. Otherwise, `<VirtualScroller/>` will only enable it when `itemsContainerComponent === "tbody"`.
|
|
186
|
+
<!-- * There's no longer such option in the ["core"](#core) component because it's autodetected there. The reason why it can't always be autodetected in React is because of server-side rendering when there's no items container DOM element whose tag name could be examined to detect the use of a `<tbody/>` tag as an items container. -->
|
|
187
|
+
* Only the initial value of this property is used, and any changes to it will be ignored.
|
|
242
188
|
|
|
243
189
|
* `getColumnsCount(): number` — Returns the count of the columns.
|
|
244
|
-
* This is simply a proxy for the ["core"](#core) component's `getColumnsCount` option.
|
|
190
|
+
* This is simply a proxy for the ["core"](#core) component's `getColumnsCount` [option](#options).
|
|
245
191
|
* Only the initial value of this property is used, and any changes to it will be ignored.
|
|
246
192
|
|
|
247
|
-
* `getInitialItemState
|
|
248
|
-
* This is simply a proxy for the ["core"](#core) component's `getInitialItemState` option.
|
|
193
|
+
* `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`.
|
|
194
|
+
* This is simply a proxy for the ["core"](#core) component's `getInitialItemState` [option](#options).
|
|
249
195
|
* Only the initial value of this property is used, and any changes to it will be ignored.
|
|
250
196
|
|
|
251
197
|
* `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.
|
|
198
|
+
* This is simply a proxy for the ["core"](#core) component's `state` [option](#options).
|
|
253
199
|
* Only the initial value of this property is used, and any changes to it will be ignored.
|
|
254
200
|
|
|
255
201
|
<!-- * `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
202
|
|
|
257
203
|
* `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.
|
|
204
|
+
* This is simply a proxy for the ["core"](#core) component's `onStateChange` [option](#options).
|
|
259
205
|
* Only the initial value of this property is used, and any changes to it will be ignored.
|
|
260
206
|
|
|
261
207
|
Example of using `initialState`/`onStateChange()`:
|
|
@@ -287,33 +233,31 @@ function ListWithPreservedState() {
|
|
|
287
233
|
}
|
|
288
234
|
```
|
|
289
235
|
|
|
290
|
-
* `getItemId(item):
|
|
291
|
-
* This is simply a proxy for the ["core"](#core) component's `getItemId` option.
|
|
236
|
+
* `getItemId(item): number | string`
|
|
237
|
+
* This is simply a proxy for the ["core"](#core) component's `getItemId` [option](#options).
|
|
292
238
|
* Only the initial value of this property is used, and any changes to it will be ignored.
|
|
293
239
|
* `<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
240
|
|
|
295
241
|
* `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.
|
|
242
|
+
* This is simply a proxy for the ["core"](#core) component's `.setItems()` method's `preserveScrollPositionOnPrependItems` [option](#options).
|
|
297
243
|
* Only the initial value of this property is used, and any changes to it will be ignored.
|
|
298
244
|
|
|
299
245
|
* `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
246
|
* 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
247
|
|
|
302
248
|
* `getScrollableContainer(): Element`
|
|
303
|
-
* This is simply a proxy for the ["core"](#core) component's `getScrollableContainer` option.
|
|
249
|
+
* This is simply a proxy for the ["core"](#core) component's `getScrollableContainer` [option](#options).
|
|
304
250
|
* Only the initial value of this property is used, and any changes to it will be ignored.
|
|
305
|
-
* This function will
|
|
251
|
+
* 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
252
|
|
|
307
|
-
Example of `getScrollableContainer()
|
|
253
|
+
Example of an incorrect `getScrollableContainer()` that won't work:
|
|
308
254
|
|
|
309
255
|
```js
|
|
310
256
|
function ListContainer() {
|
|
311
257
|
const scrollableContainer = useRef()
|
|
312
258
|
|
|
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
259
|
const getScrollableContainer = useCallback(() => {
|
|
260
|
+
// This won't work: it will return `null` because `<ListContainer/>` hasn't "mounted" yet.
|
|
317
261
|
return scrollableContainer.current
|
|
318
262
|
}, [])
|
|
319
263
|
|
|
@@ -328,34 +272,53 @@ function ListContainer() {
|
|
|
328
272
|
}
|
|
329
273
|
```
|
|
330
274
|
|
|
331
|
-
|
|
332
|
-
(this property has been ignored for a long time and was eventually removed)
|
|
275
|
+
Example of a correct `getScrollableContainer()` that would work:
|
|
333
276
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
277
|
+
```js
|
|
278
|
+
function ListContainer() {
|
|
279
|
+
const getScrollableContainer = useCallback(() => {
|
|
280
|
+
return document.getElementById("scrollable-container")
|
|
281
|
+
}, [])
|
|
282
|
+
|
|
283
|
+
return (
|
|
284
|
+
<div id="scrollable-container" style={{ height: "400px", overflow: "scroll" }}>
|
|
285
|
+
<VirtualScroller
|
|
286
|
+
{...}
|
|
287
|
+
getScrollableContainer={getScrollableContainer}
|
|
288
|
+
/>
|
|
289
|
+
</div>
|
|
290
|
+
)
|
|
291
|
+
}
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
* `itemsContainerComponentRef: object` — Could be used to get access to the `itemsContainerComponent` instance.
|
|
295
|
+
* For example, if `itemsContainerComponent` is `"ul"` then `itemsContainerComponentRef.current` will be set to the `<ul/>` `Element`.
|
|
338
296
|
|
|
339
297
|
* `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.
|
|
298
|
+
* This is simply a proxy for the ["core"](#core) component's `onItemInitialRender` [option](#options).
|
|
341
299
|
* Only the initial value of this property is used, and any changes to it will be ignored.
|
|
342
300
|
|
|
343
301
|
* `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.
|
|
302
|
+
* This is simply a proxy for the ["core"](#core) component's `bypass` [option](#options).
|
|
345
303
|
* Only the initial value of this property is used, and any changes to it will be ignored.
|
|
346
304
|
|
|
347
|
-
* `
|
|
348
|
-
* This is simply a proxy for the ["core"](#core) component's `
|
|
305
|
+
* `getEstimatedVisibleItemRowsCount(): number` — Should be specified if server-side rendering is used. Can be omitted if server-side rendering is not used.
|
|
306
|
+
* This is simply a proxy for the ["core"](#core) component's `getEstimatedVisibleItemRowsCount` [option](#options).
|
|
349
307
|
* Only the initial value of this property is used, and any changes to it will be ignored.
|
|
350
308
|
|
|
351
|
-
* `
|
|
352
|
-
* This is simply a proxy for the ["core"](#core) component's `
|
|
309
|
+
* `getEstimatedItemHeight(): number` — Should be specified if server-side rendering is used. Can be omitted if server-side rendering is not used.
|
|
310
|
+
* This is simply a proxy for the ["core"](#core) component's `getEstimatedItemHeight` [option](#options).
|
|
353
311
|
* Only the initial value of this property is used, and any changes to it will be ignored.
|
|
354
312
|
|
|
355
|
-
* `
|
|
356
|
-
* This is simply a proxy for the ["core"](#core) component's `
|
|
313
|
+
* `getEstimatedInterItemVerticalSpacing(): number` — Should be specified if server-side rendering is used. Can be omitted if server-side rendering is not used.
|
|
314
|
+
* This is simply a proxy for the ["core"](#core) component's `getEstimatedInterItemVerticalSpacing` [option](#options).
|
|
357
315
|
* Only the initial value of this property is used, and any changes to it will be ignored.
|
|
358
316
|
|
|
317
|
+
* Any other ["core"](#core) component [options](#options) could be passed here.
|
|
318
|
+
* Such as:
|
|
319
|
+
* `measureItemsBatchSize`
|
|
320
|
+
* Only the initial values of those options will be used, any any changes to those will be ignored.
|
|
321
|
+
|
|
359
322
|
<!-- * `onMount()` — Is called after `<VirtualScroller/>` component has been mounted and before `VirtualScroller.onMount()` is called. -->
|
|
360
323
|
|
|
361
324
|
<!-- * `shouldUpdateLayoutOnScreenResize(event)` — The `shouldUpdateLayoutOnScreenResize` option of `VirtualScroller` class. -->
|
|
@@ -412,7 +375,7 @@ function getScrollableContainer() {
|
|
|
412
375
|
#####
|
|
413
376
|
|
|
414
377
|
<details>
|
|
415
|
-
<summary
|
|
378
|
+
<summary>Instance methods</summary>
|
|
416
379
|
|
|
417
380
|
#####
|
|
418
381
|
|
|
@@ -422,11 +385,186 @@ function getScrollableContainer() {
|
|
|
422
385
|
|
|
423
386
|
<!-- * `getItemCoordinates(i)` — A proxy for the corresponding `VirtualScroller` method. -->
|
|
424
387
|
|
|
388
|
+
<!-- * `getElement()` — Returns the items container `Element`. -->
|
|
389
|
+
|
|
425
390
|
* `updateLayout()` — Forces a re-calculation and re-render of the list.
|
|
426
391
|
* This is simply a proxy for the ["core"](#core) component's `.updateLayout()` method.
|
|
427
392
|
|
|
428
393
|
</details>
|
|
429
394
|
|
|
395
|
+
#####
|
|
396
|
+
|
|
397
|
+
<details>
|
|
398
|
+
<summary>More on <code>state</code>, <code>setState</code> and <code>onHeightChange()</code></summary>
|
|
399
|
+
|
|
400
|
+
#####
|
|
401
|
+
|
|
402
|
+
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.
|
|
403
|
+
|
|
404
|
+
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.
|
|
405
|
+
|
|
406
|
+
To fix that, `itemComponent` receives the following state management properties:
|
|
407
|
+
|
|
408
|
+
* `state` — The state of the item component. It is persisted throughout the entire lifecycle of the list.
|
|
409
|
+
|
|
410
|
+
* In the example described above, `state` might look like `{ expanded: true }`.
|
|
411
|
+
|
|
412
|
+
* This is simply a proxy for the ["core"](#core) component's `.getState().itemStates[i]`.
|
|
413
|
+
|
|
414
|
+
* `setState(newState)` — Use this function to save the item component state whenever it changes.
|
|
415
|
+
|
|
416
|
+
* In the example described above, `setState({ expanded: true/false })` would be called whenever a user clicks a "Show more"/"Show less" button.
|
|
417
|
+
|
|
418
|
+
* This is simply a proxy for the ["core"](#core) component's `.setItemState(item, newState)`.
|
|
419
|
+
|
|
420
|
+
* `onHeightDidChange()` — Call this function immediately after (if ever) the item element height has changed.
|
|
421
|
+
|
|
422
|
+
* 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.
|
|
423
|
+
|
|
424
|
+
* This is simply a proxy for the ["core"](#core) component's `.onItemHeightDidChange(item)`.
|
|
425
|
+
|
|
426
|
+
Example of using `state`/`setState()`/`onHeightDidChange()`:
|
|
427
|
+
|
|
428
|
+
```js
|
|
429
|
+
function ItemComponent({
|
|
430
|
+
item,
|
|
431
|
+
state,
|
|
432
|
+
setState,
|
|
433
|
+
onHeightDidChange
|
|
434
|
+
}) {
|
|
435
|
+
const [internalState, setInternalState] = useState(state)
|
|
436
|
+
|
|
437
|
+
const hasMounted = useRef()
|
|
438
|
+
|
|
439
|
+
useLayoutEffect(() => {
|
|
440
|
+
if (hasMounted.current) {
|
|
441
|
+
setState(internalState)
|
|
442
|
+
onHeightDidChange()
|
|
443
|
+
} else {
|
|
444
|
+
// Skip the initial mount.
|
|
445
|
+
// Only handle the changes of the `internalState`.
|
|
446
|
+
hasMounted.current = true
|
|
447
|
+
}
|
|
448
|
+
}, [internalState])
|
|
449
|
+
|
|
450
|
+
return (
|
|
451
|
+
<section>
|
|
452
|
+
<h1>
|
|
453
|
+
{item.title}
|
|
454
|
+
</h1>
|
|
455
|
+
{internalState && internalState.expanded &&
|
|
456
|
+
<p>{item.text}</p>
|
|
457
|
+
}
|
|
458
|
+
<button onClick={() => {
|
|
459
|
+
setInternalState({
|
|
460
|
+
...internalState,
|
|
461
|
+
expanded: !expanded
|
|
462
|
+
})
|
|
463
|
+
}}>
|
|
464
|
+
{internalState && internalState.expanded ? 'Show less' : 'Show more'}
|
|
465
|
+
</button>
|
|
466
|
+
</section>
|
|
467
|
+
)
|
|
468
|
+
}
|
|
469
|
+
```
|
|
470
|
+
</details>
|
|
471
|
+
|
|
472
|
+
#####
|
|
473
|
+
|
|
474
|
+
<details>
|
|
475
|
+
<summary>Server-Side Render</summary>
|
|
476
|
+
|
|
477
|
+
#####
|
|
478
|
+
|
|
479
|
+
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.
|
|
480
|
+
|
|
481
|
+
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).
|
|
482
|
+
</details>
|
|
483
|
+
|
|
484
|
+
######
|
|
485
|
+
|
|
486
|
+
<details>
|
|
487
|
+
<summary>Alternatively, instead of using <code><VirtualScroller/></code> component, one could use <code>useVirtualScroller()</code> hook</summary>
|
|
488
|
+
|
|
489
|
+
######
|
|
490
|
+
|
|
491
|
+
```js
|
|
492
|
+
import React from 'react'
|
|
493
|
+
import { useVirtualScroller } from 'virtual-scroller/react'
|
|
494
|
+
|
|
495
|
+
function List(props) {
|
|
496
|
+
const {
|
|
497
|
+
// "Core" component `state`.
|
|
498
|
+
// See "State" section of the readme for more info.
|
|
499
|
+
state: {
|
|
500
|
+
items,
|
|
501
|
+
itemStates,
|
|
502
|
+
firstShownItemIndex,
|
|
503
|
+
lastShownItemIndex
|
|
504
|
+
},
|
|
505
|
+
// CSS style object.
|
|
506
|
+
style,
|
|
507
|
+
// CSS class name.
|
|
508
|
+
className,
|
|
509
|
+
// This `ref` must be passed to the items container component.
|
|
510
|
+
itemsContainerRef,
|
|
511
|
+
// One could use this `virtualScroller` object to call any of its public methods.
|
|
512
|
+
// Except for `virtualScroller.getState()` — use the returned `state` property instead.
|
|
513
|
+
virtualScroller
|
|
514
|
+
} = useVirtualScroller({
|
|
515
|
+
// The properties of `useVirtualScroller()` hook are the same as
|
|
516
|
+
// the properties of `<VirtualScroller/>` component.
|
|
517
|
+
//
|
|
518
|
+
// Additional properties:
|
|
519
|
+
// * `style`
|
|
520
|
+
// * `className`
|
|
521
|
+
//
|
|
522
|
+
// Excluded properties:
|
|
523
|
+
// * `itemComponent`
|
|
524
|
+
// * `itemComponentProps`
|
|
525
|
+
// * `itemsContainerComponent`
|
|
526
|
+
// * `itemsContainerComponentProps`
|
|
527
|
+
//
|
|
528
|
+
items: props.items
|
|
529
|
+
})
|
|
530
|
+
|
|
531
|
+
return (
|
|
532
|
+
<div ref={itemsContainerRef} style={style} className={className}>
|
|
533
|
+
{items.map((item, i) => {
|
|
534
|
+
if (i >= firstShownItemIndex && i <= lastShownItemIndex) {
|
|
535
|
+
return (
|
|
536
|
+
<ListItem
|
|
537
|
+
key={item.id}
|
|
538
|
+
item={item}
|
|
539
|
+
state={itemStates && itemStates[i]}
|
|
540
|
+
/>
|
|
541
|
+
)
|
|
542
|
+
}
|
|
543
|
+
return null
|
|
544
|
+
})}
|
|
545
|
+
</div>
|
|
546
|
+
)
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function ListItem({ item, state }) {
|
|
550
|
+
const { username, date, text } = item
|
|
551
|
+
return (
|
|
552
|
+
<article>
|
|
553
|
+
<a href={`/users/${username}`}>
|
|
554
|
+
@{username}
|
|
555
|
+
</a>
|
|
556
|
+
<time dateTime={date.toISOString()}>
|
|
557
|
+
{date.toString()}
|
|
558
|
+
</time>
|
|
559
|
+
<p>
|
|
560
|
+
{text}
|
|
561
|
+
</p>
|
|
562
|
+
</article>
|
|
563
|
+
)
|
|
564
|
+
}
|
|
565
|
+
```
|
|
566
|
+
</details>
|
|
567
|
+
|
|
430
568
|
## DOM
|
|
431
569
|
|
|
432
570
|
`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 +649,7 @@ const virtualScroller = new VirtualScroller(
|
|
|
511
649
|
|
|
512
650
|
* `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
651
|
|
|
514
|
-
* Any other options are simply passed through to the ["core"](#core) component.
|
|
652
|
+
* Any other [options](#options) are simply passed through to the ["core"](#core) component.
|
|
515
653
|
</details>
|
|
516
654
|
|
|
517
655
|
#####
|
|
@@ -526,15 +664,15 @@ The following instance methods are just proxies for the corresponding methods of
|
|
|
526
664
|
* `start()`
|
|
527
665
|
* `stop()`
|
|
528
666
|
* `setItems(items, options)`
|
|
529
|
-
* `setItemState(
|
|
530
|
-
* `onItemHeightDidChange(
|
|
667
|
+
* `setItemState(item, itemState)`
|
|
668
|
+
* `onItemHeightDidChange(item)`
|
|
531
669
|
|
|
532
|
-
<!-- * `getItemCoordinates(
|
|
670
|
+
<!-- * `getItemCoordinates(item)` -->
|
|
533
671
|
</details>
|
|
534
672
|
|
|
535
673
|
## Core
|
|
536
674
|
|
|
537
|
-
The default export is a
|
|
675
|
+
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
676
|
|
|
539
677
|
### State
|
|
540
678
|
|
|
@@ -553,7 +691,7 @@ Sometimes though, by design, re-rendering could only be done "asynchronously" (i
|
|
|
553
691
|
|
|
554
692
|
The main `state` properties are:
|
|
555
693
|
|
|
556
|
-
* `items: any[]` — The list of items (can be updated via [`.setItems()`](#
|
|
694
|
+
* `items: any[]` — The list of items (can be updated via [`.setItems()`](#updating-items)).
|
|
557
695
|
|
|
558
696
|
* `firstShownItemIndex: number` — The index of the first item that should be rendered.
|
|
559
697
|
|
|
@@ -567,19 +705,19 @@ The following `state` properties are only used for saving and restoring `Virtual
|
|
|
567
705
|
|
|
568
706
|
* `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
707
|
|
|
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(
|
|
708
|
+
* 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
709
|
|
|
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(
|
|
710
|
+
* 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
711
|
|
|
574
712
|
* 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
713
|
|
|
576
|
-
* In this example, besides preserving the item state itself, one should also call `.onItemHeightDidChange(
|
|
714
|
+
* 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
715
|
|
|
578
716
|
* `itemHeights: number[]` — The measured heights of all items. If an item's height hasn't been measured yet then it's `undefined`.
|
|
579
717
|
|
|
580
|
-
* By default, items are only measured once: when they're initially rendered. If an item's height changes afterwards, then `.onItemHeightDidChange(
|
|
718
|
+
* 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
719
|
|
|
582
|
-
* Besides the requirement of calling `.onItemHeightDidChange(
|
|
720
|
+
* 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
721
|
|
|
584
722
|
* `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
723
|
|
|
@@ -630,112 +768,18 @@ virtualScroller.start()
|
|
|
630
768
|
virtualScroller.stop()
|
|
631
769
|
```
|
|
632
770
|
|
|
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>
|
|
642
|
-
|
|
643
|
-
#####
|
|
644
|
-
|
|
645
|
-
```js
|
|
646
|
-
import VirtualScroller from 'virtual-scroller'
|
|
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) {
|
|
771
|
+
`VirtualScroller` class constructor arguments:
|
|
680
772
|
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
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>
|
|
773
|
+
* `getContainerElement()` — returns the container "element" for the list item "elements".
|
|
774
|
+
* `items` — an array of items.
|
|
775
|
+
* `options` — (optional)
|
|
776
|
+
* `render(state, prevState)` — "re-renders" the list according to the new `state`.
|
|
777
|
+
* 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
778
|
|
|
735
779
|
#### Options
|
|
736
780
|
|
|
737
781
|
<details>
|
|
738
|
-
<summary
|
|
782
|
+
<summary>Available <code>options</code></summary>
|
|
739
783
|
|
|
740
784
|
#####
|
|
741
785
|
|
|
@@ -759,11 +803,20 @@ virtualScroller.start()
|
|
|
759
803
|
|
|
760
804
|
* `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
805
|
|
|
762
|
-
|
|
806
|
+
* `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.
|
|
807
|
+
* `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.
|
|
808
|
+
* `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.
|
|
809
|
+
* `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.
|
|
810
|
+
* 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.
|
|
811
|
+
* 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.
|
|
812
|
+
* On server side though, `getEstimatedVisibleItemRowsCount()` and `getEstimatedItemHeight()` and `getEstimatedInterItemVerticalSpacing()` completely determine the output of a "server-side render".
|
|
813
|
+
* When these parameters aren't specified, the list will render just the first item during the initial render.
|
|
814
|
+
|
|
815
|
+
#### "Advanced" (rarely-used) options
|
|
763
816
|
|
|
764
817
|
* `bypass: boolean` — Pass `true` to disable the "virtualization" behavior and just render the entire list of items.
|
|
765
818
|
|
|
766
|
-
* `getInitialItemState
|
|
819
|
+
* `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
820
|
|
|
768
821
|
* `initialScrollPosition: number` — If passed, the page will be scrolled to this `scrollY` position.
|
|
769
822
|
|
|
@@ -771,7 +824,7 @@ virtualScroller.start()
|
|
|
771
824
|
|
|
772
825
|
<!-- * `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
826
|
|
|
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.
|
|
827
|
+
* `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
828
|
|
|
776
829
|
* `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
830
|
* The function is guaranteed to be called at least once for each item that ever gets rendered.
|
|
@@ -784,17 +837,15 @@ virtualScroller.start()
|
|
|
784
837
|
|
|
785
838
|
* `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
839
|
|
|
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()`.
|
|
840
|
+
<!-- * (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
841
|
|
|
791
|
-
* `
|
|
842
|
+
* `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
843
|
</details>
|
|
793
844
|
|
|
794
845
|
#####
|
|
795
846
|
|
|
796
847
|
<details>
|
|
797
|
-
<summary
|
|
848
|
+
<summary>Instance methods</summary>
|
|
798
849
|
|
|
799
850
|
#####
|
|
800
851
|
|
|
@@ -804,7 +855,7 @@ virtualScroller.start()
|
|
|
804
855
|
|
|
805
856
|
* `getState(): object` — Returns `VirtualScroller` state.
|
|
806
857
|
|
|
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 [
|
|
858
|
+
* `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
859
|
* `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
860
|
|
|
810
861
|
#### Custom (External) State Management
|
|
@@ -841,25 +892,123 @@ When using custom (external) state management, contrary to the default (internal
|
|
|
841
892
|
|
|
842
893
|
#### "Advanced" (rarely used) instance methods
|
|
843
894
|
|
|
844
|
-
* `onItemHeightDidChange(
|
|
895
|
+
* `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
896
|
|
|
846
897
|
* 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
898
|
|
|
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.
|
|
899
|
+
* 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
900
|
|
|
850
|
-
* `setItemState(
|
|
901
|
+
* `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
902
|
|
|
852
903
|
* 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
904
|
|
|
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(
|
|
905
|
+
* 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
906
|
|
|
856
|
-
* `getItemScrollPosition(
|
|
907
|
+
* `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
908
|
|
|
858
|
-
<!-- * `getItemCoordinates(
|
|
909
|
+
<!-- * `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
910
|
|
|
860
911
|
* `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
912
|
</details>
|
|
862
913
|
|
|
914
|
+
#####
|
|
915
|
+
|
|
916
|
+
<details>
|
|
917
|
+
<summary>Example: implement <code>virtual-scroller/dom</code> component using the "core" <code>VirtualScroller</code> component
|
|
918
|
+
</summary>
|
|
919
|
+
|
|
920
|
+
#####
|
|
921
|
+
|
|
922
|
+
```js
|
|
923
|
+
import VirtualScroller from 'virtual-scroller'
|
|
924
|
+
|
|
925
|
+
const items = [
|
|
926
|
+
{ title: 'Apple' },
|
|
927
|
+
{ title: 'Banana' },
|
|
928
|
+
{ title: 'Cranberry' }
|
|
929
|
+
]
|
|
930
|
+
|
|
931
|
+
function renderItem(item) {
|
|
932
|
+
const div = document.createElement('div')
|
|
933
|
+
div.innerText = item.title
|
|
934
|
+
return div
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
const container = document.getElementById('list')
|
|
938
|
+
|
|
939
|
+
function render(state, prevState) {
|
|
940
|
+
const {
|
|
941
|
+
items,
|
|
942
|
+
beforeItemsHeight,
|
|
943
|
+
afterItemsHeight,
|
|
944
|
+
firstShownItemIndex,
|
|
945
|
+
lastShownItemIndex
|
|
946
|
+
} = state
|
|
947
|
+
|
|
948
|
+
// Set `paddingTop` and `paddingBottom` on the container element:
|
|
949
|
+
// it emulates the non-visible items as if they were rendered.
|
|
950
|
+
container.style.paddingTop = Math.round(beforeItemsHeight) + 'px'
|
|
951
|
+
container.style.paddingBottom = Math.round(afterItemsHeight) + 'px'
|
|
952
|
+
|
|
953
|
+
// Perform an intelligent "diff" re-render as the user scrolls the page.
|
|
954
|
+
// This also requires that the list of `items` hasn't been changed.
|
|
955
|
+
// On initial render, `prevState` is `undefined`.
|
|
956
|
+
if (prevState && items === prevState.items) {
|
|
957
|
+
|
|
958
|
+
// Remove no longer visible items.
|
|
959
|
+
let i = prevState.lastShownItemIndex
|
|
960
|
+
while (i >= prevState.firstShownItemIndex) {
|
|
961
|
+
if (i >= firstShownItemIndex && i <= lastShownItemIndex) {
|
|
962
|
+
// The item is still visible.
|
|
963
|
+
} else {
|
|
964
|
+
// The item is no longer visible. Remove it.
|
|
965
|
+
container.removeChild(container.childNodes[i - prevState.firstShownItemIndex])
|
|
966
|
+
}
|
|
967
|
+
i--
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
// Add newly visible items.
|
|
971
|
+
let prependBefore = container.firstChild
|
|
972
|
+
let i = firstShownItemIndex
|
|
973
|
+
while (i <= lastShownItemIndex) {
|
|
974
|
+
if (i >= prevState.firstShownItemIndex && i <= prevState.lastShownItemIndex) {
|
|
975
|
+
// The item is already being rendered.
|
|
976
|
+
// Next items will be appended rather than prepended.
|
|
977
|
+
prependBefore = undefined
|
|
978
|
+
} else {
|
|
979
|
+
if (prependBefore) {
|
|
980
|
+
container.insertBefore(renderItem(items[i]), prependBefore)
|
|
981
|
+
} else {
|
|
982
|
+
container.appendChild(renderItem(items[i]))
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
i++
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
else {
|
|
989
|
+
// Re-render the list from scratch.
|
|
990
|
+
while (container.firstChild) {
|
|
991
|
+
container.removeChild(container.firstChild)
|
|
992
|
+
}
|
|
993
|
+
let i = firstShownItemIndex
|
|
994
|
+
while (i <= lastShownItemIndex) {
|
|
995
|
+
container.appendChild(renderItem(items[i]))
|
|
996
|
+
i++
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
const virtualScroller = new VirtualScroller(() => element, items, { render })
|
|
1002
|
+
|
|
1003
|
+
// Start VirtualScroller listening for scroll events.
|
|
1004
|
+
virtualScroller.start()
|
|
1005
|
+
|
|
1006
|
+
// Stop VirtualScroller listening for scroll events
|
|
1007
|
+
// when the user navigates to another page:
|
|
1008
|
+
// router.onPageUnload(virtualScroller.stop)
|
|
1009
|
+
```
|
|
1010
|
+
</details>
|
|
1011
|
+
|
|
863
1012
|
## Updating Items
|
|
864
1013
|
|
|
865
1014
|
If the list represents a social media feed, it has to be updated periodically as new posts get published.
|
|
@@ -1129,7 +1278,7 @@ Due to the [inherent limitations](https://gitlab.com/catamphetamine/virtual-scro
|
|
|
1129
1278
|
|
|
1130
1279
|
### "Item index N height changed unexpectedly" warning on page load in development mode
|
|
1131
1280
|
|
|
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(
|
|
1281
|
+
`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
1282
|
|
|
1134
1283
|
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
1284
|
|
|
@@ -1270,7 +1419,7 @@ To include this library directly via a `<script/>` tag on a page, one can use an
|
|
|
1270
1419
|
<!--
|
|
1271
1420
|
## Possible enhancements
|
|
1272
1421
|
|
|
1273
|
-
* Use [Resize Observer](https://caniuse.com/#search=Resize%20Observer) instead of calling `.onItemHeightDidChange(
|
|
1422
|
+
* Use [Resize Observer](https://caniuse.com/#search=Resize%20Observer) instead of calling `.onItemHeightDidChange(item)` manually.
|
|
1274
1423
|
|
|
1275
1424
|
* 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
1425
|
-->
|