Master the Art of Angular Content Projection
Tags: angular, javascript, learning, programming
Published: 2023-10-29 13:10:16

### Introduction
🚦 When it comes to creating customizable components in `Angular`, think of it as customizing your morning coffee - you've got your base (`@Input()`, `@Output()`) and your extras (`*ngIf`) to create the perfect brew. However, the more toppings you add to your morning coffee, the more your component gets too cozy with your _underlying business logic_.

> _Ideally, a component’s job is to enable only the user experience._

In this blog post, we're peeling back the layers to reveal its untapped potential to build `Angular` components that are both flexible and customizable ☕🚀.

Let's begin with `Angular` content projection.

<hr />

#### _1. What is Angular content projection?_ (_\<ng-content\>_)
Content projection is a pattern in which you insert or project, the content you want to use inside another component.

To illustrate this concept, consider the following example:
```javascript
@Component({
  standalone: true,
  selector: 'app-greeny',
  template: `
    <p>Welcome to Greeny Land 🍀!</p>
    <ng-content></ng-content>
  `,
})
export class GreenyComponent {}

@Component({
  selector: 'my-app',
  standalone: true,
  imports: [CommonModule, GreenyComponent],
  template: `
    <app-greeny>
      <p>I'm a Seeding 🌱, a new beginning and a fresh start.</p>
    </app-greeny>
  `,
})
export class App {}

bootstrapApplication(App);
```

By adding `<ng-content></ng-content>` within the `GreenyComponent` template, you empower the dynamic inclusion and the seamless blending of content from the parent component.

```
Welcome to Greeny Land 🍀!
I'm a Seeding 🌱, a new beginning and a fresh start.
```

#### _2. Multi-slot content projection_

`Angular` extends content projection beyond its basic concept by introducing multi-slot content projection, which allows content to be inserted into specific designated slots within a component, granting finer control over customization.

Let's explore this advanced feature through a practical example using the [_Pokémon API_](pokeapi.co):

> `pokemon.component.ts`
```javascript
@Component({
  standalone: true,
  selector: 'app-pokemon',
  template: `
    <div class="header-wrapper">
      <ng-content select=".pokemon-header"></ng-content>
    </div>
    <div class="detail-wrapper">
      <ng-content select=".pokemon-detail"></ng-content>
    </div>
  `,
})
export class PokemonComponent {}
```
We've defined the `PokemonComponent` to support multi-slot content projections by specifying a concrete `select` attribute (_selector_) for each `<ng-content>` slot:

- `pokemon-header`
- `pokemon-detail`

`Angular` supports [_selectors_](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors) for any combination of tag name, attribute, CSS class, and the `:not` pseudo-class.

> `standard-pokemon.component.ts`

Let's create `StandardPokemonComponent` to leverage the multi-slot content projection:
```javascript
@Component({
  selector: 'app-standard-pokemon',
  standalone: true,
  imports: [PokemonComponent, TitleCasePipe],
  template: `
    <div class="standard">
      <app-pokemon [class]="pokemon.type">
        <div class="pokemon-header">
          <div class="number"><small>#{{pokemon.id}}</small></div>
          <img [src]="pokemon.image" [alt]="pokemon.name" />
        </div>
        <div class="pokemon-detail">
          <h3>{{pokemon.name | titlecase}}</h3>
          <small>Type: {{pokemon.type}}</small>
        </div>
      </app-pokemon>
    </div>
  `,
  styleUrls: ['./standard-pokemon.component.scss'],
})
export class StandardPokemonComponent {
  @Input() pokemon: any;
}
```

To project content into the corresponding slot, you simply need to define a `<div>` element containing either the `pokemon-header` or `pokemon-detail` class.

The `pokemon-header` will include the Pokemon index and image,
```html
<div class="pokemon-header">
  <div class="number"><small>#{{pokemon.id}}</small></div>
  <img [src]="pokemon.image" [alt]="pokemon.name" />
</div>
```
while the `pokemon-detail` will contain the Pokemon name and type. 
```html
<div class="pokemon-detail">
  <h3>{{pokemon.name | titlecase}}</h3>
  <small>Type: {{pokemon.type}}</small>
</div>
```

Additionally, we'll render the background color based on the Pokemon's type.

🔥 Now, let's bring our Pokémons to life:
```javascript
@Component({
  selector: 'my-app',
  standalone: true,
  imports: [CommonModule, StandardPokemonComponent],
  template: `
    <h1 class="title">Pokémon Evolution</h1>

    <div class="container">
      <ng-container *ngIf="pokemons$ | async as pokemonList">
        <ng-container *ngFor="let pokemon of pokemons">
          <app-standard-pokemon [pokemon]="pokemon"></app-standard-pokemon>
        </ng-container>
      </ng-container>
    </div>
  `,
  styles: [...]
})
export class App {
  pokemons$ = inject(PokemonService).getPokemons();
}
```

![Standard Pokemon (Angular Content Projection)](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xqvi6jxj0nsj5e3ynem3.png)

[_Link to StackBlitz_](https://stackblitz.com/edit/stackblitz-starters-spkcrt?file=src%2Fmain.ts)

So far, we've learned how Angular's multi-slot content projection works. However, what is the practical use for it? Why don't we simply compose everything inside the `PokemonComponent`? Let's address these questions in the upcoming section.

<hr />

#### _3. It's time to make Content Projection shining_
![Baby Boss](https://media.tenor.com/X9S1ftud78AAAAAC/kid-bored.gif)
Let's assume we've received a new requirement from our boss. Instead of just supporting standard Pokémon, he wants us to embrace the modern Pokémon style, as follows:

![Modern Pokemon Design](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/a9yjdjlb6vduc7r8r43e.png)

Let's see how the `ModernPokemonComponent` implementation first:

```javascript
@Component({
  selector: 'app-modern-pokemon',
  standalone: true,
  imports: [PokemonComponent, TitleCasePipe],
  template: `
    <div class="modern">
      <app-pokemon>
        <div class="pokemon-header">
          <div class="number"><small>{{pokemon.id}}</small></div>
          <img pokemon-image [src]="pokemon.artwork" [alt]="pokemon.name" />
        </div>
        <div class="pokemon-detail">
          <div class="headline">
            <h3 class="name">{{pokemon.name | titlecase}}</h3>
            <div class="type-wrapper">
              <small class="type">{{pokemon.type | titlecase}}</small>
              <small>{{getTypeEmoji(pokemon.type)}}</small>
            </div>
          </div>
          <div class="stat-wrapper">
            <div class="stat">
              <span class="stat-number">{{pokemon.attack}}</span>
              <span class="stat-name">Attack</span>
            </div>
            <div class="stat">
              <span class="stat-number">{{pokemon.defense}}</span>
              <span class="stat-name">Defense</span>
            </div>
            <div class="stat">
              <span class="stat-number">{{pokemon.speed}}</span>
              <span class="stat-name">Speed</span>
            </div>
          </div>
        </div>
      </app-pokemon>
    </div>
  `,
  styleUrls: ['./modern-pokemon.component.scss'],
})
export class ModernPokemonComponent {
  @Input() pokemon: any;

  getTypeEmoji(type: string): string {
    ...
  }
}
```

The Pokémon header remains unchanged, while the Pokémon detail section comprises two main components: the `headline` and the `stats`.

- `headline`
```html
<div class="headline">
  <h3 class="name">{{pokemon.name | titlecase}}</h3>
  <div class="type-wrapper">
    <small class="type">{{pokemon.type | titlecase}}</small>
    <small>{{getTypeEmoji(pokemon.type)}}</small>
  </div>
</div>
```
- `stats`
```html
<div class="stat-wrapper">
  <div class="stat">
    <span class="stat-number">{{pokemon.attack}}</span>
    <span class="stat-name">Attack</span>
  </div>
  <div class="stat">
    <span class="stat-number">{{pokemon.defense}}</span>
    <span class="stat-name">Defense</span>
  </div>
  <div class="stat">
    <span class="stat-number">{{pokemon.speed}}</span>
    <span class="stat-name">Speed</span>
  </div>
</div>
```
As you can see, the `pokemon-detail` is entirely different. However, we don't need to make any changes to the `PokemonComponent` to support it. Everything is handled separately within the `ModernPokemonComponent`. Therefore, we can ensure that the `StandardPokemonComponent` remains unchanged without any effects.

Thanks to **Content Projection**, we can accommodate two distinct Pokémon cards with entirely different styles.
 
> _Separation of Concerns_

The UI part is separately handled by `StandardPokemonComponent` and `ModernPokemonComponent`

> _Open-Closed Principle_

The Content Projection helps us avoid getting our hands dirty by adding `ModernPokemonComponent` without modifying the common `PokemonComponent`.

![Modern Pokemon Demo](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/03vgxapgv00lxetu948f.png)

[_Link to StackBlitz_](https://stackblitz.com/edit/stackblitz-starters-lb9plq?file=src%2Fmain.ts)

<hr />

### Final thought

Thank you for making it to the end! Content Projection is a fundamental tool that helps us create flexible components. We will continue this series by delving into another powerful Angular tool, `*ngTemplateOutlet`. If you found something interesting in this post or gained some value from it, please let me know by leaving a comment or reacting to this post. Your feedback and engagement are a significant source of motivation that keeps me inspired to continue publishing my next blog. ❤️

<hr>

> Read more:

[<p *>Demystifying the Angular Structural Directives in a nutshell</p>](https://dev.to/khangtrannn/p-demystifying-the-angular-structural-directives-in-a-nutshell-1g61)