This question already has answers here:
How do I return the response from an Observable/http/async call in angular?
(10 answers)
Closed 2 years ago.
I have such a problem.
I am returning data using Http. Everything works checked with the browser, but I cannot display it on the front. I have such a strange problem that I cannot deal with even while debugging.
I have a clientList array and theoretically it should receive data (subscribed).
When calling API, I get undefined. I assigned a button to the HTTP query.
My original query for the button looks like this:
HMTL here:
<button (click)= "getClients()" ></button>
And my component with the method:
import { Component, OnInit } from '#angular/core';
import {MapsAPILoader} from '#agm/core';
import { ClientService } from '../Services/ClientService';
import { Client } from '../Models/Client';
import { HttpClient } from '#angular/common/http';
#Component({
selector: 'app-map',
templateUrl: './map.component.html',
styleUrls: ['./map.component.css']
})
export class MapComponent implements OnInit {
clientList: Array<Client>;
constructor(private client: ClientService){}
ngOnInit(): void {
// this.getClients();
}
// tslint:disable-next-line: typedef
getClients(){
this.client.getClients().subscribe(clients => this.clientList = clients);
this.loopByArray();
}
loopByArray(){
for (let index = 0; index < this.clientList.length; ++index) {
let value = this.clientList[index];
console.log(index, value);
}
}
}
If I call the button like I wrote, I get undefined. However, if I change only one character while debugging, i.e. I add one more equal sign:
Old :
this.client.getClients().subscribe(clients => this.clientList = clients);
New :
this.client.getClients().subscribe(clients => this.clientList == clients);
And I will play it again (I will press the button), I get clients normally without a problem:
Unfortunately, I add exactly the same error and undefined again (even if I play it several times). I do not understand what is going on. Do you have to generate the array at the very beginning? Because I don't really understand it anymore.
modify your getClients function like below. And get knowledge how async/await works
getClients(){
this.client.getClients().subscribe(clients => {
this.clientList = clients;
this.loopByArray();
});
}
As far as I see you do not understand the difference between = and == and don't know about ===
this.clientList = clients will SET value of this.clientList equal to clients. And return value will be undefined
this.clientList == clients will COMPARE this.clientList and clients. And the result will be true or false. This will be a loose comparison
read more about the comparison here https://medium.com/jspoint/loose-vs-strict-equality-in-javascript-3e5136720b00
I get data from an API-Call. I have got a many to many relationship - I will explain it with people an movies. One movie is seen by many people and one person can watch many movies.
So in the Angular Frontend, when you click on a person, you should get a more detailled view of this person including a list of its watched movies.
When I run this code (person-detail.component.ts):
public selectedperson: any = []
/*Array that contains data from the selected person*/
public movie: any = []
/*Array that contains all the movie-person connections (movie-name and person_id)*/
public selectedmovie: any = []
/*array that should contain all the movies the selectedperson has watched*/
/*some code*/
getMovies(){
const url ='http://localhost:4000/api/people-movies';
this.http.get(url).subscribe(movie => {
this.movie = movie;
this.selectedmovie=this.movie.find(item=>
{
for (var i = 0; i < this.movie.arrayLength; i++) {
if(item['person_id']===this.selectedperson.person_id)
{
return true;
}
return false;
}
});
console.log(this.selectedmovie, this.selectedperson.person_id);
return this.selectedmovie;
});
}
It does not work - but without this loop (and if I leave the If-statement):
for (var i = 0; i < this.movie.arrayLength; i++) {
}
It returns only one movie of this person - so it stops as soon as it has found a movie. But since I want to list all the movies I need the loop.
So what am I doing wrong with the loop?
Edit:
sample Data in selectedperson (is already filled):
selectedperson:
[{"person_id"=34, "name"="john"}]
movie (get this data from API Call with the mentioned URL):
[{"person_id"=33, "movie"="Titanic"}{"person_id"=33, "movie"="Star Wars"}{"person_id"=34, "movie"="Titanic"}{"person_id"=34, "movie"="Star Wars"}{"person_id"=34, "movie"="Indiana Jones"}{"person_id"=35, "movie"="Titanic"}]
wanted final result (for the person with the id 34)
Titanic
Star Wars
Indiana Jones
There are a few things wrong with your code, but i'll start with what I think is the solution for you:
getMovies(){
const url ='http://localhost:4000/api/people-movies';
this.http.get(url).subscribe(movie => {
this.movie = movie;
this.selectedmovie=this.movie.filter(item=> item['person_id'] === this.selectedperson.person_id );
console.log(this.selectedmovie, this.selectedperson.person_id);
return this.selectedmovie.map(movie.movie);
});
}
Now, what is wrong with your code:
you try to populate an empty array selectedmovie by activating find on itself, but it is empty... Instead, populate it with the movies- found in the movie array.
you're using find, which a simple look at documentation (here) would make it clear it only returns the first element satisfying the test function. Instead, used filter, which is kinda the same but goes through the whole array and returns all the objects satisfying the test.
you make a for loop without using the i inside it - a strong sign you don't actually need that loop.
There is no need to return "true" or "false" when the whole check is a single boolean expression, it would return it inline.
just a side not, your varibale names are really hard to follow. For example, an array containing movies is better be called "movies", and when you iterate over it with an array function, each element can be called "movie". using same name for both is really confusing. Also try to use camel-case notation (selectedMovie), would make names clearer.
**NOTE: my solution will only return the array from "getMovies" the way you want it, because .map is chained on the return value. If you want selectedmovie to be populated with only movie-names as string and not the whole movie-perosn_id object, just chain the .map to the filter, like so:
this.selectedmovie=this.movie.filter(item=> item['person_id'] === this.selectedperson.person_id ).map(movie => movie.movie);
Hope it helped :)
You can get your desired result like this:
const movie = [
{"person_id":33, "movie":"Titanic"},
{"person_id":33, "movie":"Star Wars"},
{"person_id":34, "movie":"Titanic"},
{"person_id":34, "movie":"Star Wars"},
{"person_id":34, "movie":"Indiana Jones"},
{"person_id":35, "movie":"Titanic"}
];
let selectedperson = [{"person_id":34, "name":"john"}];
let moviesByPerson = movie.filter(f=>
selectedperson.some(s=> f['person_id'] == s['person_id']));
console.log(`moviesByPerson: `, moviesByPerson);
In your code:
getMovies(){
const url ='http://localhost:4000/api/people-movies';
this.http.get(url).subscribe(movie => {
this.movie = movie;
this.selectedmovie = movie.filter(f=>
selectedperson.some(s=> f['person_id'] == s['person_id']));
console.log(this.selectedmovie, this.selectedperson.person_id);
return this.selectedmovie;
});
}
In addition, try to move your HTTP calls into services as it is good practice to separate concerns. As Angular docs says:
Components shouldn't fetch or save data directly and they certainly
shouldn't knowingly present fake data. They should focus on presenting
data and delegate data access to a service.
You can try like this.
this.selectedmovie = this.selectedmovie.filter(item => {
for (var i = 0; i < this.movie.arrayLength; i++) {
if (item['person_id'] === this.selectedperson.person_id) {
return true;
}
}
return false;
});
Note: Javascript array.find method returns the value of the first element in the provided array that satisfies the provided testing function.
Some issues, like mentioned, you are using find, which will return just the first match (if exists). You are looking for filter to filter the movies that match the id. Also you are trying to return from subscribe, which does not work. Also I would change your practices a bit. DON'T use any, it defeats the purpose of TypeScript. So type your data, for example using interfaces, it will help you, since compiler can tell you if your doing something that does not conform to your models, which makes debugging far easier!
Also I would move all http-requests to a service, the component would only subscribe to the result of that http-request. So all in all, I suggest the following:
interface Movie {
person_id: number;
movie: string;
}
interface Person {
person_id: number;
name: string;
}
Service:
import { map } from "rxjs/operators";
// ...
getFilteredMovies(id: number) {
const url ='http://localhost:4000/api/people-movies';
return this.http.get<Movie[]>(url).pipe(
map((movies: Movie[]) => {
return movies.filter((m: Movie) => m.person_id === id)
})
)
}
Component:
movies = <Movie[]>[];
selectedPerson = <Person>{ person_id: 34, name: "john" };
constructor(private myService: MyService) {}
ngOnInit() {
this.myService
.getFilteredMovies(this.selectedPerson.person_id)
.subscribe((data: Movie[]) => {
this.movies = data;
});
}
So we handle the filtering in the service already and component only subscribes. IF you though are calling this function in other places where you don't want the filtering to be performed, then instead do the filtering in the component.
STACKBLITZ demo with the above code.
I finally solved it - thanks erveryone for trying to help, especially #Gibor!
Here is the code for people who face a similar problem:
However - know that this code is by no meand perfect and has to be improved in many ways (e. g. with an loop to find all movies). I just post it here in the hope it helps someone with similar problems ...
component.ts:
import { Component, OnInit } from '#angular/core';
import { HttpClient } from '#angular/common/http';
import { ActivatedRoute} from '#angular/router';
#Component({
selector: 'app-people-detail',
templateUrl: './people-detail.component.html',
styleUrls: ['./people-detail.component.css']
})
export class PeopleDetailComponent implements OnInit {
public data: any = []
public selectedperson: any = []
public movies: any = []
public moviesByPerson: any = []
constructor(
public http: HttpClient,
public route: ActivatedRoute) { }
getPeople()
{
const person_id = this.route.snapshot.paramMap.get('person_id')
const people_url ='http://localhost:4000/api/people'
const movies_url ='http://localhost:4000/api/person-movie'
this.http.get(people_url).subscribe
(data =>
{
this.data = data;
this.selectedperson=this.data.find
(item=>
{
if(item['person_id']===person_id)
{
return true;
}
return false;
}
)
this.http.get(movies_url).subscribe
(movies =>
{
this.movies = movies;
this.moviesByPerson=this.movies.filter(item => item.person_pk === this.selectedperson.person_pk)
}
)
console.log(this.selectedperson, this.moviesByPerson)
return this.selectedperson, this.moviesByPerson
}
)
}
ngOnInit()
{
this.getPeople()
}
}
component.html:
<div><span>ID: </span>{{selectedperson.person_id}}</div>
<h1>{{selectedperson.name}}</h1>
<p>{{selectedperson.age}}</p>
<p></p>
<table>
<thead>
<tr>
<th>Movies</th>
</tr>
</thead>
<tbody>
<tr>
<td>{{moviesByPerson[0].movie}}</td>
</tr>
<tr>
<td>{{moviesByPerson[1].movie}}</td>
</tr>
<tr>
<td>{{moviesByPerson[2].movie}}</td>
</tr>
</tbody>
</table>
I am trying to update Observable array after click on load more but it result in multiple call to API.
Here is my html:
<app-productlistother *ngFor="let item of favs$ | async" [item]="item"></app-productlistother>
<a (click)="loadMore()">Load More</a>
Component:
ngOnInit() {
this.favs$ = this._favoriteService.getFavorites(this.pageNum);
}
private loadMore() {
this.pageNum = this.pageNum + 1;
this.favs$ = this.favs$.merge(this._favoriteService.getFavorites(this.pageNum));
}
And service:
getFavorites(page): Observable<Response> {
return this._http.get(environment.baseUrl + 'favorite?page=' + page)
.map((result: Response) => result.json().data);
}
How can I solve this? On each new request to getFavorites(page) I need to push new results on bottom of array...
#Injectable()
export class MyBooleanService {
myBool$: Observable<any>;
private boolSubject: Subject<any>;
constructor() {
this.boolSubject = new Subject<any>();
this.myBool$ = this.boolSubject.asObservable();
}
...some code that emits new values using this.boolSubject...
}
Then in your component you would have something like this:
#Component({...})
export class MyComponent {
currentBool: any;
constructor(service: MyBooleanService) {
service.myBool$.subscribe((newBool: any) => { this.currentBool = newBool; });
}
}
Now depending on what you need to do with that value you may need to do chnages in your component to update, but this is the gist of using an observable
Another option is you use the async pipe within your template instead of explicitly subscribing to the stream in the constructor. Again though, that depends on what exactly you need to do with the bool values.
So you have no HTML and <app-productionother> template is just {{item.name}} ? In that case, why not structure it like this:
<ng-template *ngFor="let item of favs$ | async">
<app-productlistother [item]="item"></app-productlistother>
</ng-template>
This keeps the async pipe off the repeating element so the async pipe isn't being repeated on each iteration of item. I think that's why it's being called multiple times (likely the same number as you have items).
Also, if merge isn't working out, try concat. Concat will wait until the first observable is finished emitting and then will join the second observable. Merge doesn't wait for the first observable to finish. I imagine you'd want your data to load in order if you click load more and not have initial data interweaved with the second batch, or so on.
I searched for solutions to the problem of
core.umd.js:3523 ORIGINAL EXCEPTION: Cannot read property 'fullName' of undefined
The Exception came from Template on getting specific property:
{{project.collaborators["0"]["fullName"]}}
I found some useful answer too.
but I am looking for a way to define a global service which will check each object and replace missing/empty property with default value i.e -
instead of checking in each template's each property and will make code less buggy.
// undefined-obj.service.ts
import { Injectable } from '#angular/core';
#Injectable()
export class UndefinedObjectsGlobalService {
private charecterToReplace: string = '-'; // set defualt value
replaceDefaultCharecter(object: any, charecterToReplace: string): any {
this.charecterToReplace = charecterToReplace;
// create instance vars to store keys and final output
let keyArr: any[] = Object.keys(object),
var dataArr: any[];
// loop through the object,
// pushing values to the return array
keyArr.forEach((key: any) => {
// if key is null at any iteration then replace is with given text
if (key == null){
dataArr.push(object[key] = this.charecterToReplace);
// else push
}else{
dataArr.push(object[key]);
}
});
// return the resulting array
// need to convert is back to object any idea ?
return dataArr;
}
}
as I am new to angular so the class undefinedObjectsGlobalService may be buggy
please help.
You seem to be trying to access a property of an object that does not exist.
Angular already provides the elvis operator to handle this kind of thing.
#Component({
selector: 'my-app',
template: `
<div>
<h2>Hello {{name}}</h2>
{{collaborators["0"]?.fullname}}
</div>
`,
})
export class App {
name:string;
collaborators: [{fullname: string}];
constructor() {
this.name = 'Angular2'
this.collaborators = [];
}
}
You can see an example here.
Let's say I have an array of elements and in addition to displaying the list in my app, I want to sync the list to the server with HttpClient. How can I observe changes to the array? I tried:
#inject(ObserverLocator)
export class ViewModel {
constructor(obsLoc) {
this.list = [];
obsLoc.getObserver(this, 'list');
.subscribe(li => console.log(li));
}
}
But I got neither error nor log message.
getObserver returns a property observer which will notify you when the ViewModel class instance's list property changes. This will only happen when you assign a new value to the list property, ie this.list = [1,2,3]. If you're not assigning new values to the list property and instead are mutating the value of the property via push, pop, splice, etc, you'll want to use an array observer. Use the ObserverLocator's getArrayObserver method- it takes one parameter, the array you want to observe:
import {ObserverLocator} from 'aurelia-binding'; // or from 'aurelia-framework'
#inject(ObserverLocator)
export class ViewModel {
constructor(obsLoc) {
this.list = [];
obsLoc.getArrayObserver(this.list);
.subscribe(splices => console.log(splices));
}
}
October 2015 update
The ObserverLocator is Aurelia's internal "bare metal" API. There's now a public API for the binding engine that could be used:
import {BindingEngine} from 'aurelia-binding'; // or from 'aurelia-framework'
#inject(BindingEngine)
export class ViewModel {
constructor(bindingEngine) {
this.list = []; // any Array, Map and soon Set will be supported
// subscribe
let subscription = bindingEngine.collectionObserver(this.list)
.subscribe(splices => console.log(splices));
// be sure to unsubscribe **later**
subscription.dispose();
}
}