DEV Community

Caio Ragazzi
Caio Ragazzi

Posted on

Mastering Angular Templates

Hello Devs,

If you're an Angular developer, chances are you've worked with PrimeNG. I've been using it for some time, and there's always been one aspect that caught my attention: TEMPLATES!

Take, for instance, the PrimeNG Card component. This component provides a header property where you can pass a string, but the real power lies in customizing the header to your liking using a pTemplate. It functions as illustrated below:

<p-card header="Car Header">
    <ng-template pTemplate="header">
        <img alt="Card" src="http://fake.url.png" />
    </ng-template>
</p-card>
Enter fullscreen mode Exit fullscreen mode

That's pretty awesome, right?
If you're as curious as I am, you probably want to dive into the inner workings of this template feature. No worries, my friend – I've got your back. I've explored and figured it out, and now you can too. Here's how it works, and you'll be able to apply this strategy in your components as well.

The core idea is to capture the content inside the ng-template and render it at a specific location within our component. But not just any template – we want a specific one. In the example below, our aim is to render only the template with the directive myTemplate="header" in our component:

<my-component header="Car Header">
  <ng-template myTemplate="header">
    <button>Simple button</button>
  </ng-template>
</my-component>
Enter fullscreen mode Exit fullscreen mode

First things first, let's create the myTemplate directive:

import { Directive, Input, TemplateRef } from '@angular/core';

@Directive({
  selector: '[myTemplate]',
  host: {},
})
export class MyTemplateDirective {
  @Input() type: string | undefined;

  @Input('myTemplate') name: string | undefined;

  constructor(public template: TemplateRef<any>) {}

  getType(): string {
    return this.name!;
  }
}
Enter fullscreen mode Exit fullscreen mode

As we can see, it's a straightforward structural directive (more on structure directives here: Angular.io) with a single input to receive the type (in our case, "header"), and a function named getType that returns the input value (I'll explain the reason for this function later in the article).

Now, let's move on to creating the component .

import {
  AfterContentInit,
  Component,
  ContentChildren,
  QueryList,
  TemplateRef,
} from '@angular/core';
import { MyTemplateDirective } from '../my-template-directive/my-template-directive';

@Component({
  selector: 'my-component',
  templateUrl: './my-component.component.html',
  styleUrl: './my-component.component.scss',
})
export class MyComponent implements AfterContentInit {
  headerTemplate: TemplateRef<any> | undefined;

  @ContentChildren(MyTemplateDirective) templates:
    | QueryList<MyTemplateDirective>
    | undefined;

  ngAfterContentInit() {
    (this.templates as QueryList<MyTemplateDirective>).forEach((item) => {
      switch (item.getType()) {
        case 'header':
          this.headerTemplate = item.template;
          break;

        default:
          break;
      }
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

Let's break down this component:

1) We require a property (headerTemplate) with the type TemplateRef to store the template that is being passed to the component.
2) We need a property (templates) to store all the templates that have our custom directive assigned to them. In our case, this property is 'templates' and is decorated with @ContentChildren(MyTemplate) (you can find more about the @ContentChildren decorator Angular.io)
3) With the help of the lifecycle hook AfterContentInit (you can learn more about the AfterContentInit lifecycle hook Angular.io), we iterate over all the templates and call the function getType to retrieve only the template with the directive "header," storing it in the headerTemplate property.

That wraps up the TypeScript part of our component. Essentially, we're searching for all ng-templates with our custom directive that were passed to MyComponent and extracting only the one with the 'header' input.

Now that we have the template in the headerTemplate property, let's proceed to display it in the HTML. Take a look at the code below:

<div>
  <div>
    <div *ngIf="headerTemplate">
      <div>Template content</div>
      <ng-container *ngTemplateOutlet="headerTemplate"></ng-container>
    </div>
  </div>
  <div>--------------------------</div>
  <div>
    <div>Footer without template</div>
  </div>
</div>
Enter fullscreen mode Exit fullscreen mode

It's as simple as that. By utilizing the Angular structural directive ngTemplateOutlet (you can find more information on ngTemplateOutlet Angular.io) we use an ng-container to pass our headerTemplate as input with '*ngTemplateOutlet="headerTemplate"'. This allows us to dynamically render the template within our component.

This strategy empowers us to build highly customizable and flexible components. By implementing this approach, we ensure a robust component that only renders templates specified by the custom directive. It provides a level of control and encapsulation, allowing developers to create versatile components while maintaining a clear structure and limiting the rendering scope to specific templates. This can lead to more maintainable and modular code.

What are your thoughts on this design?
In what ways could this design be improved or extended?

If you want to explore the code for this example, you can find it in the GitHub repository: Angular Templates GitHub Repository.

Thank you so much for reading this post, everyone! As always, I wish you happy coding!

See you!

Top comments (0)