Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

rxjs combineLatest and Anguar2 change detection on Observable Arrays

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?

screen of problem

  1. Blue = The loaded state of the component is good, the user was a member.
  2. Green = The user was moved from member to manager/moderator.
  3. Red = This doesn't exist, so why is it still showing?
like image 792
Clark Avatar asked Feb 17 '17 09:02

Clark


3 Answers

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([]);
      });
 }
like image 155
kemsky Avatar answered Sep 18 '22 12:09

kemsky


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.

like image 25
martin Avatar answered Sep 18 '22 12:09

martin


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.

like image 38
AngularChef Avatar answered Sep 19 '22 12:09

AngularChef