Components

Feed List

Source
Virtualized native list with pull-to-refresh, load-more, and snap paging.
Install with the CLI
npx @vyui/cli add feed-list

Overview

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') or itemKey must 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.
  • enableBounce adds rubber-band overscroll at both edges, independent of refresh.
  • disabled stops scrolling and refresh interactions.
  • The empty slot replaces the list when items is empty.
  • bounces is 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 loadingMore model, so the loadMoreFooter slot never renders through it. Use the core FeedList when a loading footer matters.

API

Props

PropDefaultType
bouncesboolean | undefined
disabledboolean | undefined

Disable scrolling.

enableBounceboolean | undefined

Rubber-band overscroll bounce at both edges.

enableLoadMoreboolean | undefined

Enable load-more on scroll-to-lower.

enableRefreshboolean | undefined

Enable the custom rubber-band pull-to-refresh (touch worklets).

itemKey((item: T, index: number) => string) | undefined

Alternative to `itemKeyField`: a function returning the key.

itemKeyField(keyof T & string) | undefined

Field 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`.

itemSnapboolean | { factor: number; offset: number; } | undefined

Snap 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" | undefined

Layout type. `'flow'` and `'waterfall'` require `spanCount > 1`.

loadMoreThresholdItemCountnumber | undefined

Number of items from the bottom that triggers `load-more`.

noMoreDataboolean | undefined

No more data to load — stops `loadMore` and shows the end-of-list footer.

refreshingboolean | undefined

Controlled refreshing state. Bind with `v-model:refreshing`.

refreshThresholdnumber | undefined

Pull distance (px) past which release triggers a refresh. Default 64.

scrollBarEnableboolean | undefined
scrollOrientation"vertical" | "horizontal" | undefined
spanCountnumber | undefined

Columns / rows for `flow` / `waterfall`.

uiPartial<Record<string | number | symbol, ClassNameValue>> | undefined
upperThresholdItemCountnumber | undefined

Number of items from the top that triggers `scrollToUpper`.

Emits

EventPayload
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

SlotBindings
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{} | undefined

Rendered in place of the list when `items` is empty.

loadMoreFooter{ loading: boolean; }

Footer shown at the bottom while more data can be loaded.

noMoreDataFooter{} | undefined

Footer 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 items array needs a Vue-Lynx whose <list> bridge applies removals. Stock releases only append, which leaves ghost rows and a duplicate item-key crash 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.