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