{ "feature_image_attr": "", "community_link": "https://community.indepth.dev/t/heres-what-you-should-know-when-creating-flexible-and-reusable-components-in/645" }

Here's what you should know when creating flexible and reusable components in Angular

In this article we're going to explore the difference between ng-content and ng-template  and see when to use which

Here's what you should know when creating flexible and reusable components in Angular

Reusable components appear in almost every modern web application. Not only do we have to reuse the same components in complex business applications, we also have to reuse them in a different context.

When reusing a component in a different business context it's important that a part of the component is flexible and can be adjusted to the use case.

Imagine tabs, cards or an expander. All those elements have a similar base structure and behaviour but the content can differ. We need a way to integrate a flexible content with a reusable base.

Modern web technologies and especially Angular offer us great mechanisms to implement such components.

Templates and slots

The base idea of reusable and flexible components is simple. You have a reusable host component. This component contains some standard behaviour and template which is always the same.

Furthermore it also defines some slots. Those slots define where the flexible content will end up. Depending on the use case, different content can be passed to the same component.

In order to create flexible web components, modern browsers support the <slot> and <template> tag.

The <template> tag holds content but will not render anything on the page.

<template id="foo">
 <style>
  h4 { color: blue }
 </style>
 <h4><slot name="title"></slot></h4>
</template>

To render the content of a <template> tag to the DOM we need to write some lines of JavaScript. In the example above we will register our template as a customElement.

customElements.define('foo',
  class extends HTMLElement {
    constructor() {
      super();
      let template = document.getElementById('foo');
      let templateContent = template.content;

      const shadowRoot = this.attachShadow({mode: 'open'})
        .appendChild(templateContent.cloneNode(true));
    }
  }
);

Once our web component is registered we can use it and pass some dynamic content to it.

<foo>
  <span slot="title">Title of the foo web component</span>
</foo>

To pass the dynamic content we need to define it inside a tag with the slot attribute on it. The value of the slot attribute decides where the dynamic content will end up.

The foo component can be reused in various scenarios and its content can differ from use case to use case.

Components with dynamic content in Angular

Angular offers us similar concepts to handle dynamic content; ng-content and ng-template.

Ng-template

ng-template has the ability to hold content without rendering it. It is the underlying mechanism to all directives that modify the DOM structure and therefore a fundamental concept behind all structural Angular directives like *ngIf or *ngFor.

ng-template is very powerful and can be used for multiple things, one of them is creating flexible components by passing a template to a host component.

Let's have a look at a simple heading component that accepts any kind of title. Could be an h1, h2 or any other tag.

First we prepare the dynamic content so that we can hand it over to the host component.

<ng-template #theTruth>
  <h4>Real Madrid - best football club ever</h4>
</ng-template>

Since this code is wrapped in an ng-template it doesn't render anything. The beautiful thing is that we can use a template reference (#theTruth) to access this template, pass it around and use it wherever we want.

So if we want to display this title inside a heading component, we could pass the template as an @Input property and then display it.

<ng-template #theTruth>
  <h4>Real Madrid - best football club ever</h4>
</ng-template>

<heading [title]="theTruth"> </heading>

Inside our heading component we could use *ngTemplateOutlet in combination with ng-container to render it.

<!-- some other HTML -->
<ng-container *ngTemplateOutlet="title"></ng-container>

This is one way of creating flexible components in Angular. Another and more convenient way is to use ng-content.

Ng-content

ng-content is similar to the slot tag. It allows us to specify spots in the host components. Those spots define where the dynamic content will end up. When we use ng-content we usually speak of content projection.

A host component can define multiple slots. If it does, it usually uses the select property with a selector on ng-content to decide which content should end up in the slot. If no selector matches, a projected content ends up in the ng-content slot without a selector.

Therefore the template of the heading component is very simple. It has some HTML and accepts some content which will be projected to the default slot.

<!-- some other HTML -->
<ng-content></ng-content>

The heading component could then be used in the following way.

<heading>
  <h4>Real Madrid - best football club ever</h4>
</heading>

We have now seen the two approaches Angular offers us to build flexible components. The question now is, which one to use and is there a difference in the approaches? Let’s answer this question by implementing a real use case in the form of an expander component.

Creating a flexible "expander" component

A classic expander consists of a header, an expand icon, and a content. While the header is just a text that differs, the content can be any kind of HTML element, an Angular component, or even plain text. It varies from use case to use case.

Content projection in Angular — Any type of content can be projected, HTML elements, components, or plain text.

This sounds like a good match for ng-content.

Implementing an expander with ng-content

The ng-content tag is used in the host component, the component that accepts the flexible content. It acts as a placeholder for the projected content and allows us to specify where the projected content will end up.

Let’s take a closer look by implementing an expander from scratch.

Expander component that uses ng-content to project content to a dedicated slot

Our div with the header class displays the value of the heading property received via @Input. The ng-content tag specifies where our content is projected. Clicking on the header allows us to expand the expander (Show or hide the projected content).

This expander component can then be used in the following way.

Project some simple text into our expander

Projecting a component

Currently, we project text to our expander, which is not very interesting. Let’s take it a step further and project a clock component that displays the current time.

@Component({
	selector: 'clock',
	template: `{{currentTime}}`,
	styleUrls: ['./clock.component.scss']
})
export class ClockComponent implements OnInit {
	currentTime: string;
	
	ngOnInit() {
		const date = new Date();
		this.currentTime = `${date.getHours()}:${date.getMinutes()}`
	}
}

The clock component is straightforward. Inside the ngOnInit lifecycle hook, we get the hours and the minutes of the present time and assign it to a variable which we then display in the template.

Now, it’s time to show this component in our expander.

<expander heading="Expand to see the current time">
    <clock></clock>
</expander>

Which then nicely displays the time.

But, what happens if you close and reopen the expander after 5 minutes? It still displays 9:51, why is that? ?

Ng-content and lifecycle hooks

The source of the problem lies in the way ng-content calls life cycle hooks. Let’s inspect this further by logging out the ngOnInit and the ngOnDestroy lifecycle hooks of our clock component.

Let’s open up the dev tools and refresh our application.

ngOnInit is called even though the component has not been rendered yet

Even though our clock component isn’t yet rendered (the ngIf expression of the expander is stillfalse) the ngOnInit lifecycle hook is already called.

Let’s now expand our component without clearing the log.

ngOnInit is not called again — even if the component is rendered for the first time

We can see that the ngOnInit lifecycle hook is not called again, and we, therefore, don’t get the current time, but the time we started our application.

Gosh! But there’s even more to it. So far we only looked at the ngOnInit hook, what about ngOnDestroy? Let’s close the expander again without clearing the log.

ngOnDestroy is not called — even though the component is removed from the DOM

No ngOnDestroy call happened even though the component is completely removed from the DOM.

When are the ngOnInit and the ngOnDestroy hooks called?

When projecting content via ng-content the life cycle hooks are bound to the life cycle of the parent component.

Let’s inspect this by adding a new button to toggle our expander component.

Lifecycle hooks of the projected content are bound to the hooks of the host component

We can see that every time our expander is rendered or removed, the life cycle hooks of our projected content is called.

The lifecycle hooks of the projected content are bound to the lifecycle of the host.

Let’s summarize:

  1. ngOnInit is called once the host is rendered
  2. ngOnDestroy is called once the host is destroyed
  3. Removing and rendering the projected content via ngIf doesn’t call the life cycles.

When is this behavior problematic?

This behavior can become very problematic once the projected component does some heavy logic or position calculation in the ngOnInit life cycle hook.

Furthermore, since we work with RxJS, chances are there that this behavior can lead to memory leaks. The ngOnDestroy hook may never get called, and therefore, streams are never properly unsubscribed. Unhandled subscriptions can easily result in memory leaks once your application grows.

If you want to find out more about subscription managment and memory leaks I highly recommend you to check out this post How to create a memory leak in Angular

How to solve this issue?

One way of solving this issue is to change the approach from ng-content to ng-template. Let’s refactor our expander component to take advantage of ng-template.

First, we add a new @Input that accepts a TemplatRef , which is our content.

@Input() content: TemplateRef<any>;

Then we adjust the expander's HTML to render the received content.

<div class="header" (click)="toggleExpand()">
    {{heading}}
    <i *ngIf="!expanded" class="fas fa-chevron-down"></i>
    <i *ngIf="expanded" class="fas fa-chevron-up"></i>
</div>
<div class="content" *ngIf="expanded">
    <ng-container [ngTemplateOutlet]="content"></ng-container>
</div>

this also changes the API and the usage of the expander component.

<expander heading="Expand to see the current time" [content]="content">
</expander>

<ng-template #content>
  <clock></clock>
</ng-template>

We now wrap the clock component inside a ng-template. We then use a template reference to pass this content down to our expander component.

Let’s have a look if it changed the behavior of the lifecycles.

templates life cycle hooks get correctly called

The life cycle hooks are now called once the component gets rendered or destroyed. Basically, every time our expanded flag changes.

But, the API is less readable. A lot of devs prefer the readability of the ng-content approach. Inlining the projected content into the tags of a component is more readable vs. passing it via template reference.

ng-content API vs. ng-template API

With the ng-content approach you already know at first sight that the clock is projected to the expander. It’s less evident in the ng-template approach.

Mimic ng-content like API with ng-template

Even though the approach shown above is the most common approach on how to use ng-template we have other ways of using it. We are going to use a method that mimics the ng-content API. How about this as an API?

<expander>
  <ng-template>
  	<clock></clock>
  </ng-template> 
</expander>

To support this approach, we use @ContentChild to access the projected content. So instead of getting the content via @Input property we access it via @ContentChild.

@Input() content: TemplatRef<any>; 
// becomes
@ContentChild(TemplateRef) content: TemplateRef<any>;

We project the content with a convenient API and have the life cycle hooks called at the correct time.

Does this mean that we should always prefer template ref vs ng-content?

No. ng-content is the easier approach with the cleaner API. It makes perfect sense for simple scenarios where you just project content without rendering it dynamically.

ng-template on the other hand makes sense once you start to render the projected content dynamically. So basically once you find your ng-content wrapped with an ngIf you should consider using ng-template.

ng-template also makes sense for scenarios where you want to render the same content in multiple places.