Loop over Observable - html

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
}
});

Related

Angular ngFor running in infinite loop when displaying array

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>

Angular async pipe with input

i'm getting a list from json which i want to display and being able to filter using an input box. so far it's working but the list only displays once i input something into the searchbox not before that. i would like the list being displayed at the start.I tried to set the search box value at start with empty string but it's not working.
looked up the web didn't find the way to achieve. Any help would be appreciated.
my component html:
<h4><label for="search-box">Hero Search</label></h4>
<input #searchBox id="search-box" (input)=search(searchBox.value) />
<ul class="heroes">
<li *ngFor="let hero of heroes$ | async">
<a routerLink="/detail/{{hero.id}}">
<span class="badge">{{hero.id}}</span> {{hero.name}}
</a>
<button class="delete" title="delete hero"
(click)="delete(hero)">Delete</button>
</li>
</ul>
<div>
<label>Hero name:
<input #heroName />
</label>
<!-- (click) passes input value to add() and then clears the input -->
<button (click)="add(heroName.value); heroName.value=''">
add
</button>
</div>
component ts:
selector: 'app-heroes',
templateUrl: './heroes.component.html',
styleUrls: ['./heroes.component.css']
})
export class HeroesComponent implements OnInit {
heroes$: Observable<Hero[]>=null;
heroes: Hero[];
selectedHero: Hero;
private searchTerms = new Subject<string>();
constructor(private heroService: HeroService) {
}
ngOnInit(): void {
this.heroes$ = this.searchTerms.pipe(
// wait 300ms after each keystroke before considering the term
debounceTime(300),
// ignore new term if same as previous term
distinctUntilChanged(),
// switch to new search observable each time the term changes
switchMap((term: string) => this.heroService.searchHeroes(term)),
);
}
// Push a search term into the observable stream.
search(term: string): void {
this.searchTerms.next(term);
}
getHeroes(): void {
this.heroService.getHeroes()
.subscribe(heroes => this.heroes = heroes);
}
service ts:
/* GET heroes whose name contains search term */
searchHeroes(term: string): Observable<Hero[]> {
if (!term.trim()) {
// if not search term, return full array.
return this.getHeroes();
}
return this.http.get<Hero[]>(`${this.heroesUrl}/?name=${term}`).pipe(
tap(x => x.length ?
this.log(`found heroes matching "${term}"`) :
this.log(`no heroes matching "${term}"`)),
catchError(this.handleError<Hero[]>('searchHeroes', []))
);
}
You can declare your searchTerms as an BehaviorSubject instead of an simple Subject. The main difference is that as an subcriber of that BehaviorSubject, you will get the last value emitted from that observable no matter when your subscription will happen. Be aware that BehaviorSubject need an initial value that will be emitted when you initialized it.
private searchTerms: BehaviorSubject<string> = new BehaviorSubject<string>('');
Because in your case it's connected to the input event, you need to automatically trigger a first value to start your pipeline.
You can do this by using the startWith operator
this.heroes$ = this.searchTerms.pipe(
// wait 300ms after each keystroke before considering the term
debounceTime(300),
startWith(''), // <-- start your pipe with an empty string
// ignore new term if same as previous term
distinctUntilChanged(),
// switch to new search observable each time the term changes
switchMap((term: string) => this.heroService.searchHeroes(term)),
);

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>.

can't get JSON data with ng-for

I have JSON containing Stores[]--->Products[]--->ProductDetails--->ProductTags[] for the begining I want to see all the data
so I coded:
service
export class StoreService {
//private myurl: string ="apidata/products.json"
constructor(private _http: Http) {}
getProducts(): Observable < any > {
let apiUrl = './assets/products.json';
return this._http.get(apiUrl)
.pipe(map((response: Response) => {
const data = response.json();
return data;
}));
}
store component:
export class StoreComponent implements OnInit {
products = [];
constructor(private _storeService: StoreService) {}
ngOnInit() {
this._storeService.getProducts()
.subscribe(data => {
this.products = data;
console.log(data)
});
}
}
html
<h2>store</h2>
<ul>
<li *ngFor="let p of products">{{p}}</li>
</ul>
the error I get:
Error trying to diff '[object Object]'. Only arrays and iterables are
allowed
An error which you are getting suggests that your data is either an object / map instead of an array.
If you want to use *ngFor in your template, your data needs to be an array instead of an object.
Although angular team introduce a pipe called "keyvalue" in version 6.1. So you can take advantage of that and iterate over an object inside your angular template.
For example, assume that your object looks like below
const user = {
name: 'xyz',
age: 30,
gender: 'male'
};
So in your template file, you can iterate over this object like this
<ul>
<li *ngFor="let data of user | keyvalue">
Key : {{data.key}} and Value : {{data.value}}
</li>
</ul>
Please refer this link for more info about this pipe
You generally get this error when you pass an Object to *ngFor as an input. As the error states, *ngFor only works with Iteratable Data Structures, which in our case will be an array of products.
Make sure that in here: this.products = data;, data is actually an array.
UPDATE:
As it's quite obvious from this screenshot of the console log:
data is an object and not an array. PriceFilter, Stores and GenderFilter on that object are arrays. If you want to show stores, you can do:
export class StoreComponent implements OnInit {
stores = [];
constructor(private _storeService: StoreService) {}
ngOnInit() {
this._storeService.getProducts()
.subscribe(data => {
this.stores = data.Stores;
console.log(data)
});
}
}
And in template
<h2>store</h2>
<ul>
<li *ngFor="let store of stores">
<h4>Products:</h4>
<ul>
<li *ngFor="let product of store.Products">
<p>Product Id: {{ product.ProductId }}</p>
<p>Product Title: {{ product.ProductTitle }}</p>
...
</li>
</ul>
</li>
</ul>
The main purpose of this error is that your data (products) is returning an object {} - ngFor requires an array [] to iterate. Either you have to check your HTTP Response or fix your backend API.
And merely for your example you can do this this.products = data.Stores; (if it is stores is what you're looking for to iterate on).
UPDATE
According to your link in comments you have to change your code to :
this.products = data.Stores;
And then :
<div *ngFor="let p of products">
<ul>
<li *ngFor="let item of p">
<-- HERE you take whatever you want from your product object --!>
</li>
</ul>
</div>

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