Angular Components, Templating, and Reusability

Views: 2043

If you’ve been using Angular for a while now then you probably already know some of these points I’ll be mentioning here. However, if you’re new to the tech, and you want to develop the right coding habits to be really awesome in it, then you better read on.


Component reusability, its limits, and the usual practices

Coding components with reusability in mind is not only an ideal practice, but it’s actually practical and will make you more efficient. How exactly?

Well, when creating a component, we often end up with a situation like this one:

@Component({
  selector: 'app-page-list',
  templateUrl: 'page-list.component.html',
  stylesUrl: 'page-list.component.scss'
})
class PageList {
  title: string;
  actions: [];
  diplayFilters: boolean;
  displaySearch: boolean;
  displaySort: boolean;
  displayPagination: boolean;
  dataset: [];
  canEditRow: boolean;
  canDeleteRow: boolean;
}


This component will be used like that:

<app-page-list title="" actions=""
  displayFilters="" displaySearch="" dataset="" canEditRow="" ...></app-page-list>


In that kind of situation, we end up with a component that is very hard to extend and with a ton of parameters. Not customizable at all.

The customizations are done by adding more booleans. And when the day comes that we’ll want to change how the rows are displayed, we’ll have to create completely new and different components such as app-user-list and app-blog-list, which will duplicate a lot of code.

Components are great... when they are simple to use. But often, they come with a fixed template that can’t be changed. So let’s discuss how to improve this kind of situation.


Content Projection (transclusion), TemplateRef, and Containers

Angular comes with a few ways to inject content in the DOM. The first one is content projection, which is better explained with a simple example:

@Component({
  selector: 'app-my-content',
  template: '<ng-content></ng-content>',
})
class MyContent {
}


Parent component:

<app-my-content><p>This will be projected in the component</p></app-my-content>


Pretty simple, the content of the component tag is projected inside the component and replaces the ng-content tag. The problem that comes with this is if you pass a sub-component in the content that sub-component will be built in the parent component. Consequently, it will not be destroyed when the child component is destroyed.

This can cause a memory leak:

<app-my-content>
  <app-my-other-component>Ouch!</app-my-other-component>
</app-my-content>


Here, app-my-other-component is built not in app-my-content but in the component calling it. Therefore, it will not be destroyed when app-my-content is destroyed.

So, content projection is great for decorating simple HTML code, but that stops being true when you start injecting components.

Template to the rescue

It’s easy to fix that problem though, we simply need to use a template:

@Component({
  selector: 'app-my-content',
  template: '<ng-container *ngTemplateOutlet="tplRef"></ng-container>',
})
class MyContent {
  @Input() tplRef: TemplateRef<any>;
}


Parent component:

<app-my-content tplRef="myContent"></app-my-content>
<ng-template #myContent>
  <app-my-other-component>Ouch!</app-my-other-component>
</ng-template>


What have we done in the example above? We created a template using ng-template and injected it as a parameter in the child component. The child component is then using it in a container, thanks to the ngTemplateOutlet structural directive.

By doing so, we avoid the memory leak as the ng-template will be created only when it’s used inside the child component.

ng-container vs ng-template

In the example above, we also introduced the ng-container tag. The ng-container is simply an element used to group other elements without interfering with the DOM. A structural directive (ngIf, ngFor, ngSwitch, etc.) can also be attached to that element.

ng-template will create a TemplateRef object containing the elements grouped in it. It will not affect the DOM until it is actually used.


ViewChild vs ContentChild

In the previous example, we still use component parameters to inject into our template. But if we have many templates, we’ll end up having the same problem we initially wanted to solve. So let’s make a few changes to our code, shall we?

@Component({
  selector: 'app-my-content',
  template: '<ng-container *ngTemplateOutlet="tplRef"></ng-container>',
})
class MyContent {
  @ContentChild('myContent', {static: true}) tplRef: TemplateRef<any>;
}


Parent component:

<app-my-content>
  <ng-template #myContent>
    <app-my-other-component>Ouch!</app-my-other-component>
  </ng-template>
</app-my-content>


Here, we introduce the ContentChild decorator. Maybe you already know the ViewChild decorator and you’re thinking that it’s working pretty much the same. However, the ViewChild will find a child in the component, defined by it’s template while the ContentChild will find a child in the content defined by the user of the component.

By doing that small change, we avoid passing many parameters to the component, and the code gets even clearer.


Example of a Page Component

Below, we are going to create something more practical. The component will be a carded page, displaying a list of cards, a title, some actions, a filtering and sorting system, and a pagination.

I greatly simplified the HTML code here to make sure it’s still clear.

Page Carded Component

@Component({
  selector: 'app-page-carded',
  templateUrl: 'page-carded.component.html',
  stylesUrl: 'page-carded.component.scss',
  ViewEncapsulation.None,
})
class PageCarded {
  @ContentChild('title', {static: true}) titleTemplate: TemplateRef;
  @ContentChild('actions', {static: true}) actionsTemplate: TemplateRef;
  @ContentChild('search', {static: true}) searchTemplate: TemplateRef;
  @ContentChild('content', {static: true}) contentTemplate: TemplateRef;
}


Page Carded Template

<ng-container>
  <div class="header">
    <div class="title">
      <ng-container *ngTemplateOutlet="titleTemplate"></ng-container>
    </div>
    <div class="actions">
      <ng-container *ngTemplateOutlet="actionsTemplate"></ng-container>
    </div>
    <div class="search">
      <ng-container *ngTemplateOutlet="searchTemplate"></ng-container>
    </div>
  </div>
  <div class="content">
      <ng-container *ngTemplateOutlet="contentTemplate"></ng-container>
  </div>
</ng-container>



Parent Component

Articles List

Below, we reuse the PageCard component to create a list of articles:

@Component({
  selector: 'articles',
  templateUrl: './articles.component.html',
  styleUrls: ['./articles.component.scss']
})
export class ArticlesComponent implements OnInit, OnDestroy {
  public dataSource: Article[];

  /**
   * listForm is a FormGroup, by subscribing to its valueChanges we can
   * refresh the dataSource every time the filters or search are changed.
  **/
  protected listForm: FormGroup;
}


<div id="articles-list">
  <!-- Our page carded component -->
  <app-page-carded>
    <ng-template #title>
      <h1>Users List</h1>
    </ng-template>

    <ng-template #search>
      <!-- Display a search box -->
      <app-list-search [listForm]="listForm"></app-list-search>
      <!-- Display a modal form containing our filters -->
      <app-list-filters [listForm]="listForm"></app-list-filters>
    </ng-template>

    <ng-template #actions>
      <!-- Some actions we want to add on the list -->
      <button [routerLink]="'/articles/new'"><mat-icon>add</mat-icon></button>
    </ng-template>

    <ng-template #content>
      <!-- And finally our list and a pagination -->
      <app-users-list [dataSource]="dataSource"></app-users-list>
      <mat-paginator (page)="onPageEvent($event)"></mat-paginator>
    </ng-template>
  </app-page-carded>
</div>


As you can see, it’s very easy to customize our page because we have access to every placeholder in the child component. If we want to display or hide a button, we can simply add a ngIf in the template or just remove it. If we prefer to have an infinite scroll instead of a pagination, we can simply remove the pagination and customize our list accordingly.

The PageCardComponent will still define most of the CSS and HTML to display the page properly.

Styles Scoping

In Angular, all styles are strictly scoped to the component in which they are declared. This means you cannot define "global" styles reusable by all your ContentChild elements unless you specify ViewEncapsulation.None in the child component’s decorator. That will make the style in that component global to the whole application. So it's good practice to manually add some scope in the CSS file.

app-page-carded {
  h1 {
    font-size: $big;
  }
}



Some quick tips for better reusable components

Push the services up the tree.

Data querying should not be in child components since that would create a coupling between the component and the service. This is so we don't hit any snags if we want to reuse that same component but with different data.

Listen to events instead of injecting services

Whenever possible, it’s better to have the services in the parent component and simply listen to events from the child components. This is not always possible though, for example, when we want to display error messages or logs, or maybe open a modal window. But since those couplings are not too tight as these services don’t provide data, then you can simply let those slide.

Clean-up the parameters

When you start having too many parameters, think about templating. This is especially advisable when some of the parameters are just toggles for displaying or hiding elements.


The Roadmap

Here’s a quick recap of everything I’ve discussed here, so far.

  1. Component reusability, limits, and usual practices
    • Passing input to the components
    • Less flexibility, too many parameters
    • When to switch to templating
  2. ng-content vs ng-template vs ng-container
    • ng-content is used for content projection
    • ng-container provide us with an element to which we can attach structural directives without adding any element in the DOM
    • ng-template create a templateRef object that can be used
  3. Transclusion vs Content projection
    • Transclusion mean passing the template to the component, the template will be duplicated but the component will have more flexibility and will be easier to use
  4. ViewChild and ContentChild, their differences and how to use them
    • ViewChildren are provided by the component
    • ContentChildren are provided by the user
  5. What is a structural directive?
  6. templateRef and ngTemplateOutlet
  7. Styles scoping
  8. Component and template identification


Helpful tips during the design phase

Lastly, you can avoid a lot of frustrated hair-pulling and heartaches if you follow these tips right at the start of a project, especially during the design phase.

  1. Define the type of pages you have (list, form, dashboard, etc.)
  2. Define the different emplacement you want to customize in every page. It can be the menu, the topbar, the header, etc.
  3. Define elements that are kept and cannot be customized
    • Identify and define common pages/components
    • Identify and define customizable/reusable parts in the pages/components
    • Clearly separate and mark the customizable parts


Was this helpful? Then share this article with your Angular-loving friends and let me know what you think! And if you’re up for exciting new challenges in your career, remember that our door is always open at Arcanys.


Sources:
Using ViewChild in Angular to Access a Child Component, Directive or DOM Element
Angular ng-template, ng-container and ngTemplateOutlet - The Complete Guide To Angular Templates
Angular: Why you should consider using template outlet instead of content projection

About the author

Eric has been working as a software engineer for more than 20 years. As a senior architect for Arcanys, he works closely with the developers to instill the habit of learning, clean coding, re-usability and testing with the goal of increasing the overall quality of the products delivered by the teams.

Be part of our growing community.

Join us and outsource smarter.

Please verify that you are not a robot.