Understanding Angular’s Content Projection

Written by jcfrane

March 6, 2020

Content Projection is a pretty complex topic in Angular that not everyone might be familiar with. 

Content Projection is an old concept that is already present on angularjs 1.x and it’s called transclusion.  Wikipedia describes it like this.

In simple terms, Content Projection is just inserting elements/components into a slot provided by a particular element for it to render.

If you are using Angular Material library then you probably encounter Content Projection already.

  <mat-card-header>
    <div mat-card-avatar></div>
    <mat-card-title>Shiba Inu</mat-card-title>
    <mat-card-subtitle>Dog Breed</mat-card-subtitle>
  </mat-card-header>

In the implementation above,  <mat-card-title> and <mat-card-subtitle> are the projected contents.

To understand the concept well, it is always good to have a little bit example in where we implement Content Projection ourselves.

Supposed your Project Manager assigned you a task to display different shapes within a container, how would you implement it?

Our first thought might be probably like this:

app.component.html

<shape-container></shape-container>

shape-container.component.html

<h1>Shapes</h1>
<shape type="square"><shape>
<shape type="rectangle"><shape>

shape.component.html

<div [class]="type">
</div>

styles.css

  .square {
    width: 100px;
    height: 100px;
    margin: 10px;
    background: green;
  }

  .rectangle {
    margin: 10px;
    width: 200px;
    height: 100px;
    background: red;
  }

  .circle {
    width: 100px;
    height: 100px;
    background: yellow;
    border-radius: 50%
  }

  .parallelogram {
      width: 150px;
      height: 100px;
      transform: skew(20deg);
      background: blue;
  }

  .green {
    background: green;
  }

  .blue {
    background: blue;
  }

  .red {
    background: red;
  }

  .yellow {
    background: yellow;
  }

Alright, simple coding isn’t it?

However, the problem starts when we add more functionalities to our components. What if your Project Manager asked you that the container needs to have indefinite number of shapes? Again, we do it the normal way – accept inputs from app.component then loop through it inside the shape-container.component.

First we create a Shape model interface.

export interface Shape {
  type: string;
  color: string; // Yeah we want it to have vibrant colors!
}

Then in our AppComponent we add the following line:

  shapes: Shape[] = [
    { type: 'circle', color: 'blue' },
    { type: 'square', color: 'red' },
    { type: 'rectangle', color: 'green' },
    { type: 'parallelogram', color: 'yellow' }
  ];

Then we render it as follows in our app.component.html:

<shape-container [shapes]="shapes"></shape-container>

In our ShapeContainerComponent we add the shapes input as follows:

@Input() shapes: Shapes[];

Then render it as follows in our template:

<h1></h1>
<shape *ngFor="let shape of shapes" [type]="shape.type"></shape>

Easy peasy. If we can do it this way, then why the hell we still need Content Projection?


The Problem Content Projection Is Trying to Solve

What If I told you that you can render ShapeComponent directly from app.component.html without passing extra inputs to ShapeContainer ? Like how Angular Material Card Component did it?

Like so:

<app-shape-container>
 <app-shape *ngFor="let shape of shapes" [type]="shape.type">
 </app-shape>
</app-shape-container>


Isn’t it more beautiful and smartass?

This approach will save you a lot of unnecessary codes by avoiding passing inputs from the parent container down to its child component when the only thing the parent container do should be displaying its child components and listening to their interactions. This will make more sense if the parent component is holding more component and each components have their own inputs as well.

How can we achieve it?


Enter…

<ng-content>

Basically, think of <ng-content> as a placeholder. With this placeholder present inside our ShapeContainerComponent we can insert elements or components/directives directly into it. Here’s how our ShapeContainerComponent’s template will look like now:

shape-container.component.html

<h1></h1>
<ng-content select="app-shape"><ng-content>

Now we can remove @Input from the ShapeContainerComponent.

The select attribubte can have values of normal html tags (<h1>, <p>, etc) or a directive.

If you are trying to project a directive replace it like so:

<ng-content select="[myDirective]"><ng-content>

How our code looks now?

app.component.ts

export class AppComponent  {
  shapes: Shape[] = [
    { type: 'circle', color: 'blue' },
    { type: 'square', color: 'red' },
    { type: 'rectangle', color: 'green' },
    { type: 'parallelogram', color: 'yellow' }
  ];
}

app.component.html

<app-shape-container">
  <app-shape *ngFor="let shape of shapes" [shape]="shape"></app-shape>
</app-shape-container>

shape-container.component.ts

export class ShapeContainerComponent {
 // See no inputs LOL
}

shape-container.component.html

<h4>Shapes!</h4>
<ng-content select="app-shape"></ng-content>

shape.component.ts

export class ShapeComponent implements OnInit {
  @Input() shape: Shape;
  @Output() clicked: EventEmitter<Shape> = new EventEmitter<Shape>();

  constructor() { }

  // We'll just bind this to our div's class
  getClasses(): string {
    return `${this.shape.type} ${this.shape.color}`;
  }

  ngOnInit() {
  }
}

shape.component.html

<div  [ngClass]="getClasses()"></div>

What if again, your annoying Project Manager asked you that when the ShapeComponent is clicked the color must change?


Querying Projected Contents and Listening For Events

@HostListener

Add @HostListener in our ShapeComponent like so:

export class ShapeComponent implements OnInit {
  @Input() shape: Shape;
  @Output() clicked: EventEmitter<Shape> = new EventEmitter<Shape>();

  constructor() { }

  getClasses(): string {
    return `${this.shape.type} ${this.shape.color}`;
  }

  ngOnInit() {
  }

  @HostListener('click', ['$event'])
  handleClick(event) {
    console.log('CLICKED');
    this.clicked.emit(this.shape);
  }
}

Notice that we have an EventEmitter. We do this because we need our parent component to listen for this particular event then use the value emitted for its purposes. Here’s our ShapeContainerComponent‘s code:

export class ShapeContainerComponent implements OnInit, AfterContentInit {
  @ContentChildren(ShapeComponent) shapes: QueryList<ShapeComponent>;
  constructor() { }

  ngOnInit() {

  }

  ngAfterContentInit() {
    this.listenToClickEvents();
  }

  private listenToClickEvents() {
    this.shapes.forEach((shapeComponent) => {
      shapeComponent.clicked.asObservable().subscribe((shape) => {   
        const colors = ['blue', 'red', 'yellow', 'green'];
        let randomColor = shape.color;
        // We do not want to repeat
        while (randomColor === shape.color) {
          randomColor = colors[Math.floor(Math.random() * colors.length)];
        }
       
        shapeComponent.shape = {type: shape.type, color: randomColor};
      });
    });
  }
}

Conclusion

Content Projection is a nice concept to master. It will not only help you with minimizing your codes but it will also make your components less coupled and reusable. You can view the whole code at https://stackblitz.com/edit/content-projection-demo

For more lessons about Content Projection you can visit the following URLs:
https://www.thecodecampus.de/blog/content-projection-in-angular/
https://dev.to/salmankazmi6/content-projection-in-angular-34a5

Thank you so much for reading! ?

You May Also Like…

Github Code Navigation Made Easy!

Github Code Navigation Made Easy!

Did you know GitHub's unlimited private repositories are now free? Yeah, you read that right!As such, I started to use...

0 Comments

Submit a Comment

Your email address will not be published. Required fields are marked *