Build your own ngFor in Angular

Chidume Nnamdi 🔥💻🎵🎮
9 min readMar 17, 2024

NgFor is the most used and the most powerful structural in-built directive in Angular.

It is used to repeatedly render elements for each item in a collection.

A collection is an iterable object. Examples are arrays, strings, maps, sets, generator functions, argument objects, and custom iterables.

Anything iterable by the traditional for…of statement in JavaScript can also be iterated over by the ngFor. If you want to learn about for…of statement and the ngFor directive, you can follow developer and JavaScript news in daily.dev to learn more about JavaScript and Angular.

I found most of the sources I used for this article on daily.dev, it’s a great browser extension that lets you set up a personal news feed, and has a lot of great Angular content. Here’s a link to the angular posts on daily.dev — https://app.daily.dev/search?q=angular

Now, the concept or the inner workings of this ngFor structural directive seems vague and complex, but in this post, I will show you how we can build our ngFor directive.

Let’s go.

NgFor

As we said, the ngFor is used to render elements for an item in a collection.

The basic usage of ngFor is this:

<ul>

<li *ngFor=”let item of items”>{{ item }}</li>

</ul>

This example uses ngFor to iterate over an array called items and renders a list item for each element in the array.

Scaffolding project

We will start by creating an Angular project.

npm install -g @angular/cli

ng new ngfor

You will be prompted to choose some project settings, such as whether to include Angular routing and which stylesheets to use (CSS, SCSS, etc.). Make your selections based on your project requirements.

Let’s navigate into our project and serve it:

cd ngfor

yarn serve

Good!

Creating ngFor directive

Now, we begin.

First, we create a directive ngFor in a ngFor.directive.ts file.

import { Directive } from “@angular/core”;

@Directive({

selector: “[myNgFor][myNgForOf]”,

standalone: true,

})

export class MyNgFor {}

We set our variables. first, we set a _myNgFor class field. This field will hold the collection to be iterated over. Next, we will create differ and _trackByFn variables. The differ will hold the IterableDiffer instance for any changes in the _myNgFor collection.

The _trackByFn is a function that holds the track-by function passed to the ngFor.

@Directive({

selector: “[myNgFor][myNgForOf]”,

standalone: true,

})

export class MyNgFor<T, U extends NgIterable<T> = NgIterable<T>>

implements DoCheck

{

private _myNgFor: NgIterable<T> | undefined | null = null;

private differ: IterableDiffer<T> | null = null;

private _trackByFn!: TrackByFunction<T>;

}

See that the MyNgFor implements the DoCheck, this is because we will implement the ngDoCheck lifecycle in it, to detect when a change detection is run on our directive so, we check the _myNgFor for any changes and update the UI.

Next, we set our inputs:

@Directive({

selector: “[myNgFor][myNgForOf]”,

standalone: true,

})

export class MyNgFor<T, U extends NgIterable<T> = NgIterable<T>>

implements DoCheck

{

private _myNgFor: NgIterable<T> | undefined | null = null;

private differ: IterableDiffer<T> | null = null;

private _trackByFn!: TrackByFunction<T>;

@Input()

set myNgForTrackBy(fn: TrackByFunction<T>) {

this._trackByFn = fn;

}

@Input()

set myNgForOf(myNgForOf: NgIterable<T> | undefined) {

this._myNgFor = myNgForOf;

}

}

We set two @Input decorators to two class fields: myNgForTrackBy and myNgForOf.

The myNgForTrackBy is used to receive the track-by function. The myNgForOf is used to receive the collection to be iterated over.

Next, we setup constructor and inject ViewContainerRef, TemplateRef, and IterableDiffers.

@Directive({

selector: “[myNgFor][myNgForOf]”,

standalone: true,

})

export class MyNgFor<T, U extends NgIterable<T> = NgIterable<T>>

implements DoCheck

{

private _myNgFor: NgIterable<T> | undefined | null = null;

private differ: IterableDiffer<T> | null = null;

private _trackByFn!: TrackByFunction<T>;

@Input()

set myNgForTrackBy(fn: TrackByFunction<T>) {

this._trackByFn = fn;

}

@Input()

set myNgForOf(myNgForOf: NgIterable<T> | undefined) {

this._myNgFor = myNgForOf;

}

constructor(

private viewRef: ViewContainerRef,

private templateRef: TemplateRef<any>,

private differs: IterableDiffers

) {}

}

The viewRef will hold the host view of the element/directive of where the MyNgFor directive is applied. If the MyNgFor directive is applied to a div element. Then, the viewRef will hold the host view of the div element. A host view is a view where other views can be created or template views can be embedded.

The templateRef holds the embedded template of the directive.

The differs holds the IterableDiffers instance.

Next, we implement the ngDoCheck lifecycle method.

ngDoCheck(): void {

}

First, we grab the viewRef and the collection and assign them to a local variable inside the ngDoCheck.

ngDoCheck(): void {

const viewRef = this.viewRef;

const value = this._myNgFor;

}

Next, we check if the value is undefined then we exit.

ngDoCheck(): void {

const viewRef = this.viewRef;

const value = this._myNgFor;

if (!value) return;

}

Next, we check if differs is available and get the changes. If there are no changes, we exit:

ngDoCheck(): void {

const viewRef = this.viewRef;

const value = this._myNgFor;

if (!value) return;

if (!this.differ) {

this.differ = this.differs.find(value).create(this._trackByFn);

}

if (this.differ) {

const changes = this.differ.diff(this._myNgFor);

if (!changes) return;

}

}

The diff gets the changes that occurred in the collection. If there are changes, we will check for newly added items to the collection and create and embed new views to the view.

ngDoCheck(): void {

const viewRef = this.viewRef;

const value = this._myNgFor;

if (!value) return;

if (!this.differ) {

this.differ = this.differs.find(value).create(this._trackByFn);

}

if (this.differ) {

const changes = this.differ.diff(this._myNgFor);

if (!changes) return;

changes.forEachAddedItem((record: IterableChangeRecord<T>) => {

viewRef.createEmbeddedView(this.templateRef, {

$implicit: record.item,

even: -1,

odd: -1,

index: -1,

first: -1,

last: -1,

count: 0,

});

});

}

}

The forEachAddedItem gets all the added items to the collection. We loop through them, create an embedded view, and embed the view to the view. This embeds the new view next to the last view of the directive.

The viewRef.createEmbeddedView is an API used to create and embed template views to the current host view. The this.templateRef is the template view of the MyNgFor directive.

See that we passed an object as the second parameter. This object is the context object used to pass data to an ng-template view.

We set the $implicit to the current record item in the newly added items. The $implicit is the default value of the context object.

So we passed even, odd, index, first, last, and count all set to either -1 or 0. We will fill out the correct values later. We set them to -1 or 0 for now, because we are only getting the added items only, we are not getting all the items, so setting the even, odd, etc will result in wrong data.

The even, odd, index, first, last, and count are contextual variables passed to ngFor. They are used to hold the values and info based on the current iteration or the whole iteration. The even and odd are used to indicate if the current index is either an even or odd number. The first and last indicates if the current iteration is the first or last. The index holds the current index, the count holds the total number of iterations in the collection.

Next, we use the changes.forEachMovedItem to get all items in the collection that have moved places in the collection. We get the previous index of the item and get the view from the host view.

Then, we will use the move API of the ViewRef to move the view to the current location index of the view.

ngDoCheck(): void {

const viewRef = this.viewRef;

const value = this._myNgFor;

if (!value) return;

if (!this.differ) {

this.differ = this.differs.find(value).create(this._trackByFn);

}

if (this.differ) {

const changes = this.differ.diff(this._myNgFor);

if (!changes) return;

changes.forEachAddedItem((record: IterableChangeRecord<T>) => {

viewRef.createEmbeddedView(this.templateRef, {

$implicit: record.item,

even: -1,

odd: -1,

index: -1,

first: -1,

last: -1,

count: 0,

});

});

changes.forEachMovedItem((record: IterableChangeRecord<T>) => {

const viewRefItem = viewRef.get(record.previousIndex as number);

viewRef.move(viewRefItem as ViewRef, record.currentIndex as number);

});

}

}

Next, we iterate all removed items in the collection. We will remove the items from the MyNgFor host view.

ngDoCheck(): void {

const viewRef = this.viewRef;

const value = this._myNgFor;

if (!value) return;

if (!this.differ) {

this.differ = this.differs.find(value).create(this._trackByFn);

}

if (this.differ) {

const changes = this.differ.diff(this._myNgFor);

if (!changes) return;

changes.forEachAddedItem((record: IterableChangeRecord<T>) => {

viewRef.createEmbeddedView(this.templateRef, {

$implicit: record.item,

even: -1,

odd: -1,

index: -1,

first: -1,

last: -1,

count: 0,

});

});

changes.forEachMovedItem((record: IterableChangeRecord<T>) => {

const viewRefItem = viewRef.get(record.previousIndex as number);

viewRef.move(viewRefItem as ViewRef, record.currentIndex as number);

});

changes.forEachRemovedItem((record: IterableChangeRecord<T>) => {

viewRef.remove(record.previousIndex as number);

});

}

}

Next, We have all the changes in place now in our host view. This is the right place to set all the contextual variables.

ngDoCheck(): void {

const viewRef = this.viewRef;

const value = this._myNgFor;

if (!value) return;

if (!this.differ) {

this.differ = this.differs.find(value).create(this._trackByFn);

}

if (this.differ) {

const changes = this.differ.diff(this._myNgFor);

if (!changes) return;

changes.forEachAddedItem((record: IterableChangeRecord<T>) => {

viewRef.createEmbeddedView(this.templateRef, {

$implicit: record.item,

even: -1,

odd: -1,

index: -1,

first: -1,

last: -1,

count: 0,

});

});

changes.forEachMovedItem((record: IterableChangeRecord<T>) => {

const viewRefItem = viewRef.get(record.previousIndex as number);

viewRef.move(viewRefItem as ViewRef, record.currentIndex as number);

});

changes.forEachRemovedItem((record: IterableChangeRecord<T>) => {

viewRef.remove(record.previousIndex as number);

});

for (let index = 0; index < this.viewRef.length; index++) {

const item = this.viewRef.get(index) as EmbeddedViewRef<any>;

const context = item.context;

context.index = index;

context.even = index % 2 === 0;

context.odd = !context.even;

context.first = index === 0;

context.last = index === this.viewRef.length — 1;

context.count = this.viewRef.length;

}

}

}

You see how we set the contextual variables. We looped through the length of the viewRef, this is because the ViewContainerRef contains a length property that holds the number of embedded views in it.

For each loop, we get the embedded view in the current index and store it in the item variable.

Next, we retrieved the content object from it. And then we update all the contextual variables in it to the actual values.

The index takes the current index of the iteration.

even uses the % modulo operator to check if the current index is divisible by 2 and then assign true if the remainder is 0 or false if the remainder is not equal to 0.

odd is assigned by flipping the even contextual variable value.

first is assigned by checking if the current index is 0.

last is checked if the current index is equal to the length of the viewRef with 1 subtracted from it.

count is assigned to the value of the viewRef length.

Finally, we loop through the identity changes in the collection which had their identities changed. These identities are assigned by the return value of the _trackByFn track-by function.

We use the forEachIdentityChange API to get these changes and loop through them. We update the $implicit value to the current item of the record.

ngDoCheck(): void {

const viewRef = this.viewRef;

const value = this._myNgFor;

if (!value) return;

if (!this.differ) {

this.differ = this.differs.find(value).create(this._trackByFn);

}

if (this.differ) {

const changes = this.differ.diff(this._myNgFor);

if (!changes) return;

changes.forEachAddedItem((record: IterableChangeRecord<T>) => {

viewRef.createEmbeddedView(this.templateRef, {

$implicit: record.item,

even: -1,

odd: -1,

index: -1,

first: -1,

last: -1,

count: 0,

});

});

changes.forEachMovedItem((record: IterableChangeRecord<T>) => {

const viewRefItem = viewRef.get(record.previousIndex as number);

viewRef.move(viewRefItem as ViewRef, record.currentIndex as number);

});

changes.forEachRemovedItem((record: IterableChangeRecord<T>) => {

viewRef.remove(record.previousIndex as number);

});

for (let index = 0; index < this.viewRef.length; index++) {

const item = this.viewRef.get(index) as EmbeddedViewRef<any>;

const context = item.context;

context.index = index;

context.even = index % 2 === 0;

context.odd = !context.even;

context.first = index === 0;

context.last = index === this.viewRef.length — 1;

context.count = this.viewRef.length;

}

changes.forEachIdentityChange((record: IterableChangeRecord<T>) => {

const view = viewRef.get(

record.currentIndex as number

) as EmbeddedViewRef<any>;

console.log({ …view });

view.context.$implicit = record.item;

});

}

}

Now, our directive is done.

Run our directive

Now, let’s test our directive.

  • Create a component.

import { Component } from “@angular/core”;

@Component({

selector: “test-child”,

standalone: true,

template: ``,

})

export class TestComponent {}

  • Import MyNgFor.

import { Component } from “@angular/core”;

import { MyNgFor } from “src/my-ng-for/myNgFor”;

@Component({

selector: “test-child”,

standalone: true,

template: ``,

})

export class TestComponent {}

  • Add MyNgFor to imports array.

import { Component } from “@angular/core”;

import { MyNgFor } from “src/my-ng-for/myNgFor”;

@Component({

selector: “test-child”,

standalone: true,

imports: [MyNgFor],

template: ``,

})

export class TestComponent {}

  • Create a fruits class variable.

import { Component } from “@angular/core”;

import { MyNgFor } from “src/my-ng-for/myNgFor”;

@Component({

selector: “test-child”,

standalone: true,

imports: [MyNgFor],

template: ``,

})

export class TestComponent {

fruits = [“apple”, “mango”, “banana”];

}

  • Create a div and append *myNgFor=”let fruit of fruits” to it.

import { Component } from “@angular/core”;

import { MyNgFor } from “src/my-ng-for/myNgFor”;

@Component({

selector: “test-child”,

standalone: true,

imports: [MyNgFor],

template: `

<div *myNgFor=”let fruit of fruits”>

{{ fruit }}

</div>

`,

})

export class TestComponent {

fruits = [“apple”, “mango”, “banana”];

}

We will see the below in our DOM:

<test-child

><div>apple</div>

<div>mango</div>

<div>banana</div>

<! — bindings={

“ng-reflect-my-ng-for-of”: “apple,mango,banana”

} →</test-child

>

We have successfully created our custom ngFor directive.

Summary

In this blog post, we were able to see how powerful ngFor is.

Also, we learned how to create a custom ngFor directive. We learned along the way:

  • how to use @Input decorators.
  • TemplateRef to get an emebedded view of our directive.
  • ViewContainerRef to get the host view of where to append our views.
  • IterableDiffers to get changes in our ngFor collection.
  • How to pass contextual variables we have in the ngFor directive.

--

--

Chidume Nnamdi 🔥💻🎵🎮

JS | Blockchain dev | Author of “Understanding JavaScript” and “Array Methods in JavaScript” - https://app.gumroad.com/chidumennamdi 📕