ngTemplateOutlet
Valentine Awe
Posted on August 8, 2021
"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>
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>
//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<any>
@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 {
}
}
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<any>
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>
and in our template, we created the template container for the view.
<ng-container *ngTemplateOutlet="booksTemplate ? booksTemplate : defaultBooks;">
</ng-container>
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.
- pass in your custom template.
<app-books-view [booksTemplate]="myCustomeTemplate"></app-books-view>
- Allow the default template to be used
<app-books-view></app-books-view>
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
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).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>
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>
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?
- The data for the let-books can be gotten directly from the parent
From a default value set in the
*ngTemplateOutlet
context
propertyTo get the value from the parent
<app-books-view [booklist]='booklist'></app-books-view>
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
- Our
books-views
will first check if booklist has been passed as an input value. - If the value is passed, it then assign the value of the
booklist
tolet-books
through the context - If there is no
booklist
passed to it, it then assign the data specified in the context to thelet-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>
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>
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
Posted on August 8, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 20, 2024
November 15, 2024