feat(VirtualList): VirtualList now supports pagination, it loads batches when user scrolls near the end of current batch
This commit is contained in:
@@ -59,6 +59,11 @@ export class UnifiedFontStore extends BaseFontStore<ProxyFontsParams> {
|
||||
} | null
|
||||
>(null);
|
||||
|
||||
/**
|
||||
* Accumulated fonts from all pages (for infinite scroll)
|
||||
*/
|
||||
#accumulatedFonts = $state<UnifiedFont[]>([]);
|
||||
|
||||
/**
|
||||
* Pagination metadata (derived from proxy API response)
|
||||
*/
|
||||
@@ -84,8 +89,53 @@ export class UnifiedFontStore extends BaseFontStore<ProxyFontsParams> {
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* Track previous filter params to detect changes and reset pagination
|
||||
*/
|
||||
#previousFilterParams = $state<string>('');
|
||||
|
||||
/**
|
||||
* Cleanup function for the filter tracking effect
|
||||
*/
|
||||
#filterCleanup: (() => void) | null = null;
|
||||
|
||||
constructor(initialParams: ProxyFontsParams = {}) {
|
||||
super(initialParams);
|
||||
|
||||
// Track filter params (excluding pagination params)
|
||||
// Wrapped in $effect.root() to prevent effect_orphan error
|
||||
this.#filterCleanup = $effect.root(() => {
|
||||
$effect(() => {
|
||||
const filterParams = JSON.stringify({
|
||||
provider: this.params.provider,
|
||||
category: this.params.category,
|
||||
subset: this.params.subset,
|
||||
q: this.params.q,
|
||||
});
|
||||
|
||||
// If filters changed, reset offset to 0
|
||||
if (filterParams !== this.#previousFilterParams) {
|
||||
if (this.#previousFilterParams && this.params.offset !== 0) {
|
||||
this.setParams({ offset: 0 });
|
||||
}
|
||||
this.#previousFilterParams = filterParams;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up both parent and child effects
|
||||
*/
|
||||
destroy() {
|
||||
// Call parent cleanup (TanStack observer effect)
|
||||
super.destroy();
|
||||
|
||||
// Call filter tracking effect cleanup
|
||||
if (this.#filterCleanup) {
|
||||
this.#filterCleanup();
|
||||
this.#filterCleanup = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -136,17 +186,25 @@ export class UnifiedFontStore extends BaseFontStore<ProxyFontsParams> {
|
||||
offset: response.offset ?? this.params.offset ?? 0,
|
||||
};
|
||||
|
||||
// Accumulate fonts for infinite scroll
|
||||
if (params.offset === 0) {
|
||||
// Reset when starting from beginning (new search/filter)
|
||||
this.#accumulatedFonts = response.fonts;
|
||||
} else {
|
||||
// Append new fonts to existing ones
|
||||
this.#accumulatedFonts = [...this.#accumulatedFonts, ...response.fonts];
|
||||
}
|
||||
|
||||
return response.fonts;
|
||||
}
|
||||
|
||||
// --- Getters (proxied from BaseFontStore) ---
|
||||
|
||||
/**
|
||||
* Get all fonts from current query result
|
||||
* Get all accumulated fonts (for infinite scroll)
|
||||
*/
|
||||
get fonts(): UnifiedFont[] {
|
||||
// The result.data is UnifiedFont[] (from TanStack Query)
|
||||
return (this.result.data as UnifiedFont[] | undefined) ?? [];
|
||||
return this.#accumulatedFonts;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -288,5 +346,9 @@ export function createUnifiedFontStore(params: ProxyFontsParams = {}) {
|
||||
|
||||
/**
|
||||
* Singleton instance for global use
|
||||
* Initialized with a default limit to prevent fetching all fonts at once
|
||||
*/
|
||||
export const unifiedFontStore = new UnifiedFontStore();
|
||||
export const unifiedFontStore = new UnifiedFontStore({
|
||||
limit: 50,
|
||||
offset: 0,
|
||||
});
|
||||
|
||||
@@ -15,7 +15,10 @@ import { cn } from '$shared/shadcn/utils/shadcn-utils';
|
||||
* Load more fonts by moving to the next page
|
||||
*/
|
||||
function loadMore() {
|
||||
if (!unifiedFontStore.pagination.hasMore || unifiedFontStore.isFetching) {
|
||||
if (
|
||||
!unifiedFontStore.pagination.hasMore
|
||||
|| unifiedFontStore.isFetching
|
||||
) {
|
||||
return;
|
||||
}
|
||||
unifiedFontStore.nextPage();
|
||||
@@ -24,15 +27,14 @@ function loadMore() {
|
||||
/**
|
||||
* Handle scroll near bottom - auto-load next page
|
||||
*
|
||||
* Triggered when the user scrolls within 5 items of the end of the list.
|
||||
* Only fetches if there are more pages available and not already fetching.
|
||||
* Triggered by VirtualList when the user scrolls within 5 items of the end
|
||||
* of the loaded items. Only fetches if there are more pages available.
|
||||
*/
|
||||
function handleNearBottom(lastVisibleIndex: number) {
|
||||
const { hasMore, total } = unifiedFontStore.pagination;
|
||||
const itemsRemaining = total - lastVisibleIndex;
|
||||
function handleNearBottom(_lastVisibleIndex: number) {
|
||||
const { hasMore } = unifiedFontStore.pagination;
|
||||
|
||||
// Only trigger if within 5 items of the end, more data exists, and not already fetching
|
||||
if (itemsRemaining <= 5 && hasMore && !unifiedFontStore.isFetching) {
|
||||
// VirtualList already checks if we're near the bottom of loaded items
|
||||
if (hasMore && !unifiedFontStore.isFetching) {
|
||||
loadMore();
|
||||
}
|
||||
}
|
||||
@@ -47,13 +49,8 @@ const displayRange = $derived.by(() => {
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if unifiedFontStore.pagination.total > 0 && !unifiedFontStore.isLoading}
|
||||
<div class="text-sm text-muted-foreground px-2 py-2">
|
||||
{displayRange}
|
||||
{#if unifiedFontStore.isFetching}
|
||||
<span class="ml-2 text-xs text-muted-foreground/70">(Loading...)</span>
|
||||
{/if}
|
||||
</div>
|
||||
{#if unifiedFontStore.isFetching || unifiedFontStore.isLoading}
|
||||
<span class="ml-2 text-xs text-muted-foreground/70">(Loading...)</span>
|
||||
{/if}
|
||||
|
||||
<FontVirtualList
|
||||
|
||||
@@ -84,7 +84,9 @@ interface Props {
|
||||
*
|
||||
* @template T - The type of items in the list
|
||||
*/
|
||||
children: Snippet<[{ item: T; index: number; isVisible: boolean; proximity: number }]>;
|
||||
children: Snippet<
|
||||
[{ item: T; index: number; isVisible: boolean; proximity: number }]
|
||||
>;
|
||||
}
|
||||
|
||||
let {
|
||||
@@ -99,7 +101,7 @@ let {
|
||||
}: Props = $props();
|
||||
|
||||
const virtualizer = createVirtualizer(() => ({
|
||||
count: total,
|
||||
count: items.length,
|
||||
data: items,
|
||||
estimateSize: typeof itemHeight === 'function' ? itemHeight : () => itemHeight,
|
||||
overscan,
|
||||
@@ -109,10 +111,11 @@ $effect(() => {
|
||||
const visibleItems = virtualizer.items.map(item => items[item.index]);
|
||||
onVisibleItemsChange?.(visibleItems);
|
||||
|
||||
// Trigger onNearBottom when user scrolls near the end (within 5 items)
|
||||
// Trigger onNearBottom when user scrolls near the end of loaded items (within 5 items)
|
||||
if (virtualizer.items.length > 0 && onNearBottom) {
|
||||
const lastVisibleItem = virtualizer.items[virtualizer.items.length - 1];
|
||||
const itemsRemaining = total - lastVisibleItem.index;
|
||||
// Compare against loaded items length, not total
|
||||
const itemsRemaining = items.length - lastVisibleItem.index;
|
||||
|
||||
if (itemsRemaining <= 5) {
|
||||
onNearBottom(lastVisibleItem.index);
|
||||
|
||||
Reference in New Issue
Block a user