Data from resolved Promise not loading properly in component view (Angular2, JavaScript) - javascript

I'm fairly new to Angular2 so forgive me if this is a super basic fix.
I'm working through a tutorial using PokeAPI and Angular2 but trying to deviate from the tutorial to add Pokemon types to the rendered view. However, the data from my PokedexService is not properly loading into my view template like it should.
Full codebase that is producing the error on Github.
As it deviates from the original tutorial, I've updated the Pokemon class to add a types property to it:
export class Pokemon {
id: number;
sprite: string;
name: string;
types: [];
}
In my pokedex.service.ts service file, I've added a new method getPokemonTypes() that queries the API for each individual Pokemon's types and returns an array containing either one or two types.
getPokemonTypes(id: number): Promise<any> {
return this.http.get(`${this.baseUrl}${id}/`)
.toPromise()
.then(response => response.json())
.then(details => {
const types = details.types
.map(t => {
return t.type.name;
});
return types;
});
}
In my app.component.ts, I've called a method loadMore() in my ngOnInit that should load all of the data necessary to render the view when the app initializes. Inside the loadMore() method, I've called my pokedexService's getPokemon() method and then in that Promise's resolution chain, I've called getPokemonTypes() to map Pokemon type values onto my Pokemon object.
export class AppComponent implements OnInit {
pokemon: Pokemon[] = [];
isLoading: boolean = false;
error: boolean = false;
constructor(private pokedexService: PokedexService) { }
ngOnInit() {
this.loadMore();
}
loadMore() {
this.isLoading = true;
this.pokedexService.getPokemon(this.pokemon.length, 9)
.then(pokemon => {
pokemon = pokemon.map(p => {
p.imageLoaded = false;
// Map Pokemon types onto each Pokemon object
p.types = this.pokedexService.getPokemonTypes(p.id);
return p;
});
this.pokemon = this.pokemon.concat(pokemon);
this.isLoading = false;
this.error = false;
})
.catch(() => {
this.error = true;
this.isLoading = false;
})
}
}
In my view template (app.component.html), I've added a <div> to hold the Pokemon types. Currrently the div only renders via {{p.types}} just so that I can gain feedback on what is causing the error. For reference, here is the view template:
<div class="pokedex">
<div class="pokedex-pokemon" *ngFor="let p of pokemon">
<div class="pokedex=pokemon-id">
#{{p.id}}
</div>
<img [ngClass]="{'hidden': !p.imageLoaded}" class="pokedex-pokemon-sprite" (load)="p.imageLoaded = true" [attr.src]="p.sprite" />
<div class="pokedex-pokemon-name">
{{p.name | capitalize}}
</div>
<div class="pokedex-pokemon-types" *ngIf="!isLoading">
{{p.types}}
</div>
</div>
</div>
When I reload the app, the view renders like this:
where, instead of rendering the types, it displays [object Promise]. I've also tried rendering the view with {{p.types[0]}}{{p.types[1]}} since Pokemon.types is an array of types, but when I do that, it doesn't render anything in the view:
I've hypothesized that I somehow need to resolve the Promise that is stored in the types property, but I thought calling .then() on the returned HTTP promise in the getPokemonTypes() method of PokedexService resolved the promise to a value for me.
As a side note, I would ultimately like to render the types in my view like this:
<div class="pokedex-pokemon-types">
<div *ngFor="let t of pokemon.types">
<div [ngClass]="${t}">{{t}}</div>
</div>
</div>
but thus far I haven't been able to get that to render either. I'm not entirely sure what the proper syntax is for the [ngClass] directive to get it to use the Pokemon type name as the class for each div.
Thanks for any help offered! Greatly appreciated.

Isn't a result this.pokedexService.getPokemonTypes(p.id) a promise? If is, I think you should write something like this:
pokemon = pokemon.map(p => {
p.imageLoaded = false;
this.pokedexService.getPokemonTypes(p.id)
.then(types => {
p.types = types;
this.pokemon = this.pokemon.concat(p);
this.isLoading = false;
this.error = false;
});
});

Related

How to make API response updates automatically in angular

API Model contains cpu usage categories which keeps on updating dynamically in API. But When I refresh page then only its updates Data but can It be done without refreshing page in the view. Setimeout Works fine is there any method other than setTimeoutInterval?
//app.service.ts
private behaviourData = new BehaviorSubject<Model>(null);
getBPmInfo(): Observable<Model>{
return this.http.get<Model>(`${this.url}`).pipe(
retry(3),
tap(data => this.behaviourData.next(data))
);
}
//app.component.ts
model!: Model;
ngOnInit() {
getInformation(){
this.bpmService.getBPmInfo().subscribe(data => {
this.model = data;
});
}
}
//app.component.html
<div>
<h1>CPU Usage{{model?.cpuUsage}}</h1>
</div>
But data should be updated dynamically without refreshing page. Tried with BehaviourSubject I dont know why its not working. Can anyone help me on this please.
The method of your service should just return a Observable. If you really want to use a BehaviorSubject, it should be defined in your "app.component.ts" like so:
private behaviourData = new BehaviorSubject<Model>(null);
public model$: Observable<Model>;
constructor {
this.model$ = this.behaviourData.asObservable();
}
You should then dismiss the "tap(data => this.behaviourData.next(data))" in your service and move it to the component.
And finally, just subscribe to the observable in your ngInit and forget about the "getInformation()" method like so:
ngOnInit() {
this.bpmService.getBPmInfo().subscribe(data => {
this.behaviourData.next(data)
});
}
And in your template, you just use the "model$" observable like so:
<div>
<h1 *ngIf="(model$ | async) as model">CPU Usage{{model.cpuUsage}}</h1>
</div>
You can use Interval(milliseconds) to fetch data . Usage :
counter = interval(60000); // sets 60 seconds interval
subscription: any = null;
ngOnInit(): void {
this.subscription = this.counter.subscribe(f => {
this.bpmService.getBPmInfo().subscribe(data => {
this.model = data;
});
})
}

Get observable return value without subscribing in calling class

In TypeScript / Angular, you would usually call a function that returns an observable and subscribe to it in a component like this:
this.productsService.getProduct().subscribe((product) => { this.product = product });
This is fine when the code runs in a class that manages data, but in my opinion this should not be handled in the component. I may be wrong but i think the job of a component should be to ask for and display data without handling how the it is retrieved.
In the angular template you can do this to subscribe to and display the result of an observable:
<h1>{{ product.title | async }}</h1>
Is it possible to have something like this in the component class? My component displays a form and checks if a date is valid after input. Submitting the form is blocked until the value is valid and i want to keep all the logic behind it in the service which should subscribe to the AJAX call, the component only checks if it got a valid date.
class FormComponent {
datechangeCallback(date) {
this.dateIsValid$ = this.dateService.checkDate(date);
}
submit() {
if (this.dateIsValid$ === true) {
// handle form submission...
}
}
}
You can convert rxjs Observables to ES6 Promises and then use the async-await syntax to get the data without observable subscription.
Service:
export class DateService {
constructor(private http: HttpClient) {}
async isDateValid(date): Promise<boolean> {
let data = await this.http.post(url, date, httpOptions).toPromise();
let isValid: boolean;
// perform your validation and logic below and store the result in isValid variable
return isValid;
}
}
Component:
class FormComponent {
async datechangeCallback(date) {
this.dateIsValid = await this.dateService.isDateValid(date);
}
submit() {
if (this.dateIsValid) {
// handle form submission...
}
}
}
P.S:
If this is a simple HTTP request, which completes on receiving one value, then using Promises won't hurt. But if this obersvable produces some continuous stream of values, then using Promises isn't the best solution and you have to revert back to rxjs observables.
The cleanest way IMHO, using 7.4.0 < RxJS < 8
import { of, from, tap, firstValueFrom } from 'rxjs';
const asyncFoo = () => {
return from(
firstValueFrom(
of('World').pipe(
tap((foo) => {
console.info(foo);
})
)
)
);
};
asyncFoo();
// Outputs "World" once
asyncFoo().subscribe((foo) => console.info(foo));
// Outputs "World" twice
The "more cleanest" way would be having a factory (in some service) to build these optionally subscribeable function returns...
Something like this:
const buildObs = (obs) => {
return from(firstValueFrom(obs));
};
const asyncFoo = () => {
return buildObs(
of('World').pipe(
tap((foo) => {
console.info(foo);
})
)
);
};

Why are my input variable properties undefined (Angular)

I'm fetching data from my service in the app component, and then passing it down to a child component via #Input. when I console.log the data in ngOnInit, it DOES show up in the child component, but when I try to pass it to a variable and use it in the child template, it's coming back undefined, and I'm not sure why.
Here's where I call my service in app.component.ts, the data that is having issues is this.colorcounts. Console logging here inside the ngOnInit DOES show the correct data. colorCounts has 3 properties: red, yellow & green:
ngOnInit() {
this.pciService.getPciInfo()
.subscribe((pciInfo) => {
this.spinner.hide();
this.pciData = pciInfo.sorted,
this.colorCounts = pciInfo.counts;
});
Here's the app.component.html template where I'm passing the data down:
<app-navbar *ngIf="colorCounts" [colorCounts] = "colorCounts"></app-navbar>
<app-system-status [pciData]="pciData"></app-system-status>
Here's the child component where I'm grabbing the data, doing a console.log in ngOnInit does work, but trying to use "red" in the template or save it in the class comes back undefined:
constructor(private pciService: PciService,
public dialog: MatDialog,
private decimalPipe: DecimalPipe) { }
AMVersion: any;
#Input() colorCounts: Colors;
openDialog(): void {
let dialogRef = this.dialog.open(AmVersionDialogComponent, {
panelClass: 'custom-dialog-container',
data: {}
});
(<AmVersionDialogComponent>dialogRef.componentInstance).AMVersion = this.AMVersion;
dialogRef.afterClosed().subscribe(result => {
console.log('The dialog was closed');
});
}
ngOnInit() {
this.pciService.getAMVersion()
.subscribe((AMInfo) => {
return this.AMVersion = AMInfo;
});
var red = this.colorCounts.red;
console.log(red)
console.log(this.colorCounts);
}
}
I know I'm probably missing something simple regarding the life cycle. Can someone point me in the right direction here?
All Observables are async so in template *ngIf condition will check variable and if it is null will return null but if You pipe variable as | async it will be checking this variable all time and when variable will apear not null it will show content ngIf.
*ngIf works only once !!! He not waiting for anny http calls and Observables are making one usualy. If You want to *ngIf wait for calls You need to use | async pipe inside.
Same as You subscribe to it in ngOnInit() and assign to variables in template. Subscription will assign those values later after template is allredy on screen. Read about what async means.
You need to know that this is a boilercode:
ngOnInit() {
this.pciService.getPciInfo()
.subscribe((pciInfo) => {
this.spinner.hide();
this.pciData = pciInfo.sorted,
this.colorCounts = pciInfo.counts;
});
It is better to do it like this:
ngOnInit() {
this.pciInfo$ = this.pciService.getPciInfo()
}
in template:
<ng-container *ngIf="pciInfo$ | async as pciInfo">
<app-navbar [colorCounts]="picInfo.counts"></app-navbar>
</ng-container>
Pipe Async will subscribe for you to an Observable.

Can't bind from API data Angular 6

I'm working with API to get data and bind it to the HTML, i get to show the response on the console but once i try to bind data to the User screen i cannot, nothing is showing
My HTML:
<ul *ngFor="let item of items; let i of index">
<li class="fa fa-plus">{{item.name}} - {{i}}</li>
</ul>
My TS:
id: number;
name:string;
imageUrl: string;
items:any = {};
ngOnInit() {
this.getItems();
}
getItems() {
this.getItemsService.getItems().subscribe(
res => {
console.log(res)
if (res.Success) {
this.items = res;
}
}
)
}
I'm trying to get data from an array of objects this is why i defined items as an object, if i tried to define it as an array, same result, cannot bind data to HTML
My API Link:
http://5a12745f748faa001280a746.mockapi.io/v1/stores/item
Any questions or missing info tell me but kindly i need help binding this data
Two things, HTML first : let i = index is the syntax.
The second one, you have a condition in your binding :
if (res.Success) {
this.items = res;
}
This implies that your response has a Success property.
I checked your API link, and it doesn't.
So in your service, either you use Http from #angular/http or HttpClient from #angular/common/http. Here are the two syntaxes :
// Http
return this.http.get('url').map(res => res.json());
// HttpClient
return this.http.get<any[]>('url');
For the condition, change to the following, to check if you have data. You don't need to check for success because you have a special callback for that. This means you will always have succeed where the condition is.
if (res) {
this.items = res;
}

Angular 2 change detection on array push of objects

I have a 'Schedule' typescript class and a 'selectedSchedule' variable of this 'Schedule' type.
Schedule:
export class Schedule{
scheduleId: string;
sheduleDate: Date = new Date();
scheduleName: string;
jobs: Job[];
unscheduledEmployeesCount: number = 0;
idleEquipmentCount: number = 0;
unscheduledTrucksCount: number = 0;
}
I'm binding this variable and it's properties in the HTML by utilizing interpolation. My problem is I'm using a *ngFor to iterate and display each 'job'...
<div class="job" *ngFor="let j of selectedSchedule.jobs">
<div [ngClass]="{'hasEmployees': j.jobEmployees.length > 0 }">
<div *ngFor="let e of j.jobEmployees">
{{e.employee['name'] !== null ? e.employee['name'] : e.name}}
</div>
</div>
</div>
whenever the user triggers the 'addJob()' method by clicking a button, the newly created 'job' doesn't get detected by Angular and results in the properties of the 'job' to be null, most notably the 'jobEmployees' of each 'job'. I understand Angular doesn't detect changes to an array when you push/remove objects out of the box. I'm looking for a solution to pick up on these changes when a user adds a new job. I have tried to reassign the 'selectedSchedule' and it's 'jobs' property with no success. Also have tried to slice() the jobs array with the thought of 'recreating' the array.
I'm currently making an extra http request to get the schedule's jobs again, which seems to prevent the null property problems, but I'm hoping to find a more efficient solution.
The addJob() method in component:
addJob() {
var newJob: Job;
this.data.addJob(this.selectedSchedule.scheduleId).subscribe(addedJob => {
this.selectedSchedule.jobs.push(addedJob);
this.data.loadSchedules().subscribe(success => {
if (success) {
this.selectedSchedule = this.data.schedules.find(schedule => schedule.scheduleId === this.selectedSchedule.scheduleId);
}
});
});
}
addJob() method in data service:
addJob(scheduleId: string) {
return this.http.get("/api/job/addjob", { headers: this.headers, params: { scheduleId: scheduleId } })
.map((job: Job) => {
return job;
});
}
'Job' class:
export class Job {
jobId: string;
jobName: string;
jobNumber: string;
jobEmployees: Array<Employee> = new Array<Employee>();
jobEquipment: Array<Equipment> = new Array<Equipment>();
jobTrucks: Array<Truck> = new Array<Truck>();
}
You are looking for ChangeDetectorRef,Normally this is done by Angulars change detection which gets notified by its zone that change detection should happen. Whenever you manually subscribe in a component, angular marks the component instance as dirty whenever the subscribed stream emits a value.
Try calling it manually as follows,
this.selectedSchedule = this.data.schedules.find(schedule => schedule.scheduleId === this.selectedSchedule.scheduleId);
this.cdRef.detectChanges();

Categories

Resources