Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 | 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 5x 5x 5x 12x 12x 12x 15x 15x 3x 1x 1x 3x 3x 3x 3x 3x 3x 3x | import { FlexibleVirtualScrollStrategy } from './flexible-virtual-scroll-strategy';
import { VirtualScrollerItemDirective } from './virtual-scroller-item.directive';
import { SpinnerComponent } from '../spinner/spinner.component';
import { CdkVirtualForOf, CdkVirtualScrollViewport, VIRTUAL_SCROLL_STRATEGY } from '@angular/cdk/scrolling';
import { NgTemplateOutlet } from '@angular/common';
import {
AfterViewInit,
ChangeDetectionStrategy,
Component,
computed,
contentChild,
DestroyRef,
inject,
input,
OnInit,
output,
viewChild,
} from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { filter, throttleTime } from 'rxjs';
export { VirtualScrollerItemDirective } from './virtual-scroller-item.directive';
/** Default number of items from the end to trigger loading more */
const DEFAULT_LOAD_MORE_THRESHOLD = 5;
/**
* Context provided to the item template
*/
export interface VirtualScrollerItemContext<T> {
/** The item data */
$implicit: T;
/** Index of the item in the list */
index: number;
}
@Component({
selector: 'ui-virtual-scroller',
imports: [NgTemplateOutlet, CdkVirtualScrollViewport, CdkVirtualForOf, SpinnerComponent],
templateUrl: './virtual-scroller.component.html',
styleUrl: './virtual-scroller.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [
FlexibleVirtualScrollStrategy,
{ provide: VIRTUAL_SCROLL_STRATEGY, useExisting: FlexibleVirtualScrollStrategy },
],
})
export class VirtualScrollerComponent<T> implements OnInit, AfterViewInit {
private readonly destroyRef = inject(DestroyRef);
private readonly scrollStrategy = inject(FlexibleVirtualScrollStrategy);
/** Items to display in the virtual scroll */
readonly items = input.required<readonly T[]>();
/** Fixed height of each item in pixels. When set, uses fixed-size strategy (better performance). When omitted, uses autosize strategy (variable-height items). */
readonly itemSize = input<number | undefined>();
/** Number of items from the end to trigger loadMore event */
readonly loadMoreThreshold = input<number>(DEFAULT_LOAD_MORE_THRESHOLD);
/** Total number of items available (for determining if more can be loaded) */
readonly totalItems = input<number>(0);
/** Whether more items are currently being loaded */
readonly isLoading = input<boolean>(false);
/** Track by function for cdkVirtualFor */
readonly trackByFn = input<(index: number, item: T) => unknown>((index: number) => index);
/** Emitted when more items should be loaded */
readonly loadMore = output<void>();
/** Reference to the viewport for scroll handling */
readonly viewport = viewChild.required(CdkVirtualScrollViewport);
/** Template directive for rendering each item */
readonly itemTemplateDirective = contentChild(VirtualScrollerItemDirective);
/** Get the template from the directive */
readonly itemTemplate = computed(() => this.itemTemplateDirective()?.template);
/** Whether all items have been loaded */
readonly allItemsLoaded = computed(() => {
const total = this.totalItems();
const currentCount = this.items().length;
return total > 0 && currentCount >= total;
});
/** Whether the loading indicator should be shown */
readonly showLoadingIndicator = computed(() => this.isLoading() && this.items().length > 0);
ngOnInit(): void {
this.scrollStrategy.configure(this.itemSize());
}
ngAfterViewInit(): void {
this.setupScrollListener();
}
private setupScrollListener(): void {
const viewport = this.viewport();
viewport
.elementScrolled()
.pipe(
throttleTime(100, undefined, { leading: true, trailing: true }),
filter(() => !this.isLoading() && !this.allItemsLoaded()),
filter(() => this.shouldLoadMore()),
takeUntilDestroyed(this.destroyRef)
)
.subscribe(() => {
this.loadMore.emit();
});
}
private shouldLoadMore(): boolean {
const viewport = this.viewport();
const items = this.items();
const threshold = this.loadMoreThreshold();
const renderedRange = viewport.getRenderedRange();
const lastRenderedIndex = renderedRange.end;
const itemsRemaining = items.length - lastRenderedIndex;
return itemsRemaining <= threshold;
}
}
|