Get value of current Observable state - javascript

So I am trying to get the value (the email) of my Observable<firebase.User>. I know there is something called BehaviourSubject, but I cannot use it since firebase.User requires an Observable, it seems.
retrieveUserData(){
let emailVal = "";
this.userData.subscribe(
{
next: x => emailVal += x.email && console.log(x.email),
error: err => console.error('Observer got an error: ' + err),
complete: () => console.log('Observer got a complete notification'),
}
);
return emailVal;
}
So my goal is to get x.email in the emailVal let, in order to pass it & display, for example.
The problem is, that I am getting an (by logging the whole method retrieveUserData()), but the console.log(x.email) always returns the value I am looking for.
Why is that & is there a way to get the value & store it in a string, let or something else?

It is because Observables are async. It means when you run subscribe method of the observable, it runs the command without blocking the current runtime. Also, you are assigning the value of emailVal when the observable is run but you return the value without waiting for the assignment to be happened.
What you can do?
You can keep a global variable to keep the email globally and use that variable to display the email in the html side.
#Component({
selector: "my-app",
// See here, I used emailVal to display it
template: "<span>{{emailVal}}</span>",
styles: [""]
})
export class TestComponent {
emailVal = "";
ngOnInit(): void {
this.retrieveUserData();
}
retrieveUserData(): void {
this.userData.subscribe(
x => this.handleData(x.email),
err => console.error("Observer got an error: " + err)
);
}
handleData(email) {
// Here, we assign the value of global variable (defined in class level)
this.emailVal = email;
console.log(email);
}
}
You can use rxjs library in such a way to make the observable return the value and return the observable in the method as below
retrieveUserData(): Observable<firebase.User> {
return this.userData.pipe(
map(x => x.email)
);
}
And in html side, using async pipe (as an example):
<span>{{retrieveUserData() | async}}</span>
async pipe will subscribe to observable and wait for it to complete and then take the value and put it in the value of span. You can check this StackBlitz example to understand this method deeply.

Related

BehaviorSubject observable triggers many times

I subsribed to observable of behaviorSubject and it's triggers too many times. It happens only when i am navigating on the same component route, as an example...folder-folder-folder and now ...delete file triggers x3 times.
Subscribe code:
this.headerService.selectedItems.subscribe( {
next:(value) =>
if (this.selectedRowsIds.size >= 1 && value === true) {
this.deleteDocs();
}... and here value comes x3 times
BehaviorSubject:
deleteButton = new BehaviorSubject<any>({});
selectedItems = this.deleteButton.asObservable();
deleteTrigger(trigger: boolean) : void {
this.deleteButton.next(trigger);
}
I tried to unsubcribe, to send false trigger everytime when i navigate, but nothing changes.
I mention that component DOES NOT DESTROY in this case, cause we open folder-folder-folder on the same component, with changing route params.
The Problem can due to many other factors
Due to any change in route in the process.
Due to Not emitting value at right time.
Behavior Subject fires the value as soon as its initialized new BehaviorSubject<any>({}) with an {} (and empty object it emits and this value it holds )
Insufficient working and dependable code to see the flow , if possible please provide insight.
My Solution:
deleteButton :BehaviorSubject<boolean>= new BehaviorSubject<boolean>(false);
deleteTrigger(trigger: boolean) : void {
this.deleteButton.next(trigger);
}
trigger: boolean (here we are only emitting boolean values because input is always boolean)
this.headerService.deleteButton.subscribe(response => {
if(response) {
this.selectedRowsIds.size >= 1 && value === true ?
this.deleteDocs() : '';
}
});
this would subscribe to the action when a value is emitted from deleteButton and the value is true then it would excute desired logic though tenary opeartor
You can use rxjs takeLast with behavior subject to fix the issue or Promise Resolve. takeLast, you could also use take, takeOnce all found in rxjs. Also you do not need to strong type your interfaces on behavior subjects. It's recommended for security concerns. because any is applied.
Also you should make your subscription async and await the answer from the subscription.
RXJS LINK
rxjs
import { BehaviorSubject, takeLast, tap } from 'rxjs';
private yourSubject = new BehaviorSubject<any>({});
//or your choice
private yourSubject = new BehaviorSubject({});
//or your choice
private yourSubject = new BehaviorSubject({} as any);
public yourSubject$ = this.yourSubject.asObservable();
this.yourSubject$.pipe(takeLast(1),tap((item)=>{return item})).subscribe()
Promise resolve
this.yourSubject$.subscribe((element)=>{
const item = Promise.resolve(element)
return item;
});
//or your choice
this.yourSubject$.subscribe(async (element)=> {
const item = await element;
return item;
})

Using ion-img waiting for url key?

I am wanting to use ion-slides with ion-img to produce a gallery since I am thinking there may be a fair no of images. ion-img in ionic 4 uses lazy-loading to fetch the images which I think is what I want.
I am fetching the images from an Azure storage account which needs to have a dynamic (expiring) key added to it when the url is fetched.
I have managed to make this work pretty well as follows, heres the html:
<ion-slides #photoslider [options]="sliderOptions"
(ionSlideDidChange)="checkKeyandFixText()"
(ionSlidesDidLoad)="checkKeyandFixText()">
<ion-slide *ngFor="let i of userService.user.Images">
<ion-img cache="true" width="50%" [src]="i.ImageUrl + validKey"></ion-img>
</ion-slide>
</ion-slides>
which checks the key (and fixes up an index for text use with each load or image change).
The check key function is this:
checkKeyandFixText() {
console.log(this.validKey);
this.userService.getStorageURLKey('').then(() => { // trigger key update
console.log(this.validKey);
});
.... more irrelevant stuff
}
the 'validKey' used in the html is updated by the 'getStorageURLKey function, because that function fires the service that gets the key and updates an observable, which is set up in the ngOnInit - part of which includes:
this.azureKey$ = this.userService.azurekey$.subscribe((res) => {
this.validKey = res.Key;
});
But this is unreliable, depending on the state of the key and all manner of async timing possibilities.
Its been a bit of a road to here, stating with the idea that writing the 'getStorageURLKey' function would work, by providing the url, it checking if the current key is valid or not then if not getting one, and adding it back.
That function by necessity returns a Promise which the ion-img seems unable to cope with. (Here is the function for completeness:
async getStorageURLKey(url: string): Promise<string> {
const nowplus5 = addMinutes(Date.now(), 5); // 5 mins to give some leeway
console.log(nowplus5);
console.log(url);
console.log(this.azurekey);
if (!this.azurekey || isBefore( this.azurekey.Expires, nowplus5 )) {
console.log('Getting new Key');
const keyObj = await this.getAzureKeyServer().toPromise();
await this.saveAzureKeyStore(keyObj);
this.azurekey = keyObj;
console.log(this.azurekey);
return url + keyObj.Key; // Return the url with the new Key or just the key if url is blank
} else {
console.log('Key is valid till ' + this.azurekey.Expires);
console.log(this.azurekey.Key);
const rval = Promise.resolve(url + this.azurekey.Key);
return rval ; // Key is in time so return it with url
}
}
If I just call the function from inside the html:
<ion-img cache="true" width="50%" [src]="userService.getStorageURLKey(i.ImageURL) "></ion-img>
it just loops forever due to angulars retry and event loops. If I pipe the result to async:
<ion-img cache="true" width="50%" [src]="userService.getStorageURLKey(i.ImageURL) | async "></ion-img>
Then it loops over the images the requisite no of times but doesn't do anything else... sometimes, then sometimes it seems to just make no difference and loops forever.
I have also tried adding my own custom pipe instead of the async but the inbound value always comes in as 'undefined'.
Changing the html just to reference "i.ImageURl | urkKey", here's the pipe I tried:
#Pipe({
name: 'urlKey'
})
export class URLKeyPipe implements PipeTransform {
userService: UserService;
transform(value: string): any {
console.log('At Pipe');
console.log(value);
this.userService.getStorageURLKey(value).then((key) => {
console.log(key);
return key;
});
}
}
I have tried a lot of things...:-s
Fundamentally I need to add an an async key to an async url and seem to be looped up in angular. Am I approching this wrongly, or should I just go try using plain old img instead?
Any ideas? Thanks in advance.

How to collect and return aggregated data as an Array from a table in Protractor?

I am trying to aggregate a list of dates from a data table, written in Angular, in a Protractor test. I'm doing the aggregation from a PageObject class that is called in the Protractor test. I know that my code is successfully grabbing the text I want, but when I try to console.log the returned array, I get an empty array. I'm still new to Javascript/Typescript, Angular, and Protractor and this may be a result of my newness to the asynchronous nature of this development environment.
Code is as follows,
The PageObject SpecMapper class with method:
import { browser, element, by } from 'protractor';
export class SpecMapperPage {
getImportDateSubmittedColumnValues() {
let stringDatesArray: Array<string> = [];
// currently this css selector gets rows in both import and export tables
// TODO: get better identifiers on the import and export tables and columns
element.all(by.css('md-card-content tbody tr.ng-tns-c3-0')).each(function(row, index){
// check outerHTML for presence of "unclickable", the rows in the export table
row.getAttribute('outerHTML').then(function(outerHTML:string) {
// specifically look for rows without unclickable
if(outerHTML.indexOf("unclickable") < 0){
// grab the columns and get the third column, where the date submitted field is
// TODO: get better identifiers on the import and export columns
row.all(by.css("td.ng-tns-c3-0")).get(2).getText().then(function(text:string) {
stringDatesArray.push(text);
});
}
});
});
return stringDatesArray;
}
}
I know it's not the prettiest code, but it's temporary place holder while my devs make me better attributes/classes/ids to grab my variables. Key things to note is that I create a string Array to hold the values I consider relevant to be returned when the method is finished.
I used WebStorm and put a breakpoint at the stringDatesArray.push(text) and return stringDatesArray lines. The first line shows that the text variable has a string variable that I'm looking for and is successfully getting pushed. I see the success in debug mode as I can see the stringDatesArray and see the values in it. The second line though, the array return, shows that the local variable stringDatesArray is empty. This is echoed in the following code when I try to console.log the array:
The Protractor run Spec class with my test in it:
import { SpecMapperPage } from "./app.po";
import {browser, ExpectedConditions} from "protractor";
describe('spec mapper app', () => {
let page: SpecMapperPage;
let PROJECT_ID: string = '57';
let PROJECT_NAME: string = 'DO NOT DELETE - AUTOMATED TESTING PROJECT';
beforeEach(() => {
page = new SpecMapperPage();
});
describe('import/export page', () => {
it('verify sort order is desc', () => {
browser.waitForAngularEnabled(false);
// Step 1: Launch Map Data from Dashboard
page.navigateTo(PROJECT_ID);
browser.driver.sleep(5000).then(() => {
// Verify: Mapping Screen displays
// Verify on the specmapper page by checking the breadcrumbs
expect(page.getProjectNameBreadCrumbText()).toContain(PROJECT_NAME);
expect(page.getProjectMapperBreadCrumbText()).toEqual("MAPPER");
// Verify: Verify Latest Submitted Date is displayed at the top
// Verify: Verify the Submitted Date column is in descending order
console.log(page.getImportDateSubmittedColumnValues());
});
});
});
});
I acknowledge that this code is not actively using the niceties of Protractor, there's a known issue with our app that will not be addressed for a couple of months, so I am accessing the driver directly 99% of the time.
You'll note that I call the method I posted above as the very last line in the browser.driver.sleep().then() clause, page.getImportDateSubmittedColumnValues().
I thought maybe I was running into asynchronous issues with the call being done before the page was loaded, thus I put it in the .then() clause; but learned with debugging that was not the case. This code should work once I have the array returning properly though.
The console.log is printing an empty [] array. That is synonymous with the results I saw when debugging the above method directly in the PageObject SpecMapper class. I wish to do some verification that the strings are returned properly formatted, and then I'm going to do some date order comparisons. I feel like returning an array of data retrieved from a page is not an unusual request, but I can't seem to find a good way to Google what I'm trying to do.
My apologies if I am hitting some very obvious roadblock, I'm still learning the nuances of Typescript/Angular/Protractor. Thank you for your consideration!
My attempted to used collated promises seemed promising, but fell through on execution.
My Updated PageObject SpecMapper Class
import {browser, element, by, protractor} from 'protractor';
export class SpecMapperPage {
getImportDateSubmittedColumnValues() {
let promisesArray = [];
let stringDatesArray: Array<string> = [];
// This CSS selector grabs the import table and any cells with the label .created-date
element.all(by.css('.import-component .created-date')).each(function(cell, index) {
// cell.getText().then(function(text:string) {
// console.log(text);
// });
promisesArray.push(cell.getText());
});
return protractor.promise.all(promisesArray).then(function(results) {
for(let result of results) {
stringDatesArray.push(result);
}
return stringDatesArray;
});
}
}
My Updated Spec test Using The Updated SpecMapper PO Class
import { SpecMapperPage } from "./specMapper.po";
import {browser, ExpectedConditions} from "protractor";
describe('spec mapper app', () => {
let page: SpecMapperPage;
let PROJECT_ID: string = '57';
let PROJECT_NAME: string = 'DO NOT DELETE - AUTOMATED TESTING PROJECT';
beforeEach(() => {
page = new SpecMapperPage();
});
describe('import/export page', () => {
it('TC2963: ImportComponentGrid_ShouldDefaultSortBySubmittedDateInDescendingOrder_WhenPageIsLoaded', () => {
browser.waitForAngularEnabled(false);
// Step 1: Launch Map Data from Dashboard
page.navigateTo(PROJECT_ID);
browser.driver.sleep(5000).then(() => {
// Verify: Mapping Screen displays
// Verify on the specmapper page by checking the breadcrumbs
expect(page.getProjectNameBreadCrumbText()).toContain(PROJECT_NAME);
expect(page.getProjectMapperBreadCrumbText()).toEqual("MAPPER");
// Verify: Verify Latest Submitted Date is displayed at the top
// Verify: Verify the Submitted Date column is in descending order
page.getImportDateSubmittedColumnValues().then(function(results) {
for(let value of results) {
console.log("a value is: " + value);
}
});
});
});
});
});
When I breakpoint in the PO class at the return stringDatesArray; line, I have the following variables in my differing scopes. Note that the promisesArray has 3 objects, but the results array going into the protractor.promise.all( block has 0 objects. I'm not sure what my disconnect is. :/
I think I'm running into a scopes problem that I am having issues understanding. You'll note the commented out promise resolution on the getText(), and this was my POC proving that I am getting the string values I'm expecting, so I'm not sure why it's not working in the Promise Array structure presented as a solution below.
Only other related question that I could find has to do with grabbing a particular row of a table, not specifically aggregating the data to be returned for test verification in Protractor. You can find it here if you're interested.
As you've alluded to your issue is caused by the console.log returning the value of the variable before its actually been populated.
I've taken a snippet from this answer which should allow you to solve it: Is there a way to resolve multiple promises with Protractor?
var x = element(by.id('x')).sendKeys('xxx');
var y = element(by.id('y')).sendKeys('yyy');
var z = element(by.id('z')).sendKeys('zzz');
myFun(x,y,z);
//isEnabled() is contained in the expect() function, so it'll wait for
// myFun() promise to be fulfilled
expect(element(by.id('myButton')).isEnabled()).toBe(true);
// in a common function library
function myFun(Xel,Yel,Zel) {
return protractor.promise.all([Xel,Yel,Zel]).then(function(results){
var xText = results[0];
var yText = results[1];
var zText = results[2];
});
}
So in your code it would be something like
getImportDateSubmittedColumnValues() {
let promisesArray = [];
let stringDatesArray: Array<string> = [];
// currently this css selector gets rows in both import and export tables
// TODO: get better identifiers on the import and export tables and columns
element.all(by.css('md-card-content tbody tr.ng-tns-c3-0')).each(function(row, index){
// check outerHTML for presence of "unclickable", the rows in the export table
row.getAttribute('outerHTML').then(function(outerHTML:string) {
// specifically look for rows without unclickable
if(outerHTML.indexOf("unclickable") < 0){
// grab the columns and get the third column, where the date submitted field is
// TODO: get better identifiers on the import and export columns
promisesArray.push(row.all(by.css("td.ng-tns-c3-0")).get(2).getText());
}
});
});
return protractor.promise.all(promisesArray).then(function(results){
// In here you'll have access to the results
});
}
Theres quite a few different ways you could do it. You could process the data in that method at the end or I think you could return the array within that "then", and access it like so:
page.getImportDateSubmittedColumnValues().then((res) =>{
//And then here you will have access to the array
})
I don't do the Typescript but if you're just looking to get an array of locator texts back from your method, something resembling this should work...
getImportDateSubmittedColumnValues() {
let stringDatesArray: Array<string> = [];
$$('.import-component .created-date').each((cell, index) => {
cell.getText().then(text => {
stringDatesArray.push(text);
});
}).then(() => {
return stringDatesArray;
});
}
The answer ended up related to the answer posted on How do I return the response from an asynchronous call?
The final PageObject class function:
import {browser, element, by, protractor} from 'protractor';
export class SpecMapperPage {
getImportDateSubmittedColumnValues() {
let stringDatesArray: Array<string> = [];
let promisesArray = [];
// return a promise promising that stringDatesArray will have an array of dates
return new Promise((resolve, reject) => {
// This CSS selector grabs the import table and any cells with the label .created-date
element.all(by.css('.import-component .created-date')).map((cell) => {
// Gather all the getText's we want the text from
promisesArray.push(cell.getText());
}).then(() => {
protractor.promise.all(promisesArray).then((results) => {
// Resolve the getText's values and shove into array we want to return
for(let result of results) {
stringDatesArray.push(result);
}
}).then(() => {
// Set the filled array as the resolution to the returned promise
resolve(stringDatesArray);
});
});
});
}
}
The final test class:
import { SpecMapperPage } from "./specMapper.po";
import {browser, ExpectedConditions} from "protractor";
describe('spec mapper app', () => {
let page: SpecMapperPage;
let PROJECT_ID: string = '57';
let PROJECT_NAME: string = 'DO NOT DELETE - AUTOMATED TESTING PROJECT';
beforeEach(() => {
page = new SpecMapperPage();
});
describe('import/export page', () => {
it('TC2963: ImportComponentGrid_ShouldDefaultSortBySubmittedDateInDescendingOrder_WhenPageIsLoaded', () => {
browser.waitForAngularEnabled(false);
// Step 1: Launch Map Data from Dashboard
page.navigateTo(PROJECT_ID);
browser.driver.sleep(5000).then(() => {
// Verify: Mapping Screen displays
// Verify on the specmapper page by checking the breadcrumbs
expect(page.getProjectNameBreadCrumbText()).toContain(PROJECT_NAME);
expect(page.getProjectMapperBreadCrumbText()).toEqual("MAPPER");
// Verify: Verify Latest Submitted Date is displayed at the top
// Verify: Verify the Submitted Date column is in descending order
page.getImportDateSubmittedColumnValues().then((results) => {
console.log(results);
});
});
});
});
});
The biggest thing was waiting for the different calls to get done running and then waiting for the stringDataArray to be filled. That required the promise(resolve,reject) structure I found in the SO post noted above. I ended up using the lambda (()=>{}) function calls instead of declared (function(){}) for a cleaner look, the method works the same either way. None of the other proposed solutions successfully propagated the array of strings back to my test. I'm working in Typescript, with Protractor.

How to iterate through a loop of HTTP requests and store the data of each iteration appropriately to an array

I am using Spotify's API's search functionality to iterate through an array of SongSearchParams defined as:
export class SongSearchParams {
public title: string;
public artist: string;
constructor(title: string, artist: string){
this.title = title;
this.artist = artist;
}
}
My HTTP request looks like:
searchTrack(searchParams: SongSearchParams, type='track'){
var headers = new Headers({'Authorization': 'Bearer ' + this.hash_params.access_token});
this.user_url = "https://api.spotify.com/v1/search?query="+searchParams.artist+' '+
searchParams.title+"&offset=0&limit=1&type="+type+"&market=US";
return this.http.get(this.user_url, {headers : headers})
.map(res => res.json());
}
And in one of my component's typescript file I have access to the array of SongSearchParams that I want to individually pass into this searchTrack function when a certain button is clicked and save the album-image, trackname, and artist of a song.
onClick(){
for(let searchQuery of this.songService.songSearches){
this.spotifyserv.searchTrack(searchQuery)
.subscribe(res => {
this.searchedSong.artist = res.tracks.items[0].artists[0].name;
this.searchedSong.title = res.tracks.items[0].name;
this.searchedSong.imagePath = res.tracks.items[0].album.images[0].url;
console.log(this.searchedSong);
this.songService.addSong(this.searchedSong);
})
}
}
When I run this code, the console logs the correct song for each iteration but for some reason only the last song in my songSearches array gets physically added for the length of songSearches times.
Googling for answers to this issue, it seems that I need to use Promises called right after the other using the then() functionality so I tried implementing (for the searchTrack function):
This makes me think that it is an issue with my addSong function
addSong(song: Song){
this.songs.push(song);
this.songsChanged.next(this.songs.slice());
}
Though I don't really know what could be wrong with it so my second hunch is that I should be using promises called right after the other (which I have tried) but have failed to implement.
I think take a look at forkjoin
so something along the lines of:
let obsArray = [];
for(let searchQuery of this.songService.songSearches) {
obsArray.push(this.spotifyserv.searchTrack(searchQuery));
}
Observable.forkJoin(obsArray)
.subscribe(results => {
// results[0]
// results[1]
// ...
// results(n)
});
I discovered the solution to my problem though I do not know why it fixes it (I only have an assumption).
In the class where I created the onClick() method, I had a private member of type Song called songSearched which I was trying to overwrite for each song being added to the list. I instead rewrote the function to look like:
onClick(){
for(let searchQuery of this.songService.songSearches){
this.spotifyserv.searchTrack(searchQuery,
response => {
let res = response.json();
console.log(res.tracks.items[0].album.images[0].url);
console.log(res.tracks.items[0].name);
console.log(res.tracks.items[0].artists[0].name);
let searched_song = {artist : null, title : null, imagePath : null}
searched_song.artist = res.tracks.items[0].artists[0].name;
searched_song.title = res.tracks.items[0].name;
searched_song.imagePath = res.tracks.items[0].album.images[0].url;
console.log(searched_song);
//song_queue.push(searched_song);
this.songService.addSong(searched_song);
}
)
}
}
Here I instead create a new searched_song to be appended to the list inside the function itself that way for each request, there exists a searched_Song. I think before what was happening was that I was overwriting the same searchedSong and by the time it got to the addSong(searchedSong) the final song had already been searched with Spotify's API and had already overwritten the previous searches so that one song kept getting added.
Though this still wouldn't explain why the console log works in the previous line in my original attempt.

Unable to access the dictionary property from within a callback of the constructor

I have created a dictionary and inserted values to it as shown below using https://github.com/basarat/typescript-collections#a-sample-on-dictionary.
private subscribedTopicsDict = new collections.Dictionary<string, MyTopic>();
this.subscribedTopicsDict.setValue(topic, myTopic);
When I print the value for the key "myTopic" immediately after setting, its gets printed. But when I call it inside a callback within the constructor, nothing is happening. No errors but nothing after the getValue line is getting executed. Below is how I am calling it.
constructor(url: string, mqttOptions: MqttOptions, messageReceivedCallBack: IMessageReceivedCallBack) {
if (!_.isString(url) || _.isEmpty(mqttOptions || _.isEmpty(messageReceivedCallBack))) {
throw new MyError('invalid url value');
} else {
this.client = mqtt.connect(url, mqttOptions);
this.client.on('message', (topic: string, message: string) => {
let myMessage: myMessage;
let receivedTopic: myTopic;
console.log('1.onMessageReceived called with topic : ' + topic);
receivedTopic = this.subscribedTopicsDict.getValue(topic);
console.log('2.received topic and dataTypeID' + receivedTopic.dataTypeID);
myMessage = {
myTopic: receivedTopic,
payload: message
};
console.log('about to fire the callback');
messageReceivedCallBack.onMessageReceived(myMessage);
});
}
}
The second console log is not getting printed. I tried with default dictionary functionalities of type script and got the same behavior as well.

Categories

Resources