*ngIf and *ngFor on same element causing error
AngularNgforAngular Ng-IfAngular Problem Overview
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 li
s 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?
Angular Solutions
Solution 1 - Angular
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 - Angular
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 - Angular
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.
Solution 4 - Angular
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 - Angular
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>
Solution 6 - Angular
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>
Solution 7 - Angular
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
Solution 8 - Angular
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.
Solution 9 - Angular
I solve my problem with this
<ng-container *ngFor="let item of menuItems">
<li *ngIf="canShowItem(item)"></li>
</ng-container>
Solution 10 - Angular
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.
Solution 11 - Angular
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>
Solution 12 - Angular
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>
Solution 13 - Angular
<div *ngFor="let thing of show ? stuff : []">
{{log(thing)}}
<span>{{thing.name}}</span>
</div>
Solution 14 - Angular
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
Solution 15 - Angular
<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>
Solution 16 - Angular
You can't use multiple structural directive on same element. Wrap your element in ng-template
and use one structural directive there
Solution 17 - Angular
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>