I am facing an issue with multiple nested ngIf applied on ng-template elements in Angular.js, and I can't seem to get the perfect answer. I know workarounds but they are not optimized.
This is the code that I am trying to get running:
<div class="container">
<ng-template *ngIf="booleanA;then caseA else caseB">
<ng-template #caseA>
<el>1</el>
<el>2</el>
</ng-template>
<ng-template #caseB>
<ng-template *ngIf="booleanB">
<el>3</el>
<el>4</el>
<el>5</el>
</ng-template>
</ng-template>
</ng-template>
</div>
And these are the two solutions I have found to my problem:
Placing the ngIf on every child element inside of the #caseB element:
<ng-template #caseB>
<el *ngIf="booleanB">3</el>
<el *ngIf="booleanB">4</el>
<el *ngIf="booleanB">5</el>
</ng-template>
Placing the surrounding class="container" element inside both #caseA and #caseB, and applying the second ngIf to it:
<ng-template *ngIf="booleanA;then caseA else caseB">
<ng-template #caseA>
<div class="container">
<el>1</el>
<el>2</el>
</div>
</ng-template>
<ng-template #caseB>
<div *ngIf="booleanB" class="container">
<el>3</el>
<el>4</el>
<el>5</el>
</div>
</ng-template>
</ng-template>
The issue with these solutions is in the optimization. The first one checks multiple times for the same value, and the second one uses the same HTML element twice.
Is there any way I could make the original design work?
EDIT: The two solutions wouldn't appear as blocks of code, therefore I styled them as inline code. If you know how to fix that you'd be very welcome.
EDIT 2: Bringing some clarification as to what I am looking for: The end goal is not for the code to work, I have already found workarounds that I could use if all else fails.
The end goal is to get this code working only with Angular's logical element <ng-template> and by following the original design; and without the help of additional native elements like div, which would alter the DOM.
Two changes you need to make
Using ng-container
Using div instead of nested ng-template
please see this stackblitz
<div class="container">
<ng-container *ngIf="booleanA; then caseA; else caseB">
</ng-container>
<ng-template #caseA>
<span>1</span>
<span>2</span>
</ng-template>
<ng-template #caseB>
<div *ngIf="booleanB">
<span>3</span>
<span>4</span>
<span>5</span>
</div>
</ng-template>
</div>
You can try to use ngSwitch:
https://angular.io/api/common/NgSwitch
example code from Angular:
<container-element [ngSwitch]="switch_expression">
<some-element *ngSwitchCase="match_expression_1">...</some-element>
<some-element *ngSwitchCase="match_expression_2">...</some-element>
<some-other-element *ngSwitchCase="match_expression_3">...</some-other-element>
<ng-container *ngSwitchCase="match_expression_3">
<!-- use a ng-container to group multiple root nodes -->
<inner-element></inner-element>
<inner-other-element></inner-other-element>
</ng-container>
<some-element *ngSwitchDefault>...</some-element>
</container-element>
I have found a way to keep the mindset of my original design without adding unnecessary new DOM elements, duplicating HTML code, or double-checking the same variable.
<div class="container">
<ng-template *ngIf="booleanA;then caseA else caseB">
<ng-template #caseA>
<el>1</el>
<el>2</el>
</ng-template>
<ng-template #caseB>
<ng-template *ngIf="booleanB;then caseC"></ng-template>
<ng-template #caseC>
<el>3</el>
<el>4</el>
<el>5</el>
</ng-template>
</ng-template>
</ng-template>
</div>
Thanks to everyone for giving me other paths to explore, those will serve me well.
Related
I have a class section on a DIV to be rendered. It works as expected.
<div *ngIf="decoded; then coordinates"></div>
<ng-template #coordinates>
<div class="section">...</div>
</ng-template>
I tried moving the class assignment to the DIV containing the directive. The rendition did not work, though.
<div *ngIf="decoded; then coordinates" class="section"></div>
<ng-template #coordinates>
<div>...</div>
</ng-template>
The outer DIV vanishes as a whole, being replaced by the contents of the template. It bugs me because I'm forced to add an extra DIV around everything in my template if I have several components in it. (Also, it seems kind of weird to me that we don't retain any properties of the tag used with *ngIf and can use any arbitrary one, while it seems to work for *ngFor.)
<div *ngIf="decoded; then coordinates"></div>
<ng-template #coordinates>
<div class="section">
<div>...</div>
<span>...</span>
<bzz>...</bzz>
</div>
</ng-template>
I tried cheating the browser by setting the class on template but since it's not actually rendered in the DOM as such, it failed, of course.
<div *ngIf="decoded; then coordinates"></div>
<ng-template #coordinates class="section">
<div...</div>
</ng-template>
is there a way to force the DIV with the conditional directive to retain its class when being rendered according to the template's contents?
You can try to use ng-container and apply *ngIf to it, in this case it should work as you expected
<div class="section">
<ng-container *ngIf="decoded; then coordinates"></ng-container>
</div>
<ng-template #coordinates>
<div>...</div>
</ng-template>
TL;DR: Angular is doing what you asked it to; you used then to tell it to render something other than the element the directive was on in the truth-y case. If that's not the behaviour you want, don't do that.
Structural directives like *ngIf and *ngFor are really a shorthand that gets expanded (this used to be referred to as "desugaring"), look at the examples in https://angular.io/guide/structural-directives or more specifically https://angular.io/api/common/NgIf#description:
Simple form with shorthand syntax:
<div *ngIf="condition">Content to render when condition is true.</div>
Simple form with expanded syntax:
<ng-template [ngIf]="condition">
<div>Content to render when condition is true.</div>
</ng-template>
Note that everything gets wrapped up in ng-templates for conditional rendering.
If you had written your template like this:
<div *ngIf="decoded" class="section">...</div>
it would get expanded to:
<ng-template [ngIf]="decoded">
<div class="section">...</div>
</ng-template>
and what would actually be rendered is:
<div class="section">...</div>
Note that the class is still included, consistent with what you've experienced in other structural directives.
However, when you use then, the shorthand:
<div *ngIf="decoded; then coordinates" class="section"></div>
<ng-template #coordinates>
<div>...</div>
</ng-template>
is expanded to:
<ng-template [ngIf]="decoded" [ngIfThen]="coordinates">
<div class="section"></div>
</ng-template>
<ng-template #coordinates>
<div>...</div>
</ng-template>
The content of the first ng-template is now irrelevant, because that's not what's rendered in either case. It's either going to render the #coordinates template content or nothing, so you get:
<div>...</div>
and your class seems to have disappeared. But that's what you asked for; the point of then is to not render the element the *ngIf was on, in the truth-y case, but to render something else instead.
For more on these underlying ng- elements, I wrote Angular's "ng-" elements on my blog.
I suggest to take a look on this great article Everything you need to know about ng-template, ng-content, ng-container, and *ngTemplateOutlet in Angula
It contains all you need to know about Angular's structural directives with a good explanation and examples.
I would like to use DragDropModule for my angular application so I can move panels that are stored inside templates (for practical purposes as well as for recursive plotting of child elements).
The problem that I have is that cdkDrag can't find the correct cdkDropList to drop into if cdkDrag is hidden within a template and is not directly nested under the HTML element. Example:
<div
cdkDropList
[cdkDropListData]="expanded.activities"
(cdkDropListDropped)="dropActivity($event)">
<div *ngFor="let activity of expanded.activities">
<ng-container [ngTemplateOutlet]="orangeProgramActivity"></ng-container>
</div>
</div>
<ng-template #orangeProgramActivity>
<div cdkDrag>This is just a test</div>
</ng-template>
With this code example, the orangeProgramActivity can be dragged anywhere but doesn't drop into the correct dropList as cdkDrag keyword can't find any droplist within its own template.
In the second example, everything works correctly and the item gets dropped into correct dropList:
<div
cdkDropList
[cdkDropListData]="expanded.activities"
(cdkDropListDropped)="dropActivity($event)">
<div *ngFor="let activity of expanded.activities">
<div cdkDrag>This is just a test</div>
</div>
</div>
I would like to achieve the same functionality as in the second example, but with the use of templates because I really need them for recursion. Unfortunately, I can't reveal the whole code as my employer wouldn't be happy with that.
All I need is some static reference for my cdkDrag that is inside a template, to point onto a correct element with dropList that is outside of the template.
These are the only solutions that I found on the internet and they don't seem to work for me:
Material 7 Drag and Drop ng-template incompatibility
CdkDragDrop and ngTemplateOutlet
This is my first question on Stack Overflow and I'm new to angular, so I'm sorry for any confusion in my post, and thanks for any help in advance!
This might not work for all use cases, but you could just write it like this:
<div
cdkDropList
[cdkDropListData]="expanded.activities"
(cdkDropListDropped)="dropActivity($event)">
<div *ngFor="let activity of expanded.activities" cdkDrag>
<ng-container [ngTemplateOutlet]="orangeProgramActivity"></ng-container>
</div>
</div>
<ng-template #orangeProgramActivity>
<div>This is just a test</div>
</ng-template>
I was lucky that I only need to reorder elements inside an array in which they are and I don't need to transfer them into other containers. So for my "root" activities, I used your simple solution to wrap it all in a draggable element.
<div cdkDropList [cdkDropListData]="expanded.activities" (cdkDropListDropped)="dropActivity($event)">
<!-- For every object in array make a container with template and an icon that will serve as a handle -->
<div *ngFor="let activity of expanded.activities">
<div cdkDrag>
<div cdkDragHandle>
<!-- Handle icon from FontAwesome -->
<i class="fas fa-grip-vertical"></i>
</div>
<!-- Container with a template that we need -->
<ng-container [ngTemplateOutlet]="orangeProgramActivity" [ngTemplateOutletContext]="{act: activity}"></ng-container>
</div>
<!-- Recursive template for children -->
<ng-container [ngTemplateOutlet]="childActivitiesRecursion" [ngTemplateOutletContext]="{act: activity}"></ng-container>
</div>
And for my recursion, I created a droplist inside a template that serves only for reordering these children activities.
(I had problems with pasting template so the ng-template tag is not terminated at the end of the code)
<ng-template let-act="act" #childActivitiesRecursion>
<!-- Droplist inside a template for reordering child activites -->
<div cdkDropList [cdkDropListData]="act.childActivities" (cdkDropListDropped)="dropActivity($event)">
<!-- Iterates through all child activities -->
<div *ngFor="let childActivity of act.childActivities">
<div cdkDrag>
<div cdkDragHandle>
<i class="fas fa-grip-vertical"></i>
</div>
<!-- Different template for child activities -->
<ng-container [ngTemplateOutlet]="blueProgramActivity" [ngTemplateOutletContext]="{act: childActivity}"></ng-container>
</div>
<!-- Recursion for even deeper child activites -->
<ng-container [ngTemplateOutlet]="childActivitiesRecursion" [ngTemplateOutletContext]="{act: childActivity}"></ng-container>
</div>
</div>
I will accept Richard's answer as a solution because if you are able to solve it like this, it's definitely the most elegant solution you can do. But there would be still an issue if you needed to transfer items into different containers after using templates for recursion!
I'm trying to close and open a new div in a ng-template element. When I try this my code breaks.
<div class="row">
<ng-template ngFor let-i="index" let-data [ngForOf]="dataArray">
<app-user-dashboard-gauge [gaugeData]="data" class="col"></app-user-dashboard-gauge>
<ng-template [ngIf]="i === 3">
</div>
<div class="row">
</ng-template>
</ng-template>
</div>
I'm trying to do this inside a ngFor to preserve my bootstrap rows. Does anyone know how to fix this or maybe another solution?
Edit: Ok I think I want that clear on what I wanted to happen. I update my code block and will try to explain it better. On the third iteration of my for loop I want the row div to be closed and a new one to open. And the rest of the <app-user-dashboard-gauge> to go in the second row div.
Okay I fixed it. It might not be the best or the nicest looking but i split the incomming data into two parts and made another for loop for the secend group of data.
<div style="background-color: #202020">
<div class="row">
<ng-template ngFor let-data [ngForOf]="dataArray1">
<app-user-dashboard-gauge [gaugeData]="data" class="col"></app-user-dashboard-gauge>
</ng-template>
</div>
<div class="row">
<ng-template ngFor let-data [ngForOf]="dataArray2">
<app-user-dashboard-gauge [gaugeData]="data" class="col"></app-user-dashboard-gauge>
</ng-template>
</div>
</div>
Im still open for feedback if someone knows a better solution but this fix will suffice for now.
<ng-container *ngIf="i !== 3">
<div class="row">
</div>
</ng-container>
I have a tabView and a list of tab-panels.
Based on runtime conditions, I need to hide and unhide one of the tab panels.
Can not find a way to do this using looking at their docs or here.
Any suggestions?
In general is there a way to dynamically add and remove tab panels?
Code I am using now that is not working, my test syntax must be wrong:
<div [ngSwitch]="isNEC">
<ng-template ngSwitchCase="'true'">
<p-tabView>
<p-tabPanel header="Detail">
<linechart #linechart></linechart>
</p-tabPanel>
<p-tabPanel header="Assessment">
Coming soon to a theater near you !!!!
</p-tabPanel>
</p-tabView>
</ng-template>
<ng-template ngSwitchCase="'false'">
<p-tabView>
<p-tabPanel header="Detail">
<linechart #linechart></linechart>
</p-tabPanel>
</p-tabView>
</ng-template>
</div>
unless I put a switch default, nothing ever shows up.
isNEC is a component public attribute of type string
all is well, I just needed to check for 'true' and not "'true'"
In my code im using *ngIf which should only show one of the two at any times. But the problem is that it only shows the second one , even though the second one should not be shown at some points. Im using a for loop to show all elements, but as you can see in the picture, only the second one is shown even though the second one its value is -1. Thank you for helping!
Code:
<ng-template let-internship="rowData" pTemplate="body" *ngIf="favorite?.FavoritesIds.indexOf(internship?.InternshipId) === -1;"><!--TODO BRIAN ngif-->
<a class="btn btn-default" [routerLink]="['/student/stageopdrachten', internship.InternshipId, false]"><!--Not shown TODO-->
<i class="glyphicon"></i>Meer
</a>
</ng-template>
<ng-template let-internship="rowData" pTemplate="body" *ngIf="favorite?.FavoritesIds.indexOf(internship?.InternshipId) !== -1;"><!--TODO BRIAN ngif-->
<a class="btn btn-default" [routerLink]="['/student/stageopdrachten', internship.InternshipId, true]"><!--always shown-->
<i class="glyphicon"></i>Meer{{favorite?.FavoritesIds.indexOf(internship?.InternshipId)}}
</a>
</ng-template>
Image:
Use div instead of ng-template. You cannot use the syntactic sugar * with ng-template.
Structural Directives
The asterisk is "syntactic sugar" for something a bit more
complicated. Internally, Angular desugars it in two stages. First, it
translates the *ngIf="..." into a template attribute, template="ngIf
...", like this.
<div template="ngIf hero">{{hero.name}}</div>
Then it translates the template attribute into a
element, wrapped around the host element, like this.
<ng-template [ngIf]="hero">
<div>{{hero.name}}</div>
</ng-template>
UPDATE
Like #Daniel Cooke said, I should have used a div because the ng templates dont work that well when next to eachother. Rather use divs and only one ng template did it. So the working code:
<ng-template let-internship="rowData" pTemplate="body" ><!--TODO BRIAN ngif -->
<div *ngIf="favorite?.FavoritesIds.indexOf(internship?.InternshipId) === -1">
<a class="btn btn-default" [routerLink]="['/student/stageopdrachten', internship.InternshipId, false]"><!--niet bestaat TODO-->
<i class="glyphicon"></i>Meer
</a>
</div>
<div *ngIf="favorite?.FavoritesIds.indexOf(internship?.InternshipId) != -1;">
<a class="btn btn-default" [routerLink]="['/student/stageopdrachten', internship.InternshipId, true]"><!--wel bestaat-->
<i class="glyphicon"></i>Meer{{favorite?.FavoritesIds.indexOf(internship?.InternshipId)}}
</a>
</div>
</ng-template>
Do a {{favorite?.FavoritesIds.indexOf(internship?.InternshipId) | json}} to see whats in the variable
My tip is to place the if statement in a separate function.
Its possible that === tests on string while its a number.