Angular4 ng-content gets built when ngIf is false - html

I have a problem with the new ng-content transclusion.
Let's say I have a component my-component that, in its ngOnInit() function does some heavy operation on load (for now, just a console.log()).
I have a wrapper, that displays the content via transclusion (my-wrapper.component.html).
<ng-content></ng-content>
If I set the surroundings up like this, the log statement doesn't show:
<my-wrapper *ngIf="false">
<my-component></my-component>
</my-wrapper>
I assume, the my-wrapper component does not get built, so the content is ignored.
But if I try to move the logic into the my-wrapper component like this (my-wrapper.component.html):
<ng-container *ngIf="false">
<ng-content></ng-content>
</ng-container>
I always see the console.log() output. I guess, the my-component gets built and then stored away until the *ngIf becomes true inside my-wrapper.
The intention was to build a generic "list-item + detail" component. Say I have a list of N overview-elements (my-wrapper), that get rendered in a *ngFor loop. Every of those elements has its own detail component (my-component) that is supposed to load its own data, once I decide to show more infos to a specific item.
overview.html:
<ng-container *ngFor="let item of items">
<my-wrapper>
<my-component id="item.id"></my-component>
</my-wrapper>
</ng-container>
my-wrapper.component.html:
<div (click)="toggleDetail()">Click for more</div>
<div *ngIf="showDetail">
<ng-content></ng-content>
</div>
Is there a way to tell Angular, to ignore the transcluded content until it is necessary to be added to the page? Analogously to how it was in AngularJS.

Based on the comment of #nsinreal I found an answer. I find it to be a bit abstruse, so I'm trying to post it here:
The answer is to work with ng-template and *ngTemplateOutlet.
In the my-wrapper component, set up the template like this (my-wrapper.component.html):
<div (click)="toggleDetail()">Click for more</div>
<div *ngIf="showDetail" [hidden]="!isInitialized">
<ng-container *ngTemplateOutlet="detailRef"></ng-container>
</div>
Note, that the [hidden] there is not really necessary, it hides the "raw" template of the child until it decides it is done loading. Just make sure, not to put it in a *ngIf, otherwise the *ngTemplateOutlet will never get triggered, leading to nothing happening at all.
To set the detailRef, put this in the component code (my-wrapper.component.ts):
import { ContentChild, TemplateRef } from '#angular/core';
#Component({ ... })
export class MyWrapperComponent {
#ContentChild(TemplateRef) detailRef;
...
}
Now, you can use the wrapper like this:
<my-wrapper>
<ng-template>
<my-component></my-component>
</ng-template>
</my-wrapper>
I am not sure, why it needs such complicated "workarounds", when it used to be so easy to do this in AngularJS.

By doing this:
<my-wrapper *ngIf="false">
<my-component></my-component>
</my-wrapper>
You are not calling MyComponent component, because the *ngIf is false. that means, that not calling it you are not instancing it and, therefore, not passing through its ngOnInit. And that's why you are not getting the console log.
By doing this:
<ng-container *ngIf="false">
<ng-content></ng-content>
</ng-container>
You are inside the component, you are just limiting what to render in your template, but you already instanced your component and, therefore, you passed through your ngOnInit and you get your console log done.
If, you want to limit something (component call with selector or a ng-content or even a div) until you have some data available, you can do the following:
datasLoaded: Promise<boolean>;
this.getData().subscribe(
(data) => {
this.datasLoaded = Promise.resolve(true); // Setting the Promise as resolved after I have the needed data
}
);
And in your template:
<ng-container *ngIf="datasLoaded | async">
// stuff here
</ng-container>
Or:
<my-component *ngIf="datasLoaded | async">
// Didn't test this one, but should follow the same logic. If it doesn't, wrap it and add the ngIf to the wrapper
</my-component>

It’s because Ng content happens at the build time and when you pass the content it is actually not removed or recreated with the ngIf directive. It is only moved and the component is instantiated .

I encountered this problem recently as well but settled on a different solution than the currently accepted one.
Solution (TL;DR)
(Solution is for AngularDart; I figure it's similar in Angular though)
Use a structural directive; tutorials linked below.
Instead of:
<my-wrapper>
<my-contents></my-contents>
</my-wrapper>
your usage becomes:
<div *myWrapper>
<my-contents></my-contents>
</div>
which is shorthand for the following (in AngularDart; I think Angular uses <ng-template>)
<template myWrapper>
<div>
<my-contents></my-contents>
</div>
</template>
The MyWrapper directive logic is similar to NgIf except it has its own logic to compute the condition. Both of the following tutorials explain how to create an NgIf-like directive and how to pass it your own inputs using the special microsyntax (e.g. *myWrapper="myInput: expression"). Note that the microsyntax doesn't support outputs (#Output), but you can mimic an output by using an input that is a function.
Tutorial for Angular
Tutorial for AngularDart
Caveat: Since this is just a directive, it shouldn't do anything more complicated than instantiating a template ref at the appropriate time and maybe specifying some DI providers. For example, I would avoid trying to apply styles or instantiating a complex tree of components in the directive. If I wanted to create a list component, I would probably take the #ContentChild(TemplateRef) approach described in another answer; you would lose the asterisk shorthand for creating <template> but you would gain the full power of components.
My problem
My team owns an app that's part of a larger web application with other apps owned by other teams. Our components assume they can inject a MyAppConfiguration object, but this object can only be injected after it is loaded with an asynchronous request. In our app this is not a problem: we have a "shell" component that hides everything behind an ngIf until the configuration is loaded.
The problem is when other teams want to reference our components. We don't want them to duplicate the "wait until configuration is loaded" logic every time, so I tried creating a wrapper component that can be used like so:
<my-app-wrapper>
<my-app-component></my-app-component>
</my-app-wrapper>
The wrapper injects a service object and hides its contents behind an ngIf until the service says that the configuration is loaded.
Like the question poster, I discovered that the ng-content approach doesn't work as intended: while the contents are correctly hidden from the DOM, Angular still instantiates the components causing dependency injection to fail.
The solution that I settled on was to rewrite the wrapper component as a structural directive.

Related

*ngIf causing custom directive to not work properly

A custom directive applied to both components(1/2)-in-spotlight is not working properly when using *ngIf. The issue resolves when I remove the *ngIf and one of the components that would not show in the current situation/"mode".
In the HTML file using the component (original):
<div>
<div>
<div>
<div>
<component1-in-spotlight *ngIf="mode===OptionOne"></component1-in-spotlight>
<component2-in-spotlight *ngIf="mode===OptionTwo"></component2-in-spotlight>
</div>
</div>
</div>
</div>
I found 2 solutions but both aren't effective or proper.
Duplicating the surrounding parent/grandparent components (placing the second case in an <ng-template #elseBlock>) and applying ngIf-else to the top most component (in the oversimplified example, a div) works. But, I'd have a lot of duplicate code and is a terrible solution.
Option 1 (to illustrate since it might be a bit confusing for some). In the HTML file using the component:
<div *ngIf="mode===OptionOne"; else myElseBlock">
<div>
<div>
<div>
<component1-in-spotlight></component1-in-spotlight>
</div>
</div>
</div>
</div>
</ng-template #myElseBlock>
<div>
<div>
<div>
<div>
<component2-in-spotlight></component2-in-spotlight>
</div>
</div>
</div>
</div>
</ng-template>
Using [hidden] on the 2 components instead of *ngIf seems fine. But there is never a case where the hidden component will be toggled to visible, it's decided upon creation and stays using either of the 2 components until it's destroyed. So, it should just only have one of the 2 components in DOM. Not just hiding it. Plus, that means flipping the logic--[hidden]="mode!==OptionOne". For now, it's just 2 options and seems unlikely more would be added, but I can't guarantee that.
--
It may seem like these 2 components are the same, so why not just have 1 component and pass in the mode and let the logic decide within the TS file of that component? Well, they both have different services that are injected into the constructor for the component. I was trying that before finding out and remembering that I can't use this before calling super() to decide which service to send up to the base class the component is extending.
Merging the 2 components and using #Input to get the "mode":
In the HTML file using the component:
<div>
<div>
<div>
<div>
<component-in-spotlight-merged [inputMode]="mode"></component-in-spotlight-merged>
</div>
</div>
</div>
</div>
In the component-in-spotlight-merged TS file--what I tried to do:
export class ComponentInSpotlightMergedComponent extends MyComponentBaseComponent {
#Input() inputMode: MyEnumType;
//...
constructor(
myService1: MyService1,
myService2: MyService2,
){
if(this.inputMode === Option1){
super(myService1);
}
else{
super(myService2);
}
}
//...
}
Using [hidden] can be for a quick fix, but is there a proper way to fix this?
Edit:
Not working meaning: It's a custom directive for tabbing focus between elements and the hotkey logic is binded here. Somehow the hotkey works but the focus is not working as it expected and none of my console.log() are outputted.
Angular 9+
You can use Angular NgSwitch directive as shown below.
<div [ngSwitch]="mode">
<!-- the same view can be shown in more than one case -->
<component1-in-spotlight *ngSwitchCase="option1">...</component1-in-spotlight>
<component2-in-spotlight *ngSwitchCase="option2">...</component2-in-spotlight>
<!--default case when there are no matches -->
<some-element *ngSwitchDefault>...</some-element>
</div>
The fix was to use setTimeout(() => myCallbackFn(), 0); (on my hotkey bind function that is called in ngAfterViewInit in a component class down the line--a view grandchild?).
I was in a rabbit hole of reading other stackoverflow questions and found How do I combine a template reference variable with ngIf? where a comment mentioned that ngIf takes a tick of time to evaluate. I eventually searched and found How to check whether ngIf has taken effect.

Angular html custom element from typescript [duplicate]

I'm setting HTML returned from the API in my Angular component:
<div [innerHTML]="content"></div>
content in this example is something like this:
<table>
<tr>
<td>[audioPlayer:file.mp3]</td>
</tr>
</table>
Now I would like to inject the actual component inside the table cell.
If I make a certain container, I can create the component with createComponent:
audioPlayerComponentRef: ComponentRef<AudioPlayerComponent>;
#ViewChild('placeholder', { read: ViewContainerRef }) container;
const factory: ComponentFactory<AudioPlayerComponent> =
this.componentFactoryResolver.resolveComponentFactory(AudioPlayerComponent);
this.audioPlayerComponentRef = this.container.createComponent(factory);
Then I can inject it into a container in the template:
<div #placeholder></div>
However, going back to my original goal, I can't use such a container, as the component needs to be injected into a specific position into an innerHtml block.
I've been brainstorming all day, but I can't see any way to achieve this.
Generally speaking, this is contrary to the way Angular works. [innerHTML] is not parsed for any Angular functionality. No component selectors or even a ViewContainerRef can be found there. Rather, even remotely suspicious HTML, CSS and JS is removed as a security measure as Angular only trusts its own templates.
So InnerHTML is a no-go. But I was in the same boat myself and have written a library to solve this exact problem. With it, you can freely load dynamic components into strings without compromising security. If you're still stuck on this, you might wanna have a look at it.

JS method not defined

I want to use a java-script method in a polymer Template. I am using Vaadin with Polymer Elements. In my Project I have a Vaadin-Grid of Objects that can be of different type. I want to render these types with different Templates.
This problem can be solved with a dom-if template, as described by ollitietavainen in this answer
This works perfectly, but there is a problem. When using more than two different Types of Objects in the Grid, one would need to use the same amount of booleans to set that up. Suppose we have a fictional shop that displays PC-Parts, and each type of PC-Part needs to be rendered with its own template, then we would need something like the fallowing. This is quite cumbersome.
private boolean isMemory(AbstractPcPart pcPart) {
return pcPart.getClass().equals(Memory.class);
}
private boolean isGraphicsCard(AbstractPcPart pcPart) {
return pcPart.getClass().equals(GraphicsCard.class);
}
private boolean isCPU(AbstractPcPart pcPart) {
return pcPart.getClass().equals(CPU.class);
}
// … is-checker for all other types of pcParts.
private void initColumn() {
addColumn(Objects.requireNonNull(CardFactory.getTemplate())
.withProperty("partCard", CardFactory::create)
.withProperty("isMemory", this::isMemory)
.withProperty("isGraphicsCard", this::isGraphicsCard)
.withProperty("isCPU", this::isCPU)
// add all other properties
);
}
The corresponding Templates would look something like this.
<template is='dom-if' if='[[item.isMemory]]'>"
<memory-card part-card='[[item.partCard]]'>
</memory-card>"
</template>
<template is='dom-if' if='[[item.isGraphicsCard]]'>"
<graphics-card part-card='[[item.partCard]]'>
</graphics-card-card>"
</template>
<template is='dom-if' if='[[item.isCPU]]'>"
<cpu-card part-card='[[item.partCard]]'>
</cpu-card>"
</template>
<!-- one additional template for every type of part -->
The question now is, if there is any other way, that would not be needing all these Properties.
Luckily there is, as Kuba Šimonovský explained in an answer to another question.
Using this method we could rewrite the code from above to something like the fallowing.
private String type(AbstractPcPart pcPart) {
return pcPart.getClass().getSimpleName();
}
private void initColumn() {
addColumn(Objects.requireNonNull(CardFactory.getTemplate())
.withProperty("partCard", CardFactory::create)
.withProperty("type", this::type));
}
This time we use a java-script method to conditionally select the corresponding template.
<template is='dom-if' if='[[_isEqualTo(item.type, "Memory")]]'>"
<memory-card part-card='[[item.partCard]]'>
</memory-card>"
</template>
<template is='dom-if' if='[[_isEqualTo(item.type, "GraphicsCard")]]'>"
<graphics-card part-card='[[item.partCard]]'>
</graphics-card-card>"
</template>
<template is='dom-if' if='[[_isEqualTo(item.type, "CPU")]]'>"
<cpu-card part-card='[[item.partCard]]'>
</cpu-card>"
</template>
<!-- one additional template for every type of part -->
The Polymer Template is a bit more complicated now, but on the java side, the code is much shorter, and possibly easier to maintain. There is probably still some overhead, as every template gets added to the dom. But in addition to that only the content from the templates that we want to see gets added to the dom.
I don’t think there is a better way to do this though.
So using this method, we need a java-script method called _isEqualTo. This method is not a standard method so we need to implement it ourselves. The implementation for this method is straightforward.
function _isEqualTo(one, other) {
return one == other;
}
But the answer from Kuba does not specify where to implement this method. I have tried to put the method in different places with no luck. The js console in my browser always complains that it can not find the method.
Digging a little bit deeper I found this Link. So maybe what i want to have is a global variable.
window._isEqualTo = function(one, other) {
return one == other;
}
But even with this change the same warning persists. What’s weird is that the function is visible in the interactive console in the developer tools. Setting a breakpoint in the java-script file that i have added the function; and calling the function in the console reveals that it is really the correct function that get’s called, leading me to beleave that the function gets initialized too late in the lifecycle of the application. Although I am not sure at all.
And because the function is not found, the grid in the view will be empty. It still shows the rows, but they don’t show content.
I really hope someone can help me out.
Here is a Git-Repository to reproduce my problem. The concerning views are the PartsDomIfView and the PartsDomIfElegantView.
Instead of using the deprecated TemplateRenderer, you could create a LitRenderer (v22+) and create a custom lit component that can be used there as your column's content. In there you could create complex logic based templates as a separate component, that can be better maintained.

Accessing HTML field of Parent Component in Library's component

I have created an Angular 6 library using 'ng generate library' command. This library is used in my base component after --prod build and then importing in app.module.ts of main application. The ...Component file in Library has #Input("leftPanel") leftPanel: ElementRef;
HTML Div element on base.component.html is like this: <div #leftPanel></div>
And the library element using its selector :
<lib-ng-mylibrary [leftPanel]="leftPanel"> </lib-ng-mylibrary>
Library component implements AfterViewInit. In the implementation method, this code execution fails: this.leftPanel.nativeElement.style.flexBasis = '50%';
it says, this.leftPanel.nativeElement is undefined. But i can see this.leftPanel point to the div. Wonder why it does not allow this.leftPanel.nativeElement` even tho #Input leftPanel is of type 'ElementRef'?
Thanks in Advance!
Harshad
Instead of sending the parent ElementRef my feeling is that your component should have and #Output and trigger an event handled by the parent, to change the native panel width.
Doing like you reduce the coupling between the object and make them more reusable.
See docs here: https://angular.io/guide/component-interaction
Still want to use ElementRef as parameter
If you still want to send the leftPanel as a parameter, you will need an #Input() variable in your main component as well, so it can resolve <div #leftPanel> to a local variable and that variable be used in [leftPanel]="leftPanel"
cheers

Pass data from nested Components Angular 2

I have a component where with a *ngFor print some boxes defined in an other component like this:
<div>
<commponentB *ngFor="let a of list"></componentB>
</div>
where "list" is a list ok object like this:
this.list=[{"id":"3","nome":"app3","posizione":3},{"id":"1","nome":"app1","posizione":1},{"id":"2","nome":"app2","posizione":2}];
I have to populate all the component. How can I transfer data from this two component?
TY
EDIT1:
the problem is that the list is splitted into 2 list for the repeat in 2 different bootstrap columns so the situation is this:
<div>
<commponentB *ngFor="let a of list1"></componentB>
</div>
<div>
<commponentB *ngFor="let a of list2"></componentB>
</div>
and the component is like that:
<div>
<span>{{name}}</span>
</div>
if I pass all the list I can't know how to populate the component at the right position (sorry if I don't explain the problem very well)
Since your components are directly linked, you can use the #Input() component interaction to send data from the parent to the child.
Here is a link from the official documentation :
https://angular.io/guide/component-interaction#pass-data-from-parent-to-child-with-input-binding
If you can't do that model because of a router interaction, or components are too "far" away so it's too much work to setup several inputs, you can use a shared service to share the data from one to another component.
https://angular.io/guide/component-interaction#parent-and-children-communicate-via-a-service
if you want to pass your list to componentB you can define Input() property in componentB and then pass your list
<div>
<commponentB [list]="list"></componentB>
</div>
export class ComponentB{
#Input() list: any[];
}
Update Ok maybe I'm not getting your issue, even if you have to pass different inputs to same component you can do that. Check out this plunker: