Philippe Corrèges
Philippe Corrèges

Reputation: 699

How to chain rxjs Observable operations with angular2?

I have a component getting data from a Service:

ngOnInit(): void
    {
        this.categoryService.getCategories().subscribe((data) => {
               this.categories=data;

            }
        )
    }

and here is the Service code:

getCategories(){
       return this.category.find({where: {clientId: this.userApi.getCurrentId()}}).map((data) => {
        return data;
        }, err => {
            console.log(err);
        });
    };

the returned data structure is:

{"categoryName":"Google","id":"58591d2b7672d99910497bec","clientId":"585808f6737f6aad1985eab2"},{"categoryName":"Yahoo","id":"58591d4c7672d99910497bef","clientId":"585808f6737f6aad1985eab2"},{"categoryName":"Msn","id":"585d25f6ae4b2ecb056bc514","clientId":"585808f6737f6aad1985eab2"}

I have another method on this.categories called countLinks(id); id is a category parameter like 585d25f6ae4b2ecb056bc514, taken from the data structure.

countLinks() returns an observable containing all of the links for that particular category.

I would like to add to my data structure another property containing the number of links, per category id, of course. Something like :

{"categoryName":"Google","id":"58591d2b7672d99910497bec",**"nblinks":"0"**,"clientId":"585808f6737f6aad1985eab2"},
{"categoryName":"Yahoo","id":"58591d4c7672d99910497bef",**"nblinks":"5"**,"clientId":"585808f6737f6aad1985eab2"},
{"categoryName":"Msn","id":"585d25f6ae4b2ecb056bc514",**"nblinks":"12"**,"clientId":"585808f6737f6aad1985eab2"}

To add information, categories is Array Type : Preview data structure with Augury

Any idea?

Upvotes: 0

Views: 1477

Answers (3)

VSO
VSO

Reputation: 12666

First of all, what you are doing is horrible practice. Here is how I break down your problem:

  1. Get a list of objects from backend.
  2. Get an additional property for each of those objects from the backend, with ONE call for each object.

If you are hitting the same backend for both calls, you need to write an endpoint to get all the data you need, period. If you are hitting two backends, you need to pass an array of clientIds and get number of links in one call. If you don't control the second call backend, then fine, let's do it like outline in steps 1 and 2.

Your structure will need to look like this:

  • Observable to get Data (you have this). A function to map this to ids only.
  • A function to create observables from each received Id
  • A .forkJoin Observable to make all your individual calls - this works like Q.all, if you are familiar with that.

I am too lazy to write the complete code for you, sorry, but basically, when you get your data, you need to create an Observable for EACH value returned, like this:

//This function will generate observables for you, when passed a param
function generateMockLinkCallObs(clientId){

  var mockLinksCall = new Rx.Observable(observer => {
    var mockCallTime = Math.floor(Math.random() * 5000) + 1000; 
    setTimeout(() => {
      var mockLinks = Math.floor(Math.random() * 12) + 1;  
      observer.next(mockLinks); 
    }, mockCallTime)
  })
  return mockLinksCall; 

}

//This just uses the function above to generate your observables for EACH object 
generateListOfObservableCalls(ids){
  var obsArray = [];
  var obs;
  ids.forEach(id => {
    obs = generateMockLinkCallObs(id);
    obsArray.push(obs);
  })

  return obsArray; 
}

//This just uses the above function. 
getNumberOfLinks(data){
  var ids = data.map(data => data.id)
  var observables = generateListOfObservableCalls(ids);
  return observables; 
}

So, to put it all together, you get your initial data:

httpWhatever.subscribe(
  next => {

  var observablesForIndividualCalls = getNumberOfLinks(next); 

  Observable.forkJoin(observablesForIndividualCalls)
   .subscribe((response) => {
      console.log(response[0], response[1]);
   });

  }
)

I over-simplified the last step, but forkJoin will only complete once you get results from all your individual calls.

And no, I don't know of a simpler way to do it, though it can definitely be cleaner.

Upvotes: 2

AngularChef
AngularChef

Reputation: 14087

What you describe is a typical use case for the mergeMap() operator.

Try this code:

getCategories() {
  return this.category.find({where: {clientId: ID}})
    .mergeMap(category =>
      countLinks(category.id).map(categoryLinks => {
        return Object.assign({}, category, { nblinks: categoryLinks.length })
      })
    );
};

I am mapping the first observable (returned by this.category.find()) to another observable (returned by countLinks()).

In the innermost map(), I end up with both the category and the categoryLinks and I merge them into a single object with Object.assign().


Side note: Your original code is slightly confusing. You wrote "I have another method on this.categories called countLinks(id)" but you also wrote this.categories=data. How can you have a method on a data structure?.. I wrote my answer under the assumption that both this.category.find() and countLinks() return observables, but you'll have to adapt my solution to your specific code/situation.

Upvotes: 2

cpt_redbeard
cpt_redbeard

Reputation: 215

Create two interfaces. One to represent the base of the category and one that extends it.

interface ICategory {
    categoryName: string;
    id: string;
    clientId: string;
}

interface ICategoryExpanded extends ICategory {
    nbLinks: string;
}

Then use the .map() operator in RxJS to transform the data into the 2nd interface.

        this.getCategories().map((cats: ICategory[]) => {
                let mapCategories: ICategoryExpanded[] = [];
                cats.forEach((cat: ICategory) => {
                    let transformCat: ICategoryExpanded = <ICategoryExpanded>cat;
                    transformCat.nbLinks = this.countLinks(transformCat.id);
                    mapCategories = [...mapCategories, transformCat];
                });
                return mapCategories;
            })
            .subscribe((returnCategoriesExt) => this.categories = returnCategoriesExt);

That should handle what you want and return something like this:

[{ "categoryName": "Google", "id": "58591d2b7672d99910497bec", "clientId": "585808f6737f6aad1985eab2", "nbLinks": "5" }, 
{ "categoryName": "Yahoo", "id": "58591d4c7672d99910497bef", "clientId": "585808f6737f6aad1985eab2", "nbLinks": "5" }, 
{ "categoryName": "Msn", "id": "585d25f6ae4b2ecb056bc514", "clientId": "585808f6737f6aad1985eab2", "nbLinks": "5" }]

Upvotes: 0

Related Questions