*ngIf and *ngFor on same element causing error

375,028

Solution 1

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>

<ng-template> (<template> before Angular v4) allows to do the same but with a different syntax which is confusing and no longer recommended

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

Solution 2

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>

Solution 3

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 &dash; 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.

Solution 4

in html:

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

in css:

.disabled-field {
    pointer-events: none;
    display: none;
}

Solution 5

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>
Share:
375,028
garethdn
Author by

garethdn

Updated on July 20, 2022

Comments

  • garethdn
    garethdn almost 2 years

    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?

  • maurycy
    maurycy over 8 years
    Why he cant have both? Elaborate please
  • Zyzle
    Zyzle over 8 years
    There's a discussion around that here github.com/angular/angular/issues/4792
  • maurycy
    maurycy over 8 years
    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?
  • Estus Flask
    Estus Flask over 8 years
    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.
  • Evan Plaice
    Evan Plaice over 8 years
    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.
  • Pardeep Jain
    Pardeep Jain about 8 years
    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.
  • Rajiv
    Rajiv over 7 years
    li items are only displayed if it has a name.
  • Günter Zöchbauer
    Günter Zöchbauer over 7 years
    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?
  • Günter Zöchbauer
    Günter Zöchbauer over 7 years
    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>
  • Alex Fuentes
    Alex Fuentes over 7 years
    Thanks a lot. Surprisingly is still undocumented: github.com/angular/angular.io/issues/2303
  • Yuvraj Patil
    Yuvraj Patil over 7 years
    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.
  • Günter Zöchbauer
    Günter Zöchbauer over 7 years
    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.
  • Alex Fuentes
    Alex Fuentes over 7 years
    @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?
  • davyzhang
    davyzhang about 7 years
    This is a very easy hack for <select> <option> combination, which I simply want to show filtered items instead of the full list
  • MrCroft
    MrCroft about 7 years
    <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 here
  • Günter Zöchbauer
    Günter Zöchbauer about 7 years
    *ngIf doesn't work on <template>. Either use [ngIf]="..." or <ng-container *ngIf="..."
  • MrCroft
    MrCroft about 7 years
    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.
  • Günter Zöchbauer
    Günter Zöchbauer about 7 years
    I'd suggest using <ng-container>. It behaves the same as <template> but allows to use the "normal" syntax for structural directives.
  • heringer
    heringer about 6 years
    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 reiterating
  • Günter Zöchbauer
    Günter Zöchbauer about 6 years
    @heringer thanks for the link. Such docs didn't exist back then when I posted this answer though :D
  • heringer
    heringer about 6 years
    @GünterZöchbauer Yeah, i realized that. I'm just updating the thread for future reference :)
  • Hidayt Rahman
    Hidayt Rahman almost 4 years
    You saved my day, thank you. one question why ng-container doesn't show HTML impact with styling and all
  • ankush981
    ankush981 over 2 years
    Looks like a neat solution!
  • Steffi Keran Rani J
    Steffi Keran Rani J over 2 years
    Thank you @ankush981