Home > database >  How to fix the ng-template displaying before the actual result
How to fix the ng-template displaying before the actual result

Time:09-28

I have this code that searching by a specific word

html:

    <input type="input" #term>  

    <div *ngIf="term.value.length">
    <div *ngIf="data?.length !== 0; else no_data">
      <div *ngFor="let item of data">
        <span>{{ item }}</span>
      </div>
     </div>
     <ng-template #no_data>Not Found</ng-template>
    </div>

class:

    fromEvent(this.term.nativeElement, 'keyup')
      .pipe(
        map((event: any) => event.target.value),
        switchMap((term) => {
          return this.fakeRequest(term);
        })
      )
      .subscribe((data) => {
        this.data = data;
      });

The problem with this code is ng-template. Sometime it is displayed just before the actual result appear.

So there is no problems with this.fakeRequest as per stackbliz enter image description here

Steps to reproduce:

1 search for 'bla'  
2 remove 'bla'  
3 type 'al' or 'alpha'  

Here is a stackbliz demo

CodePudding user response:

Problem is your fakeRequest returns result after a delay and till then on ui it displays the already existing result or not found if there is nothing in the list.So the best way will be to add a property like isLoading in the component set this to true before you make the request and set it to false once request comes back something like

isLoading:boolean;
  ngOnInit() {
    fromEvent(this.term.nativeElement, 'keyup')
      .pipe(
        map((event: any) => event.target.value),
        switchMap((term) => {
          this.isLoading=true;
          return this.fakeRequest(term);
        })
      )
      .subscribe((data) => {
        this.isLoading=false;
        this.data = data;
      });
  }

then on ui use this property to display a loading block

<div *ngIf="term.value.length">
  <div *ngIf="!isLoading; else loading">
    <div *ngIf="data?.length !== 0; else no_data">
      <div *ngFor="let item of data">
        <span>{{ item }}</span>
      </div>
    </div>
  </div>

  <ng-template #no_data> Not Found </ng-template>
  <ng-template #loading>Loading.....</ng-template>
</div>

Demo

CodePudding user response:

Based on the answer of jitender I think the more reactive approach would be to implement the following:

data$: Observable<string[]>;
isLoading$: Observable<boolean>;
keyUp$: Observable<string>;

ngOnInit() {
  this.keyUp$ = fromEvent(this.term.nativeElement, 'keyup').pipe(
    map((event: any) => event.target.value)
  );
  this.data$ = this.keyUp$.pipe(
    switchMap((term) => {
      return this.fakeRequest(term);
    }),
    shareReplay(1)
  );

  this.isLoading$ = merge(
    this.keyUp$.pipe(mapTo(true)),
    this.data$.pipe(mapTo(false))
  );
}

and then in your HTML

<input type="input" #term />

<div *ngIf="!(isLoading$ | async); else loading">
  <div *ngIf="(data$ | async)?.length !== 0; else no_data">
    <div *ngFor="let item of data$ | async">
      <span>{{ item }}</span>
    </div>
  </div>
</div>

<ng-template #no_data> Not Found </ng-template>
<ng-template #loading>Loading.....</ng-template>

This way you have no side effects in your observable pipes and also you don't have to worry about unsubscribing. Of course the loading message is optional but I think the idea should be clear.

Update after feedback from comment The problem with your template approach and using if/else is that you basically want to display three different states:

  1. data was found
  2. no data was found
  3. nothing should be displayed since no input was given

It's hard to achieve this having only one variable, but nevertheless you can use your variable data to have states accordingly:

  1. data is array with length > 0
  2. data is array with length === 0
  3. data is null

You HTML would look like this:

<input type="input" #term />

<div *ngIf="data !== null && data?.length !== 0">
  <div *ngFor="let item of data">
    <span>{{ item }}</span>
  </div>
</div>
<ng-container *ngIf="data !== null && data?.length === 0">Not Found</ng-container>

and your component.ts would look like this:

@ViewChild('term', { static: true }) term: ElementRef;
data: string[] | null;

ngOnInit() {
  fromEvent(this.term.nativeElement, 'keyup')
    .pipe(
      map((event: any) => event.target.value),
      switchMap((term) => {
        return this.fakeRequest(term);
      })
    )
    .subscribe((data) => {
      console.log(data);
      this.data = data;
    });
}

fakeRequest(term: string): Observable<string[] | null> {
  if (term.length === 0) return of(null);
  const getResult = [
    'Abarth',
    'Alfa Romeo',
    'Aston Martin',
    'Audi',
    'Bentley',
    'BMW',
    'Bugatti',
    'Cadillac',
    'Chevrolet',
    'Chrysler',
    'Citroën',
    'Dacia',
    'Daewoo',
    'Daihatsu',
    'Dodge',
    'Donkervoort',
    'DS',
    'Ferrari',
  ].filter((el) =>
    el.toLocaleLowerCase().startsWith(term.toLocaleLowerCase())
  );

  return of(getResult).pipe(delay(1000));
}

Stackblitz preview: https://stackblitz.com/edit/angular-ivy-dn3j1w?file=src/app/app.component.html

  • Related