Angular ngFor running in infinite loop when displaying array - html

i'm currently trying to make a Logbox for my webapp. It gets an Array of logs and should display them row by row in a div. Im using ngFor and looping trough my Array of logs and then displaying them.
My problem is now that the logs get displayed infinite times, not just 5 times (5 Array entries in list) like it should.
Does somebody have a hint what im missing?
logs.component.html
<div class="logContent">
<div class="row">
<div class="col-12" *ngFor="let log of this.logService.getLogs()">
<app-singlelog [when]="log.when" [type]="log.type" [data]="log.data"></app-singlelog>
</div>
</div>
</div>
log.service.ts
export class LogService {
private logArray = [];
/* private logObject = {} as Log; */
constructor(private httpservice: HttpserviceService) {
}
public getLogs(): Array<Log> {
this.httpservice.getLogs().subscribe(data => {
data.forEach(index => {
let logObject = {} as Log;
logObject.when = index.when;
logObject.type = index.type;
logObject.data = index.data;
this.logArray.push(logObject);
})
}
)
return this.logArray;
}
}
Thanks :)

Don't use function calls from html template to display data.
Instead, call the getLogs() function from the Angular ngOnInit() function, and store the response in a variable. Then loop on that variable:
export class LogService implements OnInit {
// ...
logs = [];
ngOnInit() {
this.getLogs();
}
getLogs(): Array<Log> {
this.httpservice.getLogs().subscribe(data => {
data.forEach(index => {
let logObject = {} as Log;
logObject.when = index.when;
logObject.type = index.type;
logObject.data = index.data;
this.logArray.push(logObject);
});
// assign the variable here:
this.logs = data;
});
}
In the template:
<div class="col-12" *ngFor="let log of logs">
<app-singlelog [when]="log.when" [type]="log.type" [data]="log.data"></app-singlelog>
</div>
The reason behind this is the fact that Angular calls your getLogs function on every page rendering cycle. But you should call the http request only once, when initalising the component.
Don't forget to unsubscribe from your Observable. ;) - corrected from the comment below.

You can also use the reactive approach and do the following:
Define the observable once in your component:
logs$ = this.httpservice
.getLogs()
.pipe(
shareReplay(),
map((data) => data.map(({ when, type, data }) => ({ when, type, data })))
);
Include in your component HTML the following:
<div class="col-12" *ngFor="let log of logs$ | async">
<app-singlelog [when]="log.when" [type]="log.type" [data]="log.data"></app-singlelog>
</div>

Related

How can i show a spinner while waiting for data from a get call inside my ngOnInit ? (Angular 15)

i'm struggling on something that should be pretty easy.
I'm trying to render a spinner, whenever a get call is ongoing, so instead of displaying an empty screen, i can use the spinner.
I thought of using two separate div, controlled by two ngIf, related to the same bool flag. Of course if one is *ngIf="flag", the other one is *ngIf="!flag".
I edit the value, inside the 'subscribe' of the my get call, but unfortunately, the bool (although it changes), does not affect the html (probably because how angular works, and lifecycle of the components).
Do you know how can i do this ?
In my data service component i have a really simple http get to fill my variable 'products : Product[]', and it works.
In my component shop.ts i have
#Component({
selector: 'app-shop',
templateUrl: './shop.component.html',
styleUrls: ['./shop.component.css'],
})
export class ShopComponent {
constructor(public ds: DataService) {}
/* Variables */
products: Product[] = [];
isDataLoaded: boolean = false;
/* With this get call, we get all the products informations, and we save'em
into products */
ngOnInit() {
this.ds.getProducts().subscribe((resp) => {
this.products = resp as Product[];
this.isDataLoaded = true;
}
});
}
In the component html i just have
<div *ngIf="!isDataLoaded">
<mat-spinner></mat-spinner>
</div>
<div *ngIf="isDataLoaded">
Data is loaded
</div>
I do this all the time. Here is the approach I use.
Store the result in a subscription and set it equal to the request like so:
#Component({
selector: 'app-shop',
templateUrl: './shop.component.html',
styleUrls: ['./shop.component.css'],
})
export class ShopComponent {
constructor(public ds: DataService) {}
products: Product[] = [];
isDataLoaded$: Subscription;
ngOnInit() {
this.isDataLoaded$ = this.ds.getProducts().subscribe((resp) =>
this.products = resp as Product[];
);
}
}
Then in your template, check if the subscription exists and is not closed:
<mat-spinner *ngIf="isDataLoaded$ && !isDataLoaded$.closed"></mat-spinner>
<div *ngIf="isDataLoaded$ && isDataLoaded$.closed">
Data is loaded
</div>
Problems with your original approach
If that request fails, your isDataLoaded variable will never update since you don't have an error block. Also, once you set that variable to true, it stays true. What happens if the user fires that request again? You need to also reset it back to false before each request so the spinner shows up.
Here is an improved version of your original code, although I do not recommend going with this approach.
ngOnInit() {
this.isDataLoaded = false;
this.ds.getProducts().subscribe((resp) => {
this.products = resp;
this.isDataLoaded = true;
}, error => {
...
this.isDataLoaded = true;
});
}
Can you show how you implemented the getProduts method?
I tried to replicate your project, like this:
constructor(public ds: DataService) {}
/* Variables */
products: Product[] = [];
isDataLoaded: boolean = false;
/* With this get call, we get all the products informations, and we save'em
into products */
ngOnInit() {
this.ds.getProducts()
.subscribe((resp) => {
this.products = resp;
this.isDataLoaded = true;
});
}
And I implemented the Data Service like this:
constructor(private http: HttpClient) {}
getProducts(): Observable<Product[]> {
return this.http.get<Product[]>('API');
}
And it works. Maybe it works for you too, but the data are loaded so fast that you don't see the spinner.
import { finalize } from 'rxjs';
...
this.ds.getProducts()
.pipe(finalize(() => this.isDataLoaded = true))
.subscribe((resp) => {
this.products = resp as Product[];
});

Loop over Observable

In my frontend part of application use a method
this.knowledgeMan.getUserAllowedCases(Item.ItemNumber)
which returns Observable. On my backend part, this method returns a List<String>.
My question is: how do I get to loop over the elements of this list of Strings?
If you have an observable you have to subscribe to it to get the actual value. Within subscribtion its up to you, here you can map or loop over your values.
this.knowledgeMan.getUserAllowedCases(Item.ItemNumber).subscribe(allowedCases => {
allowedCases.map(allowedCase => {
// your code here
});
});
If you are using this List<String> observable to show on the HTML part you can use a combination of async and *ngFor to get the desired result.
//in your html for example
<ul>
<li *ngFor="let item of (data$ | async)"> {{ item }} </li>
</ul>
//in your component
//usual angular stuff
export class MyComponent implements OnInit {
data$: Observable<String[]>;
constructor(private knowledgeMan: YourServiceInterface){}
ngOnInit() {
data$ = this.knowledgeMan.getUserAllowedCases(Item.ItemNumber);
}
}
If you are just doing this to compute some value you can do this following.
this.knowledgeMan.getUserAllowedCases(Item.ItemNumber).pipe(
flatMap(),
map(item => //do something with item here)
).subscribe();
If as you say, getUserAllowedCases returns string[] then you can do this:
this.knowledgeMan.getUserAllowedCases(Item.ItemNumber).subscribe(x => {
// assuming x is string[]
for (const item of x) {
// use item
}
});

How transform observable into another observable

I'm starting with Rxjs for Angular 6 and I have some doubts about how accomplish this:
I have a card list component which will show list with items fetched from a service which returns an observable. Data type items are different. And service observable may be updated with new items so card list should display this new items.
export class HomeComponent implements OnInit, OnDestroy {
papers$:Observable<Paper[]>;
papersPublished$:Observable<DataCard[]>;
papersOnReview$:Observable<DataCard[]>;
papersSubmitted$:Observable<DataCard[]>;
constructor(private publicationService: PublicationService) { }
ngOnInit() {
this.papers$ = merge(
this.publicationService.getAllPapersOnState("Submitted"),
this.publicationService.getAllPapersOnState("OnReview"),
this.publicationService.getAllPapersOnState("Published")
);
this.papersSubmitted$ = this.papers$.pipe(map(paper => [HomeComponent.paperToCard(paper,'Read', 'Review')]));
this.papersOnReview$ = this.papers$.pipe(map(paper => [HomeComponent.paperToCard(paper,'Read', 'Accept')]));
this.papersPublished$ = this.papers$.pipe(map(paper => [HomeComponent.paperToCard(paper,'Read', '')]));
}
private static paperToCard(paper, action_1_name, action_2_name): DataCard {
// ... other code ...
}
}
<app-cardlist
[title]="'Published'"
[description]="'Last science papers published. Yay!'"
[items$]="papersPublished$"></app-cardlist>
<app-cardlist
[title]="'On Review'"
[description]="'Last papers on review. On publish way!'"
[items$]="papersOnReview$"></app-cardlist>
<app-cardlist
[title]="'Submitted'"
[description]="'Last papers submitted for reviewing. Be the first one to check them!'"
[items$]="papersSubmitted$"></app-cardlist>
export class PublicationService {
// ... more code ...
getAllPapersOnState(state: string): Observable<Paper[]> {
return Observable.create(observer => {
this.WF_SC.deployed().then(instance => {
// State changed watcher to update on-live the observable
const event = instance.AssetStateChanged({});
event.on('data', (data) => {
console.log('StateChanged catched!');
this.getPaper((data['args']['assetAddress'])).then((paper) => observer.next(paper));
});
return instance.findAssetsByState.call(state);
}).then(addresses => {
addresses.forEach((address) => this.getPaper(address).then((paper) => observer.next(paper)));
});
});
}
}
export class CardlistComponent implements OnInit {
#Input() items$: Observable<DataCard[]>;
}
<div *ngIf="(items$ | async)" class="card-deck">
<div *ngFor="let item of items$ | async" class="card" style="width: 18rem;">
<!-- Other code -->
</div>
</div>
The issue is that only last item is displayed on the list. How can I transform papers$:Observable<Paper[]> into $papersSubmitted$:Observable<DataCard[]> to be able pass it to CardlistComponent ?
You probably want something like this:
this.papersSubmitted$ = this.papers$.pipe(
map((papers: Paper[]) =>
papers.map((paper: Paper) => HomeComponent.paperToCard(paper, 'Read', 'Review')
)
);
Same for all similar ones (and you should of course also filter them, as you mentioned in the comments).
Then, in your Service, do this:
return Observable.create(observer => {
// ...
}).pipe(toArray());
to return an Observable<Paper[]> instead of an Observable<Paper>.

Getting [Object Object] in angular2 application

I have developed angular2 application using ngrx/effects for making http calls. I have used GIT as reference application. Once the response come from http, i am not able to display it on screen. Its showing [object Object]. Here is my code.
HTML page linked to component.html
<div class="container">
<div class="left-container cf">
<mat-tab-group>
<mat-tab label="Configuration">{{jsons}}</mat-tab>
<mat-tab label="Captured Output">
</mat-tab>
</mat-tab-group>
</div>
</div>
Component.ts
export class ExperimentDetailsComponent implements OnInit {
jsons: Observable<any>;
isLoading: Observable<any>;
constructor(
private store: Store<fromStore.State>
) {
this.isLoading = store.select(fromStore.getIsLoading);
this.jsons = store.select(fromStore.getJson);
console.log(this.jsons)
}
ngOnInit() {
this.store.dispatch(new jsonAction.GetJson());
// this.jsons = this.store.select(fromStore.getJson);
}
}
Effects.ts
export class GetJsonEffects {
#Effect() json$ = this.actions$.ofType(Act.GET_JSON)
.map(toPayload)
.withLatestFrom(this.store$)
.mergeMap(([ payload, store ]) => {
return this.http$
.get(`http://localhost:4000/data/`)
.map(data => {
return new Act.GetJsonSuccess({ data: data })
})
.catch((error) => {
return Observable.of(
new Act.GetJsonFailed({ error: error })
);
})
});
constructor(
private actions$: Actions,
private http$: HttpClient,
private store$: Store<fromStore.State>
) {}
}
As you see, the result of store.select() is an observable. You cannot data bind to it directly.
You can either:
Use the async pipe to make the UI subscribe to the observable for you and extract the data, like:
<mat-tab label="Configuration">{{jsons | async}}</mat-tab>
Or subscribe yourself to the observable.
export class ExperimentDetailsComponent implements OnInit {
jsonSubscription = store.select(fromStore.getJson)
.subscribe(jsons => this.jsons = jsons);
ngOnDestroy() {
this.jsonSubscription.unsubscribe();
}
jsons: any;
// ...
}
That's one thing:
If you are using Http service (from #angular/http module):
The other thing is that you are returning the Response object not the JSON extracted from it. The map() in your effect needs to call data.json(). Like:
return this.http$
.get(`http://localhost:4000/data/`)
.map(data => {
return new Act.GetJsonSuccess({ data: data.json() })
})
Or, as I like, add another map() to make things clear:
return this.http$
.get(`http://localhost:4000/data/`)
// You could also create an interface and do:
// `response.json() as MyInterfaceName`
// to get intellisense, error checking, etc
.map(response => response.json())
.map(data => {
return new Act.GetJsonSuccess({ data: data })
})
If you are using HttpClient service (from #angular/common/http module):
(Available in Angular v4.3+)
In this case you don't need to call .json() yourself, it does it for you, so you don't need that first .map() I suggested.
You can also tell TypeScript about the type you expect the JSON to match by calling the get() like this:
return this.http$
.get<MyInterfaceName>(`http://localhost:4000/data/`)
.map(data => {
return new Act.GetJsonSuccess({ data: data.json() })
})
The get<MyInterfaceName>() bit will make Angular tell TypeScript that the JSON object matches the MyInterfaceName, so you'll get intellisense and error checking based on this (at compile time only, none of this affects runtime in anyway).
HttpClient Documentation

How to make Angular 2 render HTML template after a promise in the component is resolved?

For my app, the ItemDetailComponent is where info of an item will be displayed. I have a service that retrieves all items using promise. I use ActivatedRoute to retrieve the item ID from the url path, then run the service, get all items, then find the item with the ID retrieved above, and assign it to selectedItem variable.
Here is item-detail.component.ts:
export class ItemDetailComponent implements OnInit {
private title = 'Item Details'
private selectedItem: object
constructor(
private route: ActivatedRoute,
private itemService: ItemService
) {}
ngOnInit() {
const selectedItemId = this.route.snapshot.params.itemId
return this.itemService.getAllItems()
.then((items) => {
return _.find(items, item => item.itemId === selectedItemId)
})
.then((selectedItem) => {
this.selectedItem = selectedItem
console.log('Inside promise', this.selectedItem)
})
console.log('Outside promise', this.selectedItem)
}
}
And here is item-detail.component.html template so I could display my item, just an example:
<div>
<h1>{{title}}</h1>
<div *ngIf="selectedItem">
<div><label>Item ID: </label>{{selectedItem.itemId}}</div>
</div>
</div>
The app returns nothing but the title unfortunately. I then added the two console.log() commands and found out that the one outside of the promise as well as the html template are rendered before the promise is fulfilled, and no selectedItem is available at that time. How could I force the app to execute them only after the promise is resolved in order to have the selectedItem in place for displayed?
EDIT: I added a new line in the html template to examine further:
<div>
<h1>{{title}}</h1>
<div><label>Item ID 1: </label>{{selectedItem.itemId}}</div>
<div *ngIf="selectedItem">
<div><label>Item ID 2: </label>{{selectedItem.itemId}}</div>
</div>
</div>
The app displays "Item ID 1:" label but with no actual id there. The console shows me an error saying that "Cannot read property 'itemId' of undefined", again confirming that the whole template is rendered before promise resolved and is not re-rendered after data is loaded. So weird.
You could create a Resolver for the route that fetches the desired data.
https://angular.io/api/router/Resolve
https://blog.thoughtram.io/angular/2016/10/10/resolving-route-data-in-angular-2.html
Add a boolean variable in to your class like
private dataAvailable:boolean=false;
and in the subscription to the promise,make this true when the data is available
then((selectedItem) => {
this.selectedItem = selectedItem;
this.dataAvailable=true;
console.log('Inside promise', this.selectedItem)
})
and in the template render when the data is available
<div>
<h1>{{title}}</h1>
<div *ngIf="dataAvailable">
<div><label>Item ID: </label>{{selectedItem.itemId}}</div>
</div>
</div>
It should do the trick
Update
ngOnInit() seems to be just a event handler hook - returning anything won't affect anything it seems. Hence my old answer will not work.
There are other workarounds like using *ngIf or putting it in routes etc. but I wish there was something like resolvePromise(): Promise hook that would put a condition on resolution before rendering.
This is instead of developers putting the boilerplate in every component.
Old answer
Most likely that is because you are missing return statement in the second then.
then((selectedItem) => {
this.selectedItem = selectedItem
console.log():
return selectedItem;//
}
Is it possible that the ChangeDetection is set to OnPush somewhere up the component tree?
If that is the case, the template does not automatically rerender, because nothing triggers the ChangeDetection for this component.
Look out for a Component with the setting changeDetection: ChangeDetectionStrategy.OnPush
#Component({
selector: 'example',
template: `...`,
styles: [`...`],
changeDetection: ChangeDetectionStrategy.OnPush
})
Also you already have a valid solution by using a Resolver you could check if this helps:
export class ItemDetailComponent implements OnInit {
private title = 'Item Details'
private selectedItem: object
constructor(
private route: ActivatedRoute,
private itemService: ItemService,
// the reference to the components changeDetector is needed.
private changeDetectorRef: ChangeDetectorRef
) {}
ngOnInit() {
const selectedItemId = this.route.snapshot.params.itemId
return this.itemService.getAllItems()
.then((items) => {
return _.find(items, item => item.itemId === selectedItemId)
})
.then((selectedItem) => {
this.selectedItem = selectedItem
// this triggers the changedetection and the template should be rerendered;
this.changeDetectorRef.detectChanges();
console.log('Inside promise', this.selectedItem)
});
console.log('Outside promise', this.selectedItem)
}
}
Here is a great article about Angulars ChangeDetection: https://blog.thoughtram.io/angular/2016/02/22/angular-2-change-detection-explained.html