I am creating an app in angular, and one task is, I need read data from Firestore and save into an array, my code snippet is as below:
public getListingData(): Observable < RequestListingModel > {
console.log("getting listing...");
this.firestore.collection('Requests').snapshotChanges().subscribe(
requests => {
this._listRequestItems = requests.map(a => {
const data = a.payload.doc.data() as RequestItemModel;
data.requestId = a.payload.doc.id;
console.log("doc found - " + data.requestId);
return data;
})
});
const requestListingModel = {
items: this._listRequestItems
}
as RequestListingModel;
return of(requestListingModel);
}
And my code of RequestListingModel is as below:
import * as dayjs from 'dayjs';
export class RequestItemModel {
name: string;
address: string;
category: string;
requestId: string;
postBy: string;
description: string;
// Default mock value
// expirationDate = '12/01/2018';
expirationDate: string = dayjs().add(5, 'day').format('MM/DD/YYYY HH:mm:ss') as string;
}
export class RequestListingModel {
items: Array<RequestItemModel> = [];
constructor(readonly isShell: boolean) { }
}
And it's not working as I always get empty return when call the function getListingData(), but the console can print out the each requestId successfully, I think something wrong with the way I store into array, please help, thanks!
Your problem is happening because things are happing asynchronously:
So return of(requestListingModel); gets called before this.firestore.collection('Requests').snapshotChanges().subscribe(...) populates the array, thus returning an observable of an empty array.
Since it looks like you want to return an observable, don't subscribe() inside your method. Just do a map() on the snapshotChanges observable to transform the output, and let the consuming code subscribe to your getListingData() method.
public getListingData(): Observable < RequestListingModel > {
console.log("getting listing when someone subscribes...");
return this.firestore.collection('Requests').snapshotChanges().pipe(
map(requests => {
let requestListingModel = {
items: []
} as RequestListingModel;
requests.map(a => {
let data = a.payload.doc.data() as RequestItemModel;
data.requestId = a.payload.doc.id;
console.log("doc found - " + data.requestId);
requestListingModel.items.push(data);
});
return requestListingModel;
})
);
}
Note: still... can't test this at the moment, so it may not be 100% correct :-\
But hopefully points you in the right direction. :-)
Related
I would like to display users' data, but I have a problem with displaying the correct profile pictures. If a user does not have a profile picture, I get "undefined" in the console. If one user has a profile picture, then the same picture will be displayed for all the users. I need help finding the error in my code.
export interface UserData {
id: number,
name: string
}
export interface UserWithImage extends UserData{
image?: string
}
export interface UserProfileImage {
id: number,
url: string
}
After I get the necessary data from the services, I try to push the profile image into the userData.
user-data.ts
userData: UserWithImage[];
userProfiles: UserProfileImage[];
userProfileImage: UserProfileImage[];
getUserData() {
this.userData = this.userService.getData();
this.userProfiles = await this.imagesService.getProfilePicture(this.userData?.map(u => u.id));
this.userProfileImage = this.userProfiles.filter(u => u.url);
this.userData?.forEach((data, i) => {
data.image = this.userProfileImage[i].url;
});
}
images.service.ts
public async getProfilePicture(ids: number[]): Promise<UserProfileImage[]> {
const toLoad = ids.filter(id => !this.userProfileImages.find(up => up.id === id)).map(u => u);
if (toLoad || toLoad.length) {
const loaded = (await firstValueFrom(this.httpClient.post<UserProfile[]>
(this.imgService.getServiceUrl(customersScope, `${basePath}settings/users/profil`), JSON.stringify(toLoad), {headers}))).map(sp => {
return {
id: sp.userId,
url: sp.profilepicId ? this.imgService.getServiceUrl(customersScope,
`${basePath}web/download/profilepic/${sp.profilepicId}/users/${sp.userId}`, true) : ''
} as UserProfileImage
});
this.userProfileImages = [...loaded, ...this.userProfileImages];
}
return this.userProfileImages;
}
user-data.html
<div ngFor="data of userData">
<etc-profil [name]="data.name" [image]="data.image"></etc-profil>
</div>
this.userData = this.userService.getData();
Is this an async function (i.e. are you missing an await)?
this.userProfiles = await this.imagesService.getProfilePicture(this.userData?.map(u => u.id));
This line would fail is this.userData is a promise. this.userProfiles would be undefined due to the use of optional chaining (?.)
this.userProfileImage = this.userProfiles.filter(u => u.url);
This line appears to do nothing, the filter predicate is saying that anything with a url property that is not null or undefined is included, but the interface says that url is non-optional and doesn't support null or undefined.
this.userData?.forEach((data, i) => {
data.image = this.userProfileImage[i].url;
});
Again, if this.userData is a promise, this will do nothing due to the optional chaining.
If it does run, its assumed that there is a one-to-one relationship between users and profile images (index count and order must be the same).
I didn't consider the implementation of getProfilePicture because I think these issues need resolving first.
I have a service to get data from the backend database and display the array of objects that works fine, the only problem is I can not use it outside of the subscribe.
As i want to extract specific values and store them in an array to use with maps and other functions. Is there a way of doing this? code is below.
Edit: i can insert the values i need to an array and see the values only problem is they are strings and i need them as number with their name.
usersAddress: UserAddressDto[] = [];
ngOnInit() {
this.addressService
.getAllUserAddress()
.subscribe((address: UserAddressDto[]) => {
this.usersAddress = address;
});
//this prints to console an empty array
//unless within the subscribe
console.log('userAddress', this.usersAddress);
}
coord: any[] = [];
getAllUserAddress() {
return this.http
.get<any>(`${environment.baseApiUrl}/userAddress/all`)
.subscribe((response) => {
console.log(response);
this.allAddresses = response;
console.log('all addresses:',this.allAddresses);
this.setLatLong();
});
}
setLatLong() {
this.coord = this.allAddresses.map(
(obj) => ' lat: ' + obj.latitude + ' long: '+
obj.longitude
);
console.log('coord array:', this.coord);
this.setMarkers();
}
You can create a Service:
#Injectable()
export class UsersService {
usersAddress = [];
constructor(
) { }
setUserAddress(address) {
this.usersAddress = address;
}
getUserAddress() {
return this.usersAddress;
}
}
you can access it in other components like this:
userService.getUserAddress()
you need to instantiate the service on constructor of yours components:
constructor(
private userService: UserService,
) { }
where UserService is the name of that service.
I think that your code working properly. The problem is the console log that is not waiting the subscription. If you put the console inside subscription function you will see the right data.
usersAddress: UserAddressDto[] = [];
ngOnInit() {
this.addressService
.getAllUserAddress()
.subscribe((address: UserAddressDto[]) => {
this.usersAddress = address;
console.log('userAddress', this.usersAddress);
});
}
I'm looking for a way to wait for the user to stop interaction and then make an HTTP request, for this I'm looking into the debounceTime() operator from RxJs, but the target I'm waiting for is an array I defined.
This is the scenario:
export class KeywordSelectionComponent implements OnInit {
constructor(private proposalService: ProposalService) { }
#ViewChild(MatTable, {static: true}) kwTable: MatTable<any>;
#ViewChild(MatPaginator, {static: false}) paginator: MatPaginator;
#Input() proposalId: string;
keywordInput = new FormControl(null, Validators.required);
dataSource: MatTableDataSource<Keyword>;
displayedColumns = ['action', 'keyword', 'searches', 'competition', 'cpc'];
suggestedKeywords: Keyword[] = [];
selectedKeywords: string[] = [];
fetchSuggestions(seeds?: string[]) {
const ideas = {
seeds: [],
limit: 25
};
this.proposalService.getKeywordIdeas(this.proposalId, ideas).pipe(retry(3)).subscribe(res => {
this.suggestedKeywords = res;
});
}
}
I'm not including the whole component here, but the idea is the following:
I have a list of suggestedKeywords which I render on the page, each of these should call an addKeyword() method to add that keyword to the dataSource, and after that, I call the fetchSuggestions() method to get new keywords to populate the suggestedKeywords list.
The problem comes when I try to select multiple keywords in quick succession, since that would trigger a request for each of those clicks to update the suggestedKeywords list, so I wanted to use the debounceTime() to prevent the request to trigger until the user stops clicking items for a bit; however this requires an Observable to be the element changing as far as I know, but in my case, it is just a simple array.
Is there someway to keep track of the value of the array so it waits for a while after it changes before making the HTTP request, like an Observable?
EDIT: Used the from() operator as suggested in the comments, in order to actually listen to changes do I need to define other methods? I'm thinking something similar to valueChanges() in FormControls.
Going through more documentation I'm leaning towards Subject, BehaviorSubject, etc; but I'm not sure if this would be a correct approach, could anyone provide an example on how to do this?
Wrap your array in Observable.of() RxJS operator and it will behave like observable
What I ended up doing was using a Subject to keep track of the changes, calling it's next() function evrytime a modified the suggestedKeywords array and subscribing to it as an observable.
My component ended up looking like this:
export class KeywordSelectionComponent implements OnInit {
constructor(private proposalService: ProposalService) { }
keywordInput = new FormControl(null, Validators.required);
suggestedKeywords: Keyword[] = [];
selectedKeywords: string[] = [];
isLoadingResults = false;
tag$ = new Subject<string[]>();
ngOnInit() {
this.tag$.asObservable().pipe(
startWith([]),
debounceTime(500),
switchMap(seeds => this.getSuggestionsObservable(seeds))
).subscribe(keywords => {
this.suggestedKeywords = keywords;
});
}
addSuggestedKeyword(keyword: Keyword) {
const suggestedKeyword = keyword;
const existing = this.dataSource.data;
if (!existing.includes(suggestedKeyword)) {
existing.push(suggestedKeyword);
this.dataSource.data = existing;
}
this.tag$.next(this.getCurrentTableKeywords());
}
fetchKeywordSearch(keyword: string) {
this.isLoadingResults = true;
this.keywordInput.disable();
const search = {
type: 'adwords',
keyword
};
const currentData = this.dataSource.data;
this.proposalService.getKeywordSearch(this.proposalId, search).pipe(retry(3)).subscribe(res => {
currentData.push(res);
this.dataSource.data = currentData;
}, error => {},
() => {
this.isLoadingResults = false;
this.keywordInput.enable();
this.tag$.next(this.getCurrentTableKeywords());
});
}
getCurrentTableKeywords(): string[] {}
getSuggestionsObservable(seeds: string[] = []): Observable<Keyword[]> {
const ideas = {
type: 'adwords',
seeds,
limit: 25
};
return this.proposalService.getKeywordIdeas(this.proposalId, ideas).pipe(retry(3));
}
}
I'm working in Angular with firebase.
I want to create a function which will receive two arguments string and object and returns an Observable of filtered data according to object key-value pairs from specific collection dynamically.
For example when I will call it this way I expect this result:
dynamicFilter('users',{name: 'Jack', age: 30})
It should evaluate this call:
firebase.collection('users',ref => ref.where('name','==','Jack').where('age','==',30))
So I can subscribe to it like:
dynamicFilter('users',{name: 'Jack', age:30}).subscribe(res => {
console.log(res)
})
I want the function to be universal as I don't know in advance how many conditions will be passed through the object.
I tried this:
constructor(private db: AngularFirestore){}
dynamicFilter(collectionName: string, options: object){
let targetCollection = this.db.firestore.collection(collectionName)
let query;
let keys = Object.keys(options)
for(let i = 0; i < keys.length; i++ ){
if(i === 0){
query = targetCollection.where(keys[i],'==',options[keys[i]])
}else{
query = query.where(keys[i],'==',options[keys[i]])
}
}
return query
}
So what can do with this returned value? It is an instance of Query class and I guess I can call method get, which returns a promise like query.get().then(res => console.log(res)).
But the res is another instance of QuerySnapshot class. How can I simply get data?
To get the data from a QuerySnapshot:
querySnapshot.forEach(function(doc) {
console.log(doc.data());
});
So:
dynamicFilter('users',{name: 'Jack', age:30}).subscribe(querySnapshot => {
querySnapshot.forEach(function(doc) {
console.log(doc.data());
});
})
For more on this, see:
the Firebase documentation on getting multiple documents from a collection.
the reference documentation for `QuerySnapshot.
I've been searching all the resources you can imagine , and finally got rid of it and started to experiment on my own , and it's done , it works just perfect you wouldn't have even subscribe to it, it triggers via Callback function.
Usecase:
1.You write the code below in service to keep things clear and reusable.
For example let's call it DataService:
import { Injectable } from '#angular/core';
import { AngularFirestore } from '#angular/fire/firestore';
#Injectable({
providedIn: 'root'
})
export class DataService {
constructor(private fire: AngularFirestore) { }
dynamicQueryFilter(collectionName: string, options: object, callbackFn: Function): void{
const bindedCallback: Function = callbackFn.bind(this)
const targetCollection = this.fire.collection(collectionName).ref
const optionPairs = Object.entries(options)
let queryMap = targetCollection.where(optionPairs[0][0],'==',optionPairs[0][1])
optionPairs.slice(1).forEach(item => {
queryMap = queryMap.where(item[0],'==',item[1])
})
queryMap.get()
.then(resolve => {
const documents = []
resolve.forEach(doc => documents.push(doc.data()))
bindedCallback(documents)
})
}
2.Then inject it in your component
export class AppComponent implements OnInit {
constructor(private db: DataService){}
users: User[];
ngOnInit(){
this.db.dynamicQueryFilter
(
'users',
{name: 'Jack', age: 30, profession: 'Programmer', isMaried: true},
(res) => this.product = res
)
}
}
3.Render all married programmers whose name is Jack and who 30 years old in your template
<div *ngIf="users">
<div *ngFor="let user of users">
<h2>{{user.name}} {{user.surname}}</h2>
<hr>
<h4>Additional Info</h4>
<div><b>Age: </b>{{user.age}}</div>
<div><b>Profession: </b>{{user.profession}}</div>
<div><b>Adress: </b>{{user.adress}}</div>
<div><b>Phone: </b>{{user.phone}}</div>
//and so on....
</div>
</div>
With this tool in your hand you can do any filtration you may imagine DYNAMICALLY.
You can bind arguments of dynamicQueryFilter to inputs or any forms in your template, and let the user perform interactive realtime multi filtration.
Special thanks to Frank van Puffelen for sharing his knowledge.
I'm trying to put a JSON object into an array after an API call.
First I made my API call and then I try to add every user into in a formatted JSON object.
connectionProvider.ts
import { UserModelProvider } from './../user-model/user-model';
import { MSAdal, AuthenticationContext, AuthenticationResult } from '#ionic-native/ms-adal';
export class MsConnectionProvider {
userInfo : UserModelProvider;
users: UserModelProvider[];
constructor(...) {}
getUserInfo(){
let header = new Headers({
Authorization: this.accessToken;
});
let options = new RequestOptions({headers: header});
this.usersSubscription = this.http.get("https://graph.microsoft.com/v1.0/groups/**ID**/members", options).map(res => res.json()['value']);
this.usersSubscription.subscribe(res => {
for (let user of res){
this.addUserInfo(user.displayName, user.jobTitle, "something", user.mail);
}
});
}
addUserInfo(name, job, departement, mail){
this.userInfo = new UserModelProvider;
this.userInfo.name = name;
this.userInfo.job = job;
this.userInfo.departement = departement;
this.userInfo.mail = mail;
this.users.push(this.userInfo);
}
}
userModelProvider.ts
export class UserModelProvider {
name: string;
job: string;
departement: string;
mail: string;
photo: any;
}
The problem is when I try to push "this.userInfo = new UserModelProvider" into this.users array the function block and nothing happens.
I certainly don't understand the class, can you help me?
Thank you.
You can't push to an array that has not been initialised.
you need to change:
users: UserModelProvider[]
to:
users: UserModelProvider[] = [];
Also (may or may not help):
Nothing is probably happening because push mutates the array and as such angular change detection may not kick in.
Instead of using push, create a new array with:
this.users = [...this.users, this.userInfo]
or in ES5:
this.users = this.users.concat([this.userInfo])
Create an instance of the class before assigning the values,
this.userInfo = new UserModelProvider();