just as the title says, I want to embrace the power of rxjs Observables.
What I do now:
// dataview.html
Loading data
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>
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);
});
}
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>
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);
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>
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.
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
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.
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.