@tanstack/marko-virtual 3.14.2 → 3.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/README.md +268 -118
- package/dist/cjs/tags/virtualizer/options.d.cts +30 -1
- package/dist/cjs/tags/window-virtualizer/options.d.cts +32 -1
- package/dist/esm/tags/virtualizer/options.d.ts +30 -1
- package/dist/esm/tags/window-virtualizer/options.d.ts +32 -1
- package/dist/tags/virtualizer/index.d.marko +56 -0
- package/dist/tags/virtualizer/index.marko +59 -0
- package/dist/tags/virtualizer/options.d.ts +46 -0
- package/dist/tags/virtualizer/options.js +63 -0
- package/dist/tags/window-virtualizer/index.d.marko +55 -0
- package/dist/tags/window-virtualizer/index.marko +57 -0
- package/dist/tags/window-virtualizer/options.d.ts +45 -0
- package/dist/tags/window-virtualizer/options.js +67 -0
- package/marko.json +1 -1
- package/package.json +12 -9
- package/src/tags/virtualizer/index.marko +0 -78
- package/src/tags/virtualizer/options.ts +0 -50
- package/src/tags/window-virtualizer/index.marko +0 -75
- package/src/tags/window-virtualizer/options.ts +0 -46
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Headless UI for virtualizing scrollable elements in [Marko 6](https://markojs.com), built on top of [`@tanstack/virtual-core`](https://tanstack.com/virtual).
|
|
4
4
|
|
|
5
|
-
Part of the [TanStack Virtual](https://tanstack.com/virtual) family.
|
|
5
|
+
Part of the [TanStack Virtual](https://tanstack.com/virtual) family. Full documentation: [tanstack.com/virtual — Marko](https://tanstack.com/virtual/latest/docs/framework/marko/marko-virtual).
|
|
6
6
|
|
|
7
7
|
## Installation
|
|
8
8
|
|
|
@@ -16,7 +16,7 @@ pnpm add @tanstack/marko-virtual
|
|
|
16
16
|
|
|
17
17
|
## Setup
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
The tags are discovered automatically by the Marko compiler when the package is installed — no imports are needed in your `.marko` files. If your build setup does not pick them up (monorepo links, unusual resolution), add the taglib explicitly to your project's `marko.json`:
|
|
20
20
|
|
|
21
21
|
```json
|
|
22
22
|
{
|
|
@@ -26,118 +26,233 @@ Add to your project's `marko.json` to make the tags available:
|
|
|
26
26
|
|
|
27
27
|
## Usage
|
|
28
28
|
|
|
29
|
+
Each tag is used **self-closing** and exposes a **tag variable** (`<virtualizer/v .../>`). You own the markup: read `v.virtualItems` and `v.totalSize` to render the visible rows. Declare the virtualizer **before** the markup that reads it, and pass `getScrollElement` as an **arrow thunk** (`() => scrollEl()`) so the element ref resolves at call time.
|
|
30
|
+
|
|
29
31
|
### `<virtualizer>` — scrollable container
|
|
30
32
|
|
|
31
33
|
Use when you control the scroll element (a div with `overflow: auto`).
|
|
32
34
|
|
|
33
35
|
```marko
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
<
|
|
43
|
-
<
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
Row ${item.index}
|
|
56
|
-
</div>
|
|
57
|
-
</for>
|
|
36
|
+
<div/scrollEl
|
|
37
|
+
style="height: 400px; width: 400px; overflow-y: auto; position: relative;"
|
|
38
|
+
>
|
|
39
|
+
<virtualizer/v
|
|
40
|
+
count=10000
|
|
41
|
+
estimateSize=() => 35
|
|
42
|
+
getScrollElement=() => scrollEl()
|
|
43
|
+
/>
|
|
44
|
+
<div style=`height: ${v.totalSize}px; width: 100%; position: relative`>
|
|
45
|
+
<for|item| of=v.virtualItems>
|
|
46
|
+
<div
|
|
47
|
+
style=`
|
|
48
|
+
position: absolute;
|
|
49
|
+
top: 0;
|
|
50
|
+
left: 0;
|
|
51
|
+
width: 100%;
|
|
52
|
+
height: ${item.size}px;
|
|
53
|
+
transform: translateY(${item.start}px);
|
|
54
|
+
`
|
|
55
|
+
>
|
|
56
|
+
Row ${item.index}
|
|
58
57
|
</div>
|
|
59
|
-
</
|
|
60
|
-
</
|
|
58
|
+
</for>
|
|
59
|
+
</div>
|
|
61
60
|
</div>
|
|
62
|
-
|
|
63
|
-
</body>
|
|
64
|
-
</html>
|
|
65
61
|
```
|
|
66
62
|
|
|
67
|
-
|
|
63
|
+
No mount guard is needed: the tag renders a deterministic initial state on the server (see [SSR](#ssr-server-rendered-slices)) and the live, observing instance takes over on mount.
|
|
64
|
+
|
|
65
|
+
For columns, set `horizontal=true` and swap the width/height and `translateX` accordingly. For grids, compose two `<virtualizer>` tags (rows + columns) sharing the same scroll element — each with its own tag variable and its own `() => scrollEl()` thunk.
|
|
68
66
|
|
|
69
67
|
### `<window-virtualizer>` — full-page scrolling
|
|
70
68
|
|
|
71
69
|
Use when the page itself is the scroll container.
|
|
72
70
|
|
|
73
71
|
```marko
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
72
|
+
<window-virtualizer/v
|
|
73
|
+
count=10000
|
|
74
|
+
estimateSize=() => 35
|
|
75
|
+
/>
|
|
76
|
+
<div style=`height: ${v.totalSize}px; position: relative`>
|
|
77
|
+
<for|item| of=v.virtualItems>
|
|
78
|
+
<div
|
|
79
|
+
style=`
|
|
80
|
+
position: absolute;
|
|
81
|
+
top: 0;
|
|
82
|
+
left: 0;
|
|
83
|
+
width: 100%;
|
|
84
|
+
height: ${item.size}px;
|
|
85
|
+
transform: translateY(${item.start}px);
|
|
86
|
+
`
|
|
87
|
+
>
|
|
88
|
+
Row ${item.index}
|
|
89
|
+
</div>
|
|
90
|
+
</for>
|
|
91
|
+
</div>
|
|
92
|
+
```
|
|
77
93
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
style=`position: absolute; transform: translateY(${item.start}px);
|
|
84
|
-
height: ${item.size}px; width: 100%`
|
|
85
|
-
>
|
|
86
|
-
Row ${item.index}
|
|
87
|
-
</div>
|
|
88
|
-
</for>
|
|
89
|
-
</div>
|
|
90
|
-
</window-virtualizer>
|
|
94
|
+
If content sits above the list (it almost always does on a real page), measure the list's `offsetTop` on mount and pass it as `scrollMargin`, then subtract it from `item.start` when positioning — see the `window` example.
|
|
95
|
+
|
|
96
|
+
## SSR: server-rendered slices
|
|
97
|
+
|
|
98
|
+
Server rendering is **opt-in per virtualizer** via `initialRect` (a viewport size hint). When set, the server paints the initial visible rows directly into the HTML; without it the server renders an empty container and the client fills it on mount — byte-for-byte identical to a client-only build. Pair with `initialOffset` (a number) to server-render at a scroll position (deep links, restore):
|
|
91
99
|
|
|
92
|
-
|
|
93
|
-
|
|
100
|
+
```marko
|
|
101
|
+
<virtualizer/v
|
|
102
|
+
count=10000
|
|
103
|
+
estimateSize=() => 35
|
|
104
|
+
getScrollElement=() => scrollEl()
|
|
105
|
+
initialRect=({ width: 800, height: 400 })
|
|
106
|
+
initialOffset=3500
|
|
107
|
+
/>
|
|
94
108
|
```
|
|
95
109
|
|
|
96
|
-
|
|
110
|
+
Nothing live is serialized — the slice is computed as plain data at render time and recomputes identically on resume. See the five `ssr*` examples for the full patterns, including measurement-cache restore.
|
|
111
|
+
|
|
112
|
+
### Streaming SSR: the tag inside `<await>`
|
|
97
113
|
|
|
98
|
-
|
|
114
|
+
The tags compose with Marko's streaming out of the box — put a virtualizer inside an
|
|
115
|
+
`<await>` and the whole pattern above still holds, per streamed chunk:
|
|
99
116
|
|
|
100
117
|
```marko
|
|
101
|
-
<
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
>
|
|
105
|
-
|
|
106
|
-
</
|
|
118
|
+
<try>
|
|
119
|
+
<@placeholder>
|
|
120
|
+
<p>Loading people…</p>
|
|
121
|
+
</@placeholder>
|
|
122
|
+
<@catch|err|>
|
|
123
|
+
<p>Failed to load: ${(err as Error).message}</p>
|
|
124
|
+
</@catch>
|
|
125
|
+
|
|
126
|
+
<await|people| value=fetchPeople()>
|
|
127
|
+
<div/scrollEl class="scroll-container">
|
|
128
|
+
<virtualizer/v
|
|
129
|
+
count=people.length
|
|
130
|
+
estimateSize=() => 48
|
|
131
|
+
getScrollElement=() => scrollEl()
|
|
132
|
+
initialRect=({ width: 800, height: 400 })
|
|
133
|
+
/>
|
|
134
|
+
<!-- sizer + rows exactly as usual -->
|
|
135
|
+
</div>
|
|
136
|
+
</await>
|
|
137
|
+
</try>
|
|
107
138
|
```
|
|
108
139
|
|
|
109
|
-
|
|
140
|
+
What happens on the wire: the server flushes the page shell (with the placeholder)
|
|
141
|
+
**immediately**, keeps streaming, and when `fetchPeople()` resolves it flushes the
|
|
142
|
+
awaited subtree as a **later chunk** — including the server-painted rows when
|
|
143
|
+
`initialRect` is set. Each streamed chunk **resumes independently** on the client:
|
|
144
|
+
the live virtualizer instance for this list mounts when its chunk arrives, without
|
|
145
|
+
waiting for the rest of the page. Nothing extra to configure — the seed is computed
|
|
146
|
+
inside the awaited subtree from the resolved data, so server and resumed client
|
|
147
|
+
agree by construction.
|
|
110
148
|
|
|
111
|
-
|
|
149
|
+
Two things to know:
|
|
112
150
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
| `scrollPaddingEnd` | `number` | | Scroll padding at the end |
|
|
124
|
-
| `gap` | `number` | | Gap between items in px |
|
|
125
|
-
| `lanes` | `number` | | Number of lanes for grid layouts |
|
|
126
|
-
| `initialOffset` | `number \| (() => number)` | | Initial scroll offset |
|
|
151
|
+
- Marko streams awaited content **out of order**: the chunk arrives at the end of
|
|
152
|
+
the byte stream and an inline script swaps it into place. With JavaScript
|
|
153
|
+
disabled that swap never runs, so an `<await>`-wrapped list renders blank without
|
|
154
|
+
JS even though the row HTML is present in the source. If no-JS visibility
|
|
155
|
+
matters, fetch **before** render (await the data in the route) instead of
|
|
156
|
+
streaming.
|
|
157
|
+
- The `ssr-fetch` example shows the streamed pattern with client-rendered rows; the
|
|
158
|
+
`ssr-slice` example shows it with **server-painted** rows (`initialRect`) — its
|
|
159
|
+
test suite includes a wire-level assertion that the placeholder flushes first and
|
|
160
|
+
the painted rows arrive in a later chunk.
|
|
127
161
|
|
|
128
|
-
|
|
162
|
+
## Dynamic / variable item sizes
|
|
129
163
|
|
|
130
|
-
|
|
131
|
-
| ---------------- | ----------------------------------------------------- | -------------------------------------------------------- |
|
|
132
|
-
| `virtualItems` | `VirtualItem[]` | Items to render, with `index`, `start`, `size`, `key` |
|
|
133
|
-
| `totalSize` | `number` | Total scrollable size in px — set on the inner container |
|
|
134
|
-
| `measureElement` | `(el: Element \| null) => void` | Pass to `ref` for dynamic size measurement |
|
|
135
|
-
| `scrollToIndex` | `(index: number, options?: ScrollToOptions) => void` | Scroll to an item by index |
|
|
136
|
-
| `scrollToOffset` | `(offset: number, options?: ScrollToOptions) => void` | Scroll to a px offset |
|
|
164
|
+
For items with unknown heights, drive `measureElement` from a per-row `<script>`:
|
|
137
165
|
|
|
138
|
-
|
|
166
|
+
```marko
|
|
167
|
+
<div/scrollEl style="height: 400px; overflow-y: auto">
|
|
168
|
+
<virtualizer/v
|
|
169
|
+
count=data.length
|
|
170
|
+
estimateSize=() => 50
|
|
171
|
+
getScrollElement=() => scrollEl()
|
|
172
|
+
/>
|
|
173
|
+
<div style=`height: ${v.totalSize}px; position: relative`>
|
|
174
|
+
<for|item| of=v.virtualItems>
|
|
175
|
+
<div/el
|
|
176
|
+
data-index=item.index
|
|
177
|
+
style=`position: absolute; top: 0; width: 100%; transform: translateY(${item.start}px)`>
|
|
178
|
+
<script() {
|
|
179
|
+
// re-run when the item changes; measureElement reads the rendered
|
|
180
|
+
// height and feeds it back to the virtualizer
|
|
181
|
+
const _key = item.key
|
|
182
|
+
if (el() && v.measureElement) v.measureElement(el())
|
|
183
|
+
}/>
|
|
184
|
+
${data[item.index].text}
|
|
185
|
+
</div>
|
|
186
|
+
</for>
|
|
187
|
+
</div>
|
|
188
|
+
</div>
|
|
189
|
+
```
|
|
139
190
|
|
|
140
|
-
|
|
191
|
+
> `data-index` is required on measured elements — the virtualizer uses it to map measurements back to items. Measuring under a different attribute name requires passing that name as `indexAttribute`.
|
|
192
|
+
|
|
193
|
+
## Tag variable reference
|
|
194
|
+
|
|
195
|
+
Both tags expose the same shape. Capture it with `<virtualizer/v .../>` and read `v.property`:
|
|
196
|
+
|
|
197
|
+
| Property | Type | Description |
|
|
198
|
+
| -------------------- | ----------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
199
|
+
| `virtualItems` | `VirtualItem[]` | The currently visible virtual items (`index`, `start`, `size`, `key`, `lane`) |
|
|
200
|
+
| `totalSize` | `number` | Total scrollable size in px — set as the inner container's `height` (or `width` for columns). Margin-free: `scrollMargin` is already subtracted |
|
|
201
|
+
| `range` | `{ startIndex: number; endIndex: number } \| null` | The visible index window (excludes overscan). `null` until there is a window |
|
|
202
|
+
| `measureElement` | `(el: Element \| null) => void` | Ref callback for dynamic item sizing |
|
|
203
|
+
| `scrollToIndex` | `(index: number, options?: ScrollToOptions) => void` | Scroll to an item by index. Default `align: 'auto'` scrolls the minimum |
|
|
204
|
+
| `scrollToOffset` | `(offset: number, options?: ScrollToOptions) => void` | Scroll to a pixel offset |
|
|
205
|
+
| `measure` | `() => void` | Drop all measured sizes and re-measure everything (after a width/font change) |
|
|
206
|
+
| `resizeItem` | `(index: number, size: number) => void` | Set one item's size directly, without a DOM measure |
|
|
207
|
+
| `scrollToEnd` | `(options?: { behavior?: ScrollBehavior }) => void` | Scroll to the very end of the list |
|
|
208
|
+
| `isAtEnd` | `(threshold?: number) => boolean` | Whether the scroll position is at (or within `threshold` px of) the end. `false` before mount |
|
|
209
|
+
| `getDistanceFromEnd` | `() => number` | Pixels between the current scroll position and the end. `Infinity` before mount |
|
|
210
|
+
|
|
211
|
+
> Inside your **own** `onScroll` handler, compute end-proximity from the element (`el.scrollHeight - el.scrollTop - el.clientHeight`) rather than calling `v.isAtEnd()` — your handler runs before the virtualizer's listener during the same event, so the virtualizer's numbers are one event stale there. Everywhere else they are current.
|
|
212
|
+
|
|
213
|
+
## `<virtualizer>` input reference
|
|
214
|
+
|
|
215
|
+
| Prop | Type | Default | Description |
|
|
216
|
+
| ------------------------------------- | ----------------------------------------------- | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
217
|
+
| `count` | `number` | required | Number of items |
|
|
218
|
+
| `getScrollElement` | `() => Element \| null` | required | Returns the scroll container |
|
|
219
|
+
| `estimateSize` | `(index: number) => number` | `() => 50` | Estimated item size in px |
|
|
220
|
+
| `overscan` | `number` | `5` | Items to render beyond the visible area |
|
|
221
|
+
| `horizontal` | `boolean` | `false` | Virtualise horizontally (columns) |
|
|
222
|
+
| `paddingStart` | `number` | — | Padding before first item |
|
|
223
|
+
| `paddingEnd` | `number` | — | Padding after last item |
|
|
224
|
+
| `scrollPaddingStart` | `number` | — | Scroll padding for `scrollToIndex` |
|
|
225
|
+
| `scrollPaddingEnd` | `number` | — | Scroll padding for `scrollToIndex` |
|
|
226
|
+
| `gap` | `number` | — | Gap between items in px |
|
|
227
|
+
| `lanes` | `number` | `1` | Lanes for masonry layouts |
|
|
228
|
+
| `initialOffset` | `number \| (() => number)` | — | Scroll offset (px) for the server slice — server-render at a scroll position (deep link / restore) |
|
|
229
|
+
| `initialRect` | `{ width: number; height: number }` | — | Viewport hint for a server-rendered slice (SSR). When set, the server paints the initial visible rows; omit for client-only fill |
|
|
230
|
+
| `getItemKey` | `(index: number) => number \| string \| bigint` | the index | Stable per-item identity so cached measurements survive reorder |
|
|
231
|
+
| `rangeExtractor` | `(range: Range) => number[]` | `defaultRangeExtractor` | Hook over the visible range: force extra indexes (e.g. a pinned sticky header) into the rendered window. Compose with `defaultRangeExtractor` from `@tanstack/virtual-core` |
|
|
232
|
+
| `indexAttribute` | `string` | `'data-index'` | DOM attribute carrying the item index for `measureElement`. Give two instances measuring the same element (a grid cell) distinct attributes |
|
|
233
|
+
| `initialMeasurementsCache` | `VirtualItem[]` | — | Pre-measured items (plain data) to seed the measurement cache |
|
|
234
|
+
| `anchorTo` | `'start' \| 'end'` | `'start'` | Anchor the window to the list end (a chat pinned to newest). Client-behavioral: it does not position the server slice — use `initialOffset` for that |
|
|
235
|
+
| `followOnAppend` | `boolean \| ScrollBehavior` | `false` | With `anchorTo="end"`: stay pinned to the end as items append |
|
|
236
|
+
| `scrollEndThreshold` | `number` | `1` | How close (px) to the end still counts as "at the end" |
|
|
237
|
+
| `scrollMargin` | `number` | `0` | The list's offset (px) from the top of its scroll area, when other content sits above it in the same scroller. `item.start` values then INCLUDE this margin — subtract it when positioning items relative to the list (see the window example) |
|
|
238
|
+
| `enabled` | `boolean` | `true` | Disable switch. `false` is not a freeze: the virtualizer unobserves, clears its measurements, and renders an empty window until re-enabled |
|
|
239
|
+
| `isRtl` | `boolean` | `false` | Right-to-left horizontal lists |
|
|
240
|
+
| `isScrollingResetDelay` | `number` | `150` | ms after the last scroll event before "user is scrolling" ends |
|
|
241
|
+
| `useScrollendEvent` | `boolean` | `false` | Use the native `scrollend` event instead of the `isScrollingResetDelay` timer |
|
|
242
|
+
| `useAnimationFrameWithResizeObserver` | `boolean` | `false` | Batch ResizeObserver measurements into animation frames (avoids "ResizeObserver loop" console errors under heavy resize load) |
|
|
243
|
+
| `laneAssignmentMode` | `'estimate' \| 'measured'` | `'estimate'` | Masonry/multi-lane: assign items to lanes by estimated or measured sizes |
|
|
244
|
+
| `useCachedMeasurements` | `boolean` | `false` | Make the default measurer return the cached (or estimated) size instead of reading the DOM — freezes item sizes when they are already known |
|
|
245
|
+
| `debug` | `boolean` | `false` | Verbose engine logging |
|
|
246
|
+
| `measureElement` | `(element, entry, instance) => number` | border-box measurer | Replace HOW an item's size is read from its element (e.g. include margins, or measure a child) |
|
|
247
|
+
|
|
248
|
+
## `<window-virtualizer>` input reference
|
|
249
|
+
|
|
250
|
+
Same as `<virtualizer>` except `getScrollElement` is not accepted — the scroll element is always `window`. Two notes:
|
|
251
|
+
|
|
252
|
+
- `horizontal` is accepted (the page scrolls sideways) and defaults to `false`.
|
|
253
|
+
- `initialOffset` defaults to the live window scroll position (`window.scrollY`, or `window.scrollX` when `horizontal`) on the client and `0` on the server. Pass a number to override it — e.g. to server-render a slice at a scroll position, paired with `initialRect`.
|
|
254
|
+
|
|
255
|
+
`scrollMargin` is the signature window-virtualizer option: pass the list's offset from the top of the document and subtract it from `item.start` when positioning.
|
|
141
256
|
|
|
142
257
|
## Examples
|
|
143
258
|
|
|
@@ -147,46 +262,81 @@ All examples use `@marko/run`. Run any with:
|
|
|
147
262
|
pnpm --filter tanstack-marko-virtual-example-<name> dev
|
|
148
263
|
```
|
|
149
264
|
|
|
150
|
-
| Example
|
|
151
|
-
|
|
|
152
|
-
| `fixed`
|
|
153
|
-
| `variable`
|
|
154
|
-
| `dynamic`
|
|
155
|
-
| `grid`
|
|
156
|
-
| `
|
|
157
|
-
| `
|
|
158
|
-
| `
|
|
265
|
+
| Example | Description |
|
|
266
|
+
| ------------------ | ----------------------------------------------------------------------------------------------------------- |
|
|
267
|
+
| `fixed` | Fixed-size rows, columns, and grid |
|
|
268
|
+
| `variable` | Variable sizes via `estimateSize` |
|
|
269
|
+
| `dynamic` | Unknown sizes measured via `measureElement` |
|
|
270
|
+
| `grid` | Two virtualizers sharing one scroll element |
|
|
271
|
+
| `pretext` | Calculated text heights via `@chenglou/pretext` (no estimate error) |
|
|
272
|
+
| `padding` | `paddingStart` / `paddingEnd` |
|
|
273
|
+
| `scroll-padding` | `scrollPaddingStart` with `scrollToIndex` |
|
|
274
|
+
| `sticky` | Sticky group headers via `rangeExtractor` |
|
|
275
|
+
| `infinite-scroll` | Lazy data loading with a fixed total count |
|
|
276
|
+
| `chat` | End-anchored messaging (`anchorTo="end"`, `followOnAppend`), history prepends, a real server-streamed reply |
|
|
277
|
+
| `chat-pretext` | Chat rebuilt on calculated heights: zero-correction prepends, streamed reply growing via `resizeItem` |
|
|
278
|
+
| `smooth-scroll` | `scrollToIndex` with smooth behavior |
|
|
279
|
+
| `table` | Virtualized table rows |
|
|
280
|
+
| `window` | Full-page scrolling with `scrollMargin` measured on mount |
|
|
281
|
+
| `ssr` | SSR: no fetch, rows render on client |
|
|
282
|
+
| `ssr-fetch` | SSR: fetch on server, rows render on client |
|
|
283
|
+
| `ssr-slice` | SSR: fetch on server, rows render on server (`initialRect`) |
|
|
284
|
+
| `ssr-restore` | SSR: server slice at an offset + measurement-cache restore |
|
|
285
|
+
| `window-ssr-slice` | SSR window: rows render on server |
|
|
159
286
|
|
|
160
|
-
##
|
|
287
|
+
## TypeScript
|
|
161
288
|
|
|
162
|
-
|
|
289
|
+
The tags are fully typed. Each tag ships a committed `index.d.marko` (a types-only twin, like a `.d.ts` for a Marko template) that editors and [`@marko/type-check`](https://github.com/marko-js/language-server/tree/main/packages/type-check) prefer over the implementation file. Both tags also export a named handle interface — `VirtualizerHandle` / `WindowVirtualizerHandle` — describing the tag-variable shape, which you can use to type your own variables and functions.
|
|
163
290
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
data-index=item.index
|
|
175
|
-
style=`position: absolute; transform: translateY(${item.start}px); width: 100%`
|
|
176
|
-
>
|
|
177
|
-
${items[item.index]}
|
|
178
|
-
</div>
|
|
179
|
-
</for>
|
|
180
|
-
</div>
|
|
181
|
-
</virtualizer>
|
|
291
|
+
### Verification (CI)
|
|
292
|
+
|
|
293
|
+
`pnpm test:types` runs `mtc` (marko-type-check) over the package: the `.ts` sources, the `.marko` tags and test fixtures, the tests, and the committed `.d.marko` files. This is what CI runs — CI only ever **verifies**, it never generates. If a tag's surface changes and the `.d.marko` files were not regenerated, this check fails instead of letting the types drift silently. The project file is `tsconfig.typecheck.json`.
|
|
294
|
+
|
|
295
|
+
### Generating `.d.marko` (manual, on demand)
|
|
296
|
+
|
|
297
|
+
The `.d.marko` files are **generated, committed artifacts** — regenerate them whenever a tag's `Input` or return surface changes:
|
|
298
|
+
|
|
299
|
+
```bash
|
|
300
|
+
pnpm types:generate
|
|
182
301
|
```
|
|
183
302
|
|
|
184
|
-
|
|
303
|
+
The script (`scripts/generate-d-marko.mjs`) verifies the sources type-check, deletes the old `.d.marko` (mtc prefers them as input when present), emits fresh ones via `tsconfig.emit.json`, copies them back beside the sources, and re-verifies. Review the diff and commit. Two notes: `TS6059` rootDir warnings during the emit step are a known benign artifact of the cross-package `virtual-core` paths mapping (the script accounts for them), and generation relies on the tags' **named** return interfaces — mtc's emitter truncates wide anonymous inline types, so keep returns cast to the exported handle types.
|
|
185
304
|
|
|
186
|
-
|
|
305
|
+
### Running all e2e Tests
|
|
306
|
+
|
|
307
|
+
To run all the e2e tests under each of `examples/marko/<example-name>/e2e` use the command
|
|
308
|
+
|
|
309
|
+
```bash
|
|
310
|
+
pnpm -r --workspace-concurrency=1 --filter "./examples/marko/*" run test:e2e
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
_Note:_ This has to be run sequentially as they all start the same port.
|
|
314
|
+
|
|
315
|
+
_Note:_ All suites run against **dev servers**; production builds are not exercised by
|
|
316
|
+
any test. After bumping `marko` (or other build-path dependencies) or changing the
|
|
317
|
+
package's build/packaging config, sanity-check production builds manually:
|
|
318
|
+
`pnpm --filter <example> build` then `preview` — and check **both** kinds of example:
|
|
319
|
+
`fixed` (render-only — rows must appear; this is the case production tree-shaking can
|
|
320
|
+
silently break) and `chat` (interactive — click through Stream reply).
|
|
321
|
+
|
|
322
|
+
### Running all unit tests
|
|
323
|
+
|
|
324
|
+
To run all the unit tests specifically for `marko-virtual/tests` use the command
|
|
325
|
+
|
|
326
|
+
```bash
|
|
327
|
+
pnpm --filter @tanstack/marko-virtual exec vitest run --reporter=verbose
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
### Pre-requisite before running tests
|
|
331
|
+
|
|
332
|
+
```bash
|
|
333
|
+
pnpm install
|
|
334
|
+
pnpm --filter @tanstack/virtual-core build
|
|
335
|
+
pnpm --filter @tanstack/marko-virtual build
|
|
336
|
+
```
|
|
187
337
|
|
|
188
|
-
|
|
338
|
+
## Author & license
|
|
189
339
|
|
|
190
|
-
|
|
340
|
+
Authored and maintained as part of [TanStack Virtual](https://tanstack.com/virtual) by [Tanner Linsley](https://github.com/tannerlinsley) and contributors; Marko adapter contributed by `defunkt-dev` (`@DSz340`). If this library helps you, consider [sponsoring TanStack](https://github.com/sponsors/tannerlinsley).
|
|
191
341
|
|
|
192
|
-
MIT
|
|
342
|
+
[MIT](https://github.com/TanStack/virtual/blob/main/LICENSE) © Tanner Linsley
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { VirtualizerOptions } from '@tanstack/virtual-core';
|
|
1
|
+
import { Virtualizer, Range, VirtualItem, VirtualizerOptions } from '@tanstack/virtual-core';
|
|
2
2
|
export interface VirtualizerInput {
|
|
3
3
|
count: number;
|
|
4
4
|
estimateSize?: (index: number) => number;
|
|
@@ -12,5 +12,34 @@ export interface VirtualizerInput {
|
|
|
12
12
|
gap?: number;
|
|
13
13
|
lanes?: number;
|
|
14
14
|
initialOffset?: number | (() => number);
|
|
15
|
+
initialRect?: {
|
|
16
|
+
width: number;
|
|
17
|
+
height: number;
|
|
18
|
+
};
|
|
19
|
+
getItemKey?: (index: number) => number | string | bigint;
|
|
20
|
+
rangeExtractor?: (range: Range) => Array<number>;
|
|
21
|
+
indexAttribute?: string;
|
|
22
|
+
initialMeasurementsCache?: Array<VirtualItem>;
|
|
23
|
+
anchorTo?: 'start' | 'end';
|
|
24
|
+
followOnAppend?: boolean | ScrollBehavior;
|
|
25
|
+
scrollEndThreshold?: number;
|
|
26
|
+
scrollMargin?: number;
|
|
27
|
+
enabled?: boolean;
|
|
28
|
+
isRtl?: boolean;
|
|
29
|
+
isScrollingResetDelay?: number;
|
|
30
|
+
useScrollendEvent?: boolean;
|
|
31
|
+
useAnimationFrameWithResizeObserver?: boolean;
|
|
32
|
+
laneAssignmentMode?: 'estimate' | 'measured';
|
|
33
|
+
useCachedMeasurements?: boolean;
|
|
34
|
+
debug?: boolean;
|
|
35
|
+
measureElement?: (element: Element, entry: ResizeObserverEntry | undefined, instance: Virtualizer<Element, Element>) => number;
|
|
15
36
|
}
|
|
16
37
|
export declare function buildOptions(input: VirtualizerInput, notify: () => void): VirtualizerOptions<Element, Element>;
|
|
38
|
+
export declare function renderSlice(input: VirtualizerInput): {
|
|
39
|
+
items: Array<VirtualItem>;
|
|
40
|
+
size: number;
|
|
41
|
+
range: {
|
|
42
|
+
startIndex: number;
|
|
43
|
+
endIndex: number;
|
|
44
|
+
} | null;
|
|
45
|
+
};
|
|
@@ -1,13 +1,44 @@
|
|
|
1
|
-
import { VirtualizerOptions } from '@tanstack/virtual-core';
|
|
1
|
+
import { Virtualizer, Range, VirtualItem, VirtualizerOptions } from '@tanstack/virtual-core';
|
|
2
2
|
export interface WindowVirtualizerInput {
|
|
3
3
|
count: number;
|
|
4
4
|
estimateSize?: (index: number) => number;
|
|
5
5
|
overscan?: number;
|
|
6
|
+
horizontal?: boolean;
|
|
6
7
|
paddingStart?: number;
|
|
7
8
|
paddingEnd?: number;
|
|
8
9
|
scrollPaddingStart?: number;
|
|
9
10
|
scrollPaddingEnd?: number;
|
|
10
11
|
gap?: number;
|
|
11
12
|
lanes?: number;
|
|
13
|
+
initialOffset?: number | (() => number);
|
|
14
|
+
initialRect?: {
|
|
15
|
+
width: number;
|
|
16
|
+
height: number;
|
|
17
|
+
};
|
|
18
|
+
getItemKey?: (index: number) => number | string | bigint;
|
|
19
|
+
rangeExtractor?: (range: Range) => Array<number>;
|
|
20
|
+
indexAttribute?: string;
|
|
21
|
+
initialMeasurementsCache?: Array<VirtualItem>;
|
|
22
|
+
anchorTo?: 'start' | 'end';
|
|
23
|
+
followOnAppend?: boolean | ScrollBehavior;
|
|
24
|
+
scrollEndThreshold?: number;
|
|
25
|
+
scrollMargin?: number;
|
|
26
|
+
enabled?: boolean;
|
|
27
|
+
isRtl?: boolean;
|
|
28
|
+
isScrollingResetDelay?: number;
|
|
29
|
+
useScrollendEvent?: boolean;
|
|
30
|
+
useAnimationFrameWithResizeObserver?: boolean;
|
|
31
|
+
laneAssignmentMode?: 'estimate' | 'measured';
|
|
32
|
+
useCachedMeasurements?: boolean;
|
|
33
|
+
debug?: boolean;
|
|
34
|
+
measureElement?: (element: Element, entry: ResizeObserverEntry | undefined, instance: Virtualizer<Window, Element>) => number;
|
|
12
35
|
}
|
|
13
36
|
export declare function buildOptions(input: WindowVirtualizerInput, notify: () => void): VirtualizerOptions<Window, Element>;
|
|
37
|
+
export declare function renderSlice(input: WindowVirtualizerInput): {
|
|
38
|
+
items: Array<VirtualItem>;
|
|
39
|
+
size: number;
|
|
40
|
+
range: {
|
|
41
|
+
startIndex: number;
|
|
42
|
+
endIndex: number;
|
|
43
|
+
} | null;
|
|
44
|
+
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { VirtualizerOptions } from '@tanstack/virtual-core';
|
|
1
|
+
import { Virtualizer, Range, VirtualItem, VirtualizerOptions } from '@tanstack/virtual-core';
|
|
2
2
|
export interface VirtualizerInput {
|
|
3
3
|
count: number;
|
|
4
4
|
estimateSize?: (index: number) => number;
|
|
@@ -12,5 +12,34 @@ export interface VirtualizerInput {
|
|
|
12
12
|
gap?: number;
|
|
13
13
|
lanes?: number;
|
|
14
14
|
initialOffset?: number | (() => number);
|
|
15
|
+
initialRect?: {
|
|
16
|
+
width: number;
|
|
17
|
+
height: number;
|
|
18
|
+
};
|
|
19
|
+
getItemKey?: (index: number) => number | string | bigint;
|
|
20
|
+
rangeExtractor?: (range: Range) => Array<number>;
|
|
21
|
+
indexAttribute?: string;
|
|
22
|
+
initialMeasurementsCache?: Array<VirtualItem>;
|
|
23
|
+
anchorTo?: 'start' | 'end';
|
|
24
|
+
followOnAppend?: boolean | ScrollBehavior;
|
|
25
|
+
scrollEndThreshold?: number;
|
|
26
|
+
scrollMargin?: number;
|
|
27
|
+
enabled?: boolean;
|
|
28
|
+
isRtl?: boolean;
|
|
29
|
+
isScrollingResetDelay?: number;
|
|
30
|
+
useScrollendEvent?: boolean;
|
|
31
|
+
useAnimationFrameWithResizeObserver?: boolean;
|
|
32
|
+
laneAssignmentMode?: 'estimate' | 'measured';
|
|
33
|
+
useCachedMeasurements?: boolean;
|
|
34
|
+
debug?: boolean;
|
|
35
|
+
measureElement?: (element: Element, entry: ResizeObserverEntry | undefined, instance: Virtualizer<Element, Element>) => number;
|
|
15
36
|
}
|
|
16
37
|
export declare function buildOptions(input: VirtualizerInput, notify: () => void): VirtualizerOptions<Element, Element>;
|
|
38
|
+
export declare function renderSlice(input: VirtualizerInput): {
|
|
39
|
+
items: Array<VirtualItem>;
|
|
40
|
+
size: number;
|
|
41
|
+
range: {
|
|
42
|
+
startIndex: number;
|
|
43
|
+
endIndex: number;
|
|
44
|
+
} | null;
|
|
45
|
+
};
|