Angular 2 - Show loading-information when (observableData | async) is not yet resolved

我的未来我决定 提交于 2020-06-24 08:12:01

问题


just as the title says, I want to embrace the power of rxjs Observables.

What I do now:

// dataview.html
<div *ngIf="isLoading">Loading data...div>
<ul *ngIf="!isLoading">
    <li *ngFor="let d of data">{{ d.value }}</li>
</ul>


// dataview.ts

data: any[] = [];
isLoading: boolean = false;

getData() {

this.isLoading = true;
this._api.getData().subscribe(
        data => {
            this.data = data;
            this.isLoading = false;
        },
        error => {
            this.error = error;
            this.isLoading = false;
        });
}

What I want to do:

1. Use async pipe in my template

  1. Make data an Observable array

  2. Still display loading information for the user

I'm a big fan of clean code, so how can this be done nicely using rxjs and Angular 2?


回答1:


This is how I do it. Also i use $ at the and of the variable name to remind me that it is a stream.

// dataview.html
<div *ngIf="isLoading$ | async">Loading data...</div>
<ul *ngIf="!(isLoading$ | async)">
    <li *ngFor="let d of data">{{ d.value }}</li>
</ul>


// dataview.ts

data: any[] = [];
isLoading$: BehaviorSubject<boolean> = new BehaviorSubject(false);

getData() {

this.isLoading$.next(true);

this._api.getData().subscribe(
        data => {
            this.data = data;
        },
        error => {
            this.error = error;
        },
        complete => {
            this.isLoading$.next(false);
        });
}



回答2:


I Came up with the following:

export enum ObsStatus {
  SUCCESS = 'Success',
  ERROR = 'Error',
  LOADING = 'Loading',
}

export interface WrapObsWithStatus<T> {
  status: ObsStatus;
  value: T;
  error: Error;
}

export function wrapObsWithStatus<T>(obs: Observable<T>): Observable<WrapObsWithStatus<T>> {
  return obs.pipe(
    map(x => ({ status: ObsStatus.SUCCESS, value: x, error: null })),
    startWith({ status: ObsStatus.LOADING, value: null, error: null }),
    catchError((err: Error) => {
      return of({ status: ObsStatus.ERROR, value: null, error: err });
    })
  );
}

And then in your component:

TS

public ObsStatus: typeof ObsStatus = ObsStatus;

public obs$: Observable<WrapObsWithStatus<YOUR_TYPE_HERE>> = wrapObsWithStatus(this.myService.getObs());

HTML

<div *ngIf="obs$ | async as obs" [ngSwitch]="obs.status">
  <div *ngSwitchCase="ObsStatus.SUCCESS">
    Success! {{ obs.value }}
  </div>

  <div *ngSwitchCase="ObsStatus.ERROR">
    Error! {{ obs.error }}
  </div>

  <div *ngSwitchCase="ObsStatus.LOADING">
    Loading!
  </div>
</div>



回答3:


I did it by using the async pipe. But this approach still required you to catch it manually to handle the error. See here for more detail.

app.component.html

<div class="wrapper">
    <div class="form-group" *ngIf="pickupLocations$ | async as pickupLocations; else loading">    
        <ul class="dropdown-menu" *ngIf="pickupLocations.length">
            <li *ngFor="let location of pickupLocations">
                <strong>{{location.Key}}</strong>
            </li>
        </ul>
        <span *ngIf="!pickupLocations.length">There are no locations to display</span>
    </div>

    <ng-template #loading>
        <i class="fa fa-circle-o-notch fa-spin fa-3x fa-fw"></i>
        <span class="sr-only">Loading...</span>
    </ng-template>
</div>

app.component.ts

this.pickupLocations$ = this.apiService.getPickupLocations(storeId);



回答4:


This is my current best attempt for displaying search results.

I thought about extending Observable somehow to include an isLoading property - or returning a tuple but in the end a helper function (in my service) that returns a pair of observables seems to be the cleanest way. Like you I was looking for some 'magic' but I can't see any better way to do it than this.


So in this example I have a FormGroup (a standard reactive form) which contains search criteria:

{ email: string, name: string } 

I get the search criteria from the form's valueChanges observable when it changes.

Component Constructor

Note: The search isn't actually run until the criteria change, which is why this is in the constructor.

// get debounced data from search UI
var customerSearchCriteria = this.searchForm.valueChanges.debounceTime(1000);

// create a pair of observables using a service (data + loading state)
this.customers = this.customersService.searchCustomers(customerSearchCriteria);

// this.customers.data => an observable containing the search results array
// this.customers.isLoading => an observable for whether the search is running or not

Search Service

public searchCustomers(searchCriteria: Observable<CustomersSearch>):
                       { data: Observable<CustomerSearchResult[]>, 
                         isLoading: Observable<boolean> }
{
    // Observable to track loading state
    var isLoading$ = new BehaviorSubject(false);

    // Every time the search criteria changes run the search
    var results$ = searchCriteria
                    .distinctUntilChanged()
                    .switchMap(criteria =>
                    {
                        // update isLoading = true
                        isLoading$.next(true);

                        // run search
                        var search$ = this.client.search(new CustomersSearch(criteria)).shareReplay();

                        // when search complete set isLoading = false
                        search$.subscribe({ complete: () => isLoading$.next(false) });

                        return search$;
                    })
                    .shareReplay();

    return { data: results$, isLoading: isLoading$ };
}

Need to find some way to make this generic, but that's pretty easy. Also note that if you don't care about isLoading you simply do searchCustomers(criteria).data and then you're just getting to the data.

Edit: needed to add an extra ShareReply to prevent search firing twice.

Component HTML

Use both customers.data and customers.isLoading as observables as normal. Remember customers is just an object with two observable properties on it.

<div *ngIf="customers.isLoading | async">Loading data...</div>
<ul *ngIf="!(customers.isLoading | async)">
    <li *ngFor="let d of customers.data | async">{{ d.email }}</li>
</ul>

Also note that you need the async pipe for both observables. I realize that looks a little clumsy for the isLoading, I believe that it is faster to use an observable than a property anyway. There could be a refinement to this, but I'm not yet an expert but would certainly welcome improvements.




回答5:


One way to do that without any member property could be evaluating the async observable results in the template: !(yourAsyncData$ | async) or !(yourAsyncData$ | async)?.length.

For instance: <p-dataView #dv [value]="bikes$ | async" [loading]="!(bikes$ | async)"> ... </p-dataview>




回答6:


Perhaps this could work for you. This show the data when the observable exists and there is async data. Otherwise shows a loading template.

<ul *ngIf="data$ && (data$ | async);else loading">
    <li *ngFor="let d of data$ | async">{{ d.value }}</li>
</ul>
<ng-template #loading>Loading...</ng-template>



来源:https://stackoverflow.com/questions/38262083/angular-2-show-loading-information-when-observabledata-async-is-not-yet-re

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!