I am struggling a lot with some basic change detection inside my application. I have been at this for a couple of days and losing the will! I wonder if anyone here can point me in the right direction.
I have a group
with 2 user
lists. One is for members
and the other is for moderators
.
This method connects to the database to get a list of objects each with a $key
value. Now I want to use those keys to go and get the actual UserData
for each user. I tried everything and combineLatest
is the only thing I could seem to get working.
So both call the function and I am using an async
pipe on my template.
members$ = myService.getMemberListByType("blah", "members");
moderators$ = myService.getMemberListByType("blah", "moderators");
My function looks like this.
private getMemberListByType(groupKey: string, memberType: string) {
return this.af.database.list(`groups/${groupKey}/${memberType}`)
.switchMap(userListKeyMap => {
console.log("UserListKeyMap", memberType, userListKeyMap);
let usersObservables: Observable<UserData>[] = [];
userListKeyMap.forEach((object) => {
usersObservables.push(this.af.database.object(`users/${object.$key}`)
.map(UserData.fromJSON));
});
return Observable.combineLatest(usersObservables);
});
}
However, Angular does not detect changes to this list as following. Say that a different method elsewhere removes a member
and adds him to moderator
. In this case, the members
list is emitting a empty list of users []
.
My *ngIf and *ngFor do not detect changes with this new empty object. The result is that the user (on the template) is now part of 2 lists but the data under this is perfectly accurate as highlighted by the log.
I feel that when I combineLatest on an empty array, this is the source of my problem. Nothing can emit.... so how can Angular change? I don't know how to solve it though. Is there an alternative to combineLatest in this "empty" use case? Since I am using an async
pipe, I need something that makes sense.
Would anyone be able to shine a light on my issue?
Thank you!
Update
I am convinced the problem is with angular2 not detecting an empty observable list. Specifically when an Observable Array has a value, but later that value becomes empty. Angular2 will not see the empty array. I have no way to solve this problem at this time. Either my approach is wrong, or something specific needs to happen to register an empty observable list?
There is simple workaround:
private getMemberListByType(groupKey: string, memberType: string) {
return this.af.database.list(`groups/${groupKey}/${memberType}`)
.switchMap(userListKeyMap => {
console.log("UserListKeyMap", memberType, userListKeyMap);
let usersObservables: Observable<UserData>[] = [];
userListKeyMap.forEach((object) => {
usersObservables.push(this.af.database.object(`users/${object.$key}`)
.map(UserData.fromJSON));
});
return usersObservables.length > 0 ? Observable.combineLatest(usersObservables) : Observable.of([]);
});
}
Your approach seems correct to me. Using combineLatest()
with an empty array shouldn't end with an error (similarly to eg. forkJoin
, using empty array is a valid use-case). Then even if it completes because of an empty array you're using switchMap
so this shouldn't be the problem I think.
I don't have any experience with AngularFire2 so I don't know whether this.af.database.object
just emits all items once and then completes or it emits a value on every change (which is probably what you want if I guess).
Anyway a very common issue with using combineLatest()
or forkJoin()
is that their source Observables always have to emit at least one item. In other words if any of the this.af.database.object
is empty then combineLatest()
won't emit anything.
You can make sure there's always at least one value with startWith()
. For example like the following:
userListKeyMap.forEach((object) => {
usersObservables.push(this.af.database.object(`users/${object.$key}`)
.startWith('{}')
.map(UserData.fromJSON));
});
If this doesn't help try using just Observable.timer()
instead of this.af.database.object
or this.af.database.list
to make sure Angular2 subscribes to it and keeps the subscription even when new items are emitted.
You should try something like this:
private getMemberListByType(groupKey: string, memberType: string) {
return this.af.database.list(`groups/${groupKey}/${memberType}`)
// Force the source obs to complete to let .toArray() do its job.
.take(1)
// At this point, the obs emits a SINGLE array of ALL users.
.do(userList => console.log(userList))
// This "flattens" the array of users and emits each user individually.
.mergeMap(val => val)
// At this point, the obs emits ONE user at a time.
.do(user => console.log(user))
// Load the current user
.mergeMap(user => this.af.database.object(`users/${user.$key}`))
// At this point, the obs emits ONE loaded user at a time.
.do(userData => console.log(userData))
// Transform the raw user data into a User object.
.map(userData => User.fromJSON(userData))
// Store all user objects into a SINGLE, final array.
.toArray();
}
Note. The lines starting with do()
are only here to explain what's going on. No need to keep them in the final code.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With