I’m working on a demo for upgrading https://globalofficelink.com/ to a single page app using Angular.

I want to give the listings search page a better user experience. The minimum search is by type of listing (buy/rent versus selling) and country. There aren’t that many countries with listings so having a user select one of 200+ countries to find a listing is a terrible experience. I want the user to only have to select from countries that have listings.

I could easily do this as a query on a database:

   select distinct c.* 
   from country c
   left join listing l on (c.countryCode == l.countryCode)
   where l.id is not null 

except relational databases and SQL are so passé. I’m getting my countries from a REST service and if I want to be a UI guy I have to do everything on the UI side, pulling from different services. I’ll assume I have a listing service but for now I’ll pull dummy ones from an array.

For some reason I thought pulling countries from a service would return a stream of countries, but in reality it returns a stream of country arrays with a single item. So right off the bat I had it wrong as httpClient.get<Country>(this.countriesAPI) but Typescript didn’t complain because it had no idea what the service was returning. Plus I assumed an Observable was iterable, but it wasn’t. I wasn’t sure how to turm my stream of country arrays into a stream of country so I just filtered the countries array with the array filter method and a static array of country codes.

   // observable via array filter of country codes
   public getActiveCountriesTakeOne(): Observable<Country[]> {
     return this.countries.pipe(
       map(countries => countries.filter(country => ['US', 'CA', 'UK'].includes(country.alpha2Code))), tap(console.log));
   }

the rxjs map takes the array as one item, applies the function countries.filter(...) and returns the correct set of countries I want. This seems inelegant and kind of like cheating so the next step was to turn my country array into a stream of countries. This stumped me for a bit going through the documentation of functions, for example the description for flatMap:

An Observable that emits the result of applying the projection function (and the optional deprecated resultSelector) to each item emitted by the source Observable and merging the results of the Observables obtained from this transformation.

while I’m sure this is technically correct it is indecipherable to me and the fact that flatMap(country => country) converts a stream of Country[] into a stream of Country seems like witchcraft. It takes an item from the source stream, in this case a Country[] and makes that an Observable, subscribes to it and then applies the projection function, in this case country, to the items in that stream, which just returns the item.

After flattening and mapping I could then filter the countries filter(country => ['US', 'CA', 'MX'].includes(country.alpha2Code)) but now I’m returning a stream of Countries, but the Select control doesn’t like that as it isn’t iterable. I need to return a single item with an array. Hello reduce, you have a funny name, why isn’t it collect? It in Lisp and XSLT you use tail recursion but this is more like unwinding recursion into iteration with a collection, anyway now I have a pure observable way of generating my active countries:

   // observable filtering by array
   public getActiveCountriesTakeTwo(): Observable<Country[]> {
     return this.countries.pipe(
       flatMap(country => country),
       filter(country => ['US', 'CA', 'MX'].includes(country.alpha2Code)),
       reduce((countries, country) => countries.concat(country), []), tap(console.log));
   }

The final problem to solve is the static array of active country codes, this needs to be from the active listings. I mocked up listings as an array listings with only a countryCode and then making it into an observable. I struggled trying to do this as a single pipeline. I then outlined what I wanted. I want to take my stream of countries and if the country was in the collection of distinct country codes from the listings, return it. I need the countries individually to see if they are in the list, but I need the active country codes collectively. I broke the problem into two parts, make a distinct list of country codes and then apply that to the countries. I already know that I can use flatMap to turn my stream of listing[] into a stream of listings, I thought I could be clever and map directly to countryCode like flatMap(country => country.alpha2Code) but that didn’t work. There is probably a way to get that to work but using https://rxjs-dev.firebaseapp.com/operator-decision-tree I found pluck() and that is pretty clean. Then distinct() is just like SQL, finally turn this back into an array with reduce. How do I bring two streams together? Using the operator decision tree when I get my countries I want to then apply the filtering to see if it is in the active country code array, it came up with withLatestFrom(). Using this took some time to wrap my head around but basically I take a country from the main observable and the array from the country codes and combine into {country,activeCountryCodes} I then pass that single item into a filter filter((countryWithCountryCodes) => countryWithCountryCodes.countryCodes.includes(countryWithCountryCodes.country.alpha2Code)) and if the country matches a code in the array it gets returned, otherwise not. Finally I collect that back into an array of countries for my select list and voilà.

I don’t know if this is true, but I expect if I setup my service observables to poll the services and then switched to use combineLatest() instead of withLatestFrom() then my select list of countries for searching would magically get updated if a new listing in a new country were added.

 import { Listing } from './../listings.types';
 import { Code, SIZE_UNITS } from './../../codes/codes.types';
 import { CodesService } from './../../codes/codes.service';
 import { Injectable } from '@angular/core';
 import { Observable, of } from 'rxjs';
 import { tap, filter, flatMap, map, reduce, withLatestFrom, distinct, pluck } from 'rxjs/operators';
 import { HttpClient } from '@angular/common/http';
 import { Country } from '../../country.types';
 
 
 @Injectable({
   providedIn: 'root'
 })
 export class ListingsDataService {
   // https://restcountries.eu/rest/v2/all?fields=name;alpha2Code  -- returns just {"name":"Afghanistan","alpha2Code":"AF"}
   private countriesAPI = 'https://restcountries.eu/rest/v2/all';
 
   private httpClient: HttpClient;
   private codesService: CodesService;
   private countries: Observable<Country[]>;
   private listings: Observable<Listing[]>;
 
   constructor(httpClient: HttpClient) {
     this.httpClient = httpClient;
 
     this.countries = this.httpClient.get<Country[]>(this.countriesAPI);
     this.listings = of([
       {id: 1, countryCode: 'US'},
       {id: 2, countryCode: 'US'},
       {id: 3, countryCode: 'US'},
       {id: 4, countryCode: 'US'},
       {id: 5, countryCode: 'CA'},
       {id: 6, countryCode: 'CA'},
       {id: 7, countryCode: 'CA'},
       {id: 8, countryCode: 'MX'}
     ]);
    }
 
   public getAllCountries(): Observable<Country[]> {
     return this.countries.pipe(tap(console.log));
   }
 
   // observable via array filter of country codes
   public getActiveCountriesTakeOne(): Observable<Country[]> {
     return this.countries.pipe(
       map(countries => countries.filter(country => ['US', 'CA', 'UK'].includes(country.alpha2Code))), tap(console.log));
   }
 
   // observable filtering by array
   public getActiveCountriesTakeTwo(): Observable<Country[]> {
     return this.countries.pipe(
       flatMap(country => country),
       filter(country => ['US', 'CA', 'MX'].includes(country.alpha2Code)),
       reduce((countries, country) => countries.concat(country), []), tap(console.log));
   }
   
   // observable countries filtered by unique country code observable from listings
   public getActiveCountries(): Observable<Country[]> {
     // observable of unique country codes array from listings
     let countryCodesFromListings: Observable<string[]> = this.listings.pipe(
       flatMap(listing => listing), 
       pluck('countryCode'),
       distinct(), 
       reduce((uniqueCountryCodes,countryCode) => uniqueCountryCodes.concat(countryCode), []),
       tap(console.log)
     );
     // obserable of active countries filtered by listing country codes
     return this.countries.pipe(
       flatMap(country => country),
       withLatestFrom(countryCodesFromListings, (country, countryCodes) => ({country,countryCodes})),
       filter((countryWithCountryCodes) => countryWithCountryCodes.countryCodes.includes(countryWithCountryCodes.country.alpha2Code)),
       reduce((activeCountries, countryWithCountryCodes) => activeCountries.concat(countryWithCountryCodes.country), []), 
       tap(console.log)
     );
   }
 }