import { Directive , DoCheck , EmbeddedViewRef , Input , IterableChangeRecord , IterableChanges , IterableDiffer , IterableDiffers , OnDestroy , OnInit , Optional , Renderer2 , TemplateRef , TrackByFunction , ViewContainerRef , ChangeDetectorRef } from '@angular/core' ; import { distinctUntilChanged , takeUntil } from 'rxjs/operators' ; import { Subject } from 'rxjs' ; import { VirtualForRange , VirtualForService } from './virtual-for.service' ; /**
 * This implementation is inspired by the CDK virtual for:
 * https://github.com/angular/material2/blob/master/src/cdk/scrolling/virtual-for-of.ts
 * However the CDK requires a container component which limits use in places such
 * as fixed header tables, so this is a more generic implementation that does not
 * require a parent element but instead uses an attribute on the parent container instead
 */ @ Directive ( { selector : '[uxVirtualFor][uxVirtualForOf]' , } ) export class VirtualForDirective < T > implements OnInit , DoCheck , OnDestroy { /** Store the list of items to display */ @ Input ( ) set uxVirtualForOf ( dataset ) { this . _virtualScroll . dataset . next ( dataset ) ; this . _dataset = dataset ; if ( this . _renderedRange ) { this . onRangeChange ( ) ; } } /** Provide a trackBy function to optimize rendering */ @ Input ( ) uxVirtualForTrackBy : TrackByFunction < T > | undefined = this . defaultTrackBy ; /** The instance of the differ we create */ private _differ : IterableDiffer < T > ; /** Keep a local reference to the dataset */ private _dataset : T [ ] ; /** Store the currently rendered range */ private _renderedRange : VirtualForRange ; /** Store a list of all the currently rendered items */ private _renderedItems : T [ ] ; /** Indicate whether we need to perform a view update */ private _isDirty : boolean = false ; /** Store a cache of recently disposed views for reuse */ private _templateCache : EmbeddedViewRef < VirtualForOfContext < T > > [ ] = [ ] ; /** Limit the size of the cache as it can use a lot of memory */ private _cacheSize : number = 20 ; /** Unsubscribe from all observables */ private _onDestroy = new Subject < void > ( ) ; constructor ( /** A reference to the container element where we will insert elements. */ private _viewContainerRef , /** The template for all items */ private _templateRef , /** Gets the set of Angular differs for detecting changes. */ private _differs , /** Get the renderer to perform DOM manipulation */ private _renderer , private _changeDetector , /** A service to share values between the container and child elements */ @ Optional ( ) private _virtualScroll ) { if ( ! this . _virtualScroll ) { throw new Error ( 'The "uxVirtualFor" directive requires the "uxVirtualForContainer" directive to be added to the parent element.' ) ; } } ngOnInit ( ) { this . _virtualScroll . range . pipe ( distinctUntilChanged ( this . isRangeSame ) , takeUntil ( this . _onDestroy ) ) . subscribe ( range => { this . _renderedRange = range ; this . onRangeChange ( ) ; this . _changeDetector . detectChanges ( ) ; } ) ; } ngDoCheck ( ) { if ( this . _isDirty && this . _differ ) { const changes = this . getChanges ( ) ; if ( changes ) { this . applyChanges ( changes ) ; } else { this . updateContexts ( ) ; } this . _isDirty = false ; } } ngOnDestroy ( ) { this . _onDestroy . next ( ) ; this . _onDestroy . complete ( ) ; } /** If an itemSize is not specified we need to calculate it */ getHeight ( context , length ) { const view = this . createView ( 0 ) ; view . context . $implicit = context ; view . context . count = length ; view . context . even = true ; view . context . odd = false ; view . context . first = true ; view . context . last = length === 1 ; view . detectChanges ( ) ; const height = view . rootNodes [ 0 ] . offsetHeight ; this . _viewContainerRef . remove ( 0 ) ; view . destroy ( ) ; return height ; } /** Determine if the range has changed (performance optimization) */ private isRangeSame ( previous , current ) { return previous . start === current . start && previous . end === current . end ; } private onRangeChange ( ) { this . _renderedItems = this . _dataset . slice ( this . _renderedRange . start , this . _renderedRange . end ) ; if ( ! this . _differ ) { this . _differ = this . _differs . find ( this . _renderedItems ) . create ( this . uxVirtualForTrackBy ) ; } this . _isDirty = true ; } /** Determine which items have changed */ private getChanges ( ) { return this . _differ . diff ( this . _renderedItems ) ; } /** Insert, move and remove any items within the view */ private applyChanges ( changes ) { changes . forEachOperation ( ( record , previousIndex , currentIndex ) => { if ( previousIndex === null ) { const view = this . createView ( currentIndex ) ; view . context . $implicit = record . item ; } else if ( currentIndex === null ) { const view = this . _viewContainerRef . detach ( currentIndex ) as EmbeddedViewRef < VirtualForOfContext < T > > ; const index = this . _viewContainerRef . indexOf ( view ) ; if ( this . _templateCache . length < this . _cacheSize ) { this . _templateCache . push ( view ) ; } else { index === - 1 ? view . destroy ( ) : this . _viewContainerRef . remove ( index ) ; } } else { const view = this . _viewContainerRef . get ( previousIndex ) as EmbeddedViewRef < VirtualForOfContext < T > > ; this . _viewContainerRef . move ( view , currentIndex ) ; view . context . $implicit = record . item ; } } ) ; changes . forEachIdentityChange ( ( record ) => { const view = this . _viewContainerRef . get ( record . currentIndex ) as EmbeddedViewRef < VirtualForOfContext < T > > ; if ( view ) { view . context . $implicit = record . item ; } } ) ; this . updateContexts ( ) ; } updateContexts ( ) { for ( let idx = 0 ; idx < this . _viewContainerRef . length ; idx ++ ) { const view = this . _viewContainerRef . get ( idx ) as EmbeddedViewRef < VirtualForOfContext < T > > ; view . context . index = this . _renderedRange . start + idx ; view . context . count = this . _dataset . length ; view . context . first = view . context . index === 0 ; view . context . last = view . context . index === view . context . count - 1 ; view . context . even = view . context . index % 2 === 0 ; view . context . odd = ! view . context . even ; view . rootNodes . forEach ( ( node ) => { this . _renderer . setStyle ( node , 'position' , 'absolute' ) ; this . _renderer . setStyle ( node , 'width' , '100%' ) ; this . _renderer . setStyle ( node , 'top' , '0' ) ; this . _renderer . setStyle ( node , 'transform' , `translateY(${ view . context . index * this . _virtualScroll . itemSize }px` ) ; } ) ; view . detectChanges ( ) ; } } private createView ( index ) { const cachedTemplate = this . _templateCache . pop ( ) ; if ( cachedTemplate ) { cachedTemplate . context . $implicit = null ; cachedTemplate . context . index = - 1 ; cachedTemplate . context . count = - 1 ; cachedTemplate . context . first = false ; cachedTemplate . context . last = false ; cachedTemplate . context . even = false ; cachedTemplate . context . odd = false ; this . _viewContainerRef . insert ( cachedTemplate , index ) ; return cachedTemplate ; } return this . _viewContainerRef . createEmbeddedView < VirtualForOfContext < T > > ( this . _templateRef , { $implicit : null , index : - 1 , count : - 1 , first : false , last : false , even : false , odd : false , } , index ) ; } private defaultTrackBy ( index ) { return index ; } } /** We want to supply the same properties as `ngFor` */ export interface VirtualForOfContext < T > { $implicit : T ; index : number ; count : number ; first : boolean ; last : boolean ; even : boolean ; odd : boolean ; }