top of page
Writer's pictureThe Tech Platform

Advanced Angular Structural Directive to Render Long Lists



This article shows you how to create an advanced structural directive similar to Angular’s own ngFor and use that to render a long list using progressive rendering concept.


Pre-requisites

Basic understanding of JavaScript and Angular.


Setup and brief structural directives refresher.

We plan to use this directive like this in our components.

<ul>
  <li *ngxtFor="
      let item of longList; 
      itemsAtOnce: 500;
      intervalLength: 50">
   Item - {{item.id}}
  </li>
</ul>

The “*” syntax in the directive basically tells angular to wrap the element in an ngTemplate and not render it right away. We can then access the template in our directive by injecting TemplateRef and ViewContainer services and use our custom logic to render the template.


itemsAtOnce and intervalLength will be inputs specify how many items it should render at a time and how long should it wait before rendering it.


Next, in our app component we create a list of 50000 items.

ngOnInit() {
   const longList = [];
   for (let i = 0; i < 50000; i++) {
     longList.push({ id: i });
   }
   this.longList = longList;
}

That will be all for our app component. Next let’s start creating our directive. We will start with basic structure without any logic first.

import{Directive,Input,TemplateRef,ViewContainerRef}from'@angular/core';

@Directive({selector: '[ngxtFor]',
})
export class NgxtForDirective
{
    private items: any[]=[];
    constructor(
        private templateRef: TemplateRef<any>,
        private viewContainer: ViewContainerRef
    ){}  
        
    @Input('ngxtForOf')
    public set ngxtForOf(items: any)
    {
        this.items=items;
        //Clear any existing items
        this.viewContainer.clear();
    }  
    
    @Input('ngxtForItemsAtOnce')
    public itemsAtOnce: number=10;  
    
    @Input('ngxtForIntervalLength')
    public intervalLength: number=50;
}

The of in the ngxtForOf input lets us use the let items of items syntax with the directive. The other inputs also have to be prefixed with the selector of the directive for them to be used in the syntax shown in the first snippet.


If you are completely new to structural directives, you can refer to the angular guide on structural directives here


Introducing IterableDiffers angular service.

At this point we could just take the items and render them using viewContainer.createEmbeddedView method in the ngOnInit handler but that would not work if items get added to the list or if they get removed from it. That is where IterableDiffers comes into picture. It provides us a diffing strategy using which we can handle scenarios when items get added to or removed from our list without having to write the logic ourselves. So let’s add that to our directive.

import 
{  
    Directive,  
    EmbeddedViewRef,  
    Input,  
    IterableChangeRecord,  
    IterableDiffer,  
    IterableDiffers,  
    OnInit,  
    TemplateRef, 
    ViewContainerRef,  
    ViewRef,
} from '@angular/core';
    
@Directive(
{  
    selector: '[ngxtFor]',
})
export class NgxtForDirective 
{  
    private items: any[] = [];  
    private _diffrence: IterableDiffer<any> | undefined;  
    
    constructor(    
        private templateRef: TemplateRef<any>,    
        private viewContainer: ViewContainerRef  
    ) {}  
    
    @Input('ngxtForOf')  
    public set ngxtForOf(items: any) 
    {    
        this.items = items;     
        if (items) 
        {      
            this._diffrence = this.differs.find(items).create();    
        }    
        //Clear any existing items    
        this.viewContainer.clear();  
    }  
    
    @Input('ngxtForItemsAtOnce')  
    public itemsAtOnce: number = 10;  
    
    @Input('ngxtForIntervalLength')  
    public intervalLength: number = 50;  
    
    public ngDoCheck(): void 
    {    
        if (this._diffrence) 
        {      
            const changes = this._diffrence.diff(this.items);      
            if (changes) 
            {        
                const itemsAdded: any[] = [];        
                changes.forEachAddedItem((item) => 
                {          
                    itemsAdded.push(item);        
                });        
                
                //TODO: Logic to progressively render our items at a fixed 
                interval.       
                 //viewRefsMap will be populated as part of this process.        
                 
                 changes.forEachRemovedItem((item) => 
                 {          
                     //TODO: Logic to remove the deleted item from view        
                 });      
             }    
         }  
     }
}

As seen above, when we receive the items, we are creating an instance of IterableDiffer.This is what we will use to find when items are added/removed to the list.


IterableDiffers.find(items) will check if Angular has an IterableFactory which provides diffing strategy for the items which were passed to it. In our case, itemsis an iterable and Angular does have a factory for that. So that will be returned. Then we call create on that factory to get the instance of the

IterableDiffer which can be used to diff the list.


Important thing to note is that forEachAddedItem and forEachRemovedItem iterate over the IterableRecord objects. These hold a lot of additional information in addition to list item itself. We will use the currentIndex property to ensure we are adding/removing the template references from the correct index.


For learning more about IterableDiffers in Angular, refer to this wonderful article


Final step: Add the logic to progressively render the list.

private viewRefsMap: Map<any,ViewRef> = new Map<any, ViewRef>();

public ngDoCheck(): void
{
    if(this._diffrence)
    {
        const changes = this._diffrence.diff(this.items);
        if(changes)
        {
            const itemsAdded: any[] = [];
            changes.forEachAddedItem((item)=>
            {
                itemsAdded.push(item);
            });
            this.progressiveRender(itemsAdded);
                
            changes.forEachRemovedItem((item)=>
            {
                const mapView = this.viewRefsMap.get(item.item)asViewRef;
                    if(mapView)
                    {
                        const viewIndex = this.viewContainer.indexOf         
                                                            (mapView);
                        this.viewContainer.remove(viewIndex);
                        this.viewRefsMap.delete(item.item);
                    }
                });
            }
        }
    }
    
    private progressiveRender(items: IterableChangeRecord<any>[])
    {
        let interval: any;
        let start=0;
        let end=start + this.itemsAtOnce;
        if(end>items.length)
        {
            end=items.length;
    }
    this.renderItems(items,start,end);
    
    interval = setInterval(()=>
    {
        start = end;
        end = start+this.itemsAtOnce;
        if(end>items.length)
        {
            end = items.length;
        }
        this.renderItems(items,start,end);
        if(start> = items.length)
        {
            clearInterval(interval);
        }
    },this.intervalLength);
    }
    
    private renderItems(
        items: IterableChangeRecord<any>[],
        start: number,
        end: number
    ){
    items.slice(start,end).forEach((item)=>
    {
        const embeddedView = this.viewContainer.createEmbeddedView(
            this.templateRef,
            {
                $implicit: item.item,
            },item.currentIndex||0);
            this.viewRefsMap.set(item.item,embeddedView);
    });
}

Now we have all the pieces together. The progressiveRender method takes the array of IterableChangeRecord objects and passes it on to the renderItems method fixed number items at a time depending on the inputs. This method creates them on the DOM using createEmbeddedView method which you might have already seen before in simple structural directives.


Note the third argument to createEmbeddedView . This index argument ensures items are rendered in their correct spot when list items are removed or replaced.


The viewRefsMap is a private map which the directive uses to store references to the embedded view objects. It is populated as and when list items are processed. This map is then used in ngDoCheck to remove the view items when they are removed from the list from our app component


Conclusion

We have covered a lot of advanced topics in this article. I hope everyone reading this article finds it useful and is able to take something away from it. This is my first article on Medium and I had great fun writing it.



Resource: Medium - Sanjay Bhavnani


The Tech Platform

0 comments

コメント


bottom of page