Angular 2 - 在 (observableData | async) 尚未解析时显示 loading-information

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

正如标题所说,我想拥抱 rxjs Observables 的强大功能。

我现在做什么:

// 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;
        });
}

我想做的事情:

1.在我的模板中使用 async 管道

  1. 使data成为一个Observable数组

  2. 仍然为用户显示加载信息

我是干净代码的忠实拥护者,那么如何使用 rxjs 和 Angular 2 很好地完成此操作?

我就是这样做的。此外,我在变量名的和处使用 $ 来提醒我这是一个流。

// 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);
        });
}

这是我目前对显示搜索结果的最佳尝试。

我考虑过以某种方式扩展 Observable 以包含一个 isLoading 属性 - 或者返回一个元组,但最后是一个辅助函数(在我的服务中),returns 一对可观察对象似乎是最干净的方法。和你一样,我一直在寻找一些 'magic',但我看不出有比这更好的方法了。


所以在这个例子中,我有一个 FormGroup(标准反应形式),其中包含搜索条件:

{ email: string, name: string } 

我从表单的 valueChanges 可观察到的变化中获取搜索条件。

组件构造函数

注意:搜索实际上 运行 直到标准发生变化,这就是构造函数中的原因。

// 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

搜索服务

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$ };
}

需要找到一些方法使它通用,但这很容易。另请注意,如果您不关心 isLoading,您只需执行 searchCustomers(criteria).data 然后您就可以获取数据。

编辑:需要添加额外的 ShareReply 以防止搜索触发两次。

组件HTML

像往常一样使用 customers.datacustomers.isLoading 作为可观察对象。请记住 customers 只是一个具有两个可观察属性的对象。

<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>

另请注意,两个可观察对象都需要 async 管道。我意识到 isLoading 看起来有点笨拙,我相信无论如何使用 observable 比 属性 更快。可以对此进行改进,但我还不是专家,但当然欢迎改进。

我是用异步管道做的。但是这种方法仍然需要你手动捕获它来处理错误。有关详细信息,请参阅 here

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);

在没有任何成员 属性 的情况下执行此操作的一种方法可能是评估模板中的异步可观察结果:!(yourAsyncData$ | async)!(yourAsyncData$ | async)?.length.

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

我想出了以下内容:

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 });
    })
  );
}

然后在你的组件中:

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>

也许这对你有用。当可观察对象存在且存在异步数据时,这会显示数据。否则显示加载模板。

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

使用合并的更优雅和反应性的方式。

// dataview.html
<ul *ngIf="data$ | async as data; else loading">
    <li *ngFor="let d of data">{{ d.value }}</li>
</ul>

<ng-template #loading>
  <div >Loading data...</div>
</ng-template>

// dataview.ts
data$: Observable<any[] | null>;

getData(): void {
  const showLoading$: Observable<any[]> = of([]);
  const request$: Observable<any[] | null> = this._api.getData().pipe(
    map(data => data), // disable loading on successfull request
    catchError(() => of([])) // disable loading on error request
  );

  // 1. showLoading$ will show loading before the request is made
  // 2. request$ then request comes in with delay and tell Us if success = any[] or error = null
  //                      1.          2.
  this.data$ = merge(showLoading$, request$);
}