DEV Community

Cover image for Angular Structural Directives, Part 2: NgFor
ng-conf
ng-conf

Posted on • Edited on

Angular Structural Directives, Part 2: NgFor

Michi DeWitt | ng-conf | Oct 2020

This is part of an ongoing series covering all the built-in Angular structural directives. This series is intended for new and experienced Angular developers. Previously we covered the ngIf directive.


image

Introduction

In Part 2 of our Angular Structural Directive series we’ll be covering the ngFor directive. Like the ngIf directive we covered previously, this directive is added as an attribute to any element in an HTML template.

What is the NgFor Directive and How is it Used?

The ngFor directive is used to iterate over a collection of objects and add an instance of a specified template for each item in the collection.

Okay — that sounds very technical. What does that mean?

Here’s a simple example.

I’ve created a custom To Do list application for myself (because all demos must use To Do lists, right?). In my application, I have the following component that shows all my To Do list items:

<my-todo-list>
  <my-todo-item 
    title="Walk Dog" 
    description="Take the dog for a walk befor the gym">
  </my-todo-item>
  <my-todo-item 
    title="Gym" 
    description="Gym class at 5:30pm">
  </my-todo-item>
  <my-todo-item 
    title="Blog Post" 
    description="Write NgFor blog post">
  </my-todo-item>
</my-todo-list>
Enter fullscreen mode Exit fullscreen mode

Here I have 3 items on my To Do list — “Walk Dog”, “Gym”, and “Blog Post”.

There are two problems with the list in this format.

First, this is a very static. If I want to add or remove something from my To Do list, I need to change the code and re-deploy my application.

Second and potentially more problematic, I can’t have any other people use my application because they would have no way to edit their own To Do list. This isn’t very flexible.

Instead, I can use the ngFor directive to iterate over a list of To Do items and add a new my-todo-item component for each item in my list.

Here’s the new code:

@Component({
    selector: 'my-ng-for-example',
    templateUrl: './my-ng-for-example.component.html',
})
export class MyNgForExample {
    public todoItems: TodoItem[] = [
        {
            title: "'Walk Dog',"
            description: 'Take the dog for a walk before the Gym'
        },
        {
            title: "'Gym',"
            description: 'Gym class at 5:30pm'
        },
        {
            title: "'Blog Post',"
            description: 'Write ngFor Blog Post'
        }
    ]
}

export interface TodoItem {
    title: "string;"
    description: "string;"
}
Enter fullscreen mode Exit fullscreen mode
<my-todo-list>
  <my-todo-item *ngFor="let item of todoItems" 
    [title]="item.title" 
    [description]="item.description">
  </my-todo-item>
</my-todo-list>
Enter fullscreen mode Exit fullscreen mode

Note: There is no example of the code in the MyTodoItemComponent, but you can assume it shows all the data a user needs to see in the To Do card and provides a UI for any actions on the card.

Much better! The refactored code that utilizes the ngFor directive has the following improvements:

  1. DRY — I am not repeating the same block of code for each To Do item.
  2. Dynamic — I can hook the todoItems array up to an Observable returned by a service or store so the list is always up to date and can be changed without re-deploying my code.
  3. Extendible — If I want to add additional features, such as a button or completed state, I just have to update the one line in my template and all my To Do list items in the template will be updated.

Basic Usage

Let’s take a step back and see how the ngFor directive works. The directive was added using the following snippet of code

*ngFor="let item of todoItems"
Enter fullscreen mode Exit fullscreen mode

Let’s break that down.

  1. The directive is added using the * syntax and the name of the directive — ngFor. The * let’s any developer know the attribute represents a structural directive
  2. todoItems is a public property in my component that represents an array of data, in this case an array of TodoItem objects
  3. The code <my-todo-item *ngFor="let item of todoItems" ...></my-todo-item> tells the compiler to add a new my-todo-item component for each item in the todoItems array. The directive will do this by iterating over each object in the todoItems array and keeps a reference to each object in a variable called item
  4. Finally, I am able to customize each MyTodoItemComponent by referencing the item variable and binding data from the item to the MyTodoItemComponent inputs with <my-todo-item *ngFor="let item of todoItems" [title]="item.title" [description]="item.description"></my-todo-item>

Like the ngIf directive, the ngFor directive is easy to use because it maps so closely to the for-of Javascript syntax many Angular developers are already familiar with. You can mentally map the HTML syntax above to:

for (let item of todoItems) {
    // Execute this code for each item in todoItems
    console.log(`${item.title}: ${item.description}`);
}
Enter fullscreen mode Exit fullscreen mode

Using Exported NgFor variables

Sometimes you need to know additional information about each object’s position in the array to properly render each instance of the template.

Some common needs are:

  • Add a different CSS class for every other element (example: different colors for rows in a table)
  • Know the index of the object in the array when an action is executed (there will be an example for this later on)
  • Know if the item is the first or last item in the array (example: add a margin to the bottom of the template if the item is not the last item in the array)

Luckily, Angular exports variables for all of these use cases!

  • Need to add a different CSS class for every other element? Use even or odd
  • Need to know the of the index the object in the array when an action is executed? Use index
  • Need to know if the item is the first or last item in the array? Use first or last, respectively

Angular has documentation for all the variables that are available, but we’ll use index as an example in this post.

Let’s go back to our To Do list example. I want to add a new feature to delete an item from my To Do list (I don’t feel like going to the gym today). Let’s say the MyTodoItem component has a delete button that will emit an event whenever a user clicks on the button. But it’s the responsibility of the owner of the list to actually remove the To Do item from the list.

I’ve added a simple delete function, deleteItem, that will remove the To Do item at a specified index from my array of To Do items.

@Component({
    selector: 'my-ng-for-example',
    templateUrl: './my-ng-for-example.component.html',
})
export class MyNgForExample {
    public todoItems: TodoItem[] = [...];

    public deleteItem(index: number): void {
        todoItems.splice(index, 1);
    }
}
Enter fullscreen mode Exit fullscreen mode

Now I’ll update the template to listen for delete output events from the MyTodoItemComponent.

<my-todo-list>
  <my-todo-item *ngFor="let item of todoItems; index as i" 
    [title]="item.title" 
    [description]="item.description"
    (delete)="deleteItem(i)">
  </my-todo-item>
</my-todo-list>
Enter fullscreen mode Exit fullscreen mode

That’s all I need! Now each time the delete event is emitted for any To Do list item, the deleteItem function will be called with the index of item the delete output event fired from.

Let’s break down what code we added:

  1. We stored the index variable that Angular provides using the as syntax in the ngFor directive with the code index as i. Now we can reference the index for each item with the i variable.
  2. We added a listener on the delete output with (delete)="deleteItem(i)". This calls the deleteItem function we added earlier and passes in the i variable we created from ngFor index local variable.

This example used the index variable specifically, but the same syntax can be used to store any of the other local variables, and there is no limit for how many variables may be added. That means you could do something like:

<my-todo-item *ngFor="let item of todoItems; index as i; even as 
isEven; first as isFirst">
   ...
</my-todo-item>
Enter fullscreen mode Exit fullscreen mode

Tracking Changes

The one bit of magic we haven’t covered yet is how changes are tracked. The ngFor directive will track changes in the array and will update the DOM whenever a change is detected for an object in the array. The following changes will trigger an update so the DOM properly reflects the contents of the array:

  • A new object is added to the array
  • A object is removed from the array (this is why the delete function we added above works)
  • The items in the array are re-ordered

No additional code is required from the developer to make change detection work.

By default, the changes are tracked using the reference identity of each object in the array. The downside of tracking using the reference identity is that if you’re using a Reactive Programming strategy with immutable arrays and objects, you may be rebuilding all the objects in the array. This will cause the DOM to flash when there is a change to the array because it’s rebuilding each element created by the ngFor directive. Even if only one element was added or removed. Not only is this ugly, this can be very expensive!

As with other problems we’ve encountered, Angular provides a simple solution for this: add a custom trackBy function that is used to track changes. The trackBy function allows the developer to specify which value should be used to compare the objects if the object has changed. Generally, it’s best to return a unique id for the object that does not change when the object is rebuilt.

Let’s take a look at track by in action.

@Component({
    selector: 'my-ng-for-example',
    templateUrl: './my-ng-for-example.component.html',
})
export class MyNgForExample {
    public todoItems: TodoItem[] = [
        {
            id: '1',
            title: "'Walk Dog',"
            description: 'Take the dog for a walk before the Gym'
        },
        {
            id: '2',
            title: "'Gym',"
            description: 'Gym class at 5:30pm'
        },
        {
            id: '3',
            title: "'Blog Post',"
            description: 'Write ngFor Blog Post'
        }
    ];

    public deleteItem(index: number): void { ... }

    public trackByFn(index: number, item: TodoItem): string {
        return item.id;
    }
}

export interface TodoItem {
    id: string;
    title: "string;"
    description: "string;"
}
Enter fullscreen mode Exit fullscreen mode

In the code above, I’ve made the following changes:

  • Updated the TodoItem interface to added a unique id for each To Do item
  • Updated our array to add an id to each To Do item in the array
  • Added a track by function that returns the id of the To Do item

Next, we’ll update the template:

<my-todo-list>
  <my-todo-item *ngFor="let item of todoItems; index as i; trackBy: trackByFn" 
    [title]="item.title" 
    [description]="item.description"
    (delete)="deleteItem(i)">
  </my-todo-item>
</my-todo-list>
Enter fullscreen mode Exit fullscreen mode

Now, no matter how we update the array of todoItems, we’ll only rebuild the parts of the DOM that change rather than rebuilding the entire template.

Let’s take a step back and break down these changes we made.

  1. We added the track by function to the ngFor directive with the following code trackBy: trackByFn. This tells the ngFor directive to use the function named trackByFn in my component to retrieve the value used to compare the changes to the array rather than the reference identity of the object.
  2. We implemented the track by function in the component which takes in the index of the object in the array and the object itself. We return the value held in the id of the object. (In our case, we didn’t need to use the index).
  3. Now when we add or remove an element to the todoItems array, instead of checking the reference identity of each item in the todoItems array, the directive will compare the id of the object before the change and the id after. If they are the same, it will rebuild the DOM item for that particular To Do item.

That’s all we need to do to use our own custom logic to track changes in the ngFor directive!

Summary

We’ve now successfully used the ngFor directive to make a dynamic To Do list component that can be used by any user of our application.

Let’s go over the main points:

🛠 The ngFor directive is allows you to iterate over an array and add a new instance of a template for each item in the array.

ℹ️ Make use of the local variables Angular provides such as index, even, and first to get additional information about the position of each object in the array as you are iterating through.

🎯 Use the trackBy function to have better control over when the DOM rebuilds your template when you make changes to the array bound to your ngFor directive.

Now you are ready to use the ngFor directive in your next Angular application. Happy coding!


Additional Resources

Angular *ngFor Documentation: https://angular.io/api/common/NgForOf

ng-conf: The Musical is coming

ng-conf: The Musical is a two-day conference from the ng-conf folks coming on April 22nd & 23rd, 2021. Check it out at ng-conf.org

Top comments (0)