57
votes

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

4 Answers

100
votes

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

19
votes

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.

8
votes

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.

0
votes

I use a custom function that combines all class observables providing the following advantages:

  • Reduces async pipe count to 1 without the boilerplate proposed in other answers.
  • Allows you to synchronously read all observables in your component code
  • Allows you to view the current value of your observables in the Angular devtools extension.

COMPONENT CODE:

@Component({
  template: `
    selector: 'app-component',
    <ng-container *ngIf="observables$ | async; let observe;">
      <div *ngIf="observe.language$ && observe.user$">
        <b>{{observe.language$}}</b> and <b>{{observe.user$}}</b>
      </div>
    </ng-container>
  `
})
export class MyComponent {

  language$ = of('English');
  user$ = of('John');
  observables$ = combineComponentObservables<MyComponent>(this);

  ngAfterViewInit() {
    // We can read values synchronously from this field
    const value = this.observables$.value.language;
  }
}

UTILITY CODE:

import { EventEmitter } from '@angular/core';
import { combineLatest, Observable } from 'rxjs';
import { map } from 'rxjs/operators';

type FunctionParameter<T> = T extends (arg: infer H) => any ? H : never;
type ClassObservables<T> = {
  [I in keyof T]: T[I] extends Observable<any> ? FunctionParameter<Parameters<T[I]['subscribe']>[0]> : never;
};
type SubType<Base, Condition> = Pick<Base, {
  [Key in keyof Base]: Base[Key] extends Condition ? Key : never
}[keyof Base]>;
export type Observables<T> = ClassObservables<SubType<Omit<T, 'templateObservables$' | '$observables'>, Observable<any>>>;

export const combineComponentObservables = <T>(component: T): Observable<Observables<T>> & { value: Observables<T> } => {
  const keysOfObservableMembers = Object.keys(component)
    .filter(key => component[key] instanceof Observable && !(component[key] instanceof EventEmitter));
  const res = combineLatest(
    keysOfObservableMembers.map(key => component[key] as Observable<any>)
  ).pipe(
    map(observers => {
      const result = {};
      observers.forEach((obs, idx) => result[keysOfObservableMembers[idx]] = obs);
      (component as any).$observables = result;
      (res as any).value = result;
      return result as Observables<T>;
    })
  );
  return res as Observable<Observables<T>> & { value: Observables<T> };
};

NOTES:

  • To get template type-checking, You will need to be using a recent version of Angular with Ivy enabled. Also ensure that you have the Language Service enabled and that you enable strictTemplates in the angularCompilerOptions within your tsconfig.json.
  • The content within the ng-container will only display once all observables have resolved. Therefore you will experience issues if any observables that depend on dom elements. That being said, all other answers posted here suffer from the same problem. To prevent this from being an issue, I tend to use RouteResolvers to pre-fetch all API data.