*ngIf and *ngFor on same element causing error
Asked Answered
T

19

649

I'm having a problem with trying to use Angular's *ngFor and *ngIf on the same element.

When trying to loop through the collection in the *ngFor, the collection is seen as null and consequently fails when trying to access its properties in the template.

@Component({
  selector: 'shell',
  template: `
    <h3>Shell</h3><button (click)="toggle()">Toggle!</button>

    <div *ngIf="show" *ngFor="let thing of stuff">
      {{log(thing)}}
      <span>{{thing.name}}</span>
    </div>
  `
})

export class ShellComponent implements OnInit {

  public stuff:any[] = [];
  public show:boolean = false;

  constructor() {}

  ngOnInit() {
    this.stuff = [
      { name: 'abc', id: 1 },
      { name: 'huo', id: 2 },
      { name: 'bar', id: 3 },
      { name: 'foo', id: 4 },
      { name: 'thing', id: 5 },
      { name: 'other', id: 6 },
    ]
  }

  toggle() {
    this.show = !this.show;
  }

  log(thing) {
    console.log(thing);
  }

}

I know the easy solution is to move the *ngIf up a level but for scenarios like looping over list items in a ul, I'd end up with either an empty li if the collection is empty, or my lis wrapped in redundant container elements.

Example at this plnkr.

Note the console error:

EXCEPTION: TypeError: Cannot read property 'name' of null in [{{thing.name}} in ShellComponent@5:12]

Am I doing something wrong or is this a bug?

Toting answered 7/1, 2016 at 14:37 Comment(2)
#40530037 i'd go with ng-containerStrip
Possible duplicate of Angular filtered tableFrigorific
E
910

Angular v2 doesn't support more than one structural directive on the same element.
As a workaround use the <ng-container> element that allows you to use separate elements for each structural directive, but it is not stamped to the DOM.

<ng-container *ngIf="show">
  <div *ngFor="let thing of stuff">
    {{log(thing)}}
    <span>{{thing.name}}</span>
  </div>
</ng-container>

Updates for Angular v17 and beyond

The <ng-container> approach can no longer be recommended, since the introduction of the control flow in Angular v17.

@if(show){
  @for(thing of stuff; track thing.id){
  <div>
    {{log(thing)}}
    <span>{{thing.name}}</span>
  </div>
 }
}
Equiponderance answered 19/9, 2016 at 5:27 Comment(13)
Thanks a lot. Surprisingly is still undocumented: github.com/angular/angular.io/issues/2303Gaullist
How will code look like when we have to have *ngIf inside *ngFor ? I.e. IF condition will be based on value of a loop element.Corin
Just put ngFor at the <ng-container> element and the ngIf at the <div>. You can also have two nested <ng-container> wrapping the <div>. <ng-container> is just a helper element that will not be added to the DOM.Insurrectionary
@user3640967 I agree with both. There is a lot of documentation but ng-container seems too much-needed functionality for everyone, should be better documented, Don't you think?Gaullist
<md-autocomplete #auto="mdAutocomplete" [displayWith]="displayFn"> <template *ngIf="!selectedCategories.includes(category)"> <md-option *ngFor="let category of categories" [value]="category" (onSelectionChange)="selectCategory($event, category.id)"> {{ category.title }} </md-option> </template> </md-autocomplete> It shows nothing in my case. If I just ngFor over md-option (without the <template *ngIf="">, I actually have options... Sorry, but it doesn't do multiline hereMolokai
*ngIf doesn't work on <template>. Either use [ngIf]="..." or <ng-container *ngIf="..."Insurrectionary
Yup, it's a single ` and no space after it. I've just realized, used [ngIf]. It works if I explicitly put true or false. So, there must be a problem with my !selectedCategories.includes(category) condition (selectedCategories is an array of objects, of the category form - {"id": 1, "title": "Some title"} Anyway, I'm already off topic. Using <template> should work.Molokai
I'd suggest using <ng-container>. It behaves the same as <template> but allows to use the "normal" syntax for structural directives.Insurrectionary
Documentation says: "One structural directive per host element": "There's an easy solution for this use case: put the *ngIf on a container element that wraps the *ngFor element." - just reiteratingIconium
@Iconium thanks for the link. Such docs didn't exist back then when I posted this answer though :DInsurrectionary
@GünterZöchbauer Yeah, i realized that. I'm just updating the thread for future reference :)Iconium
Awesome answer. Fairly the answer everyone should be looking for.Kalpak
Great! The best thing is (conditional rendering + not stamping to DOM). Docs: one-structural-directive-per-elementCosmo
B
110

As everyone pointed out even though having multiple template directives in a single element works in angular 1.x it is not allowed in Angular 2. you can find more info from here : https://github.com/angular/angular/issues/7315

2016 angular 2 beta

solution is to use the <template> as a placeholder, so the code goes like this

<template *ngFor="let nav_link of defaultLinks"  >
   <li *ngIf="nav_link.visible">
       .....
   </li>
</template>

but for some reason above does not work in 2.0.0-rc.4 in that case you can use this

<template ngFor let-nav_link [ngForOf]="defaultLinks" >
   <li *ngIf="nav_link.visible">
       .....
   </li> 
</template>

Updated Answer 2018

With updates, right now in 2018 angular v6 recommend to use <ng-container> instead of <template>

so here is the updated answer.

<ng-container *ngFor="let nav_link of defaultLinks" >
   <li *ngIf="nav_link.visible">
       .....
   </li> 
</ng-container>
Bodycheck answered 14/7, 2016 at 16:22 Comment(1)
You saved my day, thank you. one question why ng-container doesn't show HTML impact with styling and allMousetail
N
29

As @Zyzle mentioned, and @Günter mentioned in a comment (https://github.com/angular/angular/issues/7315), this is not supported.

With

<ul *ngIf="show">
  <li *ngFor="let thing of stuff">
    {{log(thing)}}
    <span>{{thing.name}}</span>
  </li>
</ul>

there are no empty <li> elements when the list is empty. Even the <ul> element does not exist (as expected).

When the list is populated, there are no redundant container elements.

The github discussion (4792) that @Zyzle mentioned in his comment also presents another solution using <template> (below I'm using your original markup ‐ using <div>s):

<template [ngIf]="show">
  <div *ngFor="let thing of stuff">
    {{log(thing)}}
    <span>{{thing.name}}</span>
  </div>
</template>

This solution also does not introduce any extra/redundant container elements.

Naturopathy answered 7/1, 2016 at 15:59 Comment(1)
I'm not sure why this isn't the accepted answer. <template> is the way to add a parent element that won't show up in the output.Zacek
M
11

I solve my problem with this

<ng-container *ngFor="let item of menuItems">
  <li *ngIf="canShowItem(item)"></li>
</ng-container>
Matchboard answered 12/10, 2021 at 16:40 Comment(0)
F
9

in html:

<div [ngClass]="{'disabled-field': !show}" *ngFor="let thing of stuff">
    {{thing.name}}
</div>

in css:

.disabled-field {
    pointer-events: none;
    display: none;
}
Freddafreddi answered 19/4, 2019 at 5:14 Comment(0)
K
7

This will work but the element will still in the DOM.

.hidden {
    display: none;
}

<div [class.hidden]="!show" *ngFor="let thing of stuff">
    {{log(thing)}}
    <span>{{thing.name}}</span>
</div>
Kalpa answered 15/8, 2016 at 17:25 Comment(1)
This is a very easy hack for <select> <option> combination, which I simply want to show filtered items instead of the full listCleanthes
S
6

You can not use more than one Structural Directive in Angular on the same element, it makes a bad confusion and structure, so you need to apply them in 2 separate nested elements(or you can use ng-container), read this statement from Angular team:

One structural directive per host element

Someday you'll want to repeat a block of HTML but only when a particular condition is true. You'll try to put both an *ngFor and an *ngIf on the same host element. Angular won't let you. You may apply only one structural directive to an element.

The reason is simplicity. Structural directives can do complex things with the host element and its descendents. When two directives lay claim to the same host element, which one takes precedence? Which should go first, the NgIf or the NgFor? Can the NgIf cancel the effect of the NgFor? If so (and it seems like it should be so), how should Angular generalize the ability to cancel for other structural directives?

There are no easy answers to these questions. Prohibiting multiple structural directives makes them moot. There's an easy solution for this use case: put the *ngIf on a container element that wraps the *ngFor element. One or both elements can be an ng-container so you don't have to introduce extra levels of HTML.

So you can use ng-container (Angular4) as the wrapper (will be deleted from the dom) or a div or span if you have class or some other attributes as below:

<div class="right" *ngIf="show">
  <div *ngFor="let thing of stuff">
    {{log(thing)}}
    <span>{{thing.name}}</span>
  </div>
</div>
Shelby answered 2/7, 2017 at 8:26 Comment(0)
P
5

You can't have ngFor and ngIf on the same element. What you could do is hold off on populating the array you're using in ngFor until the toggle in your example is clicked.

Here's a basic (not great) way you could do it: http://plnkr.co/edit/Pylx5HSWIZ7ahoC7wT6P

Penury answered 7/1, 2016 at 14:48 Comment(5)
Why he cant have both? Elaborate pleaseChristine
There's a discussion around that here github.com/angular/angular/issues/4792Penury
I know why that's happening, it's just to improve quality of the answer, plainly saying you can't is not really a good answer, wont you agree?Christine
Sure, they shouldn't be used together just because putting them in certain order to template doesn't guarantee that they will be executed in the same order. But this does not explain what exactly happens when 'Cannot read property 'name' of null' is thrown.Haematoxylon
Both *ngFor and *ngIf (with asterisk) are structural directives and they generate <template> tag. Structural directives, like ngIf, do their magic by using the HTML 5 template tag.Lighting
T
4

You can also use ng-template (instead of template. See the note for the caveat of using template tag) for applying both *ngFor and ngIf on the same HTML element. Here is an example where you can use both *ngIf and *ngFor for the same tr element in the angular table.

<tr *ngFor = "let fruit of fruiArray">
    <ng-template [ngIf] = "fruit=='apple'>
        <td> I love apples!</td>
    </ng-template>
</tr>

where fruiArray = ['apple', 'banana', 'mango', 'pineapple'].

Note:

The caveat of using just the template tag instead of ng-template tag is that it throws StaticInjectionError in some places.

Tide answered 1/2, 2019 at 10:39 Comment(2)
Looks like a neat solution!Baccivorous
Thank you @BaccivorousTide
S
4

I didn't want to wrap my *ngFor into another div with *ngIf or use [ngClass], so I created a pipe named show:

show.pipe.ts

export class ShowPipe implements PipeTransform {    
  transform(values: any[], show: boolean): any[] {
    if (!show) {
      return[];
    }
    return values;
  }
}

any.page.html

<table>
  <tr *ngFor="let arr of anyArray | show : ngIfCondition">
    <td>{{arr.label}}</td>
  </tr>
</table>
Sulfanilamide answered 23/10, 2020 at 8:3 Comment(0)
L
3

Updated to angular2 beta 8

Now as from angular2 beta 8 we can use *ngIf and *ngFor on same component see here.

Alternate:

Sometimes we can't use HTML tags inside another like in tr, th (table) or in li (ul). We cannot use another HTML tag but we have to perform some action in same situation so we can HTML5 feature tag <template> in this way.

ngFor using template:

<template ngFor #abc [ngForOf]="someArray">
    code here....
</template>

ngIf using template:

<template [ngIf]="show">
    code here....
</template>    

For more information about structural directives in angular2 see here.

Lighting answered 4/4, 2016 at 11:52 Comment(0)
M
3

Table below only lists items that have a "beginner" value set. Requires both the *ngFor and the *ngIf to prevent unwanted rows in html.

Originally had *ngIf and *ngFor on the same <tr> tag, but doesn't work. Added a <div> for the *ngFor loop and placed *ngIf in the <tr> tag, works as expected.

<table class="table lessons-list card card-strong ">
  <tbody>
  <div *ngFor="let lesson of lessons" >
   <tr *ngIf="lesson.isBeginner">
    <!-- next line doesn't work -->
    <!-- <tr *ngFor="let lesson of lessons" *ngIf="lesson.isBeginner"> -->
    <td class="lesson-title">{{lesson.description}}</td>
    <td class="duration">
      <i class="fa fa-clock-o"></i>
      <span>{{lesson.duration}}</span>
    </td>
   </tr>
  </div>
  </tbody>

</table>
Musket answered 19/12, 2016 at 19:59 Comment(1)
I don't think a <div> inside a table is a goos idea, especially when there are better alternatives. Have you checked if thus works in IE which is especially picky about elements in <table>Insurrectionary
P
1
<div *ngFor="let thing of show ? stuff : []">
  {{log(thing)}}
  <span>{{thing.name}}</span>
</div>
Phanotron answered 16/1, 2018 at 10:13 Comment(0)
D
1

On other solution might be to put an empty array in your for loop in the case where you don't want to display it

<div *ngFor="let thing of show ? stuff : []">

Where "stuff" is an array of "thing" and "show" the boolean to display or not the content

Delicatessen answered 2/9, 2020 at 14:24 Comment(0)
P
0

<!-- Since angular2 stable release multiple directives are not supported on a single element(from the docs) still you can use it like below -->


<ul class="list-group">
                <template ngFor let-item [ngForOf]="stuff" [ngForTrackBy]="trackBy_stuff">
                    <li *ngIf="item.name" class="list-group-item">{{item.name}}</li>
                </template>
   </ul>
Periodic answered 2/11, 2016 at 10:55 Comment(2)
li items are only displayed if it has a name.Periodic
How does this answer add value here? It doesn't provide anything that's not provided by the other answers already or did I miss something?Insurrectionary
C
0

You can't use multiple structural directive on same element. Wrap your element in ng-template and use one structural directive there

Cartomancy answered 17/11, 2018 at 16:28 Comment(0)
T
0

You can do this another way by checking the array length

<div *ngIf="stuff.length>0">
  <div *ngFor="let thing of stuff">
    {{log(thing)}}
    <span>{{thing.name}}</span>
  </div>
</div>
Tadd answered 14/4, 2020 at 7:45 Comment(0)
P
0

Can two structural directives work together if one of them is *ngIf ?

No, you can't use multiple structural directives on same element If any of the directive is *ngIf then other directives will fail.

To avoid this directive failure, either you can use *ngSwitch or use ng-container tag for the loop and inside you can use ngIf to have a working code

Like below

    <ng-container *ngFor="let e of emp;">
        <tr *ngIf="e.name == txtname.value">
            <td>{{ e.name }}</td>
        </tr>
    </ng-container>
Pinkeye answered 12/3, 2023 at 10:18 Comment(0)
A
-1

With angular 17, the question is deprecated since *ngIf and *ngFor are replaced by the new syntax:

@if(...){}
@for(...){}
Anoxemia answered 12/12, 2023 at 8:38 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.