I am facing a weird issue in assigning response to a class's global variable from inside a observable. So my program logic is as follows:
Get latest playlists ID's from elastic search (i use elastic search from a type definition file). This returns me a PromiseLike to which i hook a then operator.
Inside the promise resolution, i make another http get call (i.e an observable)
In Observable subscription, i assign my global array with the response from the server.
Code is working correctly, I am getting responses as they should be but i cant assign the variable to the global one.
Here is my code:
import {Component, OnInit} from '#angular/core';
import {PlaylistService} from '../api/services'
#Component({
selector: 'app-playlists',
templateUrl: './playlists.component.html',
styleUrls: ['./playlists.component.css']
})
export class PlaylistsComponent implements OnInit {
public playlists: any[] = [];
constructor(private playlistService: PlaylistService) {
}
ngOnInit() {
let that = this;
this.playlistService.listIds().then((val) => { // <-- promise resolution
return this.playlistService.getByIds(val).toPromise(); // <-- http get call which i then convert to promise for simplicity
}).then((res) => { // <-- resolution of the http get call
console.log(this.playlists); <-- in this log, i get my desired results
// here is my problem, this assignment doesn't happens
this.playlists = res.data;
});
}
}
The listIds function is as follows:
listIds() {
return this.api.listing('playlist').then((body) => {
let hits = body.hits.hits;
return _.keys(_.groupBy(hits, '_id'));
});
}
and here is my api.listing function (elastic search client)
listing(type: string) {
let es = this.prepareES();
return es.search({
index: 'test',
_source: ["_id"],
type: type
});
}
The return type of es.search is
search(params: SearchParams): PromiseLike>;
Any ideas why i am not being able to assign value to global variable?
It looks like the promise returned by this.playlistservice.listIds() doesn't run inside Angulars zone. This is why Angular2 doesn't run change detection and doesn't recognize the change.
You can invoke change detection explicitly after the change:
constructor(private playlistService: PlaylistService, private cdRef:ChangeDetectorRef) {
...
ngOnInit() {
let that = this;
this.playlistService.listIds().then((val) => { // <-- promise resolution
return this.playlistService.getByIds(val).toPromise(); // <-- http get call which i then convert to promise for simplicity
}).then((res) => { // <-- resolution of the http get call
console.log(this.playlists); <-- in this log, i get my desired results
// here is my problem, this assignment doesn't happens
this.playlists = res.data;
this.cdRef.detectChanges();
});
}
Can you try passing
this.playlistService.listIds()
call inside your
return this.playlistService.getByIds(val)
replace val with first service call and see if your view gets updated. Just for testing purpose like
return this.playlistService.getByIds(this.playlistService.listIds())
.then((results)=>{/*rest of logic here*/});
Related
I have a method in an Angular component that pulls data via HttpClient subscription, and assigns it to an attributes this.allData, then instantiates an empty dictionary of parameters based on this, to pass to a second function:
export class TestComponent implements OnInit {
allData: object[] = []
activeData: object = {}
constructor(private http: HttpClient) { }
ngOnInit(): void {
this.getData()
this.makeRequestBasedOnData()
}
getData() {
this.http.get(this.url).subscribe(res => {
for (let datum of res["data"]) {
this.allData.push({
"key": Object.keys(datum)[0],
"values": Object.values(datum)[0]
})
this.activeData[Object.keys(datum)[0]] = ""
}
})
}
makeRequestBasedOnData() {
let testParams = this.activeData
console.log(testParam)
}
}
I need these steps to happen sequentially. At the moment, logging the testParams in makeRequestBasedOnData() simply shows an empty object {}. When I try to return arbitrarily in the first method, I get a TypeScript error that you cannot assign a promise to type void.
How do I enforce synchronicity here even though neither method actually returns anything?
You can return Observable from getData method and proceed with any other methods within subscribe:
ngOnInit(): void {
this.getData().subscribe(() => this.makeRequestBasedOnData());
}
getData() {
return this.http.get(this.url).pipe(
tap(res => {
// update allData
})
);
}
where:
pipe method allows us to provide any kind of transformation with the data returned from http.get(...) call.
tap is a rxjs operator for side-effects, meaning that we can do everything we want with the response without modifying Observable flow.
I have a service that connects with api
export class ConsolidadoApi {
constructor(private http: HttpClient) { }
getInvestiments(search?: any): Observable<any> {
return this.http.get<any>(`${environment.basePosicaoConsolidada}`);
}
}
Response this api:
https://demo5095413.mockable.io/consolidado
This one is responsible for the logic before reaching the component
#Injectable({
providedIn: 'root'
})
export class CoreService {
public test;
constructor(private api: ConsolidadoApi, private state: StateService) { }
public createMenu() {
this.api.getInvestiments()
.subscribe(response => {
console.log(response.carteiras[0])
this.products = response.carteiras[0]
return this.products;
})
}
In my component
export class MenuComponent implements OnInit {
constructor( private coreService : CoreService ) {}
ngOnInit(): void {
console.log(this.coreService.createMenu())
}
}
But when createMenu is called in menu.component.ts it comes undefined.
The raw response is an object. forEach works only on an array. If you are aiming for forEach in 'categorias', you should try
this.test.categorias.forEach()
When you return Observable<any>, that means the argument of the lambda you create when you do subscribe (which you named response) is type any. This doesn't necessary have the function forEach defined (unless the API returns an object with that prototype). That's generally why using any is not good practice; you can't have any expectations on what the object can contain. In fact, it's possible that it's not on object (it could be an array since any is not exclusively an object). If you do want to use forEach, you will want to make sure that response is type array. You can inspect the object's type before using it (e.g. using typeof) and make a judgement on what to call or even just check if the function you're trying to use is defined first, e.g. if (response.forEach !== undefined). You don't actually need to compare to undefined though, so if (response.forEach) suffices. In the examples, I used response, but you can use this.test since they are the same object after the first line in the lambda.
Based on the link you shared, the response is an object. You can log it to the console to confirm.
You can only call for each on an array, so for example, based on the response api, you can call forEach on the property ‘categorias’ and on that array’s children property ‘produtus’
Edit: this answer was based on the op original api and question
https://demo5095413.mockable.io/carteira-investimentos
public createMenu() {
return this.api.getInvestiments()
}
ngOnit() {
this.coreService.createMenu().subscribe(x => console.log(x.categorias))};
{
"codigo":1,
"categorias":[
{
"nome":"Referenciado",
"valorTotal":23000.0,
"codigo":"2",
"produtos":[
{
"nome":"CDB Fácil Bradesco",
"valor":2000.0,
"codigo":1,
"quantidade":0.0,
"porcentagem":0.5500,
"aplicacaoAdicional":500.0,
"codigoInvest":1,
"salaInvestimento":"CDB",
"permiteAplicar":true,
"permiteResgatar":true,
"movimentacaoAutomatica":false,
"ordemApresentacao":37,
"horarioAbertura":"08:30",
"horarioFechamento":"23:59",
"codigoGrupo":0,
"codigoMF":"001
I'm trying to get the current value of a BevhaviorSubject in Angular. I printed each, the whole thing and the value only, to the console to check its content by using these two lines:
console.log(this._isNumeric)
console.log(this._isNumeric.getValue())
...but what I am receiving is this:
closed: false
hasError: false
isStopped: false
observers: []
thrownError: null
_isScalar: false
_value: true
value: true
__proto__: Subject
for the subject (note that the value parameter is set to true) and
false
if I am just printing the value. Maybe I am making an obvious mistake, but does someone has a clue how to get the actual value of the BehaviorSubject? Using .value instead of .getValue() does not change the outcome. Thank you! :)
In your service you can create and expose the BehaviorSubject like this:
private _isNumeric$ = new BehaviorSubject<boolean>(false); // first value is false
/** Protects the _isNumeric$ BehaviorSubject from outside access */
public get IsNumeric$(): Observable<boolean> {
return this._isNumeric$.asObservable();
}
// NOTE: This is how you can access the value within the service
private get IsNumeric(): boolean {
return this._isNumeric$.getValue();
}
To clarify the use of the '$' at the end of the variable name. It does not do anything, it is just sometimes used by convention to indicate that a variable holds an Observable or that a function will return an Observable.
If you wanted to emit a new value you can do so using '.next()' on the BehaviorSubject
this._isNumeric$.next(true); // Emit a value of true
If you wanted to access the data from a component you can retrieve the data by subscribing to it like so. Remember to unsubscribe from the BehaviorSubject as well.
this.yourService.IsNumeric$
.pipe(takeUntil(this.onDestroy$)) // This is just a subject used to unsubscribe later
.subscribe((value: boolean) => {
// Use the result from the BehaviorSubject
});
Every Behavior Subject will require 3 things: a) initial value, b) a source that can be turned into an observable c) a public variable that can be subscribed. You can use Behavior Subject like this:
// In your service file, do this
#Injectable({ provideIn: 'root' })
export class YourServiceName {
// Set the initialvalues in this format ==>
// initialValuesForBehaviorSubject: YourModel = initialValue;
// In your case:
initialValue: boolean = false;
// Make a source using Behavior Subject as a type and above initial
// values as the initial value as a private variable
// This source is required to SET the data
// Your case:
private behaviorSubjectSource: BehaviorSubject<boolean> =
new BehaviorSubject<boolean>(this.initialValue);
// Create a public observable to GET the data
// Your case:
observableForBehaviorSubject: Observable<boolean> =
this.behaviorSubjectSource.asObservable();
// Create a method to set the data from anywhere in your application
setData(data: boolean) {
this.behaviorSubjectSource.next(data);
}
// Create a method to get data from anywhere in your application
getData(): Observable<boolean> {
return this.observableForBehaviorSubject;
}
}
///// In your component-a.ts file, set the value like this:
ngOnInit() {
this.yourService.setData(true);
}
//// In your component-b.ts, get the value like this:
ngOnInit() {
localVariableToStoreObservableData: boolean;
this.yourService.getData().subscribe(data => {
if(data) {
this.localVariableToStoreObservableData = data;
}
})
}
Under my Angular 6 app , i have this service ; where i'm declaring a varibale called permittedPefs , this variable is setted asychronsouly within a httpClient.get call.
#Injectable()
export class myService implements OnInit {
permittedPefs = [];
constructor(){}
ngOnInit() {
// STEP 1
this.loadUserPefsService.getUserRolePefs(roleId).subscribe(
(returnedListPefs) => {
this.permittedPefs = returnedListPefs;
},
error => {
console.log(error);
});
}
// STEP 2
this.myMethod(1);
After that , i ve a call of this method which is using my -supposed setted - var
myMethod(pefId): boolean {
return this.permittedPefs.includes(pefId);
}
the problem is it seems that permittedPefs , haven't got its value yet , and the call of myMethod() point to a wrong value of "permittedPefs"
So what's the simpliest way to make it wait to the just after the http response , without calling if from the http Response callback (as i 'm using it in several places)
Sugesstions??
Asynchronous Hell ! the best choice here is to get an Observable instead of a value
in your service :
getValue (): Observable<any>{
return this.loadUserPefsService.getUserRolePefs(roleId);
}
in your method :
myMethod(pefId): boolean {
this.yourservice.getValue().subscribe(
data => {
if(data){
return data.includes(pefId);
}
});
}
This happens because your method is called when you have not received the result yet. so just move the function call in subscribe function
Call the method this.myMethod(1); from subscription block, so that you wait for asynchronous call to be completed, which will then set the value of permittedPefs.
ngOnInit() {
// STEP 1
this.loadUserPefsService.getUserRolePefs(roleId).subscribe(
(returnedListPefs) => {
this.permittedPefs = returnedListPefs;
// STEP 2
this.myMethod(1); // check this line
},
error => {
console.log(error);
});
}
So in normal javascript if I wanted to assign a value to a variable and then use that value outside of a function it would be done by declaring the variable first and then define it's value in the function. I'm brand new to typescript and angular so I am missing how to do this.
In the code below I am trying to get the value from a method in a service and then pass that value into my return. (I hope that makes sense). However I keep getting undefined on console.log(url) with no other errors.
emailsAPI() {
let url: any
this.apiUrlsService.urlsAPI().subscribe(
data => {
this.results = data
url = this.results.emails
}
);
console.log(url)
return this.http.get('assets/api/email_list.json')
}
api-urls service:
import { Injectable } from '#angular/core';
import { HttpClient, HttpErrorResponse } from '#angular/common/http';
#Injectable()
export class ApiUrlsService {
constructor(
private http: HttpClient
) { }
urlsAPI () {
return this.http.get('assets/api/api_urls.json')
}
}
That's because you're calling async method subscribe and then trying to log the coming value before subscription is resolved. Put last two statements (console.log and return) inside the curly braces just after assigning this.results.emails to the url variable
emailsAPI(): Observable<any> {
let url: any
return this.apiUrlsService.urlsAPI()
.flatMap(data => {
this.results = data
url = this.results.emails
// you can now access url variable
return this.http.get('assets/api/email_list.json')
});
}
As per reactive programming, this is the expected behaviour you are getting. As subscribe method is async due to which you are getting result later on when data is received. But your console log is called in sync thread so it called as soon as you are defining subscribe method. If you want the console to get printed. Put it inside your subscribe data block.
UPDATE:
As per your requirement, you should return Subject instead of Observable as Subject being data consumer as well as data producer. So it will consume data from httpget request from email and act as a producer in the method from where you called emailsAPI method.
emailsAPI(): Subject<any> {
let emailSubject:Subject = new Subject();
this.apiUrlsService.urlsAPI()
.flatMap(data => {
this.results = data
return this.results.emails;
}).
subscribe(url=> {
this.http.get(your_email_url_from_url_received).subscribe(emailSubject);
});
return emailSubject;
}
The subject can be subscribed same as you will be doing with Observable in your calee method.