Skip to content

Commit 3d16486

Browse files
docs: add an Examples section and drop core from the virtual examples
docs: add an Examples section and drop core from the virtual examples
2 parents c525e3a + 2ba636a commit 3d16486

18 files changed

Lines changed: 2143 additions & 22 deletions

‎docs/docs/.vitepress/config.ts‎

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,34 @@ export default defineConfig({
129129
nav: [
130130
{ text: 'Tutorial', link: '/tutorial' },
131131
{ text: 'Guide', link: '/guide/getting-started' },
132+
// A dropdown rather than a single link: the examples are the pages people
133+
// arrive looking for, and grouping them keeps the top bar to five items.
134+
{
135+
text: 'Examples',
136+
activeMatch: '^/examples/',
137+
items: [
138+
{ text: 'All examples', link: '/examples/' },
139+
{
140+
text: 'Basics',
141+
items: [
142+
{ text: 'Basic feed', link: '/examples/basic-feed' },
143+
{ text: 'Pagination strategies', link: '/examples/pagination' },
144+
{ text: 'Errors and retry', link: '/examples/retry' },
145+
{ text: 'Manual controls', link: '/examples/manual-controls' },
146+
],
147+
},
148+
{
149+
text: 'Going further',
150+
items: [
151+
{ text: 'Cancellation', link: '/examples/cancellation' },
152+
{ text: 'Horizontal rail', link: '/examples/horizontal' },
153+
{ text: 'Virtual list', link: '/examples/virtual' },
154+
{ text: 'Devtools', link: '/examples/devtools' },
155+
{ text: 'Events and plugins', link: '/examples/events' },
156+
],
157+
},
158+
],
159+
},
132160
{ text: 'Demo', link: '/demo' },
133161
{ text: 'API', link: '/api/core' },
134162
{ text: 'Decisions', link: '/decisions' },
@@ -150,6 +178,21 @@ export default defineConfig({
150178
{ text: 'Events & plugins', link: '/guide/events-and-plugins' },
151179
],
152180
},
181+
{
182+
text: 'Examples',
183+
items: [
184+
{ text: 'All examples', link: '/examples/' },
185+
{ text: 'Basic feed', link: '/examples/basic-feed' },
186+
{ text: 'Pagination strategies', link: '/examples/pagination' },
187+
{ text: 'Errors and retry', link: '/examples/retry' },
188+
{ text: 'Manual controls', link: '/examples/manual-controls' },
189+
{ text: 'Cancellation', link: '/examples/cancellation' },
190+
{ text: 'Horizontal rail', link: '/examples/horizontal' },
191+
{ text: 'Virtual list', link: '/examples/virtual' },
192+
{ text: 'Devtools', link: '/examples/devtools' },
193+
{ text: 'Events and plugins', link: '/examples/events' },
194+
],
195+
},
153196
{
154197
text: 'API reference',
155198
items: [
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
<script setup lang="ts">
2+
/**
3+
* Virtualization on its own — no API, no pagination. Ten thousand rows exist in
4+
* an array; a dozen exist in the DOM. The counters underneath are the whole
5+
* point: watch `rendered` stay flat while `first`/`last` slide.
6+
*
7+
* Row heights here are decided by the stylesheet, not by `estimateSize` — the
8+
* `measure` ref corrects the estimate on first paint, which is why the scrollbar
9+
* settles instead of drifting.
10+
*/
11+
import { useVirtualizer } from '@scrollstackjs/vue/virtual'
12+
import { computed } from 'vue'
13+
14+
const ROLES = ['engineer', 'designer', 'analyst', 'writer', 'operator'] as const
15+
16+
const rows = Array.from({ length: 10_000 }, (_, index) => ({
17+
id: index + 1,
18+
name: `Record ${(index + 1).toLocaleString('en-US')}`,
19+
role: ROLES[index % ROLES.length]!,
20+
}))
21+
22+
const { state, scrollTarget, measure, scrollToIndex } = useVirtualizer({
23+
count: rows.length,
24+
estimateSize: () => 44,
25+
overscan: 4,
26+
})
27+
28+
const inDom = computed(() => state.value.items.length)
29+
</script>
30+
31+
<template>
32+
<div class="ss-demo">
33+
<div class="ss-bar">
34+
<span class="ss-bar__title">
35+
<span class="ss-dot" /> 10,000 rows — nothing fetched, nothing paginated
36+
</span>
37+
<div class="ss-bar__controls">
38+
<button type="button" class="ss-btn" @click="scrollToIndex(0, { align: 'start' })">
39+
Top
40+
</button>
41+
<button type="button" class="ss-btn" @click="scrollToIndex(5000, { align: 'center' })">
42+
Middle
43+
</button>
44+
<button
45+
type="button"
46+
class="ss-btn"
47+
@click="scrollToIndex(rows.length - 1, { align: 'end' })"
48+
>
49+
End
50+
</button>
51+
</div>
52+
</div>
53+
54+
<div :ref="scrollTarget" class="ss-box">
55+
<!-- The spacer holds the scrollbar honest: it is as tall as all 10,000 rows. -->
56+
<div :style="{ height: `${state.totalSize}px`, position: 'relative' }">
57+
<div
58+
v-for="item in state.items"
59+
:key="item.key"
60+
:ref="measure"
61+
:data-index="item.index"
62+
class="ss-row"
63+
:style="{
64+
position: 'absolute',
65+
top: 0,
66+
left: 0,
67+
right: 0,
68+
transform: `translateY(${item.start}px)`,
69+
}"
70+
>
71+
<span class="ss-row__name">{{ rows[item.index]!.name }}</span>
72+
<span class="ss-row__role">{{ rows[item.index]!.role }}</span>
73+
<!-- `.ss-row__id` carries margin-left:auto, so it belongs last. -->
74+
<span class="ss-row__id">#{{ rows[item.index]!.id }}</span>
75+
</div>
76+
</div>
77+
</div>
78+
79+
<div class="ss-counters">
80+
<div class="ss-counter">
81+
<span class="ss-counter__n">{{ inDom }}</span>
82+
<span class="ss-counter__l">rows in the DOM</span>
83+
</div>
84+
<div class="ss-counter">
85+
<span class="ss-counter__n">{{ rows.length.toLocaleString('en-US') }}</span>
86+
<span class="ss-counter__l">rows in the array</span>
87+
</div>
88+
<div class="ss-counter">
89+
<span class="ss-counter__n">{{ state.startIndex }}–{{ state.endIndex }}</span>
90+
<span class="ss-counter__l">rendered window</span>
91+
</div>
92+
<div class="ss-counter">
93+
<span class="ss-counter__n">{{ state.totalSize.toLocaleString('en-US') }}px</span>
94+
<span class="ss-counter__l">total size</span>
95+
</div>
96+
</div>
97+
</div>
98+
</template>
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
<script setup lang="ts">
2+
import { connectInfiniteScroll } from '@scrollstackjs/virtual'
3+
/**
4+
* Both stores at once: the engine loads pages, the virtualizer renders a window
5+
* of them. `connectInfiniteScroll` is what replaces the sentinel — a virtual list
6+
* cannot render the element after the last row, so loading triggers on the
7+
* *index* the window has reached instead of on geometry.
8+
*
9+
* The only thing tying the two together is `count`. The engine never hears about
10+
* pixels; the virtualizer never hears about pages.
11+
*/
12+
import { useInfiniteScroll } from '@scrollstackjs/vue'
13+
import { useVirtualizer } from '@scrollstackjs/vue/virtual'
14+
import { computed, onScopeDispose } from 'vue'
15+
16+
import { fetchPokemon, POKEMON_LIMIT, pokemonSprite, type Pokemon } from './api'
17+
18+
const {
19+
state: feed,
20+
engine,
21+
reset,
22+
} = useInfiniteScroll<readonly Pokemon[], number>({
23+
initialPageParam: 0,
24+
fetchPage: ({ pageParam, signal }) => fetchPokemon(pageParam, signal),
25+
getNextPageParam: (last, all) =>
26+
last.length < POKEMON_LIMIT ? null : all.length * POKEMON_LIMIT,
27+
})
28+
29+
const rows = computed(() => feed.value.pages.flat())
30+
31+
const { state, scrollTarget, measure, virtualizer } = useVirtualizer({
32+
count: () => rows.value.length,
33+
estimateSize: () => 56,
34+
overscan: 5,
35+
})
36+
37+
// Loads the first page too — with no sentinel, nothing else would start it.
38+
onScopeDispose(connectInfiniteScroll(virtualizer, engine, { threshold: 4 }))
39+
</script>
40+
41+
<template>
42+
<div class="ss-demo">
43+
<div class="ss-bar">
44+
<span class="ss-bar__title">
45+
<span class="ss-dot" :class="{ 'ss-dot--warn': feed.isFetching }" />
46+
pokeapi.co — offset pagination, virtualized
47+
</span>
48+
<div class="ss-bar__controls">
49+
<button type="button" class="ss-btn" @click="reset()">Reset</button>
50+
</div>
51+
</div>
52+
53+
<div :ref="scrollTarget" class="ss-box">
54+
<div v-if="feed.isError" class="ss-state" role="alert">
55+
<p>Couldn’t reach the API — {{ String(feed.error) }}</p>
56+
<button type="button" class="ss-btn" @click="engine.retry()">Try again</button>
57+
</div>
58+
59+
<div v-else :style="{ height: `${state.totalSize}px`, position: 'relative' }">
60+
<div
61+
v-for="item in state.items"
62+
:key="item.key"
63+
:ref="measure"
64+
:data-index="item.index"
65+
class="ss-row"
66+
:style="{
67+
position: 'absolute',
68+
top: 0,
69+
left: 0,
70+
right: 0,
71+
transform: `translateY(${item.start}px)`,
72+
}"
73+
>
74+
<img
75+
class="ss-avatar ss-avatar--sprite"
76+
:src="pokemonSprite(rows[item.index]!.url)"
77+
:alt="rows[item.index]!.name"
78+
loading="lazy"
79+
/>
80+
<span class="ss-row__name ss-row__name--caps">{{ rows[item.index]!.name }}</span>
81+
<!-- `.ss-row__id` carries margin-left:auto, so it belongs last. -->
82+
<span class="ss-row__id">#{{ item.index + 1 }}</span>
83+
</div>
84+
</div>
85+
86+
<p v-if="feed.isLoading" class="ss-sentinel">Loading the first page…</p>
87+
</div>
88+
89+
<div class="ss-counters">
90+
<div class="ss-counter">
91+
<span class="ss-counter__n">{{ state.items.length }}</span>
92+
<span class="ss-counter__l">rows in the DOM</span>
93+
</div>
94+
<div class="ss-counter">
95+
<span class="ss-counter__n">{{ rows.length }}</span>
96+
<span class="ss-counter__l">rows loaded</span>
97+
</div>
98+
<div class="ss-counter">
99+
<span class="ss-counter__n">{{ feed.pages.length }}</span>
100+
<span class="ss-counter__l">pages fetched</span>
101+
</div>
102+
<div class="ss-counter">
103+
<span class="ss-counter__n">{{
104+
feed.isFetchingNextPage ? 'fetching' : feed.fetchStatus
105+
}}</span>
106+
<span class="ss-counter__l">engine</span>
107+
</div>
108+
</div>
109+
110+
<p class="ss-footer__note">
111+
Scroll to within four rows of the end and the next page is requested — the virtualizer reports
112+
where the window is, the engine does the fetching.
113+
</p>
114+
</div>
115+
</template>

‎docs/docs/.vitepress/theme/index.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import RailDemo from './demo/RailDemo.vue'
1212
import RetryDemo from './demo/RetryDemo.vue'
1313
import StatesDemo from './demo/StatesDemo.vue'
1414
import TutorialFeed from './demo/TutorialFeed.vue'
15+
import VirtualDemo from './demo/VirtualDemo.vue'
16+
import VirtualFeedDemo from './demo/VirtualFeedDemo.vue'
1517

1618
import './custom.css'
1719

@@ -31,5 +33,7 @@ export default {
3133
app.component('StatesDemo', StatesDemo)
3234
app.component('Playground', Playground)
3335
app.component('DevtoolsDemo', DevtoolsDemo)
36+
app.component('VirtualDemo', VirtualDemo)
37+
app.component('VirtualFeedDemo', VirtualFeedDemo)
3438
},
3539
} satisfies Theme

0 commit comments

Comments
 (0)