Angular experts! I'm trying to understand async pipes in Angular, but I'm stuck in a basic scenario. I have two select elements in the UI, one containing posts and one containing related comments. I want to set a posting (the last one) as the initially selected one for the select element displaying posts, and I want to use the selected item to filter related comments in the second select. This is not working in my code, for which I have created a simplified version in Stackblitz:
https://stackblitz.com/edit/angular-p6ynuy
Can any of you explain to me what I'm doing wrong? This is the relevant code fragment and HTML:
ngOnInit() {
this.postList$ = this.getPostList();
// latestPost$ is not is use yet, but maybe it could be used to set the selected post?
this.latestPost$ = this.postList$
.pipe(
map(posts => posts[posts.length - 1])
);
this.selectedPost$ = combineLatest([
this.postList$,
this.postSelectedAction$
])
.pipe(
map(([posts, selectedPostId]) => posts.find(post => post.id === selectedPostId))
);
this.commentList$ = this.selectedPost$
.pipe(switchMap(
post => this.getCommentList(post)
));
}
<select [ngModel]="selectedPost$ | async" (change)="onSelected($event.target.value)">
<option *ngFor="let post of postList$ | async" [ngValue]="post">
{{post.id}} {{post.title}}
</option>
</select>
<select>
<option *ngFor="let comment of commentList$ | async" [ngValue]="comment">
{{comment.id}} {{comment.postId}} {{comment.name}}
</option>
</select>
Angular Compares Objects by Reference by Default
You are almost there. The problem is that your select
gets a list of option
s referencing Post
s as part of ngFor
. Now to find out which option
is currently selected, Angular compares each post
object with the current value of selectedPost$ | async
.
The way that's done is, by default, using the ===
operator. The ===
operator compares primitives by value, but objects by reference. Example:
console.log('a' === 'a');
const obj = {'a': 'b'};
const obj2 = obj;
console.log(obj === obj2);
console.log(obj === {'a': 'b'});
So in order for post
to count as the same post as selectedPost$ | async
, they'd have to be the actual same object, not just an object that looks the same.
You Actually Retrieve Multiple Copies of The Same Post, Not Just one Post
Now that's not the case: As you're using the async
pipe, it can happen that during change detection, posts are reloaded from the API. When you check out your browser's network tab, you can actually see that there are three requests:
The response payload of all requests is the same, but as the Post
objects are returned three times, they are stored in memory three times. JavaScript has no way of knowing they are actually the same, and the ===
comparison returns false
.
Solution: Provide Your Own compareWith
function
How can you solve this? You can help Angular compare Post
objects properly in your select
. You just have to ask yourself the question: How do I know that two Post
objects are in fact the same object? The answer, in this case, is: When they have the same ID.
You can now write your own instruction for Angular to compare objects or your select: Just add a compareWith
input to the select:
<select [ngModel]="selectedPost$ | async"
(change)="onSelected($event.target.value)"
[compareWith]="comparePosts"
>
Now Angular knows to use a method called comparePosts
to compare two posts. Now how can that method look? For example like this:
comparePosts(p1: Post, p2: Post) {
return p1.id === p2.id;
}
Now Angular knows how to properly compare two Post
objects and your problem is solved.
PS: Please make sure to write a better comparePosts
method than I did, for example also properly handling undefined
and null
values.