Home > Back-end >  How to use combinedLatestWith properly?
How to use combinedLatestWith properly?

Time:01-06

https://stackblitz.com/edit/angular-ivy-s2ujmr?file=src/app/country-card/country-card.component.html

I'm trying to create a search bar that filters the "countries$" Observable based on the text input.


The only way I can think of doing it is to create 2 other Observables, one for the search term and another for loading the flags, and use "combinedLatestWith" to filter the flags every time there's input for the search term.

I'm new to angular and have never used "combinedLatestWith" and can't get it to work. This is the code I've come up with so far:

import { Component, OnInit } from '@angular/core';
import { ApiService } from '../../services/api.service';
import {FormControl} from '@angular/forms';
import { Observable, combineLatestWith} from 'rxjs'; 
import {map, tap, filter, startWith} from 'rxjs/operators'
import { Country } from '../../types/api';

export class HomeComponent {
  allCountries: Country[] = [];
  searchTerm$?: Observable<string | null>;
  loadCountries$!: any; (IDK WHAT TO PUT HERE)
  countries$!: Observable<Country[]>;
  myControl = new FormControl('');
  constructor(private apiService: ApiService) {}

  ngOnInit() {
    this.loadCountries$ = this.apiService.getAllCountries().pipe(
      tap((countries: any) => this.allCountries = countries)
    );

    this.searchTerm$ = this.myControl.valueChanges.pipe(
      startWith('')
    );

    this.countries$ = combineLatestWith(this.loadCountries$, this.searchTerm$).pipe(
      map(([countries, searchTerm]) => filter(countries, searchTerm)),
    );
  }

This doesn't work however and I'm getting the TS errors:

Property 'pipe' does not exist on type 'OperatorFunction<unknown, [unknown, unknown, string | null]>'.ts(2339)

Property 'filter' does not exist on type 'HomeComponent'.ts(2339)


Can someone help me fix this? Is my approach correct at least? If I solve these Typescript errors would "combinedLatest" work as intended and filter the results? Am I using "combinedLatestWith" correctly?

CodePudding user response:

@Eric Andre, why you want to use combineLatestWith? What do you think about to add some filter pipe:

https://stackblitz.com/edit/angular-ivy-fhvkvt?file=src/app/pages/home/home.component.ts,src/app/pages/home/home.component.html,src/app/pipes/filter-countries.pipe.ts,src/app/app.module.ts,src/app/country-card/country-card.component.ts,angular.json

import { Pipe, PipeTransform } from '@angular/core';
            
@Pipe({name: 'filterCountries'})
export class FilterCountriesPipe implements PipeTransform {
   transform(items, input: string): any {
      if (!items || !input || !input.replace(/[\n\r\s\t] /g, '')) {
         return items;
      }
      return items.filter((item) => item.name.official.toLowerCase().includes(input.toLowerCase()));
 }
}

HomeComponent template

<input placeholder="Filter Countries" [(ngModel)]="inputValue" type="text" />
<app-country-card *ngFor="let country of countries$ | async | filterCountries: inputValue" [country]="country"></app-country-card>

HomeComponent

import { Component, OnInit } from '@angular/core';
import { ApiService } from '../../api.service';
    
    @Component({
      selector: 'app-home',
      templateUrl: './home.component.html',
      styleUrls: ['./home.component.css'],
    })
    export class HomeComponent implements OnInit {
      countries$: any;
      inputValue: string;
      constructor(private api: ApiService) {}
    
      ngOnInit() {
        this.countries$ = this.api.getAllCountries();
      }
    }`

App Module:

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';

import { AppRoutingModule } from './app-routing/app-routing.module';
import { AppComponent } from './app.component';
import { CountryCardComponent } from './country-card/country-card.component';
import { HomeComponent } from './pages/home/home.component';
import { HttpClientModule } from '@angular/common/http';
import { FilterCountriesPipe } from './pipes/filter-countries.pipe';

@NgModule({
  declarations: [
    AppComponent,
    CountryCardComponent,
    HomeComponent,
    FilterCountriesPipe,
  ],
  imports: [BrowserModule, AppRoutingModule, HttpClientModule, FormsModule],
  providers: [],
  bootstrap: [AppComponent],
})
export class AppModule {}

CodePudding user response:

While you can use combineLatestWith for this, the same result can be achieved in a much simpler manner.

Looking into the problem, we can identify the main issues that we face is that we receive a dataset and only want to display the results that match our search term.

This can be achieved with the following code:

export class HomeComponent implements OnInit, OnDestroy {
  constructor(private api: ApiService){}

  countries: any[];
  displayCountries: any[];

  destroy$ = new Subject<void>()
  countryControl = new FormControl();

  ngOnInit(){
    this.api.getAllCountries().subscribe(response => {
      this.countries = response;
      this.displayCountries = response;
    });

    this.countryControl.valueChanges.pipe(takeUntil(this.destroy$),debounceTime(100)).subscribe((value: string) => this.updateDisplayCountries(value))
  }

  private updateDisplayCountries(searchTerm: string): void {
    this.displayCountries = this.countries.filter(country => this.isCountrySearched(country, searchTerm.toLowerCase()))
  }

  private isCountrySearched(country: { name: { common: string, official: string} }, searchTerm: string): boolean {
    return country.name.common.toLowerCase().includes(searchTerm) || country.name.official.toLowerCase().includes(searchTerm)
  }

  ngOnDestroy(): void {
    this.destroy$.next();
    this.destroy$.complete();
  }
}

Starting with the variables, we have 3 new variables:

  • displayCountries: these are the list of countries that we want to display to our user when they are filtering. It is a subset of the countries. Doing so allows us always have access to the entire list of countries and avoid troublesome issues with combineLatestWith.
  • countryControl: is a FormControl that will assist us in using the values provided by the input field.
  • destroy$: is a subject that we use to ensure that no memory leaks occur.

Continuing to our methods:

  • ngOnInit: here we are performing the request to get all the countries. At the start, all countries are being displayed, so we can set the response from the API to also be the value of displayCountries. We don't need to worry about subscriptions here. HttpClient request auto-complete so there is no risk of a memory leak here (if there was, we would use pipe(takeUntil(destroy$) to handle it). Lastly, we subscribe to any changes in the value of our input field by using our form control instance, countryControl. To handle the memory leak from subscribing, we use the pipe(takeUntil(destroy$). We also use the debounceTime(x) function so that we only filter the countries every 100ms, which avoids triggering the filtering while the user has not finished typing (I put the value 100, but other values can also work).
  • updateDisplayCountries: is a helper that will call the filter function for each countries. I separate this in case we have special keywords like "USA" or "EU". These would not match the naming convection search, so we could include them here by performing an if or ´switch´. I also convert the inputted search term to lowercase to avoid annoying miss-matches caused by casing issues.
  • isCountrySearched: return true when the search term that the user inputted matches the common name or the official name of the country. Again, conversion of the names is done to avoid miss-matches from casing.
  • ngOnDestroy: is used to handle the memory leaks.

An minor updated to the HTML of the HomeComponent is also needed.

<input placeholder="Filter Countries" [formControl]="countryControl" type="text" value="">
<app-country-card *ngFor="let country of displayCountries" [country]="country"></app-country-card>

The formControl countryControl was added to the input field and the array of countries to be displayed was updated from the countries array to the displayCountries array.

We also need to add the ReactiveFormsModule to the imports of the module where we are declaring our HomeComponent. In this case, it is the AppModule.

  • Related