Display loading while using Observable with Async pipe in template
Asked Answered
R

4

17

Situation: I am using FirebaseObjectObservable to populate my Ionic 2 (rc0) template. Template code:

<ion-card-content>
  <p>{{(course | async)?.description}}</p>
  <br>
  <h2>Learning Objectives</h2>
  <ul>
    <li *ngFor = "let objective of (course | async)?.objectives">{{objective.text}}</li>
  </ul>
  <h2>Takeaway</h2>
  <ul>
    <li *ngFor = "let takeaway of (course | async)?.takeaways">{{takeaway.text}}</li>
  </ul>
</ion-card-content>

TS code:

this.course = this.af.database.object('/bbwLocations/courses/' + courseId); 

this.course is a Firebase Object Observable. Everything works! But whenever I come into the template, there is a flash of empty no data. Then all the data jump out! Very not UX friendly. So I want to use some kind of pre-loading strategy. But since there is not TS logic here. Everything is controlled in template level with async pipe. How would I add loading in this situation?

Rebekah answered 4/10, 2016 at 17:38 Comment(3)
Route resolver is a convenient way to handle view prerequisites.Safko
What do you mean Router resolver? I am in Ionic 2 RC0. There is no router I believe.Rebekah
I don't use Ionic, but at least this is achievable with A2 router angular.io/docs/ts/latest/guide/router.html#!#resolve-guard I'm not aware of how good I2 plays with A2 router. Anyway, the best way to treat it is to resolve the dependency before component initialization (e.g. before nav.push call if you use it for navigation).Safko
L
17

Maybe a little late but in case someone else is wondering how to manage this... What about using a template?

you could for example use something like:

<ion-card-content *ngIf='(course$ | async) as course; else loading'>
  <p>{{course.description}}</p>
  <br>
  <h2>Learning Objectives</h2>
  <ul>
     <li *ngFor = "let objective of course.objectives"> 
         {{objective.text}}</li>
  </ul>
  <h2>Takeaway</h2>
  <ul>
     <li *ngFor = "let takeaway of course.takeaways"> 
         {{takeaway.text}}</li>
  </ul>
</ion-card-content>

<ng-template #loading>
  Loading stuff...
</ng-template>

so your ion-card-content will be hidden, showing the #template, until the async pipe is loaded.

Layby answered 28/4, 2019 at 21:23 Comment(0)
R
12

You could do something like this:

<style>
  pre {
   color: orange;
   // or whatever you want
  }
</style>
<ion-card-content>
  <p>{{(course | async)?.description}}</p>
  <br>
  <h2>Learning Objectives</h2>
  <pre *ngIf="!(course | async)">loading objectives...</pre>
  <ul>
    <li *ngFor = "let objective of (course | async)?.objectives">{{objective.text}}</li>
  </ul>
  <h2>Takeaway</h2>
  <pre *ngIf="!(course | async)">loading takeaways...</pre>
  <ul>
    <li *ngFor = "let takeaway of (course | async)?.takeaways">{{takeaway.text}}</li>
  </ul>
</ion-card-content>
Rationalism answered 4/10, 2016 at 17:49 Comment(5)
This works. But if I want exit animation, like fade out the loader when ngIf become false. I won't be able to do that. Also, is that possible to tap into the observable and use Ionic Loader component for this?Rebekah
If you want to use ngIf I believe Animations are the way to go. You could use ngClass instead of ngIf and do animations/transitions with CSS. transition: 1s opacity 0.5s linear, and such...Rationalism
Could you update your solution and give me an example how to easily use animations to do a fadeout of the pre element? Thank you!Rebekah
But this make lots of unecessary requests... :(Equally
If it's duplicating your request: you are giving observers access to a cold observable. You should use share() or similar to setup course as a hot observable. this.course = this.httpClient.get('whatever').pipe(shareReplay(1)). this will store the most recent result, so that if you have multiple subscribers: they can observe a replay of the result instead of initiating a new HTTP request.Onset
E
4

Lets say we have an observable meals$. We need to show loader when the observable is getting resolved(i.e fetching data). Below is the solution.

<div *ngIf="meal$ | async as meal; else loading;" >
   //use the meal variable to show some data
</div>
<ng-template #loading>
   //Show your loader here
</ng-template>
Exam answered 22/6, 2020 at 10:51 Comment(0)
M
2

The problem with using ngIf="course$|async";else loading is that when the observable resolves to a falsy value like 0 or false we are stuck with the loader screen.

Inorder to bypass this problem, you can think about using custom pipes which are more robust

Step 1 : Create a loading.pipe.ts file

import { Pipe, PipeTransform } from "@angular/core"
import { Observable, isObservable, of } from "rxjs"
import { map, startWith, catchError } from "rxjs/operators"

@Pipe({
  name: "loading",
})
export class LoadingPipe implements PipeTransform {
  transform<T>(val: Observable<T>): Observable<{loading:boolean,value:T,error:unknown}> {
    return isObservable(val)
      ? val.pipe(
          map((value: any) => ({ loading: false, value })),
          startWith({ loading: true }),
          catchError((error) => of({ loading: false, error }))
        )
      : val
  }
}

Step 2: Register the pipe in the declarations array in your *.module.ts file

import { LoadingPipe } from "./loading.pipe"

@NgModule({
  declarations: [LoadingPipe,…],
  imports: [...],
  exports: [...],
})
export class SharedComponentsModule {}

Step 3: Usage in component file *.component.ts

<ng-container *ngIf="course$ | loading | async as course">
 <div *ngIf="course?.value">
  display my resolved values...
 </div>
 <div *ngIf="course?.error">
   Error!
 </div>
 <div *ngIf="course?.loading">
   my loader component...
 </div>
</ng-container>

Reference and credits Angular docs Async loading pipe

Memorialize answered 14/4, 2023 at 3:17 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.