Projecting content in Angular or lost ng-content documentation

When learning Angular, very often they miss or do not pay enough attention to such a concept as projection of content. This is a very powerful tool for creating flexible and reusable components. But its documentation only mentions a couple of paragraphs in the Lifecycle hooks section . Let's try to fix this omission.



Projecting content using ng-content


Content projection is a way to import HTML content from outside a component and paste it into a component template in a specific location. (free translation of documentation)
The definition is quite complicated, but in fact, everything is much simpler. We have some kind of component, and everything that is between its opening and closing tags is content.

<app-parent>
    <!-- content -->
    I'm content!
    <!-- content -->
</app-parent>

And Angular allows you to embed any HTML code (content) into the template of this component using an element ng-content.

Let's try to figure out why this is needed and how it works with an example. Let's say we have a simple button component. The text of this button we pass to the template through input property.

// button.component.ts
import {Component, Input} from '@angular/core';

@Component({
  selector: 'app-button',
  template: '<button>{{text}}</button>'
})
export class ButtonComponent {
  @Input() text: string;
}

// app.component.ts
import {Component} from '@angular/core';

@Component({
  selector: 'app-root',
  template: `<app-button [text]="'Button'"></app-button>`,
})
export class AppComponent {
}

It seems to look good. But suddenly we needed to add an icon to the text for some buttons. We already have an icon component. You just need to add it to the button template, attach a directive ngIfand write another one input propertyfor the dynamic display of the icon.

// icon.component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'app-icon',
  template: '☻',
})
export class IconComponent {
}

// button.component.ts
import {Component, Input} from '@angular/core';

@Component({
  selector: 'app-button',
  template: `<button>
               <app-icon *ngIf="showIcon"></app-icon>
               {{text}}
             </button>`,
})
export class ButtonComponent {
  @Input() text: string;
  @Input() showIcon = true;
}

Everything is working. But what happens if you need to change the location of the icon relative to the text? Or add another new item? You have to edit existing code, add new properties, etc.

All of this can be avoided with ng-content. It can be considered as a placeholder for content. It displays everything that you put between the opening and closing tags of the component.

// button.component.ts
import {Component} from '@angular/core';

@Component({
  selector: 'app-button',
  template: `<button>
               <ng-content></ng-content>
             </button>`,
})
export class ButtonComponent {
}

// app.component.ts
import {Component} from '@angular/core';

@Component({
  selector: 'app-root',
  template: `<app-button>
               <app-icon></app-icon>
               Button
             </app-button>`,
})
export class AppComponent {
}

Stackblitz code

Now, if we needed an icon button, we simply put the icon component between the button tags. You can add anything and whatever. Isn't that heaven? Our button component has become flexible and beautiful.

What role does the select attribute play for ng-content?


Sometimes we need to arrange some content in a certain place relative to the rest of the content, in this case we can use an attribute selectthat accepts a selector ( .some-class, some-tag, [some-attr]).

// button.component.ts
import {Component} from '@angular/core';

@Component({
  selector: 'app-button',
  template: `<button>
               <ng-content></ng-content>
               <div>
                 <ng-content select="app-icon"></ng-content>
               </div>
             </button>`,
})
export class ButtonComponent {
}

Stackblitz code

Now the icon is always shown below, regardless of the rest of the content. Perfecto!

What is ngProjectAs?


The selecty attribute ng-contentcopes with tags that are at the first level of nesting of the parent component. But what happens if we increase the nesting level for the icon component by wrapping it in a tag?

// app.component.ts
import {Component} from '@angular/core';

@Component({
  selector: 'app-root',
  template: `<app-button>
               <ng-container>
                 <app-icon></app-icon>
               </ng-container>
               Button
             </app-button>`
})
export class AppComponent {}

We will see that it selectdoes not work, as if it does not exist at all. This happens because it <ng-content select="...">searches only at the first level of nesting of the parent’s content. There is an attribute to solve this problem ngProjectAs. It takes in the selector and “masks” the entire DOM node under it.

// app.component.ts
import {Component} from '@angular/core';

@Component({
  selector: 'app-root',
  template: `<app-button>
               <ng-container ngProjectAs="app-icon">
                 <app-icon></app-icon>
               </ng-container>
               Button
             </app-button>`
})
export class AppComponent {}

Stackblitz Code

Case * ngIf + ng-content


Let us examine another interesting case. Suppose we need to click on the hide / show icon button. Add a boolean property to the button component class that is responsible for displaying the icon, change it by clicking on the button and hang it ngIf.

// button.component.ts
import {Component, Input} from '@angular/core';

@Component({
  selector: 'app-button',
  template: `<button (click)="toggleIcon()">
               <ng-content></ng-content>
               <div *ngIf="showIcon">
                 <ng-content select="app-icon"></ng-content>
               </div>
             </button>`,
})
export class ButtonComponent {
  showIcon = true;

  toggleIcon() {
    this.showIcon = !this.showIcon;
  }
}

The icon is hidden / appears by click. Fine! But let's add some logs for hooks OnInitand OnDestroyfor the icon component. It is a well-known fact that a directive, ngIfwhen a condition is changed, completely removes / creates an element, and OnDestroy/ OnInitmust work accordingly each time.

// icon.component.ts
import { Component, OnInit, OnDestroy } from '@angular/core';

@Component({
  selector: 'app-icon',
  template: '☻',
})
export class IconComponent implements OnInit, OnDestroy {
  ngOnInit() {
    console.log('app-icon init');
  }

  ngOnDestroy() {
    console.log('app-icon destroy')
  }
}

Code on Stackblitz

A couple of times we click on the button, make sure that the icon disappears, and then appears. Then we go to the developer's console in the hope of seeing our coveted logs, however ... they are not!

There is only one log for creating a component. It turns out that our icon component is never deleted, but simply hidden. Why is this happening?

ng-content doesn’t create new content, it simply projects existing ones. Therefore, the component in which the content is declared is responsible for creating and deleting. For me it was a completely non-obvious moment. Let's fix our solution so that it works as expected initially.

// button.component.ts
import {Component} from '@angular/core';

@Component({
  selector: 'app-button',
  template: `<button>
                <ng-content></ng-content>
                <ng-content select="app-icon"></ng-content>
             </button>`,
})
export class ButtonComponent {
}

// app.component.ts
import { Component } from "@angular/core";

@Component({
  selector: 'app-root',
  template: `<app-button (click)="toggleIcon()">
              <div *ngIf="showIcon" ngProjectAs="app-icon">
                <app-icon></app-icon>
              </div>
              Button
            </app-button>`,
})
export class AppComponent {
  showIcon = true;

  toggleIcon() {
    this.showIcon = !this.showIcon;
  }
}

Code on Stackblitz

Having opened the logs, we can see that the component of the icon is created and deleted as it should.

Instead of a conclusion


I hope this article has helped you a bit with projecting content in Angular.
It is categorically incomprehensible to me why the official documentation ignored this topic. The Angular repository even hangs the issue on this since 2017. Apparently, the Angular team has more important things to do.

All Articles