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.
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>
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;"
}
<my-todo-list>
<my-todo-item *ngFor="let item of todoItems"
[title]="item.title"
[description]="item.description">
</my-todo-item>
</my-todo-list>
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:
- DRY — I am not repeating the same block of code for each To Do item.
- 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.
- 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"
Let’s break that down.
- 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 -
todoItems
is a public property in my component that represents an array of data, in this case an array ofTodoItem
objects - The code
<my-todo-item *ngFor="let item of todoItems" ...></my-todo-item>
tells the compiler to add a newmy-todo-item
component for eachitem
in thetodoItems
array. The directive will do this by iterating over each object in thetodoItems
array and keeps a reference to each object in a variable calleditem
- Finally, I am able to customize each
MyTodoItemComponent
by referencing theitem
variable and binding data from theitem
to theMyTodoItemComponent
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}`);
}
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
orodd
- 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
orlast
, 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);
}
}
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>
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:
- We stored the
index
variable that Angular provides using theas
syntax in thengFor
directive with the codeindex as i
. Now we can reference the index for each item with thei
variable. - We added a listener on the
delete
output with(delete)="deleteItem(i)"
. This calls thedeleteItem
function we added earlier and passes in thei
variable we created fromngFor
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>
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;"
}
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>
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.
- We added the track by function to the
ngFor
directive with the following codetrackBy: trackByFn
. This tells thengFor
directive to use the function namedtrackByFn
in my component to retrieve the value used to compare the changes to the array rather than the reference identity of the object. - 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). - Now when we add or remove an element to the
todoItems
array, instead of checking the reference identity of each item in thetodoItems
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)