16

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)?

Jota.Toledo
  • 27,293
  • 11
  • 59
  • 73
Sebastian
  • 5,177
  • 4
  • 30
  • 47
  • Is it possible to make a new observable that combines the two? An alternative is to use an object variable that starts off as undefined, or null, and after the observables have sent the next value, you can set your object variable to something. – John Sep 07 '17 at 13:12
  • observables are used to get stream of values in your case the variables shall have only one value at a time. you either subscribe to array of observable and then use async for only array not single observable – Sunil Kumar Sep 07 '17 at 13:26
  • @John I am using the reactive approach with ChangeDetectionStrategy.onPush so I can't use an object in the code that is then being updated in the template. Combining the observables using combineLatest was my ideas but I really would like to avoid doing that in the code – Sebastian Sep 07 '17 at 13:34
  • @SunilKumar How do I subscribe to an array of observables? – Sebastian Sep 07 '17 at 13:35

3 Answers3

24

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.

Jota.Toledo
  • 27,293
  • 11
  • 59
  • 73
  • Thanks. I found this out, too. So there is no way of doing this really just in the ngIf with some cool syntax, only outside of the template? – Sebastian Sep 07 '17 at 19:26
  • Updated my answer, I have already made stuff like this in some projects and works fine – Jota.Toledo Sep 07 '17 at 19:29
  • Interesting. So I was indeed just missing one set of parentheses... I agree that the new explicit pipe is cleaner, though. – Sebastian Sep 07 '17 at 21:32
  • @Sebastian yup. If this solved your question consider to close it by selecting this as answer – Jota.Toledo Sep 08 '17 at 09:23
7

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!

Community
  • 1
  • 1
Simon_Weaver
  • 140,023
  • 84
  • 646
  • 689
  • 1
    I expected someone to have already made a library with all these situations in mind. If anyone finds something similar please post here :-) – Simon_Weaver Jan 16 '19 at 23:40
  • 1
    Exactly what I was searching for. So much duplicate code can be avoided with those operators! – afrish Jul 27 '20 at 15:18
1

Jota.Toledo's answer is perfectly fine, however I'd like to follow up on my 'bonus' question of how to access a part of the expression after the ngIf.

Essentially all that I'm doing is to combine two or more observables in the class as Jota.Toledo described. However one observable is not boolean but contains items that need to be available after the ngIf.

Then it is easy to just do the following:

newObs = combineLatest(
  this.itemObs,
  this.boolObs1
  this.boolObs3,
  (item, bool1, bool2) => ((item || bool1) && bool2) ? item : null
);

since we can rely on the truthyness of a non-null object in the ngIf. The ngIf then just looks like

<div *ngIf="newObs | async as item"></div>

This will of course not work if more than one item is required after the ngIf. In this case you would have to use two ngIfs on nested div sections.

Sebastian
  • 5,177
  • 4
  • 30
  • 47