Reputation: 2013
My model has a one to many relationship between Post and Imgs i.e. each Post has a number of Imgs. One of the simplified ways of displaying this structure would be:
<ul>
<li *ngFor="let post of posts">
{{post.postId}}
<p *ngFor="let img of getImgs(post.postId)">{{img.url}}</p>
</li>
</ul>
My problem is that the page goes into an Infinite Loop due to Angular's Change Detection mechanism. Is there a better way to restructure this code? (I'm using Angular 5) A simplified version of my code is as below:
My AppComponent looks like:
import { Component } from '@angular/core';
import {Post} from "./post"
import {PostService} from "./post.service";
import {Observable} from "rxjs/Observable";
import {Img} from "./img";
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
providers: [PostService]
})
export class AppComponent {
title = 'app';
posts : Post[];
constructor(private postService: PostService) { }
ngOnInit(): void {
this.getPosts();
}
getPosts(): void {
this.postService.getPosts().subscribe(posts => this.posts = posts);
}
getImgs(postId : string) : Img[] {
var retImgs : Img[];
this.postService.getImgsByPostId(postId).subscribe( imgs => retImgs = imgs);
return retImgs;
}
}
I get Posts and Imgs from a Web Service. The Posts do not contain the Imgs as part of themselves, but the Imgs are retrieved using the postId. If I would like to keep this model where the Imgs are not part of the Posts, I find it difficult to store the Imgs in the AppCompnent
. This forces me to put the getImgs()
call in the HTML Template, which gets called on every Change Detection event. What is the normal way in Angular 5 to handle this scenario?
I can ofcourse do some hacks to save/cache the output of getImgs()
, so I don't need to call the Web Service for subsequent requests or I could just change my model, but I am wondering how this is normally done.
Is there anyway to call a method from the Template such that it does not get called on every change detection mechanism?
Edit
In Response to @Floors suggestion to use a smarter getImgs()
that would cache the Images in a Map
I have tried the following AppComponent
: (The Imports and the Component Decorator are as above)
export class AppComponent {
title = 'app';
posts : Post[];
postIdToImgs : Map<string, Img[]>; //Added
constructor(private postService: PostService) {
this.postIdToImgs = new Map<string, Img[]>(); //Added
}
ngOnInit(): void {
this.getPosts();
}
getPosts(): void {
this.postService.getPosts().subscribe(posts => this.posts = posts);
}
getImgs(postId : string) : Img[] {
var retImgs : Img[];
console.log(`Call to getImgs(${postId})`); //Added
if(this.postIdToImgs.has(postId)) { //Added
console.log('Found value in Cache'); //Added
retImgs = this.postIdToImgs.get(postId); //Added
return retImgs; //Added
} //Added
this.postService.getImgsByPostId(postId).subscribe( imgs => {
this.postIdToImgs.set(postId, imgs); //Added
retImgs = imgs;
});
return retImgs;
}
}
Yes, this stops calls to the Backend after the first iteration, but I still get an Infinite sequence of
Cal to getImgs(#postId)
Found value in Cache
Where #postId is one of the 10 posts that I have on this page. I'm trying to learn Angular, so I'm not trying to just get this to work. I'm trying to find out:
Is there a way to have a method/function in the Template that is not called on every change detection?
Upvotes: 1
Views: 1657
Reputation: 34435
You can move your code to a pipe, which is not called everytime change detection takes place
https://angular.io/guide/pipes#pure-pipes
@Pipe({
name: 'getImages',
})
export class GetImagesPipe implements PipeTransform {
postIdToImgs : Map<string, Img[]> = new Map<string, Img[]>();
constructor(private postService: PostService) { }
transform(postId: string): any {
var retImgs : Img[];
console.log(`IN PIPE: Call to getImgs(${postId})`);
/*This commented cache implementation is not needed as the pipe won't be called again if the post does not change
if(this.postIdToImgs.has(postId)) {
console.log('IN PIPE: Found value in Cache');
retImgs = this.postIdToImgs.get(postId);
return retImgs;
} */
this.postService.getImgsByPostId(postId).subscribe( imgs => {
this.postIdToImgs.set(postId, imgs); //Added
retImgs = imgs;
});
return retImgs;
}
}
I created a stackblitz https://stackblitz.com/edit/angular-9n9h18?file=app%2Fimg.pipe.ts
Upvotes: 2
Reputation:
The best way to solve this problem is to put both object into a wrapper. Fill this wrapper with both the post and it's belonging images. And then perform ngFor using this wrapper.
e.g.
import { Component } from '@angular/core';
import {Post} from "./post"
import {PostService} from "./post.service";
import {Observable} from "rxjs/Observable";
import {Img} from "./img";
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
providers: [PostService]
})
export class AppComponent {
title = 'app';
myWrapperList: Array<MyWrapper> = [];
constructor(private postService: PostService) { }
ngOnInit(): void {
this.init();
}
init(): void {
this.postService.getPosts().subscribe( posts =>
posts.forEach(post => {
this.postService.getImgsByPostId(post.postId).subscribe(imgs =>
let myWrapper = new MyWrapper(post, imgs);
this.myWrapperList.push(myWrapper);
});
);
}
}
export class MyWrapper {
constructor(
public post: Post,
public images: Array<Images>
){}
}
And in your template
<ul>
<li *ngFor="let wrapper of myWrapperList">
{{wrapper?.post?.postId}}
<p *ngFor="let img of wrapper.images">{{img?.url}}</p>
</li>
</ul>
Upvotes: 2