6

Update:

The issues I was encountering with the empty value fields had to do with non-existent keys in my database, so most of the discourse here won't apply to your question. If you're looking for a way to 'join' queries in AngularFire2, the accepted answer below does a fine job of this. I'm currently using combineLatest instead of forkJoin. In order to do this you have to import 'rxjs/add/observable/combineLatest';.

I have the following denormalized Firebase structure:

members
  -memberid1
    -threads
       -threadid1: true,
       -threadid3: true
    -username: "Adam"
    ...

threads
  -threadid1
      -author: memberid3
      -quality: 67
      -participants
         -memberid2: true,
         -memberid3: true
     ...

I want to render username in my threads view, which is sorted by quality.

My service:

getUsername(memberKey: string) {
    return this.af.database.object('/members/' + memberKey + '/username')
}

getFeaturedThreads(): FirebaseListObservable<any[]> {
    return this.af.database.list('/threads', {
        query: {
            orderByChild: 'quality',
            endAt: 10
        }
    });
}

My component:

ngOnInit() {
    this.threads = this.featuredThreadsService.getFeaturedThreads()
    this.threads.subscribe( 
        allThreads => 
        allThreads.forEach(thisThread => {
            thisThread.username = this.featuredThreadsService.getUsername(thisThread.author)
            console.log(thisThread.username)
        })
    )
} 

For some reason this logs what looks like unfulfilled observables to the console.

enter image description here

I'd like to get these values into a property of threads so I can render it in my view like this:

<div *ngFor="let thread of threads | async" class="thread-tile">
    ...
    {{threads.username}}
    ...
</div>

Updated: console.log for allThreads and thisThread

enter image description here

enter image description here

Updated: subscribed to getUsername()

this.featuredThreadsService.getUsername(thisThread.author)
        .subscribe( username => console.log(username))

The result of this is objects with no values:

enter image description here

J. Adam Connor
  • 1,694
  • 3
  • 18
  • 35
  • Your `getUsername` method does not return anything. Is that intended? – eko Jan 20 '17 at 17:36
  • My apologies. That's a remnant from another implementation. I've changed it. The current method behaves the way I describe in my question. – J. Adam Connor Jan 20 '17 at 17:39
  • Can you put console.log for `allThreads ` and `thisThread` to make sure they are defined please? – eko Jan 20 '17 at 17:40
  • console.log for both up. – J. Adam Connor Jan 20 '17 at 17:44
  • 1
    But if `thisThread` logs that object, how can `thisThread.username` can be undefined? – eko Jan 20 '17 at 17:46
  • I've just updated the console.log image for that. It wasn't undefined, sorry. It looks like an empty object. Take a look at the console.log image for `thisThread.username`. – J. Adam Connor Jan 20 '17 at 17:48
  • 1
    Hmm can you double check this line too please? `this.af.database.object('/members/' + memberKey + 'username')` shouldn't it be `this.af.database.object('/members/' + memberKey + '/username')`? – eko Jan 20 '17 at 17:50
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/133666/discussion-between-j-adam-connor-and-echonax). – J. Adam Connor Jan 20 '17 at 17:51
  • 1
    getUsername(memberKey: string) isn't that an observable object? Dont you need to subscribe to that? Like what you did for this.thread. – penleychan Jan 20 '17 at 17:52
  • @12seconds I've subscribed to the observable but get objects with no values back. – J. Adam Connor Jan 20 '17 at 18:05
  • On the allThread screen capture, is username FirebaseObjectObservable? If it is, can you subscribe to that instead of using your service? – penleychan Jan 20 '17 at 19:44
  • Since the service assigns the `username` key to `allThreads`, I don't see how that's possible. Yes, after the service assigns the key with`allThreads.forEach(thisThread => { thisThread.username = this.featuredThreadsService.getUsername(thisThread.author)` if I log `allThreads` to the console its `username` value will be a FirebaseObjectObservable. Am I not subscribing to that when I append `.subscribe( username => console.log(username))`? – J. Adam Connor Jan 20 '17 at 20:12
  • 1
    Just a reminder that if you use `combineLatest`, remove the `first` operators so that the inner observables don't complete. – cartant Jan 21 '17 at 02:21

2 Answers2

4

You can compose an observable based on getFeaturedThreads that queries members and replaces the values in each thread's participants property with user names:

import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/forkJoin';
import 'rxjs/add/operator/do';
import 'rxjs/add/operator/first';
import 'rxjs/add/operator/switchMap';

let featuredThreadsWithUserNames = this.getFeaturedThreads()

  // Each time the getFeaturedThreads emits, switch to unsubscribe/ignore
  // any pending member queries:

  .switchMap(threads => {

    // Map the threads to the array of observables that are to be
    // joined. When the observables emit a value, update the thread.

    let memberObservables = [];
    threads.forEach(thread => {

      // Add the author:

      memberObservables.push(this.af.database
        .object(`members/${thread.author}`)
        .first()
        .do(value => { thread.author = value.username; })
      );

      // Add the participants:

      Object.keys(thread.participants).forEach(key => {
        memberObservables.push(this.af.database
          .object(`members/${key}`)
          .first()
          .do(value => { thread.participants[key] = value.username; })
        );
      });
    });

    // Join the member observables and use the result selector to
    // return the threads - which will have been updated.

    return Observable.forkJoin(...memberObservables, () => threads);
  });

This will give you an observable that emits each time getFeaturedThreads emits. However, if the user names change, it won't re-emit. If that's important, replace forkJoin with combineLatest and remove the first operator from the composed member observables.

cartant
  • 57,105
  • 17
  • 163
  • 197
  • Thanks, I'll work on this implementation. I don't know if it's important to point out yet that `participants` are not the same as `author`. In this case I have the member's key available to me in the `author` property of the thread. Also, I keep seeing `${foo}` in the path, but there is nothing about it in docs. Is this just shorthand for `('/path' + foo)`? – J. Adam Connor Jan 20 '17 at 20:08
  • 2
    They are ES6 template literals: http://stackoverflow.com/questions/27565056/es6-template-literals-vs-concatenated-strings – cartant Jan 20 '17 at 20:09
  • I had to fix some bracket issues in this. I think I got rid of the right ones... I removed the `participants` block just to focus on the `author` username at the moment. For some reason, if I log `memberObservables` to the console after the `forEach` block, I end up with an array of empty observables. I realize this is a potentially stupid statement, because that may be the point, but I suppose I expect the `forEach` block to populate that array. I'll give you an opportunity to answer this before my final question regarding this solution. – J. Adam Connor Jan 20 '17 at 23:06
  • 1
    I've fixed the bracket/syntax errors. Are you saying that the array of observables is empty? That should only be possible if there are no threads. – cartant Jan 20 '17 at 23:13
  • https://gyazo.com/240ca948eb3a14f9396c8548498e7c40 There are 44 observables in the array. They all look like that. – J. Adam Connor Jan 20 '17 at 23:33
  • 1
    That looks fine. What happens after that? Errors? Do you have the imports in your code? And what version of RxJS are you using? – cartant Jan 20 '17 at 23:40
  • That brings me to my next question. How would I put this in my view? RxJS is 5.0.1. Right now I'm doing all of this in my component and assigning it to the existing Observable `threads`: `this.threads = featuredThreadsWithUserNames`. I'm rendering everything in the view with `*ngFor="let thread of threads | async`, EXCEPT for username. Not sure how to render that. Everything else is `{{thread.value}}`. – J. Adam Connor Jan 20 '17 at 23:46
  • 1
    For logging purposes, add a `do` after the `forkJoin` like this `Observable.forkJoin(...memberObservables, () => threads).do(threads => console.log(threads));` then you should see what is happening. Each `thread` should have been updated, with the author and participants replaced with user names. So the template should use `{{thread.author}}`, etc. Participants will be an object, so you'll need to enumerate those as in that [earlier question](http://stackoverflow.com/q/41710022/6680611) of yours. – cartant Jan 20 '17 at 23:51
  • As I said, I removed the block for `participants` to focus on `authors` for now. `authors` is empty. https://gyazo.com/2b27459a9ecb81e81b804bdcabdcf1f1 – J. Adam Connor Jan 20 '17 at 23:56
  • 1
    Log the values in the member observable's `do`: `.do(value => { console.log(value); thread.author = value.username; })` and check that the keys exist, etc. And make sure the paths that I've used in the answer are correct regarding your database structure, etc. The mechanics of the answer should be sound, it'll just be some niggling detail that's hard to debug via comments, etc. I think you are almost there. – cartant Jan 21 '17 at 00:04
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/133684/discussion-between-j-adam-connor-and-cartant). – J. Adam Connor Jan 21 '17 at 00:07
1

To solve joins on users, I wrote a service that caches the users already fetched and maps them into the referencing data with minimal code. It uses a nested map structure to do the join:

constructor(public db: AngularFireDatabase, public users:UserProvider) {
    this.threads = db.list('threads').valueChanges().map(messages => {
      return threads.map((t:Message) => {
        t.user = users.load(t.userid);
        return m;
      });
    });
}

And the UserProvider service looks like so:

@Injectable()
export class UserProvider {
  db: AngularFireDatabase;
  users: Map<String, Observable<User>>;

  constructor(db: AngularFireDatabase) {
    this.db = db;
    this.users = new Map();
  }

  load(userid:string) : Observable<User> {
    if( !this.users.has(userid) ) {
      this.users.set(userid, this.db.object(`members/${userid}`).valueChanges());
    }
    return this.users.get(userid);
  }
}

There's a complete working example of the joins and all the boilerplate here

Kato
  • 40,352
  • 6
  • 119
  • 149