Feed List
npx @vyui/cli add feed-listOverview
VyFeedList renders a Lynx native <list> through the @vyui/core FeedList primitive: rows are virtualized by the platform, and the primitive adds a rubber-band pull-to-refresh, load-more on scroll-to-lower, and optional snap paging. The kit wrapper only adds the base sizing class, so the look of a feed comes from the item slot.
Usage
Give each item a stable key field and render a row in the item slot.
<script setup lang="ts">
import { ref } from 'vue'
import { VyFeedList } from '@vyui/kit/feed-list'
const posts = ref([{ id: '1', title: 'First' }, { id: '2', title: 'Second' }])
const refreshing = ref(false)
async function onRefresh() {
refreshing.value = true
posts.value = await fetchPosts()
refreshing.value = false
}
</script>
<template>
<view style="height: 640px">
<VyFeedList
v-model:refreshing="refreshing"
:items="posts"
item-key-field="id"
enable-refresh
@refresh="onRefresh"
>
<template #item="{ item }">
<view class="p-4 border-b border-default">
<text class="text-highlighted">{{ item.title }}</text>
</view>
</template>
</VyFeedList>
</view>
</template>
The list must sit in a box with a definite height. A native <list> inside a flex-1 min-h-0 parent measures 0px and renders nothing — measure the container and pass pixels.
Pull to refresh
enableRefresh turns on the gesture; refresh fires once when the pull passes refreshThreshold and the finger lifts. Bind v-model:refreshing so the spinner clears when the fetch resolves, and use the refreshHeader slot to draw the pull state yourself.
<template>
<VyFeedList v-model:refreshing="refreshing" :items="posts" enable-refresh @refresh="onRefresh">
<template #refreshHeader="{ state, progress }">
<view class="h-16 items-center justify-center">
<text class="text-muted">
{{ state === 'refreshing' ? 'Refreshing…' : progress >= 1 ? 'Release to refresh' : 'Pull to refresh' }}
</text>
</view>
</template>
<template #item="{ item }">…</template>
</VyFeedList>
</template>
state moves through idle, pulling, releaseReady, refreshing, and done; progress is the pull distance as a fraction of the threshold.
Load more
enableLoadMore emits loadMore when scrolling comes within loadMoreThresholdItemCount rows of the bottom. Set noMoreData once the last page has arrived to stop it and show the noMoreDataFooter slot.
Grids and paging
listType: 'flow' or 'waterfall' with spanCount greater than 1 renders columns instead of rows. itemSnap snaps each item to a rest position after a scroll — true gives full-screen paging, and snap reports the settled index in event.detail.position.
Features and behavior
itemKeyField(default'id') oritemKeymust produce a stable, unique key per row. The native diff appends and removes by key but does not reorder, so replace keys when the data order changes rather than permuting them.enableBounceadds rubber-band overscroll at both edges, independent of refresh.disabledstops scrolling and refresh interactions.- The
emptyslot replaces the list whenitemsis empty. bouncesis ignored while pull-to-refresh is on: the native bounce is forced off so the top-edge pull is not stolen from the gesture.- The kit wrapper does not forward the core's
loadingMoremodel, so theloadMoreFooterslot never renders through it. Use the coreFeedListwhen a loading footer matters.
API
Props
| Prop | Default | Type |
|---|---|---|
bounces | — | boolean | undefined |
disabled | — | boolean | undefinedDisable scrolling. |
enableBounce | — | boolean | undefinedRubber-band overscroll bounce at both edges. |
enableLoadMore | — | boolean | undefinedEnable load-more on scroll-to-lower. |
enableRefresh | — | boolean | undefinedEnable the custom rubber-band pull-to-refresh (touch worklets). |
itemKey | — | ((item: T, index: number) => string) | undefinedAlternative to `itemKeyField`: a function returning the key. |
itemKeyField | — | (keyof T & string) | undefinedField on `T` to use as the unique key per row. Defaults to `'id'`. |
items* | — | T[]Items to render. Each becomes a `<list-item>` with an `item-key`. |
itemSnap | — | boolean | { factor: number; offset: number; } | undefinedSnap each item to a rest position after scrolling (native `item-snap`). `true` = full-screen paging; pass `{ factor, offset }` to customise. `listType: 'single'` only. |
listType | — | "single" | "flow" | "waterfall" | undefinedLayout type. `'flow'` and `'waterfall'` require `spanCount > 1`. |
loadMoreThresholdItemCount | — | number | undefinedNumber of items from the bottom that triggers `load-more`. |
noMoreData | — | boolean | undefinedNo more data to load — stops `loadMore` and shows the end-of-list footer. |
refreshing | — | boolean | undefinedControlled refreshing state. Bind with `v-model:refreshing`. |
refreshThreshold | — | number | undefinedPull distance (px) past which release triggers a refresh. Default 64. |
scrollBarEnable | — | boolean | undefined |
scrollOrientation | — | "vertical" | "horizontal" | undefined |
spanCount | — | number | undefinedColumns / rows for `flow` / `waterfall`. |
ui | — | Partial<Record<string | number | symbol, ClassNameValue>> | undefined |
upperThresholdItemCount | — | number | undefinedNumber of items from the top that triggers `scrollToUpper`. |
Emits
| Event | Payload |
|---|---|
update:refreshing | [value: boolean] |
refresh | [] |
refreshStateChange | [state: FeedListRefreshState] |
loadMore | [] |
scrollToLower | [event: unknown] |
scrollToUpper | [event: unknown] |
scroll | [event: unknown] |
scrollStateChange | [event: unknown] |
snap | [event: unknown] |
Slots
| Slot | Bindings |
|---|---|
item | { item: T; index: number; }Row template. Receives the item and its current index. |
refreshHeader | { state: FeedListRefreshState; progress: number; }Pull-to-refresh header. `state` is the lifecycle phase; `progress` is the pull distance as a fraction of the threshold (0..1). |
empty | {} | undefinedRendered in place of the list when `items` is empty. |
loadMoreFooter | { loading: boolean; }Footer shown at the bottom while more data can be loaded. |
noMoreDataFooter | {} | undefinedFooter shown at the bottom once `noMoreData` is true. |
Platform notes
- Pull-to-refresh runs on
:main-thread-bindtouch*worklets and only takes the gesture while pulling down from the top edge, so it does not fight native scrolling. - Rows are virtualized by the platform. Keep row templates cheap; anything measured per row runs during scrolling.
- A shrinking
itemsarray needs a Vue-Lynx whose<list>bridge applies removals. Stock releases only append, which leaves ghost rows and a duplicateitem-keycrash on the next append. - Desktop web has no synthesized touch, so the pull also binds mouse events; it engages only at the top edge and leaves ordinary wheel scrolling alone.
Related components
Scroll Viewfor non-virtualized scrolling with edge thresholds.Swiperfor paged horizontal content.Swipe Actionfor per-row swipe controls.