What is the best way to use form input to filter an Observable?

Within my component, I have declared the variable "countries$":

countries$!: Observable<Country[]>;

To populate this variable with data from this API, I use the following code in the "ngOnInit" lifecycle hook:

ngOnInit(){
    this.countries$ = this.apiService.getAllCountries();
}

In order to display the data in the HTML template, I utilize the following syntax:

<div>
  <app-country-card *ngFor="let country of countries$ | async" [country]="country"></app-country-card>
</div>

I aim to add a search feature that dynamically filters the list based on user input.

Initially, I attempted to implement this functionality using the "filter" function within a pipe as shown below:

searchFilterCountries(searchTerm: string){
    this.countries$.pipe(filter((country: any) => 
      country.name.common.toLowerCase().includes(searchTerm.toLowerCase())))
}

The corresponding input field is incorporated into the HTML template like so:

<input type="text" class="form-control" (input)="searchFilterCountries($event.target.value)"/>

Unfortunately, this approach did not yield the desired outcome and triggered TypeScript errors:

Object is possibly 'null'.ngtsc(2531)

Property 'value' does not exist on type 'EventTarget'.ngtsc(2339)


Subsequently, I referenced a filtered list example using Material UI at the following link:

https://material.angular.io/components/autocomplete/examples (The FILTER one)

My attempt to replicate this resulted in the following code snippet:

export class HomeComponent {
    countries$!: Observable<Country[]>;
    myControl = new FormControl('');
    constructor(private apiService: ApiService) { }
    
    ngOnInit(){
        this.countries$ = this.apiService.getAllCountries();
    }

    private _filter(value: string): Observable<Country[]> {
        const filterValue = value.toLowerCase();
    
        return this.countries$.pipe(filter(option => 
          option.name.common.toLowerCase().includes(filterValue)));
    }
}

However, I encountered issues due to dealing with observables instead of the actual data inside them.

An error was detected under the "name" property within "option.name.common", stating:

option.name.common TS error

Property 'name' does not exist on type 'Country[]'

Although modifying the code to access the first element resolved the error temporarily, it reduced the searching capabilities:

option => option[0].name.common.toLowerCase().includes(filterValue)))

Seeking guidance on the utilization of correct operators and resolving TypeScript errors, I questioned if employing operators like mergeMap or switchMap would provide a solution. Additionally, I pondered the effectiveness of rectifying TypeScript errors and whether the implemented approach was fundamentally flawed. Any assistance in refining this functionality would be greatly appreciated.

Can someone assist me in overcoming these challenges?

Answer №1

Sample pipe

import { Pipe, PipeTransform } from '@angular/core';
import { Country } from './country';

@Pipe({
  name: 'filterList',
})
export class FilterListPipe implements PipeTransform {
  transform(countries: Country[]|null, searchText: string): Country[] {
    if(!countries) return []
    return countries.filter(country=>country.name.indexOf(searchText) != -1);
  }
}

app.component.html

<form [formGroup]="controlsGroup">
  <input type="text" formControlName="searchInput"/>

  <div *ngFor="let country of countries | async | filterList:searchText">
    <div>Name: {{country.name}}</div>
    <div>Ranking: {{country.ranking}}</div>
    <div>Metric: {{country.metric}}</div>
  </div>
</form>

app.component.ts

import { Component } from '@angular/core';
import { FormBuilder, FormControl, FormGroup } from '@angular/forms';
import { Observable, of } from 'rxjs';
import { Country } from './country';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  title = 'piper-example-app';
  searchText = ''

  controlsGroup: FormGroup

  constructor(public fb:FormBuilder){
    this.controlsGroup = fb.group({
      searchInput: new FormControl('')
    })

    this.controlsGroup.get('searchInput')?.valueChanges.subscribe(value => this.searchText=value)
  }

  countries: Observable<Country[]> = of([{
    name: 'United States of America',
    ranking: 1,
    metric: 'burgers per capita'
  },
  {
    name: 'China',
    ranking: 9000,
    metric: 'power level lower bound'
  }])
}

Indeed, there are some unconventional practices in the code where filtering the incoming observable stream of arrays of countries could be more efficient. It is also important to expand the filter function to check all properties or adjust the criteria as needed.

A more comprehensive example demonstrating the filter part with different property types being filtered slightly differently:

custom-filter.pipe.ts (alternative)

import { Pipe, PipeTransform } from '@angular/core';
import { Country } from './country';

@Pipe({
  name: 'filterList',
})
export class FilterListPipe implements PipeTransform {
  transform(countries: Country[]|null, searchText: string): Country[] {
    if(!countries) return []
    return countries.filter(country => {
      let foundMatch = false;
      let property: keyof typeof country
      for(property in country) {
        if(typeof country[property] === 'string') {
          if((country[property] as string).indexOf(searchText) != -1)
          foundMatch = true
        }else {
          if((country[property] as number) == parseInt(searchText))
          foundMatch = true
        }
      }
      return foundMatch
    });
  }
}

Answer №2

Here is an expanded version of your current code with some suggested changes:

export class HomeComponent {
  allCountriesList: Country[] = [];
  countriesStream$!: Observable<Country[]>;
  myControlInput = new FormControl('');
  constructor(private apiServiceCall: ApiService) {}

  ngOnInit() {
    this.apiServiceCall
      .getAllCountries()
      .subscribe((countriesData) => (this.allCountriesList = countriesData));

    this.countriesStream$ = combineLatest({
      searchTermValue: this.myControlInput.valueChanges.pipe(startWith('')),
      countriesData: this.apiServiceCall
        .getAllCountries()
        .pipe(tap((countriesData) => (this.allCountriesList = countriesData))),
    }).pipe(map(({ searchTerm }) => this._filterData(searchTerm)));
  }

  private _filterData(criteria: string | null): Country[] {
    if (criteria === null) {
      return this.allCountries;
    }
    const filteredValue = criteria?.toLowerCase();

    return this.allCountries.filter((countryItem) =>
      countryItem.name.common.toLowerCase().includes(filteredValue)
    );
  }
}

The updated approach involves maintaining the original country list separately and utilizing the form control's value change event to filter the relevant countries for display.

The corresponding template should be structured as follows:

<input type="text" [formControl]="myControlInput" />

<div *ngFor="let country of countriesStream$ | async">
  <div>Name: {{ country.name.common }}</div>
</div>

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Displaying a pop-up window upon clicking on an item in the list view

After creating a list where clicking an item triggers a popup, issues arose when testing on small mobile screens. Many users found themselves accidentally tapping buttons in the popup while trying to view it. What is the most effective way to temporarily ...

When running Rollup, a syntax error occurs due to the use of the "import" syntax in an internal Rollup file using Javascript

I am working on a nodeJS/express application with a rollup bundler. My rollup configuration file defines a command in the package.json like this: "build": "env ROLLUP_OPTIONS='prod' rollup --config configs/rollup.config.js". However, when I run " ...

How can I automatically choose the first element in an ng-repeat loop using AngularJS?

Do you have any thoughts on how to accomplish this task? Currently, I have set up one controller where if you click on an element, an animation appears above. However, my goal is to have the first element automatically active/clicked as soon as the page l ...

The application component seems to be stuck in a loading state and is not appearing as expected on my index.html

Click here to view the Plunkr <html> <head> <base href="/"> <title>Angular QuickStart</title> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <l ...

Using Rails AJAX to dynamically load partials without the need to submit

Imagine creating a dynamic page layout with two interactive columns: | Design Your Pizza Form | Suggested Pizzas | As you customize your pizza using the form in the left column, the right column will start suggesting various types of pizzas based on your ...

I'm only appending the final element to the JavaScript array

Currently, I have the following code: I'm endeavoring to create a new JSON object named dataJSON by utilizing properties from the GAJSON object. However, my issue arises when attempting to iterate over the GAJSOn object; only its last element is added ...

When utilizing CKEDITOR, the default TEXTAREA is obscured, and CKEDITOR does not appear

I am trying to incorporate CKEDITOR into my project. I have added the ckeditor script in the footer and replaced all instances of it. <script src="<?= site_url('theme/black/assets/plugins/ckeditor/ckeditor.min.js') ?>" type="text/javasc ...

Comparing Twitter Bootstrap and PrimeNg: The Ultimate Guide for UI Development in Angular 2

After using twitter bootstrap for a while, I transitioned to Angular 2 and began exploring different CSS libraries. I discovered options like ngSemantic, Angular2 Material, ng-bootstrap, and ng2-bootstrap, but primeNg caught my eye with its wide range of c ...

What strategies can be used to manage Error return types in Typescript?

I have a situation where I need to handle either an object of type Person or an Error being returned by a function. My goal is to read the values of keys in the returned object only if it's not an Error. The code snippet below throws an error on the ...

Error while conducting unit testing: Element 'X' is unrecognized

While running the command npm run test, I encountered a specific error in my terminal: 1. If 'app-general-screen' is an Angular component, then verify that it is a part of an @NgModule where this component is declared. 2. If 'app-general-sc ...

Utilizing Leaflet-geotiff in an Angular 6 Environment

I'm currently facing an issue where I am unable to display any .tif image on my map using the leaflet-geotiff plugin. I downloaded a file from gis-lab.info (you can download it from this link) and attempted to add it to my map, but I keep encountering ...

Using Redux and Typescript to manage user authentication states can help streamline the process of checking whether a user is logged in or out without the need for repetitive checks in the mapStateToProps function

In the process of developing a web application utilizing React & Redux, I am faced with defining two primary "states" - Logged In and Logged Out. To tackle this challenge, I have structured my approach incorporating a union type State = LoggedIn | LoggedO ...

Simultaneously updating the states in both the child and parent components when clicked

In my code, I have two components: the parent component where a prop is passed in for changing state and the child component where the function is called. The function changes the state of the parent component based on an index. changeState={() => this ...

Using AngularJS to auto-populate additional fields after selecting an option from the typeahead autocomplete feature

Just starting with AngularJS and finally figured out how to implement Auto-complete in Angularjs. Now, when a user selects a value from the auto-complete, I want other fields to be populated based on that selection. For example, upon loading the screen, d ...

Developing forms in Angular 5 involves the decision of either constructing a specific model for the form group inputs or opting for a more generic variable with two

As a newcomer to Angular, I've noticed that most courses and tutorials recommend using a model or interface for form data. However, I have found it more efficient and straightforward to just use a generic variable with two-way data binding. Could som ...

Developing a Universal Type in Typescript

Encountered an issue with generic types while working on a user-defined type(interface) structured like this: IList1: { prop1: string, prop2: number, prop3: string . . . } ILi ...

Angular 5 - Error: Uncaught ReferenceError: req variable is not defined

After upgrading my Angular project from version 4.4.3 to 5, I made sure to update all angular packages to the latest Angular 5.0.0 as per the guidelines provided. Additionally, I updated the angular CLI to version 1.5.0. However, ever since this update, I ...

Transformation of Array of Objects to Array of Values using Node.js

I am looking to transform the following: [{ prop1: '100', prop2: false, prop3: null, prop4: 'abc' }, { prop1: '102', prop2: false, prop3: null, prop4: 'def' } ] into this format: [[100,false,null,'abc&ap ...

Steps for customizing the dropdown arrow background color in react-native-material-dropdown-v2-fixed

Currently, I am utilizing react-native-material-dropdown-v2-fixed and I am looking to modify the background color of the dropdown arrow. Is there a way for me to change its color? It is currently displaying as dark gray. https://i.stack.imgur.com/JKy97.pn ...

I'm trying to figure out how to switch the Heroes array to the res array while utilizing the react useState hook. It's been successful in my past projects, but for some reason,

const [Heroes, setHeroes] = useState([]) useEffect(() => { API.findFavorites().then((res) => { console.log(res) setHeroes([...Heroes, res]); console.log(Heroes) }) }, []); I am struggling ...