How to show a loading spinner while waiting on an observable getting data from a service in Angular

前端 未结 6 1396
情书的邮戳
情书的邮戳 2020-11-27 08:35

I have set up an observable service that helps me persist data, but I need a way to show a loading spinner while the data is waiting to come from the observable.

My

相关标签:
6条回答
  • 2020-11-27 08:51

    You can create own service that included behavior subject and public loading function. (app.service.ts)

    public loading: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false)
    public isLoading(state: boolean): void { this.loading.next(state) }
    

    And in your app.component.ts. Inject the AppService in constructor so can be observed:

    public loading$ = this.appService.loading.asObservable()
    

    In app.component.html view, import the shared spinner as below:

    <spinner [showSpinner]="loading$ | async"></spinner>
    

    The shared component @input showSpinner with *ngIf flag to decide to show the spinner or not.

    At final, in the your return api calling can write something like:

    this.appService.isLoading(true)
    return this.callApiByMethod(options).pipe(
      finalize(() => this.appService.isLoading(false)),
      shareReplay(),
    ) as Observable<T>
    

    I use rx finalize here so it will create a callback function that can handle with both success and error response.

    0 讨论(0)
  • 2020-11-27 08:57

    Thank you all for your responses, but none of these suggestions worked for the purposes outlined in the question, but I did work on a solution and eventually got one working. Here we go:

    In the service I created a new Observable called loading$ and set it up as a new BehaviorSubject<boolean>(true);

    Then I created a getter:

    getLoading(): Observable<boolean> { return this.loading$; }

    Then I wrapped the HTTP call with a true then set loading to false in the 3rd argument on the subscription (the onCompleted function).

    So the service looks like this:

    import { Observable, BehaviorSubject, of } from 'rxjs';
    
    @Injectable({
      providedIn: 'root'
    })
    export class OrganisationsService {
      private organisations$ = new BehaviorSubject<Organisation[]>([
        {
          [...]
        }
      ]);
    
      //initiate the new Loading variable via BehaviorSubject and set it to "true" from the beginning.
      public loading$ = new BehaviorSubject<boolean>(true);
    
      constructor(private http: HttpClient) {
    
        //set the loading$ to true again just before we start the HTTP call
        this.loading$.next(true);
    
    
        this.http
          .get(environment.apicall)
          .subscribe(
    
            //onNext method, where we get the data
            (data) => this.organisations$.next(data['organisations']),
    
            //onError method (Blank for now)
            () => {},
    
            //onComplated method, now that we have the data we can set the loading to false
            () => {
              this.loading$.next(false);
            }
          );
      }
    
      getLoading(): Observable<boolean> {
        return this.loading$;
      }
    
      getList(): Observable<Organisation[]> {
        return this.organisations$;
      }
    

    Notice in the subscribe method on the HTTP call, I'm adding a blank second method (This is for onError) and then in the 3rd method, I've added a function to set the loading to false: this.loading$.next(false); . This means that Loading will now be false when the subscription is complete.

    Then in my component, I get the loading$ subscription:

          public loading$: Observable<boolean>;
          public organisations$: Observable<Organisation[]>;
    
          constructor(public organisationsService: OrganisationsService) {}
    
          ngOnInit() {
            this.getData();
          }
    
          async getData() {
    
            //initially loading$ will be true because the service makes it so
            this.loading$ = this.organisationsService.getLoading();
    
            //when this completes, loading$ will be set to false via the service
            this.organisations$ = this.organisationsService.getList();
          }
    

    And in my view:

      <div>
        <div uk-spinner *ngIf="loading$ | async"></div>
        <ul class="uk-list">
          <li *ngFor="let organisation of organisations$ | async">
            {{ organisation.name }}
          </li>
        </ul>
      </div>
    
    0 讨论(0)
  • 2020-11-27 08:58

    You can create a loading pipe for that, the benefit is that you can use it everywhere in your app

    import { Pipe, PipeTransform } from '@angular/core';
    import { isObservable, of } from 'rxjs';
    import { map, startWith, catchError } from 'rxjs/operators';
    
    @Pipe({
      name: 'withLoading',
    })
    export class WithLoadingPipe implements PipeTransform {
     transform(val) {
       return isObservable(val)
          ? val.pipe(
             map((value: any) => ({ loading: false, value })),
             startWith({ loading: true }),
             catchError(error => of({ loading: false, error }))
          )
         : val;
     }
    }
    

    here is you http call

    httpCallObservable$ = this.httpClient.get(url);
    

    and use it like this in your template

     <div *ngIf="httpCallObservable$ | withLoading | async as data">
      <div [ngIf]="data.value">Value: {{ data.value }}</div>
      <ng-template [ngIf]="data.error">Error {{ data.error }}</ng-template>
      <ng-template [ngIf]="data.loading">Loading...</ng-template>
     </div>
    
    0 讨论(0)
  • 2020-11-27 09:01

    change the below line

    async getData() {
        this.organisations$ = this.organisationsService.getList();
      }
    

    to

    async getData() {
       this.organisations$ = this.organisationsService.getList();
       this.organisations$.subscribe((data) => 
         this.loading = false;
       );
      }
    

    this should work because this is the observable you are waiting for completion, not the getData method

    0 讨论(0)
  • You can more changes in OrganisationlistComponent

     export class OrganisationlistComponent implements OnInit {
    
          public loading = false; // Default false 
    
          public organisations$: Observable<Organisation[]>;
    
          constructor(public organisationsService: OrganisationsService) {}
    
          ngOnInit() {
    
            this.getData().then((data) => {
            this.organisations$ = data; 
            this.loading = false; //  false one response
            });
          }
    
         async getData() {
            this.loading = true //  true whene loading data;
            return  this.organisationsService.getList();  // return data
          }
    }
    
    0 讨论(0)
  • 2020-11-27 09:08

    Complementary my comment using the nils' operator. I like this solution because is "transparent", see that a call is simply,e.g.

    return this.httpClient.get('.....').pipe(
          indicate(this.loading$))
    

    Moreover, You can use with any observable, not only call http. Using rxjs operators for, e.g. make severals http call using forkJoin, or use switchMap to make two calls you has only a pipe, don't overload the calls and don't cause a flick

    The operator as

    export const prepare = <T>(callback: () => void) => {
        return (source: Observable<T>): Observable<T> =>
            defer(() => {
                callback();
                return source;
            });
    };
    
    export const indicate = <T>(indicator: Subject<boolean>) => {
        let alive = true;
        return (source: Observable<T>): Observable<T> =>
            source.pipe(
                prepare(() =>
                    timer(500)
                        .pipe(
                            takeWhile(() => alive),
                            take(1)
                        )
                        .subscribe(() => {
                            indicator.next(true);
                        })
                ),
                finalize(() => {
                    alive = false;
                    indicator.next(false);
                })
            );
    };
    
    export const toClass = <T>(ClassType: { new(): T }) => (
        source: Observable<T>
    ) => source.pipe(map(val => Object.assign(new ClassType(), val)));
    

    I put a timer because I don't want the "loading" is showed if the call not spend more than 500 miliseconds

    A service like

    export class DataService {
    
      loading$ = new Subject<boolean>()
    
    
      getData():Observable<any>
      {
        //one call, I simulate a simple response
        //in general will be like this.httpClient.get(....)
        const observable=of('New Data in'+new Date()).pipe(delay(1000))
    
        return observable.pipe(
          indicate(this.loading$))
      }
    }
    

    I has, e.g. in our app.component

    <button (click)="loadData()">load</button>
    {{data}}
    <div *ngIf="(dataService.loading$ | async)">
        loading...
    </div>
    

    And the function loadData

    export class AppComponent  {
      data:any
      //don't forget declare as public
      constructor(public dataService:DataService){}
      loadData()
      {
        this.dataService.getData().subscribe(res=>{
          this.data=res
        })
      }
    }
    

    Update Well, I make that $loading belongs to the service, but you can make that $loading belongs to the component and use the pipe in the component

      loading$ = new Subject<boolean>()
      loadData2()
      {
        this.data="";
        this.dataService.getData2().pipe(
          indicate(this.loading$))
          .subscribe(res=>{
          this.data=res
        })
      }
    
    <div *ngIf="(loading$ | async)">
        loading...
    </div>
    

    You can see in this stackblitz

    0 讨论(0)
提交回复
热议问题