import { AfterContentInit , Component , ContentChild , ElementRef , EventEmitter , HostListener , Input , OnChanges , OnDestroy , OnInit , Output , SimpleChanges , TemplateRef } from '@angular/core' ; import { BehaviorSubject , Observable , Subject , Subscription } from 'rxjs' ; import { takeUntil } from 'rxjs/operators' ; import { ResizeService } from '../../../directives/resize/index' ; import { VirtualScrollCellDirective } from './directives/virtual-scroll-cell.directive' ; import { VirtualScrollLoadButtonDirective } from './directives/virtual-scroll-load-button.directive' ; import { VirtualScrollLoadingDirective } from './directives/virtual-scroll-loading.directive' ; @ Component ( { selector : 'ux-virtual-scroll' , templateUrl : './virtual-scroll.component.html' } ) export class VirtualScrollComponent < T > implements OnInit , AfterContentInit , OnChanges , OnDestroy { /** Provide the collection of items to display */ @ Input ( ) collection : Observable < T [ ] > = Observable . create ( ) ; /** Specify the height of each cell */ @ Input ( ) cellHeight : number ; /** Indicate whether pages should be loaded on scroll or button click */ @ Input ( ) loadOnScroll : boolean = true ; /** Emit when we need to load another page */ @ Output ( ) loading : EventEmitter < number > = new EventEmitter < number > ( ) ; @ ContentChild ( VirtualScrollCellDirective , { read : TemplateRef , static : false } ) cellTemplate : TemplateRef < any > ; @ ContentChild ( VirtualScrollLoadingDirective , { read : TemplateRef , static : false } ) loadingIndicatorTemplate : TemplateRef < any > ; @ ContentChild ( VirtualScrollLoadButtonDirective , { read : TemplateRef , static : false } ) loadButtonTemplate : TemplateRef < any > ; cells : BehaviorSubject < VirtualCell < T > [ ] > = new BehaviorSubject ( [ ] ) ; scrollTop : number = 0 ; isLoading : boolean = false ; pageNumber : number = 0 ; data : ReadonlyArray < T > = [ ] ; loadingComplete : boolean = false ; private _buffer : number = 5 ; private _subscription : Subscription ; private _height : number ; private _onDestroy = new Subject < void > ( ) ; constructor ( private _elementRef , resizeService ) { resizeService . addResizeListener ( _elementRef . nativeElement ) . pipe ( takeUntil ( this . _onDestroy ) ) . subscribe ( event => this . _height = event . height ) ; } ngOnInit ( ) { if ( ! this . cellHeight ) { throw new Error ( 'Virtual Scroll Component requires "cellHeight" property to be defined.' ) ; } this . setupObservable ( ) ; this . loadNextPage ( ) ; } ngAfterContentInit ( ) { this . renderCells ( ) ; } ngOnChanges ( changes ) { if ( changes . collection && changes . collection . currentValue !== changes . collection . previousValue && ! changes . collection . isFirstChange ( ) ) { this . setupObservable ( ) ; this . reset ( ) ; } } ngOnDestroy ( ) { this . _subscription . unsubscribe ( ) ; this . _onDestroy . next ( ) ; this . _onDestroy . complete ( ) ; } setupObservable ( ) { if ( this . _subscription && this . _subscription . unsubscribe ) { this . _subscription . unsubscribe ( ) ; } this . _subscription = this . collection . subscribe ( collection => { this . data = [ ... this . data , ... collection ] ; this . renderCells ( ) ; this . isLoading = false ; } , null , ( ) => { this . loadingComplete = true ; } ) ; } @ HostListener ( 'scroll' ) renderCells ( ) { this . cells . next ( this . getVisibleCells ( ) ) ; if ( this . loadOnScroll && ! this . isLoading && ! this . loadingComplete ) { const remainingScroll = this . _elementRef . nativeElement . scrollHeight - ( this . _elementRef . nativeElement . scrollTop + this . _elementRef . nativeElement . clientHeight ) ; if ( remainingScroll <= this . _elementRef . nativeElement . clientHeight ) { this . loadNextPage ( ) ; } } } getVisibleCells ( ) { if ( ! this . _height ) { this . _height = this . _elementRef . nativeElement . offsetHeight ; } const scrollTop = this . _elementRef . nativeElement . scrollTop ; const startCell = Math . floor ( scrollTop / this . cellHeight ) ; const endCell = Math . ceil ( this . _height / this . cellHeight ) ; const startBuffer = Math . max ( 0 , startCell - this . _buffer ) ; const endBuffer = startCell + ( startCell - startBuffer ) + Math . min ( this . data . length , endCell + this . _buffer ) ; this . scrollTop = ( scrollTop - ( scrollTop % this . cellHeight ) ) - ( ( startCell - startBuffer ) * this . cellHeight ) ; const cells = this . data . slice ( startBuffer , endBuffer ) ; return cells . map ( ( cell , index ) => ( { data : cell , index : startBuffer + index } ) ) ; } getTotalHeight ( ) { return this . cellHeight * this . data . length ; } loadNextPage ( ) { this . isLoading = true ; this . loading . next ( this . pageNumber ) ; this . pageNumber ++ ; } reset ( ) { this . scrollTop = 0 ; this . data = [ ] ; this . _height = undefined ; this . pageNumber = 0 ; this . loadingComplete = false ; this . _elementRef . nativeElement . scrollTop = 0 ; this . renderCells ( ) ; this . loadNextPage ( ) ; } } export interface VirtualCell < T > { data : T ; index : number ; }