Alexander Mills
Alexander Mills

Reputation: 100010

"return" asynchronous data using Observables

Say I have this in my HTML:

  <div>
    <mat-label>Matched events count: {{(getMatchedEventsCount() | async)?}}</mat-label>
  </div>
  <div>
    <mat-label>Total events count: {{(getTotalEventsCount() | async)?}}</mat-label>
  </div>

my question is, what do I return (as far as an Observable) from these helper methods?

  getMatchedEventsCount(){
    return Observable.of(3);
  }

  getTotalEventsCount(){
    return Observable.of(5);
  }

but my question is - how can we do something actually asynchronous?

At the moment, I am getting this HTML parse error though:

Uncaught Error: Template parse errors: Parser Error: Unexpected end of expression: Matched events count: {{(getMatchedEventsCount() | async)?}} at the end of the expression [Matched events count: {{(getMatchedEventsCount() | async)?}}] in ng:///AppModule/EventsListComponent.html@40:21 (" [ERROR ->]Matched events count: {{(getMatchedEventsCount() | async)?}} "): ng:///AppModule/EventsListComponent.html@40:21 Parser Error: Conditional expression (getMatchedEventsCount() | async)? requires all 3 expressions at the end of the expression [Matched events count: {{(getMatchedEventsCount() | async)?}}] in ng:///AppModule/EventsListComponent.html@40:21 (" [ERROR ->]Matched events count: {{(getMatchedEventsCount() | async)?}} "): ng:///AppModule/EventsListComponent.html@40:21

Upvotes: 1

Views: 592

Answers (1)

vince
vince

Reputation: 8306

I notice that you have already troubleshooted the ? in the comment thread. The reason the ? (called the "save navigation operator") does not work there is that it is guarding against null and undefined values in property paths, meaning you need to attempt to access a property after using the ?. Right now you are trying to use it retroactively to see if an object is null or undefined, but it can only look forward into the object, not backwards, and it needs a property to look for.

You are correct that you should return an Observable from the methods and give that to the async pipe. Here is some documentation on the async pipe just to be thorough: https://angular.io/api/common/AsyncPipe.

As to your question in the comment thread about how to use a Subscription to load the data instead of an Observable...

You can do that using the subscribe method and assigning the data to a property on your component, like so:

matchedEventsSub: Subscription;
matchedEventsCount: number;

getMatchedEventsCount() {
  this.matchedEventsSub = Observable.of(3).subscribe(value => {
    this.matchedEventsCount = value;
  });
}

Note that subscribeing to an Observable returns a Subscription. Then you must remember to unsubscribe from that subscription in your OnDestroy lifecycle hook to prevent memory leaks:

ngOnDestroy() {
  if (this.matchedEventsSub) { this.matchedEventsSub.unsubscribe(); }
}

As you can imagine, this becomes cumbersome when you have 2, 3, 10 subscriptions in one component. That's why the Angular team created the async pipe.

Finally,

how can we do something actually asynchronous?

It's actually quite simple. Let's say you have an EventsService that you inject into your component:

constructor(private eventsService: EventService) {}

That service may encapsulate an Http request or something -- Angular's HttpClient module uses Observables to represent asynchronous data. You could use your EventsService to get your asynchronous stream of events like this:

matchedEventsCount$: Observable<number>;

getMatchedEventsCount(): Observable<number> {
  const allEvents$ = this.eventsService.getEvents();
  return allEvents$.map(events => events.length);
}

Call the method in your OnInit lifecycle hook to populate your data:

ngOnInit() {
  this.getMatchedEventsCount();
}

And then display it in your template:

<h1>Events: {{ matchedEventsCount$ | async }}</h1>

Upvotes: 1

Related Questions