|
13 | 13 | .item { display: flex; align-items: center; padding: 0 16px; font-size: 14px; border-bottom: 1px solid #f3f4f6; box-sizing: border-box; } |
14 | 14 | .item-even { background: #f9fafb; } |
15 | 15 | .item-odd { background: #ffffff; } |
16 | | - .status { color: #9ca3af; font-style: italic; font-size: 13px; } |
| 16 | + .placeholder { color: #9ca3af; font-style: italic; font-size: 13px; } |
17 | 17 | .footer { font-size: 12px; color: #9ca3af; margin-top: 8px; text-align: right; } |
18 | 18 | </style> |
19 | 19 | </head> |
20 | 20 | <body> |
21 | 21 | <h1>Infinite Scroll Virtualisation</h1> |
22 | | - <p>Rows are fetched in pages as you scroll toward the end of the list.</p> |
| 22 | + <p>500 rows fetched in pages of 20 as you scroll. The virtualizer reserves all 500 slots upfront — unloaded rows show a placeholder.</p> |
23 | 23 |
|
24 | | - <const/TOTAL = 500/> |
| 24 | + <const/TOTAL = 500/> |
25 | 25 | <const/PAGE_SIZE = 20/> |
26 | 26 |
|
27 | | - <let/rows = ([] as string[])/> |
28 | | - <let/hasMore = true/> |
| 27 | + <let/rows = ([] as string[])/> |
29 | 28 | <let/loading = false/> |
30 | | - <let/nextOffset = 0/> |
| 29 | + <let/hasMore = true/> |
| 30 | + <let/offset = 0/> |
31 | 31 | <let/mounted = false/> |
32 | 32 |
|
33 | | - <const/loadPage = async () => { |
34 | | - if (loading || !hasMore) return |
35 | | - loading = true |
36 | | - await new Promise<void>(r => setTimeout(r, 500)) |
37 | | - const offset = nextOffset |
38 | | - const count = Math.min(PAGE_SIZE, TOTAL - offset) |
39 | | - const newRows = Array.from({ length: count }, (_, k) => |
40 | | - "Row #" + (offset + k + 1) + " — loaded at offset " + offset |
41 | | - ) |
42 | | - rows = [...rows, ...newRows] |
43 | | - nextOffset = offset + count |
44 | | - hasMore = nextOffset < TOTAL |
45 | | - loading = false |
46 | | - }/> |
47 | | - |
48 | 33 | <script() { |
49 | 34 | mounted = true |
50 | | - loadPage() |
| 35 | + loading = true |
| 36 | + new Promise<void>(r => setTimeout(r, 400)).then(() => { |
| 37 | + const n = Math.min(PAGE_SIZE, TOTAL) |
| 38 | + rows = Array.from({ length: n }, (_, k) => "Row #" + (k + 1)) |
| 39 | + offset = n |
| 40 | + hasMore = n < TOTAL |
| 41 | + loading = false |
| 42 | + }) |
51 | 43 | }/> |
52 | 44 |
|
53 | | - <const/count = hasMore ? rows.length + 1 : rows.length/> |
54 | | - |
55 | 45 | <div/scrollEl class="scroll-container"> |
56 | 46 | <if=mounted> |
57 | 47 | <virtualizer|{ virtualItems, totalSize }| |
58 | | - count=count |
| 48 | + count=TOTAL |
59 | 49 | estimateSize=() => 52 |
60 | 50 | getScrollElement=scrollEl |
61 | 51 | overscan=5 |
62 | 52 | > |
63 | 53 | <script() { |
64 | 54 | const last = virtualItems[virtualItems.length - 1] |
65 | | - if (last && last.index >= rows.length - 1) loadPage() |
| 55 | + if (last && last.index >= rows.length - 1 && !loading && hasMore) { |
| 56 | + const off = offset |
| 57 | + loading = true |
| 58 | + new Promise<void>(r => setTimeout(r, 400)).then(() => { |
| 59 | + const n = Math.min(PAGE_SIZE, TOTAL - off) |
| 60 | + rows = [...rows, ...Array.from({ length: n }, (_, k) => "Row #" + (off + k + 1))] |
| 61 | + offset = off + n |
| 62 | + hasMore = offset < TOTAL |
| 63 | + loading = false |
| 64 | + }) |
| 65 | + } |
66 | 66 | }/> |
67 | 67 |
|
68 | 68 | <div style=`height: ${totalSize}px; width: 100%; position: relative`> |
|
75 | 75 | ${rows[item.index]} |
76 | 76 | </if> |
77 | 77 | <else> |
78 | | - <span class="status">${loading ? "Loading more…" : hasMore ? "Scroll to load more" : "All rows loaded"}</span> |
| 78 | + <span class="placeholder"> |
| 79 | + ${loading && item.index === rows.length ? "Loading…" : "—"} |
| 80 | + </span> |
79 | 81 | </else> |
80 | 82 | </div> |
81 | 83 | </for> |
|
84 | 86 | </if> |
85 | 87 | </div> |
86 | 88 |
|
87 | | - <p class="footer"> |
88 | | - ${rows.length} of ${TOTAL} rows loaded |
89 | | - </p> |
| 89 | + <p class="footer">${rows.length} of ${TOTAL} rows loaded</p> |
90 | 90 | </body> |
91 | 91 | </html> |
0 commit comments