How to merge 2 searches in Angular Heroes tutorial?

350 Views Asked by At

I am trying to merge 2 HTTP get searches in Angular/TypeScript, but I'm having trouble getting it to work.

The situation is illustrated using the Angular 2 / 4 Tour of Heroes tutorial, which uses an Observable to search heroes for by"name" value (https://angular.io/tutorial/toh-pt6#add-the-ability-to-search-by-name). The code in hero-search.service.ts is as follows:

import { Injectable } from '@angular/core';
import { Http }       from '@angular/http';

import { Observable }     from 'rxjs/Observable';
import 'rxjs/add/operator/map';

import { Hero }           from './hero';

@Injectable()
export class HeroSearchService {

  constructor(private http: Http) {}

  search(term: string): Observable<Hero[]> {
    return this.http
               .get(`api/heroes/?name=${term}`)
               .map(response => response.json().data as Hero[]);
  }
}

I need to do the equivalent of adding a second string value for "synonym" to the Hero class:

export class Hero {
  id: number;
  name: string;
  synonym: string;
}

and add a synonym to each Hero:

import { InMemoryDbService } from 'angular-in-memory-web-api';
export class InMemoryDataService implements InMemoryDbService {
  createDb() {
    const heroes = [
      { id: 0,  name: 'Zero', synonym: 'little' },
      { id: 11, name: 'Mr. Nice', synonym: 'pleasant' },
      { id: 12, name: 'Narco', synonym: 'sleep' },
      { id: 13, name: 'Bombasto', synonym: 'loud' },
      { id: 14, name: 'Celeritas', synonym: 'vegetable' },
      { id: 15, name: 'Magneta', synonym: 'color' },
      { id: 16, name: 'RubberMan', synonym: 'hose' },
      { id: 17, name: 'Dynama', synonym: 'motor' },
      { id: 18, name: 'Dr IQ', synonym: 'smart' },
      { id: 19, name: 'Magma', synonym: 'volcanic' },
      { id: 20, name: 'Tornado', synonym: 'wind' }
    ];
    return {heroes};
  }
}

I can then search in hero-search.service.ts by synonym by substituting:

       .get(`api/heroes/?synonym=${term}`)

My question is how to search for both name and synonym and return matches with either name or synonym.

From the ReactiveX documentation it seems like the answer is to use the Merge operator: http://reactivex.io/documentation/operators/merge.html. However, I'm having trouble getting this to work. I've tried various ways of using merge, including the following new version of the hero-search.service.ts code above:

import { Injectable } from '@angular/core';
import { Http }       from '@angular/http';

import { Observable }     from 'rxjs/Observable';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/merge';

import { Hero }           from './hero';

@Injectable()
export class HeroSearchService {

  constructor(private http: Http) {}

  search(term: string): Observable<Hero[]> {
    const nameResults = this.http.get(`api/heroes/?name=${term}`);
    const synonymResults = this.http.get(`api/heroes/?synonym=${term}`);    
    nameResults.merge(synonymResults);
    return nameResults.map(response => response.json().data as Hero[]);
  }
}

The not-working-properly code is at https://plnkr.co/edit/4mrL5ReQCSvuzOODixJU?p=preview.

I don't see any evidence that the merge succeeded, and nameResults give only names, not synonyms. No error messages appear.

The RxJS code at http://reactivex.io/documentation/operators/merge.html uses code of the form

const mergedResults = Rx.Observable.merge(nameResults, synonymResults);

but when I try such code I get an error flagged:

Cannot find name 'Rx'

So my questions are:

How can one use RxJS merge here?

Is there some other approach using RxJS that is more suitable?

Is there some other approach at the level of the Hero class that will work such as computing namePlusSynonym without having to add namePlusSynonym to each line in the Hero array?

ADDENDUM: following suggestions here and code at http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html#instance-method-merge I created a new version of hero-search.service.ts with console outputs.

import { Injectable } from '@angular/core';
import { Http } from '@angular/http';

import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/merge';

import { Hero } from './hero';

@Injectable()
export class HeroSearchService {

  constructor(private http: Http) {}

  search(term: string): Observable<Hero[]> {
    const nameResults = this.http.get(`api/heroes/?name=${term}`);
    nameResults.subscribe(nameData => console.log('nameData', nameData));
    const synonymResults = this.http.get(`api/heroes/?synonym=${term}`);
    synonymResults.subscribe(synonymData => console.log('synonymData', synonymData));
    const combinedResults = nameResults.merge(synonymResults);
    combinedResults.subscribe(combinedData => console.log('combinedData', combinedData));
    return combinedResults.map(response => response.json().data as Hero[]);
  }
}

But it only displays synonymResults, not nameResults. Both nameResults and synonymResults look OK to me in the console output, but combinedResults has only data and the url for synonym results and seems unaffected by the merge method.

I also tried with concat, with similar results.

It seems that I'm not combining at the right stage, but combining at a different stage gives the same result, only showing synonyms:

  search(term: string): Observable<Hero[]> {
    const nameResults = this.http.get(`api/heroes/?name=${term}`).map(response => response.json().data as Hero[]);
    nameResults.subscribe(nameData => console.log('nameData', nameData));
    const synonymResults = this.http.get(`api/heroes/?synonym=${term}`).map(response => response.json().data as Hero[]);
    synonymResults.subscribe(synonymData => console.log('synonymData', synonymData));
    const combinedResults = nameResults.merge(synonymResults);
    combinedResults.subscribe(combinedData => console.log('combinedData', combinedData));
    return combinedResults;
  }
1

There are 1 best solutions below

12
On

To get the merge operator to work, you need to import Observable and merge from rxjs, just as it is in the plunker:

import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/merge';

How can one use RxJS merge here?

I think your main misunderstanding here is that calling merge() on an observable returns a new merged observable. As rxjs functions are pure, calling it here does not mutate namedResults

nameResults.merge(synonymResults);

Instead, you should capture the results in a new constant

const combinedResults = nameResults.merge(synonymResults);
return combinedResults;

Is there some other approach using RxJS that is more suitable?

This is how I solved the problem (plunker here) to get it to work without changing the existing angular component, only changing the hero-search.service.ts. You could make this look much nicer by chaining operators together, but I think that seeing helpful variables names with types at each stage is very helpful for learning. You could put an intermediate subscribe() & console.log() on any of the observables for testing to see what the value actually looks like.

search(term: string): Observable<Hero[]> {
  // get the response observable from requesting the backend      
  const nameResults: Response = this.http.get(`api/heroes/?name=${term}`);
  // boil that response down to just the data array we care about
  const nameResultsArr: Observable<Hero[]> = nameResults.map(res => res.json().data);
  // do the same for the synonym request
  const synonymResults: Response = this.http.get(`api/heroes/?synonym=${term}`);
  const synonymResultsArr: Observable<Hero[]> = synonymResults.map(res => res.json().data);

  // combine the two observable arrays into a stream of arrays
  const combinedHeroListObservables: Observable<Hero[]> =
    nameResultsArr.merge(synonymResultsArr);
  // concat the observable arrays onto each other
  const concatenatedArrays: Observable<Hero[]> =
    combinedHeroListObservables.reduce((accumulator, current) => accumulator.concat(current));

  // return the combined array
  return concatenatedArrays;
}

but when I try such code I get an error flagged: Cannot find name 'Rx'

If you don't care about needlessly importing things you're not using, you can get this to work with

import * as Rx from 'rxjs/Rx';

But, it is good practice to only import the operators/types that you need as seen in the plunker example.

PS: Like some said in the comments, it would be good to have a rest route that returned both of the results combined already so the client doesn't have to make two round trips to the server. But, you can't really do this because you are using the Angular team's example.