Display values from API In Angular 6 page - arrays

I am completely new to frontend dev and trying to display API data in an Angular 6 application and can't figure out how to do it.
I can display values in the top level of the returned details but it's the sub level details I am struggling with.
I am using an Angular 6 app using Routing.
Below is all my code
Homepage.component.html
<h2>Book ID List</h2>
<button (click)="getBooks()">Get</button>
<div *ngFor="let book of books.items">
<p>ID: {{book.id}}</p>
</div>
I can get the 'ID'
I am using a service to get the data from the test API
Service.component.ts
import { Injectable } from '#angular/core';
import { HttpClient } from '#angular/common/http';
import { Observable, of } from 'rxjs';
import { map } from 'rxjs/operators';
#Injectable({
providedIn: 'root'
})
export class ApiServiceService {
url = 'https://www.googleapis.com/books/v1/volumes?q=HTML5 Wire-frames';
constructor(private http: HttpClient) { }
private extractData(res: Response) {
const body = res;
return body || {};
}
getBooks(): Observable<any> {
return this.http.get(this.url).pipe(
map(this.extractData));
}
}
Homepage.component.ts
import { Component, OnInit } from '#angular/core';
import { ApiServiceService } from '../../services/api-service.service';
#Component({
selector: 'app-homepage',
templateUrl: './homepage.component.html',
styleUrls: ['./homepage.component.css']
})
export class HomepageComponent implements OnInit {
books: any = [];
constructor(private apiService: ApiServiceService) { }
ngOnInit() { }
getBooks() {
this.books = [];
this.apiService.getBooks().subscribe((data: {}) => {
console.log(data);
this.books = data;
});
}
}
At present this return the following:
What I want to do is display the value from the 'eBook' which is under the 'saleInfo' level. I know I need to the change the loop for each array returned in the HTML but this is where I'm stuck.
Also I'm not sure the code I have is the best, but it's working. As I said I'm new to all this, and can pull values from top level but not sublevels in the API.

I would recommend better naming for your service, Service.compoent.ts isn't ideal, api.service.ts is much more understandable.
Also you can see that when you subscribe, you are using data: {}, this means that the function should expect a value of type Object, but I would use any, since you use Observable<any>
Now for the problem.
I have created stackblitz which does just what you wanted. I think you have got confused with the comments. You don't want to change let book of books.items to let book of books because you would be iterating over object, which you cannot do in *ngFor.

Change the line this.books = data; to this.books.push(data);
Since, if it is this.books = data; and because the books is of type any. It will accept anything. So, now after this line, this.books = data; it becomes object which contains value of data variable. So, you should use,
this.books.push(data);
To make it behave like an array too. Then, you can access books with *ngFor.
So, now in the HTML you can access via *ngFor as:
<div *ngFor="let book of books">
<div *ngFor="let items of book.items">
<p>ID: {{items.id}}</p>
<p>ebook: {{items.saleInfo.isEbook}}</p>
</div>
</div>

Related

How to exclude certain keys/values when iterating through array and displaying on table in Angular

Have just run into a bit of a roadblock and any advice would be much appreciated
I am currently building out an application in angular to fetch data from an API and display it on the page
I have a service called "Api Service using the HTTPClient to call the API
apiservice.service.ts
import { HttpClient } from '#angular/common/http';
import { Injectable } from '#angular/core';
#Injectable({
providedIn: 'root'
})
export class ApiserviceService {
constructor(private _http:HttpClient) { }
getData(){
return this._http.get('APIURL');
}
}
Then I have another component to observe this call and map the data to an array
data.component.ts
import { Component, OnInit } from '#angular/core';
import { map, toArray } from 'rxjs';
import { ApiserviceService } from '../apiservice.service';
#Component({
selector: 'app-data',
templateUrl: './data.component.html',
styleUrls: ['./data.component.scss']
})
export class VenueComponent implements OnInit {
data: any = [];
constructor(private service:ApiserviceService) {}
MapData() {
this.service.getData().subscribe(result=>{
this.data=result;
console.log(this.data);
})
}
ngOnInit() {
this.MapData()
}
}
I am then displaying the data in a table
data.component.html
<table>
<tr *ngFor= "let item of data | keyvalue" >
<td>{{item.key}} : {{item.value}}</td>
</table>
Now at the moment I am iterating thrugh the array using *ngFor but there are keys/values that I want to exclude from being displayed
For example I want to display key2 and value 2 but not key 1 and value 1
[Key1 : value1], [Key2 :value2]
How would I go about doing this? Any advice would be much appreciated
in my opinion you have at least 2 options, filter the observable:
this.service.getData().pipe(
filter(o => {
return // your condition here
})
).subscribe(result=>{
the second option is to write a custom pipe. You can create a pipe that extend the keyvalue pipe and filters the data as you need. You will find numerous tutorial where they explain how to create a pipe.
I think you can use Angular Slice Pipe[1]
<table>
<tr *ngFor= "let item of data | keyvalue | slice: 1" >
<td>{{item.key}} : {{item.value}}</td>
</table>
Hope this help, Thanks!
[1]: https://angular.io/api/common/SlicePipe
First of all: use types for your variables. It gives you the benefit of knowing exactly which kind of data you are handling and it helps us to understand what exactly you want to achieve.
Here is my take on this: I suggest you to put this kind of logic into your service, so that this logic can be reused somewhere else.
I also suggest you to use some kind of reactive approach, so that you don't have to manually unsubscribe your requests.
The following approach is based on the assumption that your API returns an array, not an object.
apiservice.service.ts
...
export class ApiserviceService {
data$ = this._http.get('APIURL').pipe(
map(data => data.filter(item => item.key !== 'Key1'))
);
constructor(private _http:HttpClient) { }
...
data.component.ts
...
public data$ = this.service.data$;
...
data.component.html
<table>
<tr *ngFor= "let item of data$ | async" >
<td>{{item.key}}</td>...
</table>
I suggest you to not use pipes in this case, since pipes should be generic and your filtering logic seems to be too specific for a pipe.

Angular ngFor sees string array as one string

I have a popup dialog, which recieves a list of strings from the backend. I want to print every string as a list item, using ngFor. But when the dialog pops up, the whole array is shown as one concatenated string.
needs-dialog.component.ts
import { Component, Inject, OnInit } from '#angular/core';
import {MAT_DIALOG_DATA} from '#angular/material/dialog';
import { MatDialogRef} from "#angular/material/dialog";
#Component({
selector: 'app-needs-dialog',
templateUrl: './needs-dialog.component.html',
styleUrls: ['./needs-dialog.component.css']
})
export class NeedsDialogComponent implements OnInit {
needs!: String[];
constructor( private dialogRef: MatDialogRef<NeedsDialogComponent>, #Inject(MAT_DIALOG_DATA) data)
{
console.log("logging data:")
console.log(data);
this.needs=data
console.log("logging needs array in NeedsDialogComponent:");
console.log(this.needs);
}
ngOnInit(): void {
}
close() {
this.dialogRef.close();
}
}
needs-dialog.component.html
<h2 mat-dialog-title>Szükségletek</h2>
<mat-dialog-content >
<ul *ngFor="let value of needs; index as i" >
<li>{{needs}}</li>
</ul>
</mat-dialog-content>
<mat-dialog-actions>
<button mat-button mat-dialog-close>Cancel</button>
</mat-dialog-actions>
**opening matdialog with method: **
openDialog(dogid:any): void {
this.matdialog.open(NeedsDialogComponent, {data:this.getDogNeeds(Number(dogid)),width: '500px',
height: '500px'});
}
console output from dialog logging
dialog window
By your logging, the data is a 2D array with the list you want as the first element.
this.needs = data[0];
should work, but it seems like either your getDogNeeds() function or your back end is returning data in the wrong format.

Angular problem of How can I show only one record in an array object?

I'm facing a problem about the when I get one record from array like this
data.service.ts
getOneBookDetail(isbn:any) {
const headers = new HttpHeaders().set("Content-Type", "application/json");
// console.log("=============" + isbn )
console.log(headers)
return this.http.get('http://localhost:10888/bookdetail/?isbn='+ isbn).subscribe(
(val) => { // Get has no error and response has body
console.log("Get successful value returned in body", val);
},
response => {
console.log("Get call in error", response);
},
() => { // Get has no error, response has no body
console.log("The Get observable is now completed.");
});
}
home.component.ts
getBookDetail(book) {
this.data.getOneBookDetail(book.isbn) //isbn of book
}
and I can click the title of book
<a routerLink="/bookdetail/{{book.isbn}}" (click)="getBookDetail(book)"><h3>{{ book.title }}</h3></a>
and I can get a object I saw it in console
Get successful value returned in body [{…}]
0: {_id: "5fc91e5aa700213eb8c52de0", title: "A Promised Land"
[{…}] is 0: {_id: "5fc91e5aa700213eb8c52de0", title: "A Promised Land"
....
and now I want to get this object to a page call bookdetail to show only this book details but now still show all book
the below is the bookdetail component
import { Component, OnInit } from '#angular/core';
import { DataService } from '../data.service';
#Component({
selector: 'app-bookdetail',
templateUrl: './bookdetail.component.html',
styleUrls: ['./bookdetail.component.scss']
})
export class BookDetailComponent implements OnInit {
h1Style: boolean = false;
books: Object;
constructor(private data: DataService) {}
ngOnInit() {
this.data.getBooks().subscribe(data=> {
console.log({data}) //show data
this.books = data
//console.log(this.books);
})
}
}
in bookdetail html
<h1>Book-detail</h1>
<div *ngIf="books" class="bookdetail-block">
<div *ngFor="let bookdetail of books" class="bookdetail">
<h1>{{bookdetail.title}}</h1>
<p><img [src]="bookdetail.image" ></p>
<p>{{bookdetail.author}}</p>
<p>{{bookdetail.price}}</p>
<p>{{bookdetail.isbn}}</p>
<p>{{bookdetail.description}}</p>
</div>
</div>
How can I only show I have choose?
I think the issue is in bookdetail ngOnInit()??
Zico, the idea generally is that you subscribe to ActiveRouter.params IN your "detail-component", see the docs: Well you use a switchMap to, after get the parameter, make the dataService.getOneBookDetail(id). In subscribe you equal the response to a variable and ony show the variable
book:any
constructor(private activatedRoute:ActivatedRoute,private dataService:DataService){}
ngOnInit() {
this.route.paramMap.pipe(
switchMap(params => {
const ibs=params.get('isbn');
return this.dataService.getOneBookDetail(ibs);
}).subscribe(res=>{
book=res;
})
);
}
Other idea is pass data between routes like show Netanel Basal

Angular 2 How do I call a service method when an observable changes/when a services returns new results?

I have a simple app component with a search input and an observable resultItems: Observable<Array<string>>; powered by a search service that returns results to the UI via *ngFor. There is also a leaflet map that should render the locations of the results. The search service works well and I can render the location of one result in the map onclick. My question is: What is the recommended way to call the map service mapResults every time the search service returns new results or the observable changes? I can imagine how I could create a custom pipe that would iterate over the parks in the service results and call mapservice.mapResult but that seems odd since the pipe wouldn't return anything to the UI and I'm a little concerned with performance, understanding little about pure and impure pipes. I have also seen mention of a process by which you subscribe to changes of an observable, but I'm lost with the semantics and changes among API versions.
I apologize if this is a problem of poor design. I only have a few weeks of Angular learning and I admittedly haven't read the documentation thoroughly. Please point any and all issues you see.
simple Search Service
import { Injectable } from '#angular/core';
import { URLSearchParams, Jsonp } from '#angular/http';
#Injectable()
export default class ParkSearchService {
constructor(private jsonp: Jsonp) { }
search(parkSearchterm: string) {
var search = new URLSearchParams()
search.set('q', 'PARK_NAME:*' + parkSearchterm+'*');
search.set('wt', 'json');
search.set('json.wrf','JSONP_CALLBACK')
var test = this.jsonp
.get('http://parksearch/parks_full/select?', { search })
.map((response) => response.json()['response']['docs']);
return test
}
}
exert from app.component.html
<md-card *ngFor="let item of resultItems | async; let i = index"
class="search-result"
[ngClass]="{ 'selected-result': selectedIndex === i }">
<md-card-header class="clickable"
(click)="showBoundary(item)"
md-tooltip="Zoom to park">
<md-card-title>{{item.PARK_NAME}}</md-card-title>
</md-card-header>
<md-card-content style="height: 75px; overflow-y: auto">
<button md-button
color="primary"
md-tooltip="more info"
(click)="openDtl(item.PARK_HOME_PAGE_URL)">
<md-icon>info</md-icon>
<span>Details...</span>
</button>
<button md-button
color="primary"
md-tooltip="open park website"
(click)="openParkURL(item.PARK_HOME_PAGE_URL)">
<md-icon>web</md-icon>
<span>WebSite</span>
</button>
Amenties: {{ item.AMEN_LIST }}
</md-card-content>
</md-card>
app.component.ts (forgot to include)
export class AppComponent {
private selectedIndex: number;
public events: any[] = [];
//park search items
resultItems: Observable<Array<string>>;
parkSearchterm = new FormControl();
//setup resultitems
ngOnInit() {
this.mapService.initialize();
this.resultItems = this.parkSearchterm.valueChanges
.debounceTime(400)
.distinctUntilChanged()
.switchMap(parkSearchterm => this.parkSearchService.search(parkSearchterm));
map service:
//Thanks for the help getting started https://github.com/haoliangyu
import { Injectable } from '#angular/core';
import { Map, GeoJSON } from 'leaflet';
#Injectable()
export default class MapService {
public map: Map;
private currentLayer: GeoJSON;
private resultsLayer: any;
private resultfeatureCollection: any;
constructor() {
}
initialize() {
if (this.map) {
return;
}
this.map = L.map('map', {
zoomControl: true,
zoom: 6,
minZoom: 3,
maxZoom: 19
});
L.tileLayer('http://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap, Tiles courtesy of Humanitarian OpenStreetMap Team'
}).addTo(this.map);
L.control.scale().addTo(this.map);
//Add the results layer
this.resultsLayer = L.geoJSON(this.resultfeatureCollection, {
style: () => {
return {
color: '#ff00005',
fillColor: '#3F51B5'
};
}
}).addTo(this.map);
}
mapResults(park) {
//update the restults layer
let resultfeatureCollection: GeoJSON.FeatureCollection<any> = {
type: 'FeatureCollection',
features: [
{
type: 'Feature',
geometry: {
type: "Polygon",
coordinates: JSON.parse(park.BBOX[0])
},
properties: {
name: 'test'
}
}
]
};
this.resultsLayer.addData(resultfeatureCollection);
this.map.fitBounds(this.resultsLayer.getBounds());
}
}
You are very close to what you need: You already have an observable stream created for your results, called resultItems, that is right. Then on your template, when you use it through the async pipe what Angular does internally is subscribes to this stream to get its values.
So if you want to "also" map when resultItems yields is by subscribing to it by yourself as well. There's a catch though: by default every subscription duplicates the workload for the stream, meaning that every time the user makes a new search it would run the API call twice: 1 for the async subscription and another one for your .subscribe.
The way to resolve that is by using .publish(): What this allows is to share the result of an stream between many subscribers, so your code would look like this:
ngOnInit() {
this.mapService.initialize();
this.resultItems = this.parkSearchterm.valueChanges
.debounceTime(400)
.distinctUntilChanged()
.switchMap(parkSearchterm => this.parkSearchService.search(parkSearchterm))
.publish();
// With publish() we are sharing the items from this stream to all of their subscribers
// We just need to tell it to do the "first subscription"
this.resultConnection = this.resultItems.connect();
// This "resultConnection" is the base subscription... We will have to dispose it in our ngOnDestroy method or we might get memory leaks
// Now we can tell the map service to update when we get a value:
this.resultItems.subscribe((park) => this.mapService.mapResults(park));
// (Not sure if the data types are correct, I assume you can map them)
// When `async` calls .subscribe() to do his job, we won't get duplicate API calls thanks to .publish()
}
To clarify, what connect() does is subscribe to the original stream, and start forwarding the values recieved to the subscribers of the published stream (this.resultItems). Now you are the owner of that subscription, so you are responsible of disposing it whenever you don't need more searches.
Subscribe on observer and call then search is changed. https://angular.io/docs/ts/latest/guide/router.html#!#reuse
After get result from server, put it to array. Angular will update the data in the template itself, this is the fastest way.

Angular2 component view updated continuously

I have an Angular 2 component that displays a list of Items, and that registers to a service that publishes events. The problem is that even if I don't do anything when I receive an event, Angular updates the view (or at least do something when, I guess, it shouldn't).
Here is a plunker.
As you can see in the console, the "getTitle()" method of my items is called each time my service publishes a message.
Even if I don't register to my service and if my component doesn't implement the MyServiceListener interface, getTitle is called each time the service gets a message. If I don't give the service to my component in its constructor, everything is fine. So, something seems wrong with my dependency injection, but what?
Here is the relevant code of the plunker:
My service and its listeners interface:
export interface MyServiceListener {
onMessage(_message: any);
}
export class MyService {
private m_listener: MyServiceListener;
constructor() {
window.setInterval(() => {
if (this.m_listener !== undefined) {
this.m_listener.onMessage("Hi");
}
}, 500);
}
setListener(_listener: MyServiceListener) { this.m_listener = _listener; }
}
The Item class:
export class Item {
m_title: string;
constructor(_title: string) {
this.m_title = _title;
}
getTitle(): string { console.log("getTitle"); return this.m_title; }
}
My component:
#Component({
selector: 'my-app',
template : `
<div>
<ul>
<li *ng-for="#item of m_items">
{{item.getTitle()}}
</li>
</ul>
</div>
`
})
export class App implements TestBugAngularServiceListener {
private m_items: Array<Item> = new Array<Item>();
constructor(_communicationService: MyService) {
this.m_items.push(new Item("A"));
this.m_items.push(new Item("B"));
this.m_items.push(new Item("C"));
_communicationService.setListener(this);
}
onMessage(_message: any) {
}
}
bootstrap(App, [MyService]).catch(err => console.error(err));
Both articles : Change detection and Angular immutability explain a lot of thing about how Angular 2 detect changes of object, and how the tree of components in angular 2 is traversed to perform data binding...
In your sample, I think your component "my-app" can be considered to be "Immutable", so changing its "change detection strategy" to OnPush solve your problem.
You can write this :
#Component({
selector: 'my-app',
changeDetection: ChangeDetectionStrategy.OnPush,
template : `
<div>
<ul>
<li *ng-for="#item of m_items">
{{item.getTitle()}}
</li>
</ul>
</div>
`
})
And after adding the import to ChangeDetectionStrategy, databinding of "my-app" will not be computed after each browser event, but only when its input change, so never...

Resources