How to execute API call after entering 3 characters in field?

Viewed 5571

I'm working with a parent and child component. The child component has the input field and will emit the value entered by the user to the parent component like this:

<parent-component (sendInputValue)="getInputValue($event)"><parent-component>

Now in parent component I have this:

getInputField(data){
 console.log(data); // this prints the data (Example: abc)
 //  then here I'm just executing the API call ONLY if data length is 3
  if(data.length === 3){
    this.myService.getDataFromService(data).subscribe(rs=>console.log(rs));
  }
}

Now let's say this happens:

  1. The user enters: abc // API call gets execute that is good
  2. Now, user enters: abcd // No API call gets executed, that is good
  3. Now user deletes letter "d" and the new value of data will be "abc" I DONT want to execute API call again because we already execute API call for "abc"
  4. Now if the user deletes the letter "c" the new value of data is now "ab". At this point, no API call is expected
  5. Now if the user adds the letter "c" the new value will be "abc". At this point, the API call is expected. (this is working)

So how to always execute API call if input data is 3 characters and if the user enters more characters nothing should happen, and if he deletes characters and goes back to the first 3 characters nothing should happen because the API already happened? Thanks a lot in advance!

9 Answers

I think you need distinctUntilChanged And you can use the filter in the pipe

this.myService.getDataFromService(data)
  .pipe(
    filter(_ => data.length === 3), 
    distinctUntilChanged()
  ).subscribe(rs => console.log(rs));

Below is the little tweak in your code and it will fulfill the requirement told by you.

You can definitely improve this process using debounce, distinctUntilChanged, switchMap operators.

previousData = '' // create a property which will track of previous data from parent component.

getInputField(data){
  if(data && data.length === 3 && data.length > previousData.length){
    this.myService.getDataFromService(data).subscribe(rs=>console.log(rs));
  }
  this.previousData = data || ''; // update previous data to current data after backend call.
}

Use RxJs FromEvent method to listen input event from the input field.

@ViewChild('#input', {static:true}) inputField: ElementRef<any>;

ngOnInit(){
   FromEvent(inputField, 'input')
     .pipe(
         map(event => event.target.value),
         map((value:string) => value.trim()),
         filter((value:string) => value.length === 3), 
         debounceTime(500),
         distinctUntilChanged()
     )
     .subscribe(keyword => {
         // API Call(keyword)
     })
}

Here is a simple way stackblitz :

setInputField(data){
  if(data.length >= 3){
    let wasCalled  = this.calledInputs.includes(data) ? true : false;
    if(wasCalled){
      return;
    }else{
      this.calledInputs.push(data)
      console.log("call Service For", data)
    }
  }
}

Try to add the called values to an array, then check that array before making calls. I've made an example that calls the API when the length is bigger than 3 because I think this way is more reasonable than doing it like what have you done.

Now user deletes letter "d" and the new value of data will be "abc" I DONT want to execute API call again because we already execute API call for "abc"

You might want to try implementing it using distinct (Returns an Observable that emits all items emitted by the source Observable that are distinct by comparison from previous items.)

distinctUntilChanged emits values that have been emitted before the previous emitted value, while distinct doesn't.

  valueChanges.pipe(
    filter(x => x.length === 3),
    distinct()
  )

Here's working CodeSandbox Demo

Hope it help!

child component

import { Component, Input, Output, EventEmitter, OnDestroy } from "@angular/core";
import { Observable, Subject, interval } from "rxjs";
import { debounce, map, filter, tap } from "rxjs/operators";

@Component({
  selector: "hello",
  template: `
    <input type="text" id="textInput" (keyup)="keyupHandler($event)" />
  `
})
export class HelloComponent implements OnDestroy {
  requestCompleted: { [key: string]: boolean } = {};

  @Output()
  sendInputValue = new EventEmitter<string>();

  observableFilter = new Subject<string>();
  constructor() {
    this.observableFilter
      .pipe(
        debounce(() => interval(500)),
        filter(x => x && x.length === 3 && !this.requestCompleted[x]),
        tap(x => (this.requestCompleted[x] = true))
        //tap( x => console.log(x)),
      )
      .subscribe(x => {
        this.sendInputValue.emit(x);
      });
  }

  ngOnInit() {
  }

  ngOnDestroy(): void {
    delete this.requestCompleted;
  }

  keyupHandler(e) {
    this.observableFilter.next(e.target.value);
  }
}

within parent component html

<hello (sendInputValue)="getInputValue($event)"></hello>

Parent component typescript

getInputValue(data){
   console.log(data); // this prints the data (Example: abc)
}

Click here Full working example with parent child component

First, let's make your input data observable - so that it's easier to implement everything else:

private inputData$ = new Subject<string>();

public getInputField(data: string){
  this.inputData$.next(data);
}

Now we can do whatever we want with this input stream. E.g. adapt the approach suggested by @Thorsten Rintelen above

ngOnInit() {
  this.inputData$
    .pipe(
      filter(data => data.length === 3),
      distinctUntilChanged(), // this makes sure api call is sent only if input has changed
      switchMap(data => this.myService.getDataFromService(data)),
    )
    .subscribe(apiResult => console.log(apiResult));
}

NOTE: This approach only caches the last input. If you want to cache and reuse all api responses, you could wrap your api service method into some caching layer, without changing anything else.

    private lastLetter = "";

    getInputField(data) {
     if(!this.lastLetter === data.toLowerCase()) && data.length === 3) {
        this.myService.getDataFromService(data).subscribe(rs=>{
        this.lastLetter = data.toLowerCase();
        console.log(rs)
        });
      }
    }

I presume this would work

I have made a generic function that will let you use it for any number of characters to limit your API call.

const cached = {};
/**
 * @desc Check if API call is allowed or not 
 * based on provided input and length condition
 * @param {String} input - input value
 * @param {Number} len   - length of the input over which API has to be called.
 * @returns {Boolean}
 */
function doCallAPI(input, len) {
  if(input.length === len) {
    if(!cached[input]) {
      // Call the API
      cached[input] = 1;
      return true;
    }
  }
  else if(input.length < len) for(let i in cached) cached[i] = 0;
  return false
}

Explanation:

  1. Check if the length of the input is equal to the conditional length (Here, 3).
    • IF YES, then, check if the cached object has a key with the input value and it has value (Such as 1)
      • IF NO, then, API is not called for this input value. Now,
      • SET cached[input value] = 1, to insert value in cached object.
    • Return true, to state API call is allowed.
  2. Check if the length (Say, 2) of the input is less than the conditional length.
  3. Then, loop through the cached object and set everything to 0, to tell that now on API call is allowed for cached values of conditional length (Here, 3).
  4. Return false, to tell API call is not allowed.

Here is how to use this,

getInputField(data){
 console.log(data); // this prints the data (Example: abc)
 //  then here I'm just executing the API call ONLY if data length is 3
  if(doCallAPI(data, 3)){
    this.myService.getDataFromService(data).subscribe(rs=>console.log(rs));
  }
}
Related