fabio.sang
fabio.sang

Reputation: 905

Wait for Observable to complete in order to submit a form

I have a 'new trip' form, where the user can write the names of the participants and then submit the form to create the trip. On submit, I query a Firebase database with the names, in order to get the IDs of the participants (/users). I then add the IDs to the participantsID field of the trip object and then I push the new trip to Firebase.

The problem is that the Firebase query is async and returns an Observable, therefore my function will proceed to push the object before the Observable has completed, so the participantsID field of the new object is empty.

Is there any method to wait for the observable to complete (in a kind of synchronous way) so that i can manipulate the data and then proceed? All my attempts to fix this have failed so far.

Here's my simple code.

getUserByAttribute(attribute, value) {
  return this.db.list('/users', {
    query: {
      orderByChild: attribute,
      equalTo: value,
      limitToFirst: 1
    }
  });
}

createTrip(trip) {
  for(let name in participantsName.split(',')) {
    getUserByAttribute('username', name)
      .subscribe( user => trip.participantsID.push(user[0].$key) );
  }

  this.db.list('/trips').push(trip);
}

Upvotes: 1

Views: 1239

Answers (3)

fabio.sang
fabio.sang

Reputation: 905

In the end I used part of @Pankaj Parkar's answer to solve the problem. I forkJoin all the Observables returned by mapping the splitted names and I subscribe to that Observable which result contains an array of arrays, where the inner arrays contain a user object.

getUserByAttribute(attribute, value) {
  return this.db.list('/users', {
    query: {
      orderByChild: attribute,
      equalTo: value,
      limitToFirst: 1
    }
  }).first();
}

createTrip(trip) {
  Observable.forkJoin(
    trip.participantsName.split(',')
      .map(name => getUserByAttribute('name', name))
    ).subscribe(
      participants => {
        trip.participants = participants.map( p => p[0].$key);
        this.tripService.createTrip(trip);
      }
    );
  }   
}

Upvotes: 1

Romain
Romain

Reputation: 61

You have a difficult problem. You have to get users info before push a new trip.

You can't just make new subscriptions every time because of the memory leak problem (or be careful with unsubscribes). If you are using Firebase, you can use AngularFire subject support.

You can update a subscription by using a subject in your query (with the equal to) and then push a user to retrieve with .next(user).

Then you still have to wait for all users. For that, you can have only one subscription and get all IDs synchronously or have multiple subscriptions to get multiple results faster (but it's difficult).

To solve this problem, I created:

  • a queue of callbacks (just arrays but use push() and unshift() methods)

  • a queue of values

  • one subject for one subscription.

If you want an ID, you have to:

  • push the value
  • push the callback that will retrieve the value returned.

You should use functions to push because you'll have to call .next() if there is no value in the stack (to start !). And in your subscription, in its callback, i.e when you receive the distant user object, you can call the first callback in the stack. Don't forget to pop your value and callback of the stacks and call the next() for the next value if there is one.

This way, you can push your trip in the last callback for the last user. And it's all callbacks, it means your app is not interrupted.

I still not decided if we should do that in a cloud function. Because the user have to stay connected, and this use his data / processor. But it's good to have all the code in the same place, and cloud functions are limited for a free version of Firebase. What would a Firebase developer advice?

I made a lot of searches to find a better solution, so please share it if you have one. It's a little complicated I think, but it's working very fine. I had the same problem when a user want to add a new flight, I need to get the airports information before (coords) and push multiple objects (details, maps, etc.)

Upvotes: 0

Pankaj Parkar
Pankaj Parkar

Reputation: 136144

You could treat all Observables into a single Observable by doing forkJoin

createTrip(trip) {
  var observableArray: any = participantsName.split(',')
    .switchMap((name)=> getUserByAttribute('username', name))
  Observable.forkJoin(observableArray).subscribe(
    trips => trips.forEach((trip) => {
       this.db.list('/trips').push(trip);
    })
  );
} 

Upvotes: 1

Related Questions