@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 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
- Add to your project's `marko.json` to make the tags available:
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
- <!DOCTYPE html>
35
- <html>
36
- <body>
37
-
38
- <let/mounted = false/>
39
- <script() { mounted = true }/>
40
-
41
- <div/scrollEl style="height: 400px; overflow-y: auto">
42
- <if=mounted>
43
- <virtualizer|{ virtualItems, totalSize }|
44
- count=10000
45
- estimateSize=() => 35
46
- getScrollElement=scrollEl
47
- >
48
- <div style=`height: ${totalSize}px; position: relative`>
49
- <for|item| of=virtualItems>
50
- <div
51
- data-index=item.index
52
- style=`position: absolute; transform: translateY(${item.start}px);
53
- height: ${item.size}px; width: 100%`
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
- </virtualizer>
60
- </if>
58
+ </for>
59
+ </div>
61
60
  </div>
62
-
63
- </body>
64
- </html>
65
61
  ```
66
62
 
67
- > **Why `mounted`?** The virtualizer measures the scroll element's dimensions on mount. During SSR the DOM has no layout, so `offsetHeight` is 0 and no items would be calculated. The `<if=mounted>` guard ensures the virtualizer only renders after the browser has real dimensions available.
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
- <!DOCTYPE html>
75
- <html>
76
- <body>
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
- <window-virtualizer|{ virtualItems, totalSize }| count=10000 estimateSize=() => 35>
79
- <div style=`height: ${totalSize}px; position: relative`>
80
- <for|item| of=virtualItems>
81
- <div
82
- data-index=item.index
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
- </body>
93
- </html>
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
- ## Tag parameters
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
- Both tags use Marko 6's tag parameters pattern. The body receives virtual state via `|{ ... }|` destructuring:
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
- <virtualizer|{ virtualItems, totalSize, measureElement, scrollToIndex, scrollToOffset }|
102
- count=...
103
- getScrollElement=...
104
- >
105
- <!-- virtualItems, totalSize etc are in scope here -->
106
- </virtualizer>
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
- ## API Reference
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
- ### `<virtualizer>`
149
+ Two things to know:
112
150
 
113
- | Attribute | Type | Required | Description |
114
- | -------------------- | --------------------------- | -------- | --------------------------------------------------- |
115
- | `count` | `number` | ✅ | Total number of items |
116
- | `getScrollElement` | `() => Element \| null` | ✅ | Returns the scroll container |
117
- | `estimateSize` | `(index: number) => number` | | Estimated item size in px (default: `50`) |
118
- | `overscan` | `number` | | Items to render outside the viewport (default: `5`) |
119
- | `horizontal` | `boolean` | | Enable horizontal scrolling (default: `false`) |
120
- | `paddingStart` | `number` | | Padding before the first item in px |
121
- | `paddingEnd` | `number` | | Padding after the last item in px |
122
- | `scrollPaddingStart` | `number` | | Scroll padding at the start |
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
- **Tag parameters provided to body:**
162
+ ## Dynamic / variable item sizes
129
163
 
130
- | Parameter | Type | Description |
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
- ### `<window-virtualizer>`
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
- Same as `<virtualizer>` except there is no `getScrollElement`, `horizontal`, or `initialOffset` the window is always the scroll container.
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 | Description |
151
- | ----------------- | ----------------------------------------------- |
152
- | `fixed` | Fixed-size rows, columns, and grid |
153
- | `variable` | Variable sizes via `estimateSize` |
154
- | `dynamic` | Unknown sizes measured via `measureElement` |
155
- | `grid` | Two virtualizers sharing one scroll element |
156
- | `smooth-scroll` | `scrollToIndex` with CSS smooth scrolling |
157
- | `infinite-scroll` | Lazy data loading with a fixed total count |
158
- | `window` | Full-page scrolling with `<window-virtualizer>` |
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
- ## Dynamic sizing
287
+ ## TypeScript
161
288
 
162
- Use `measureElement` to measure items whose size isn't known upfront:
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
- ```marko
165
- <virtualizer|{ virtualItems, totalSize, measureElement }|
166
- count=items.length
167
- estimateSize=() => 50
168
- getScrollElement=scrollEl
169
- >
170
- <div style=`height: ${totalSize}px; position: relative`>
171
- <for|item| of=virtualItems>
172
- <div
173
- ref=measureElement
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
- > `data-index` is required on measured elementsthe virtualizer uses it to map measurements back to items.
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
- ## TypeScript
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
- The tags are fully typed. The Marko language server reads the source `.marko` files directly — no `.d.ts` generation is needed. Ensure `@marko/language-tools` is installed in your editor for IDE support.
338
+ ## Author & license
189
339
 
190
- ## License
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 © [Tanner Linsley](https://github.com/tannerlinsley)
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
+ };