Angular4 ng-content gets built when ngIf is false

Viewed 20123

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.

4 Answers

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