87

Been trying to combine two observables into one *ngIf and show the user interface when both have emitted.

Take:

<div *ngIf="{ language: language$ | async, user: user$ | async } as userLanguage">
    <b>{{userLanguage.language}}</b> and <b>{{userLanguage.user}}</b>
</div>

From: Putting two async subscriptions in one Angular *ngIf statement

This works as far as it compiles however in my case language$ and user$ would be from two HTTP requests and it seems user$ throws runtime errors like TypeError: _v.context.ngIf.user is undefined.

Essentially what I really want is (this doesn't work):

<div *ngIf="language$ | async as language && user$ | async as user">
    <b>{{language}}</b> and <b>{{user}}</b>
</div>

Is the best solution:

  • Subscribe inside the component and write to variables
  • To combine the two observables inside the component with say withLatestFrom
  • Add null checks {{userLanguage?.user}}
Frederik Struck-Schøning
  • 12,981
  • 8
  • 59
  • 68
Phil
  • 4,012
  • 5
  • 39
  • 57

3 Answers3

140

This condition should be handled with nested ngIf directives:

<ng-container *ngIf="language$ | async as language">
  <div *ngIf="user$ | async as user">
    <b>{{language}}</b> and <b>{{user}}</b>
  </div>
<ng-container>

The downside is that HTTP requests will be performed in series.

In order to perform them concurrently and still have language and user variables, more nesting is required:

<ng-container *ngIf="{ language: language$ | async, user: user$ | async } as userLanguage">
  <ng-container *ngIf="userLanguage.language as language">
    <ng-container *ngIf="userLanguage.user as user">
      <div><b>{{language}}</b> and <b>{{user}}</b></div>
    </ng-container>
  </ng-container>
</ng-container>

More efficient way way to do this is to move logic from template to component class at this point and create a single observable, e.g. with withLatestFrom

Frederik Struck-Schøning
  • 12,981
  • 8
  • 59
  • 68
Estus Flask
  • 206,104
  • 70
  • 425
  • 565
  • 16
    The last sentence in this answer is the answer! Create a single observable in the component class instead of nesting! – KTCO May 28 '19 at 20:26
  • 1
    If you're only going to use the "combined observable" in the template anyway, why write it out longhand in the class, when the object syntax is easy to read and understand, and lives in the same place where it's being used? I would just get rid of the two inner `ng-container`s, and refer to `obj.user` / `obj.language` directly in the templates. – Coderer Apr 29 '20 at 13:47
  • 1
    @Coderer The OP has the requirement to use values as `{{language}} and {{user}}`. `ngIf` is the way `language`, etc temporary variables can be assigned in a template without the use of third-party directives. In case this is unnecessary, nested `ng-container`s can be omitted. – Estus Flask Apr 29 '20 at 16:56
  • Thanks for this! creating an object of piped async members is what I was looking to do :) – Ian Grainger Dec 11 '20 at 13:19
41

You can also use the following trick. You will need one additional nesting.

<ng-container *ngIf="{a: stream1$ | async, b: stream2$ | async, c: stream3$ | async} as o">
  <ng-container *ngIf="o.a && o.b && o.c">
    {{o.a}} {{o.b}} {{o.c}}
  </ng-container>
</ng-container>

The object o is ever truthy, therefore the first *ngIf is simple used to save the stream values. inside you have to namespace your variables with o.

westor
  • 1,426
  • 1
  • 18
  • 35
  • 2
    Excellent, haven't seen the outter `ngIf` object approach before – Drenai Jul 29 '20 at 22:55
  • 5
    I like that this is an option, but really, this is the same as myStreams$=combineLatest([stream$1,stream2$,stream3$], (a, b, c,) => ({a, b, c})).pipe( filter((c) => Object.values(c).every((v) => v != null)) ); and then ... – chrismarx Aug 06 '20 at 16:21
  • 2
    @chrismarx that would be a better approach all right - but the template solution is interesting too – Drenai Aug 15 '20 at 22:01
  • @chrismarx, could `filter((c) => Object.values(c).every((v) => v != null))` be extracted as a function reference like this: `pipe(filterNonNull)` ... and re-used? I tried it but got confused how to make it generic. – andy Mar 03 '23 at 21:06
  • @andy yes, maybe review how rxjs pipes work - https://stackoverflow.com/a/69377067/228369 – chrismarx Mar 03 '23 at 22:20
9

That's depend what do you want but I think forkJoin operator with a loaded flag, could be a good idea.

https://www.learnrxjs.io/operators/combination/forkjoin.html

The forkJoin wait that all Observable are completed to return their values in its subscribe

Observable.forkJoin(
  Observable.of("my language").delay(1000),
  Observable.of("my user").delay(1000),
).subscribe(results => {
  this.language = results[0]
  this.user = results[1]
})

You can catch errors into onError of the subscribe and display it.

Frederik Struck-Schøning
  • 12,981
  • 8
  • 59
  • 68
mickaelw
  • 1,453
  • 2
  • 11
  • 28