20

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?

3
  • Route resolver is a convenient way to handle view prerequisites. Commented Oct 4, 2016 at 18:09
  • What do you mean Router resolver? I am in Ionic 2 RC0. There is no router I believe.
    – Hugh Hou
    Commented Oct 4, 2016 at 18:26
  • 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). Commented Oct 4, 2016 at 19:00

4 Answers 4

18

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.

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>
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?
    – Hugh Hou
    Commented Oct 4, 2016 at 18:25
  • 1
    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...
    – Sasxa
    Commented Oct 4, 2016 at 18:39
  • 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!
    – Hugh Hou
    Commented Oct 4, 2016 at 18:56
  • 8
    But this make lots of unecessary requests... :( Commented Dec 12, 2017 at 1:19
  • 3
    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.
    – Birchlabs
    Commented Oct 3, 2018 at 10:43
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>
3

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

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.