Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Combine two or more (boolean) observables on single ngIf using async pipe

Without observables I can write the following line in the HTML template:

<div *ngIf="(myVarA || myVarB) && myVarC !== x"></div>

How do I translate this line if all myVar variables are now actually observables?

<div *ngIf="((myVarA | async) || (myVarB | async)) && (myVarC | async) !== x">

does not work.

In another question (Putting two async subscriptions in one Angular *ngIf statement) the possibility to combine two or more observables into one ngIf can be achieved like

<div *ngIf="{ a: myVarA | async, b: myVarB | async } as result"></div>

However, I do not see the possibility to use any boolean operators (or any operators for that matter) on the expression that is then being used to evaluate the ngIf.

How can I tackle this issue? Please note, all my Observables use a BehaviorSubject underneath. I think essentially what I want is to use the combineLatest operator inside the template.

Bonus: Is there any way to extract the single value of myVarA if the whole expression evaluates to true for later use in the template (as in myVarA | async as varA)?

like image 313
Sebastian Avatar asked Sep 07 '17 13:09

Sebastian


People also ask

Can I use async pipe in ngIf?

Use the async pipe with ngIf We above example uses the async pipe with interpolation. We can also use it with the ngIf or ngFor etc. The following example shows how NOT to use the observable with ngIf directive. The condition (obsValue | async) becomes true , when the observable returns a value.

Can we use async pipe in NgFor?

NgFor has a not-so-obvious feature that lets us will help us deal with asynchronous operations - the async pipe. The async pipe takes care of subscribing/unsubscribing to Observable streams for us.

What does pipe async do?

The async pipe subscribes to an Observable or Promise and returns the latest value it has emitted. When a new value is emitted, the async pipe marks the component to be checked for changes. When the component gets destroyed, the async pipe unsubscribes automatically to avoid potential memory leaks.


2 Answers

What about using combineLatest?

For example:

import { combineLatest } from 'rxjs/observable/combineLatest';
import { Observable } from 'rxjs/Observable';    

@Component({...})
export class FooComponent {
  obs1$: Observable<bolean>;
  obs2$: Observable<bolean>;
  obs3$: Observable<bolean>;

  constructor(){
    // set observables
  }

  get combined$(){
    return combineLatest(
      this.obs1$,
      this.obs2$
      this.obs3$,
      (one,two,three)=>(one || two) && three);
  }
}

// template
<div *ngIf="combined$ | async">

Check the following fiddle for guidance:

https://jsfiddle.net/uehasmb6/11/

More info about the combineLatest operator here

UPDATE: But if you still want to keep all that logic inside of the template, you could try something like:

<div *ngIf="((myVarA | async) || (myVarB | async)) && ((myVarC | async) !== x)">

But I would advice you against this. Keeping the HTML template as clean as possible is a good practice.

like image 101
Jota.Toledo Avatar answered Oct 24 '22 06:10

Jota.Toledo


Create helper observable 'generators' for common AND / OR / ALL logic

At first it's simple enough to put paymentSelected == false && mode == 'addPayment' but then when you need to add a new condition you have to update the UI in several places.

It's far better to expose an observable that's called showPaymentPanel$ and then it's clear in both the .ts and template file exactly what it's for : *ngIf="showPaymentPanel$ | async". This also makes it easier to test.

However I was ending up with a lot of code like this:

showTokenizedPaymentMethods$ = combineLatest(this.hasTokenizedPaymentMethods$, 
                                             this.showAvailablePaymentMethods$).
                              pipe(map(([ hasTokenizedMethods, showAvailableMethods ]) => 
                              showAvailableMethods && hasTokenizedMethods));

And that's a mess! Potentially even worse than multiple async pipes!

So I created helper functions to generate new observables: (globally somewhere)

export const allTrue = (...observables: Array<ObservableInput<boolean>> ) => combineLatest(observables).pipe(map(values => values.every(v => v == true) ), distinctUntilChanged());
export const allFalse = (...observables: Array<ObservableInput<boolean>> ) => combineLatest(observables).pipe(map(values => values.every(v => v == false) ), distinctUntilChanged());
export const anyTrue = (...observables: Array<ObservableInput<boolean>> ) => combineLatest(observables).pipe(map(values => values.find(v => v == true) != undefined ), distinctUntilChanged());
export const anyFalse = (...observables: Array<ObservableInput<boolean>> ) => combineLatest(observables).pipe(map(values => values.find(v => v == false) != undefined), distinctUntilChanged());

Note: These are not operators to be use in a pipe.

In the ts file you create the observable (named to be UI specific) like this:

public showPaymentPanel$ = allTrue(this.hasTokenizedPaymentMethods$, this.showAvailableMethods$);

I will typically create UI observables them even if an existing observable exists:

public showAccountDetails$ = this.isLoggedIn$;     // this condition is always subject to change

You can also compose them:

public showSomethingElse$ = allTrue(this.showPaymentPanel$, this.whateverItIs$);

Sometimes I'll expose them to the UI grouped together like this:

public ui = { this.showPaymentPanel$, this.showSomethingElse$ );

Then usage:

`*ngIf="ui.showPaymentPanel$ | async"` 

(only ui should be public so in the template it makes it super clear what you want to allow)

Limit to one pipe as much as possible!

like image 7
Simon_Weaver Avatar answered Oct 24 '22 07:10

Simon_Weaver