1

I am looking for a way to use a loading bar component on an observable stream?

I have a component that displays data from an observable using the async pipe. This Observable can also manipulate the data such as using map.

Currently I have this in component:

 this.listData$ = this.http.get('/itemList')
            .map(data => this.manipulate(data))

and in template:

{{listData$ | async}}

The server can return the data as empty [], 'unavailable' or array of items.

How can I tap into the stream and feed a loading component that would display a loading bar when loading the data, and display error messages when empty or unavailable? Something along the lines of...

this.listData$ = this.http.get('/itemList')
                //.something here??
                .map(data => this.manipulate(data))


<loading-bar [data]='listData$' [noDataMsg]='"no data"' [errorMsg]='unavailable'>
    {{listData$ | async}}
</loading-bar>

Ideally I would like to keep using the async pipe and not subscribe in my component. But, passing the Observable to the loading-bar component and subscribing in there, that makes 2 http calls...then I need to .share() on the Observable...

What approach do you guys take to display loading, no data and error messages with your streams?

Thibs
  • 8,058
  • 13
  • 54
  • 85
  • Actually you can use some `loading` flag and set to true it before http and to false inside `map` if you want avoid subscribe. So your loading can listen `loading` variable state – VadimB Feb 16 '17 at 14:42
  • you can use `do` operator, it allows you to add an operation without using map, like `this.http.get(...).do(loading = false).map(... => ...);` – Supamiu Feb 16 '17 at 14:48

1 Answers1

1

Your idea to pass the observable to <loading-bar> seems fine.

If your only concern is to avoid multiple subscriptions, you have two options:

1. Share the original observable

this.listData$ = this.http.get('/itemList')
                   .map(data => this.manipulate(data))
                   .share();

2. Create a intermediary BehaviorSubject

The subject will act as a "buffer" between the original observable and the template:

  • You pipe the values of the original obs into your BehaviorSubject.
  • Your template subscribes (via async) to your BehaviorSubject.

There can still be multiple subscriptions from the template but they are to YOUR subject. The original observable, however, is only subscribed ONCE by your subject (which means the HTTP request is only executed once).

The code would go something like this (I haven't tested it):

In the class:

this.listDataBuffer$ = new BehaviorSubject<any>(null);
this.listData$.subscribe(listDataBuffer$);

In the template:

{{listDataBuffer$ | async}}

ADDITIONAL NOTES:

  1. You said your original observable emits ONE array of items. This means it's a binary operation: either you have ALL the items, or you have NONE. With this configuration, you'll never be able to display progress. You'll only be able to toggle between two statuses, "loading" and "complete".

  2. No need to use [ ] around your inputs when you're passing string literals:

// YOUR CODE:
<loading-bar [noDataMsg]='"no data"' [errorMsg]='"unavailable"'>

// COULD BE:
<loading-bar noDataMsg="no data" errorMsg="unavailable">
AngularChef
  • 13,797
  • 8
  • 53
  • 69
  • Merci! This seems like sound advice, I will try these concepts later today and report back. – Thibs Feb 16 '17 at 15:15
  • Looking closer at this: this.listDataBuffer$ = new BehaviorSubject(null); this.listData$.subscribe(listDataBuffer$); Doesn't it now mean I need to unsubscribe from listData$ in onDestroy? I'd assume using .share() is less of a pain than creating a subject and having to worry about unsubscribing, or am I missing something? Thanks again – Thibs Feb 17 '17 at 04:21
  • You are completely right. As per http://stackoverflow.com/a/40819423/1153681. Looking back on it, my solution seems like overkill, I will update my answer accordingly. – AngularChef Feb 17 '17 at 08:50
  • Thank-you, not sure if you know, but it is unclear at this point to me if the .share() under the covers does a .publishLast(). Meaning, if there is a 'slight' chance that the loading subscribes too late and misses the data, but so far so good as both the component and loading component appear to subscribe at the same time because of Angular's init cycle. Worse case I can always replace the .share() with .publishLast().refCount(). – Thibs Feb 17 '17 at 14:52
  • Good. I'm not share() expert. I did see here and there it was equivalent to .publishLast().refCount() but I've no idea if it actually uses these operators under the hood. – AngularChef Feb 17 '17 at 15:03