30

There's quite good doc of using *ngIf in Angular: https://angular.io/api/common/NgIf But, is that possible to have *ngIf async variable and multiple checks on that? Something like:

<div *ngIf="users$ | async as users && users.length > 1">
...
</div>

Of course, it's possible to use nested *ngIf, like:

<div *ngIf="users$ | async as users">
    <ng-container *ngIf="users.length > 1">
    ...
    </ng-container>
</div>

but it'd be really nice to use only one container, not two.

Vitaly
  • 580
  • 1
  • 4
  • 10

4 Answers4

26

Simply do it like this

<div *ngfor="let user of users$ | async" *ngIf="(users$ | async)?.length > 1">...</div>

For "more complex" scenario do the following

<div  *ngfor="let user of users$ | async" *ngIf="(users$ | async)?.length > 1 && (users$ | async)?.length < 5">...</div>

Edit: Previous wouldn't work since you cannot use *ngFor and *ngIf without using ng-template. You would do it like that for instance

<ng-template ngFor let-user [ngForOf]="users$ | async" *ngIf="(users$ | async)?.length > 1 && (users$ | async)?.length < 5">
  <div>{{ user | json }}</div>
</ng-template>

Here is a stackblitz.

alsami
  • 6,371
  • 3
  • 16
  • 29
  • 1
    yep, that's good suggestion, but what about more complex conditions:
    1 && users.length < 5">
    ...
    – Vitaly Mar 15 '18 at 10:18
  • what kind of ? you can have multiple statements like this. in one *ngIf, just chain em. – alsami Mar 15 '18 at 10:19
  • in general, i want to keep local variable for further usage with only one subscription and your solution has no variable and multiple subsriptions – Vitaly Mar 15 '18 at 10:21
  • Yeah you cannot keep that in a template bc it is an observable not a simple object. Subscribe in your component or always surround it with an async pipe. – alsami Mar 15 '18 at 10:23
  • I recommend using the Elvis operator to avoid errors, but otherwise good solution. –  Mar 15 '18 at 10:26
  • Updated the post. Ty @trichetriche – alsami Mar 15 '18 at 10:26
  • but still, they allow having variable (and hence only one subscription) in the simple case. Maybe in the future, they enhance the syntax for complex conditions... – Vitaly Mar 15 '18 at 10:31
  • They will not most likely bc it doesn't matter if you subscribe once or multiple times in a template. As soon as the component is destroyed they will unsubscribe it anyway for you. – alsami Mar 15 '18 at 10:37
  • Depending on treatements that occur on the observable, it's important to note that in case of multiple subscriptions, you do multiple times your treatments. – Johan Chouquet Oct 01 '18 at 15:59
  • In this case indeed you have three subscriptions. Angular will close the subscriptions for you so no need to worry that much. – alsami Oct 01 '18 at 16:32
  • 8
    Has anyone actually tested this? (I haven't but) In my understanding of Angular, you cannot use multiple structural directives on the same element. So *ngIf and *ngFor normally cannot be used together on the same element! – AlainD. Oct 11 '18 at 09:57
  • You are right. Forgot about that. Updating the answer now. – alsami Oct 11 '18 at 10:07
  • 1
    this solution will cause to fetch data from the server several time, due to using `async` multiple times. – xx yy Dec 20 '19 at 23:36
  • Depends on how you are doing it. With you have some sort of state management, like NGRX and selectors, it won't. – alsami Dec 21 '19 at 05:20
  • when i use async multiple times muliple times network request is made so cannot use ngIf anf ngFor together as leads to unnecessary request, of course ngFor is used on div after condition is checked using ngIf on parent div – Nikhil Kamani Jul 29 '20 at 12:24
  • Then you need to change the way you consume your data. You should create some sort of subject so that the request is not triggered twice. – alsami Jul 29 '20 at 14:46
16

I hit the same issue of needing an *ngIf + async variable with multiple checks.

This ended up working well for me.

<div *ngIf="(users$ | async)?.length > 0 && (users$ | async) as users"> ... </div>

or if you prefer

<div *ngIf="(users$ | async)?.length > 0 && (users$ | async); let users"> ... </div>

Explanation

Since the result of the if expression is assigned to the local variable you specify, simply ending your check with ... && (users$ | async) as users allows you to specify multiple conditions and specify what value you want the local variable to hold when all your conditions succeed.

Note

I was initially worried that using multiple async pipes in the same expression may create multiple subscriptions, but after some light testing (I could be wrong) it seems like only one subscription is actually made.

Keego
  • 2,949
  • 1
  • 14
  • 6
5

Here's the alternative version, with a little bit cleaner template:

<ng-template [ngIf]="(users$ | async)?.length > 1" [ngIfElse]="noUsersMessage">
  <div *ngFor="let user of (users$ | async)">{{ user | json }}</div>
</ng-template>

<ng-template #noUsersMessage>No users found</ng-template>

Note that we use users$ | async 2 times. That will work if you add shareReplay() operator to user$ Observable:

  public users$: Observable<any[]> = this.someService.getUsers()
    .pipe(shareReplay());

That way, inner template will be able to access last value of the Observable and display the results.

You can try it out on stackblitz.

im.pankratov
  • 1,380
  • 1
  • 12
  • 15
  • 1
    The use of share replay is a better approach. – Kieran Feb 11 '20 at 04:06
  • This should get upvoted. Multiple use of async pipe on the same observables will get multiple network call to the same resource if we inspect. In most common scenarios, shareReplay should be used to reduce that down to 1 call instead. – DriLLFreAK100 Apr 15 '21 at 03:27
5

I see everyone using *ngFor and *ngIf together in one tag and similar work-arounds, but I think that is an anti-pattern. In most regular coding languages, you don't do an if statement and a for loop on the same line do you? IMHO if you have to work around because they specifically don't want you to, you're not supposed to do that. Don't practice what's not "best practice."

✅✅✅ Keep it simple, if you don't need to declare the $implicit value from users$ | async as users:

<!-- readability is your friend -->
<div *ngIf="(users$ | async).length > 1"> ... </div>

But if you do need to declare as user, wrap it.


✅ For Complex Use Case; mind you, the generated markup will not even have an HTML tag, that's the beauty of ng-templates & ng-containers!

@alsami's accepted, edited answer works because *ngFor with asterisk is a shorthand for ng-template with ngFor (no asterisk), not to mention the double async pipe . The code really smells of inconsistency when you combine a no-asterisk ngFor with a *ngIf; you get the gist. The *ngIf takes precedence so why not just wrap it in a ng-container/ng-template? It won't wrap your inner elements using an HTML tag in the generated markup.

<div *ngIf="users$ | async as prods; then thenB; else elseB"></div>

<ng-template #thenB>
  <!-- inner logic -->
  <div *ngIf="prods?.length > 1 && users?.length < 5; else noMatchB">
    Loaded and inbound
  </div>
  <ng-template #noMatchB>Loaded and out of bound</ng-template>
</ng-template>

<ng-template #elseB>Content to render when condition is false.</ng-template>

❌ Don't do this

<!-- Big NO NO -->
<div *ngIf="(users$ | async)?.length > 1 && (users$ | async)?.length < 5"> ... </div>

Pipes are a bit daunting after you know about their sensitivity to life cycles; they update like mad. That's why Angular do NOT have SortPipe nor FilterPipe. So, erm, don't do this; you're basically creating 2 Observables that sometimes update frantically and may cause long-term data mismatch in its children if I'm correct.

David Nguyen
  • 51
  • 1
  • 1
  • what if the use case is like //this is done to avoid false and 0 1"> – Darpan Jun 16 '20 at 14:08
  • @Darpan For your case. You don't need to say `othervalue | async !== null` in the first line. The reason is `1"> ` **already checks for null & checks if it's larger than 1** due to the weird nature of JS; why do it twice to complicate it? – David Nguyen Jul 06 '20 at 18:06