-
-
Notifications
You must be signed in to change notification settings - Fork 445
feat(marko-virtual): add @tanstack/marko-virtual adapter for Marko v6 #1156
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
25 commits
Select commit
Hold shift + click to select a range
2fcb5ba
first set of changes
f5f8539
fix more breaking things
7e0343c
more fixes
34d5fb0
Fix examples
fa3e607
Fix more issues
3ca9e94
fix tests
6e0a456
fix everything
bc7f380
minor thingies
759c7b7
minor fixes
9bcc66f
more fixes
5a6d720
Fix some review comments
2a32067
Fix iframe in docs missing title
286156b
few more minor fixes and tests
9485946
ci: apply automated fixes
autofix-ci[bot] aa90c4e
fix(marko-virtual): address code review feedback and test coverage gaps
49b03ea
merge & resolve conflicts
052e9bf
Merge branch 'main' of github.com:TanStack/virtual
defunkt-dev 6cca898
merge & resolve conflicts after pull from upstream/main
defunkt-dev 42c1b18
fix: PR review comments
defunkt-dev 9114d54
Merge branch 'main' of github.com:TanStack/virtual
defunkt-dev 8fd285d
Fix the lock file
defunkt-dev ecf8034
Merge branch 'main' of github.com:TanStack/virtual
defunkt-dev 54f9629
Merge branch 'main' into main
piecyk 29f55c0
fix pr review comments
defunkt-dev 08b91cb
del files .md in examples
defunkt-dev File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| '@tanstack/marko-virtual': minor | ||
| --- | ||
|
|
||
| Add `@tanstack/marko-virtual` β a headless virtualisation adapter for Marko 6 using the runtime-tags API. Provides `<virtualizer>` and `<window-virtualizer>` tags covering fixed, variable, dynamic, grid, smooth-scroll, infinite-scroll, and window virtualisation patterns. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,248 @@ | ||
| # Marko Virtual | ||
|
|
||
| `@tanstack/marko-virtual` is the Marko 6 adapter for TanStack Virtual. It provides | ||
| row, column, and grid virtualisation via two auto-discovered Marko tags: | ||
|
|
||
| - **`<virtualizer>`** β element-based scrolling (rows, columns, grids) | ||
| - **`<window-virtualizer>`** β full-page/window scrolling | ||
|
|
||
| Tags are discovered automatically by the Marko compiler when the package is | ||
| installed. No imports are needed in your `.marko` files. | ||
|
|
||
| ## Installation | ||
|
|
||
| ```sh | ||
| npm install @tanstack/marko-virtual | ||
| ``` | ||
|
|
||
| ## Row virtualisation | ||
|
|
||
| ```marko | ||
| <let/mounted = false/> | ||
| <lifecycle onMount() { mounted = true }/> | ||
|
|
||
| <if=mounted> | ||
| <div/scrollEl | ||
| style="height: 400px; width: 400px; overflow-y: auto; position: relative;" | ||
| > | ||
| <virtualizer|{ virtualItems, totalSize }| | ||
| count=10000 | ||
| estimateSize=() => 35 | ||
| getScrollElement=scrollEl | ||
| > | ||
| <div style=`height: ${totalSize}px; width: 100%; position: relative`> | ||
| <for|item| of=virtualItems> | ||
| <div | ||
| style=` | ||
| position: absolute; | ||
| top: 0; | ||
| left: 0; | ||
| width: 100%; | ||
| height: ${item.size}px; | ||
| transform: translateY(${item.start}px); | ||
| ` | ||
| > | ||
| Row ${item.index} | ||
| </div> | ||
| </for> | ||
| </div> | ||
| </virtualizer> | ||
| </div> | ||
| </if> | ||
| ``` | ||
|
|
||
| ## Column virtualisation | ||
|
|
||
| Same tag, `horizontal=true`: | ||
|
|
||
| ```marko | ||
| <div/scrollEl | ||
| style="width: 400px; height: 100px; overflow-x: auto; position: relative;" | ||
| > | ||
| <virtualizer|{ virtualItems, totalSize }| | ||
| count=10000 | ||
| estimateSize=() => 100 | ||
| horizontal=true | ||
| getScrollElement=scrollEl | ||
| > | ||
| <div style=`width: ${totalSize}px; height: 100%; position: relative`> | ||
| <for|item| of=virtualItems> | ||
| <div | ||
| style=` | ||
| position: absolute; | ||
| top: 0; | ||
| left: 0; | ||
| height: 100%; | ||
| width: ${item.size}px; | ||
| transform: translateX(${item.start}px); | ||
| ` | ||
| > | ||
| Column ${item.index} | ||
| </div> | ||
| </for> | ||
| </div> | ||
| </virtualizer> | ||
| </div> | ||
| ``` | ||
|
|
||
| ## Grid virtualisation | ||
|
|
||
| Compose two `<virtualizer>` tags β one for rows, one for columns β sharing the | ||
| same scroll element: | ||
|
|
||
| ```marko | ||
| <div/scrollEl | ||
| style="height: 500px; width: 500px; overflow: auto; position: relative;" | ||
| > | ||
| <virtualizer|{ virtualItems: rowItems, totalSize: rowTotal }| | ||
| count=10000 | ||
| estimateSize=() => 35 | ||
| getScrollElement=scrollEl | ||
| > | ||
| <virtualizer|{ virtualItems: colItems, totalSize: colTotal }| | ||
| count=200 | ||
| estimateSize=() => 100 | ||
| horizontal=true | ||
| getScrollElement=scrollEl | ||
| > | ||
| <div style=`height: ${rowTotal}px; width: ${colTotal}px; position: relative`> | ||
| <for|row| of=rowItems> | ||
| <for|col| of=colItems> | ||
| <div | ||
| style=` | ||
| position: absolute; | ||
| top: 0; | ||
| left: 0; | ||
| width: ${col.size}px; | ||
| height: ${row.size}px; | ||
| transform: translateX(${col.start}px) translateY(${row.start}px); | ||
| ` | ||
| > | ||
| Cell ${row.index}, ${col.index} | ||
| </div> | ||
| </for> | ||
| </for> | ||
| </div> | ||
| </virtualizer> | ||
| </virtualizer> | ||
| </div> | ||
| ``` | ||
|
|
||
| ## Window virtualisation | ||
|
|
||
| Use `<window-virtualizer>` when the entire page scrolls rather than a container: | ||
|
|
||
| ```marko | ||
| <window-virtualizer|{ virtualItems, totalSize }| | ||
| count=10000 | ||
| estimateSize=() => 35 | ||
| > | ||
| <div style=`height: ${totalSize}px; position: relative`> | ||
| <for|item| of=virtualItems> | ||
| <div | ||
| style=` | ||
| position: absolute; | ||
| top: 0; | ||
| left: 0; | ||
| width: 100%; | ||
| height: ${item.size}px; | ||
| transform: translateY(${item.start}px); | ||
| ` | ||
| > | ||
| Row ${item.index} | ||
| </div> | ||
| </for> | ||
| </div> | ||
| </window-virtualizer> | ||
| ``` | ||
|
|
||
| ## Dynamic / variable item sizes | ||
|
|
||
| For items with unknown heights, use `measureElement` as an effect-driven ref | ||
| to measure each element after render: | ||
|
|
||
| ```marko | ||
| <div/scrollEl style="height: 400px; overflow-y: auto"> | ||
| <virtualizer|{ virtualItems, totalSize, measureElement }| | ||
| count=data.length | ||
| estimateSize=() => 50 | ||
| getScrollElement=scrollEl | ||
| > | ||
| <div style=`height: ${totalSize}px; position: relative`> | ||
| <for|item| of=virtualItems> | ||
| <div/el | ||
| data-index=item.index | ||
| style=`position: absolute; top: 0; width: 100%; transform: translateY(${item.start}px)`> | ||
| <effect() { | ||
| // measureElement reads the actual rendered height and updates the virtualizer | ||
| if (el() && measureElement) measureElement(el()) | ||
| }/> | ||
| ${data[item.index].text} | ||
| </div> | ||
| </for> | ||
| </div> | ||
| </virtualizer> | ||
| </div> | ||
| ``` | ||
|
|
||
| ## Tag variable reference | ||
|
|
||
| Both tags expose the same tag variable shape: | ||
|
|
||
| | Property | Type | Description | | ||
| |---|---|---| | ||
| | `virtualItems` | `VirtualItem[]` | The currently visible virtual items | | ||
| | `totalSize` | `number` | Total scrollable size in px β set as the inner container's `height` (or `width` for columns) | | ||
| | `measureElement` | `(el: Element \| null) => void` | Ref callback for dynamic item sizing | | ||
| | `scrollToIndex` | `(index: number, options?: ScrollToOptions) => void` | Imperatively scroll to an item by index | | ||
| | `scrollToOffset` | `(offset: number, options?: ScrollToOptions) => void` | Imperatively scroll to a pixel offset | | ||
|
|
||
| ## `<virtualizer>` input reference | ||
|
|
||
| | Prop | Type | Default | Description | | ||
| |---|---|---|---| | ||
| | `count` | `number` | required | Number of items | | ||
| | `getScrollElement` | `() => Element \| null` | required | Returns the scroll container | | ||
| | `estimateSize` | `(index: number) => number` | `() => 50` | Estimated item size in px | | ||
| | `overscan` | `number` | `5` | Items to render beyond the visible area | | ||
| | `horizontal` | `boolean` | `false` | Virtualise horizontally (columns) | | ||
| | `paddingStart` | `number` | β | Padding before first item | | ||
| | `paddingEnd` | `number` | β | Padding after last item | | ||
| | `scrollPaddingStart` | `number` | β | Scroll padding for `scrollToIndex` | | ||
| | `scrollPaddingEnd` | `number` | β | Scroll padding for `scrollToIndex` | | ||
| | `gap` | `number` | β | Gap between items in px | | ||
| | `lanes` | `number` | `1` | Lanes for masonry layouts | | ||
| | `initialOffset` | `number \| (() => number)` | β | Initial scroll offset | | ||
|
|
||
| ## `<window-virtualizer>` input reference | ||
|
|
||
| Same as `<virtualizer>` except `getScrollElement`, `horizontal`, and | ||
| `initialOffset` are not accepted. The scroll element is always `window`, | ||
| scrolling is always vertical, and the initial offset is read from | ||
| `window.scrollY` automatically. | ||
|
|
||
| ## SSR note | ||
|
|
||
| `<virtualizer>` and `<window-virtualizer>` are client-only tags. The | ||
| `<lifecycle>` tag inside them never runs during SSR, so the tag variable | ||
| will be empty (`virtualItems: []`, `totalSize: 0`) on the server. | ||
|
|
||
| Wrap the tag in `<if=mounted>` (where `mounted` is set by `<lifecycle onMount>`) to | ||
| ensure the scroll container exists in the DOM before the virtualizer attaches: | ||
|
|
||
| ```marko | ||
| <let/mounted = false/> | ||
| <lifecycle onMount() { mounted = true }/> | ||
|
|
||
| <if=mounted> | ||
| <div/scrollEl style="height: 400px; overflow-y: auto"> | ||
| <virtualizer|{ virtualItems, totalSize }| | ||
| count=10000 | ||
| estimateSize=() => 35 | ||
| getScrollElement=scrollEl | ||
| > | ||
| ... | ||
| </virtualizer> | ||
| </div> | ||
| </if> | ||
| ``` | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| { "taglib-imports": ["../../../packages/marko-virtual/marko.json"] } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| { | ||
| "name": "tanstack-marko-virtual-example-dynamic", | ||
| "private": true, | ||
| "type": "module", | ||
| "scripts": { | ||
| "dev": "marko-run dev", | ||
| "build": "marko-run build", | ||
| "preview": "marko-run preview" | ||
| }, | ||
| "dependencies": { | ||
| "@marko/run": "^0.7.0", | ||
| "@tanstack/marko-virtual": "workspace:*", | ||
| "marko": "^6.0.0" | ||
| }, | ||
| "devDependencies": { | ||
| "typescript": "5.6.3", | ||
| "vite": "^6.4.2" | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| <!DOCTYPE html> | ||
| <html lang="en"> | ||
| <head> | ||
| <meta charset="UTF-8"/> | ||
| <meta name="viewport" content="width=device-width, initial-scale=1.0"/> | ||
| <title>Dynamic Virtualisation β @tanstack/marko-virtual</title> | ||
| <style> | ||
| * { box-sizing: border-box; margin: 0; padding: 0; } | ||
| body { font-family: system-ui, sans-serif; padding: 24px; } | ||
| h1 { font-size: 24px; margin-bottom: 8px; } | ||
| p { color: #555; margin-bottom: 16px; font-size: 14px; line-height: 1.5; } | ||
| .scroll-container { height: 400px; overflow-y: auto; border: 1px solid #e5e7eb; border-radius: 6px; } | ||
| .item { padding: 10px 12px; font-size: 14px; border-bottom: 1px solid #f3f4f6; box-sizing: border-box; line-height: 1.5; } | ||
| .item-even { background: #f9fafb; } | ||
| .item-odd { background: #ffffff; } | ||
| </style> | ||
| </head> | ||
| <body> | ||
| <h1>Dynamic Virtualisation</h1> | ||
| <p>Item sizes are unknown at render time. Each rendered item is measured via measureElement.</p> | ||
|
|
||
| <const/items = Array.from({ length: 1000 }, (_, idx) => ({ | ||
| id: idx, | ||
| text: "Item " + idx + " β " + "lorem ipsum dolor sit amet ".repeat(1 + (idx % 5)), | ||
| }))/> | ||
|
|
||
| <let/mounted = false/> | ||
| <lifecycle onMount() { mounted = true }/> | ||
|
|
||
| <div/scrollEl class="scroll-container"> | ||
| <if=mounted> | ||
| <virtualizer|{ virtualItems, totalSize, measureElement }| | ||
| count=items.length | ||
| estimateSize=() => 80 | ||
| getScrollElement=scrollEl | ||
| > | ||
| <div style=`height: ${totalSize}px; width: 100%; position: relative`> | ||
| <for|item| of=virtualItems> | ||
| <div/itemEl | ||
| data-index=item.index | ||
| class=item.index % 2 === 0 ? "item item-even" : "item item-odd" | ||
| style=`position: absolute; top: 0; left: 0; width: 100%; transform: translateY(${item.start}px)` | ||
| > | ||
| <effect() { | ||
| const _key = item.key | ||
| const node = itemEl() | ||
| if (node && measureElement) measureElement(node) | ||
| }/> | ||
| <strong>${items[item.index]!.id}.</strong> ${items[item.index]!.text} | ||
| </div> | ||
| </for> | ||
| </div> | ||
| </virtualizer> | ||
| </if> | ||
| </div> | ||
| </body> | ||
| </html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| import { defineConfig } from 'vite' | ||
| import marko from '@marko/run/vite' | ||
|
|
||
| export default defineConfig({ | ||
| // Cast to any: @marko/run/vite types are built against Vite 6 but this | ||
| // workspace uses Vite 5. The plugin works correctly at runtime. | ||
| plugins: [marko() as any], | ||
| }) |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
π§© Analysis chain
π Script executed:
Repository: TanStack/virtual
Length of output: 735
π Script executed:
Repository: TanStack/virtual
Length of output: 669
π Script executed:
Repository: TanStack/virtual
Length of output: 42
π Script executed:
Repository: TanStack/virtual
Length of output: 93
π Script executed:
Repository: TanStack/virtual
Length of output: 393
π Script executed:
Repository: TanStack/virtual
Length of output: 183
π Script executed:
# Read the marko.json file to see if it enables auto-discovery cat packages/marko-virtual/marko.jsonRepository: TanStack/virtual
Length of output: 109
π Script executed:
Repository: TanStack/virtual
Length of output: 916
π Script executed:
Repository: TanStack/virtual
Length of output: 1145
Correct the auto-discovery claimβmanual setup is required.
Lines 9-10 claim tags are auto-discovered on install with no setup needed, but the package's own README explicitly requires users to add
@tanstack/marko-virtual/marko.jsonto their project'smarko.jsontaglib-imports. The npm README is authoritative; the docs should be corrected to reflect the actual onboarding requirement.π€ Prompt for AI Agents