DEV Community

Cover image for Ng-magical directives series (*ngTemplateOutlet)
Valentine Awe
Valentine Awe

Posted on

Ng-magical directives series (*ngTemplateOutlet)

"Magic is just science that we do not understand yet"
...Arthur C. Clarke

This article is part of what I call the magical directives series. In this series, we will unravel the mystery behind some interesting Angular directives. Afterwards, we can add this little magic to our tool box. I call them magical directives because they play a very important role in building reusable components across our Angular applications.

Below are the directives that we will be looking at in this series.

  • ng-template
  • ng-container
  • ng-content
  • *ngTemplateOutlet
links to other articles in this series are below

The ngTemplateOutlet

In our last article ng-content, We said that we are going to discuss the conditional content projection in this particular article. One of the major draw backs in using the ng-content is the ability to specify default content. What this means is that if a user does not provide the content for a select attribute, that particular content tag will be blank. This is the first problem the *ngTemplateOutlet helps to solve. The *ngTemplateOutlet helps to build configurable components and also helps in the insertion of common template in different sessions of our page.

Let us start with the simple use case for the *ngTemplateOutlet, which is the insertion of a template into different sessions of our page. for example, if you have a repeated icon across your page, instead of creating the image tag or repeating the host html that holds that image, you can create a template with the <ng-template> and then use the *ngTemplateOutlet to reference it in every session that you need to display the icon.

// Template to be used at multiple sessions in our application

  <ng-template #ourIcon>
     <img src="urImage.jpg" alt="someImge">
  </ng-template>

//reference the template across different sessions in our application

  <div>
    <div class="header">
      <ng-container *ngTemplateOutlet="ourIcon"> </ng-container>
       <h1>Our lovely page</h1>
    </div>

    <div class="body">
      <ng-container *ngTemplateOutlet="ourIcon"> </ng-container>
    </div>

    <div class="Footer">
      <ng-container *ngTemplateOutlet="ourIcon"> </ng-container>
    </div>
  </div>

Enter fullscreen mode Exit fullscreen mode

In the example above, we have been able to use the icon template across multiple sessions in our page without re-writing the html, and if we ever want to change the image url, we just have to update the template without touching the sessions where it is used.

The second use case, is for the conditional content projection

// books-view.html

<h2>Our Story Books..</h2>

<ng-container *ngTemplateOutlet="booksTemplate ? booksTemplate : defaultBooks;">
</ng-container>

<ng-template #defaultBooks>
  <div *ngFor="let bk of booklist" class="books-card_default">
    <h1>{{bk.name}}</h1>
    <p>{{bk.author}}  - {{bk.year}}</p>
 </div>
</ng-template>

Enter fullscreen mode Exit fullscreen mode
  //books.view.ts
  import { Component, Input, OnInit, TemplateRef } from '@angular/core';

@Component({
  selector: 'app-books-view',
  templateUrl: './books-view.component.html',
  styleUrls: ['./books-view.component.scss']
})
export class BooksViewComponent implements OnInit {

  @Input() booksTemplate!:TemplateRef<HTMLElement>
  @Input() booklist = [
    {name:'The young shall grow', author:'some persons', year:'1975'},
    {name:'Without a silver spoon', author:'some body', year:'1407'},
    {name:'Lara the sugar gurl', author:'everybody',year:'1947'},

  ];

  constructor() { }

  ngOnInit(): void {
  }

}

Enter fullscreen mode Exit fullscreen mode

In sample code above, we have defined a view for our lists of books.
First in the books-view .ts file, we are expecting an input of type templateRef

  @Input() booksTemplate!:TemplateRef<HTMLElement>
Enter fullscreen mode Exit fullscreen mode

and in the .html file we defined another template called defaultBooks

<ng-template #defaultBooks>
  <div *ngFor="let bk of booklist" class="books-card_default">
    <h1>{{bk.name}}</h1>
    <p>{{bk.author}}  - {{bk.year}}</p>
 </div>
</ng-template>

Enter fullscreen mode Exit fullscreen mode

and in our template, we created the template container for the view.

<ng-container *ngTemplateOutlet="booksTemplate ? booksTemplate : defaultBooks;">
</ng-container>

Enter fullscreen mode Exit fullscreen mode

This is simply saying that, hey! we are expecting an input of type templateRef called booksTemplate, If it is provided, then display it, else use the default defined template called the defaultBooks

Now, you can use the books-view in any part of our application in the following ways.

  1. pass in your custom template.
  <app-books-view [booksTemplate]="myCustomeTemplate"></app-books-view>
Enter fullscreen mode Exit fullscreen mode
  1. Allow the default template to be used
  <app-books-view></app-books-view>
Enter fullscreen mode Exit fullscreen mode

Unlike the ng-content, we have been able to provide default values to be projected.

The *ngTemplateOutlet is super useful for creating configurable components.

Two examples of how we can utilize the power of *ng-templateOutlet in our books-view

  1. create multiple templates for different use cases, eg. card view, table view, list view. e.t.c and allow the parent component that want to use our books-view to specify the type of view they want, else return default view. Here, we can even go further to automate the views depending on defined condition. Example, you can set the hostlistener to listen to screen sizes, or allow users to select the type of view that they want while you automatically switch between your defined templates (card view, table view, list view).

  2. We can also decide to allow the parent component to send in their own template like we have in the above.

Below is an example of a parent component using our books-view along side with customer templates

<ng-template  #cardView >
   <div let *ngFor="let bk of books" class="books-card">
      <h1>{{bk.name}}</h1>
      <p>{{bk.author}}  - {{bk.year}}</p>
   </div>
</ng-template>

<ng-template #tableView >
  <table>
    <tr>
      <th>Title</th>
      <th>Author</th>
      <th>Year</th>
    </tr>
    <tr *ngFor="let bk of books">
      <td>{{bk.name}}</td>
      <td>{{bk.author}}</td>
      <td>{{bk.year}}</td>
    </tr>
  </table>
</ng-template>

//passing the template to our app-view
<app-books-view [booksTemplate]="cardView"></app-books-view>

Enter fullscreen mode Exit fullscreen mode

As seen above, we have two templates defined by our parent component, the cardView and the tableView. Although we have manually assigned the cardView to the booksTemplate, but we can always dynamically change the value from the .ts file of our parent component as discussed earlier.

Hey guys!.. Isn't that super cool?

But wait.. If you look at the books-view.ts you will see that we have default data (booklist) being used. What if we want to send in our own data but still want to use the default template?.

If you look at the default template, you will notice that it directly has access to the booklist in our .ts file. And that is the same for every template embedded in the ng-template they can access variables in our .ts. We can further create a unique variable that is only accessible to our template alone and not outside of it. This is done by using let keyword as in let-[data].

Example:

<ng-container *ngTemplateOutlet="booksTemplate ? booksTemplate : defaultBooks; context:{$implicit:booklist}">

</ng-container>

<ng-template let-books #defaultBooks>
  <div *ngFor="let bk of books" class="books-card_default">
    <h1>{{bk.name}}</h1>
    <p>{{bk.author}}  - {{bk.year}}</p>
 </div>
</ng-template>
Enter fullscreen mode Exit fullscreen mode

Here, we have defined a variable books that is only accessible by our template, we no longer have direct access to the booklist in our .ts. Where does the let-books get its value from?

  1. The data for the let-books can be gotten directly from the parent
  2. From a default value set in the *ngTemplateOutlet context property

  3. To get the value from the parent

 <app-books-view [booklist]='booklist'></app-books-view> 
Enter fullscreen mode Exit fullscreen mode

Here, we are are not passing any template to the books views, this means that we want to use the default template while we are sending our own data to the template. Now, what happens is that

  1. Our books-views will first check if booklist has been passed as an input value.
  2. If the value is passed, it then assign the value of the booklist to let-books through the context
  3. If there is no booklist passed to it, it then assign the data specified in the context to the let-books. context:{$implicit:booklist} which is the default value.

This way, we can successfully send in our own data to books-view while still using the default template.

Same way, if we want to send in our custom template and also allow for dynamic data, we can also declare the template scoped variable with the let

In our parent component

<ng-template let-books #cardView >
   <div let *ngFor="let bk of books" class="books-card">
      <h1>{{bk.name}}</h1>
      <p>{{bk.author}}  - {{bk.year}}</p>
   </div>
</ng-template>

<ng-template let-books #tableView >
  <table>
    <tr>
      <th>Title</th>
      <th>Author</th>
      <th>Year</th>
    </tr>
    <tr *ngFor="let bk of books">
      <td>{{bk.name}}</td>
      <td>{{bk.author}}</td>
      <td>{{bk.year}}</td>
    </tr>
  </table>
</ng-template>

<!-- This allows you to use project your custom template and also pass in the data.   -->
<app-books-view [booklist]='booklist'  [booksTemplate]="cardView"></app-books-view>

Enter fullscreen mode Exit fullscreen mode

Here, we have also added the let-books variable to our custom templates. Whenever we pass in the data to our template, it assigns the value of the data to our context
whose value is then assigned to the let-books

<app-books-view [booklist]='booklist'  [booksTemplate]="cardView"></app-books-view>

Enter fullscreen mode Exit fullscreen mode

You can check out the github rep for the sample code above

https://github.com/valoni01/ng-magical-directives

Conclusion

you can utilize the magical power of the *ngTemplateOutlet, ng-template, ng-content and ng-container to create your own reusable component library while also giving your users the flexibility to customize the components.

Previous: ng-content

Discussion (0)