In this post, we are going to dive into some of the more advanced features of Angular Core!
You have probably already come across with the ng-template Angular core directive, such as for example while using ngIf
/else, or ngSwitch
.
The ng-template directive and the related ngTemplateOutlet directive are very powerful Angular features that support a wide variety of advanced use cases.
These directives are frequently used with ng-container, and because these directives are designed to be used together, it will help if we learn them all in one go, as we will have more context around each directive.
Let's then see some of the more advanced use cases that these directives enable. Note: All the code for this post can be found in this Github repository.
Table Of Contents
In this post, we will be going over the following topics:
- Introduction to the ng-template directive
- Template Input Variables
- The ng-template directive use with ngIf
- ngIf de-suggared syntax and ng-template
- ng-template template references and the TemplateRef injectable
- Configurable Components with Template Partial
@Inputs
- The ng-container directive, when to use it?
- Dynamic Template with the ngTemplateOutlet custom directive
- Template outlet
@Input
Properties - Final Combined Example
- Summary and Conclusions
Introduction to the ng-template directive
Like the name indicates, the ng-template directive represents an Angular template: this means that the content of this tag will contain part of a template, that can be then be composed together with other templates in order to form the final component template.
Angular is already using ng-template under the hood in many of the structural directives that we use all the time: ngIf
, ngFor
and ngSwitch
.
Let's get started learning ng-template with an example. Here we are defining two tab buttons of a tab component (more on this later):
The first thing that you will notice about ng-template
If you try the example above, you might be surprised to find out that this example does not render anything to the screen!
This is normal and it's the expected behavior. This is because with the ng-template tag we are simply defining a template, but we are not using it yet.
Let's then find an example where we can render an output, using some of the most commonly used Angular directives.
The ng-template
directive and ngIf
You probably came across ng-template for the first time while implementing an if/else scenario such as for example this one:
This is a very common use of the ngIf/else functionality: we display an alternative loading
template while waiting for the data to arrive from the backend.
As we can see, the else clause is pointing to a template, which has the name loading
. The name was assigned to it via a template reference, using the #loading
syntax.
But besides that else template, the use of ngIf also creates a second implicit ng-template! Let's have a look at what is happening under the hood:
This is what happens internally as Angular desugars the more concise *ngIf
structural directive syntax. Let's break down what happened during the desugaring:
- the element onto which the structural directive ngIf was applied has been moved into an ng-template
- The expression of *ngIf has been split up and applied to two separate directives, using the
[ngIf]
and[ngIfElse]
template input variable syntax
And this is just one example, of a particular case with ngIf. But with ngFor and ngSwitch a similar process also occurs.
These directives are all very commonly used, so this means these templates are present everywhere in Angular, either implicitly or explicitly.
But based on this example, one question might come to mind:
How does this work if there are multiple structural directives applied to the same element?
Multiple Structural Directives
Let's see what happens if for example we try to use ngIf
and ngFor
in the same element:
This would not work! Instead, we would get the following error message:
Uncaught Error: Template parse errors:
Can't have multiple template bindings on one element. Use only one attribute
named 'template' or prefixed with *
This means that its not possible to apply two structural directives to the same element. In order to do so, we would have to do something similar to this:
In this example, we have moved the ngIf directive to an outer wrapping div, but in order for this to work we have to create that extra div element.
This solution would already work, but is there a way to apply a structural directive to a section of the page without having to create an extra element?
Yes and that is exactly what the ng-container
structural directive allows us to do!
The ng-container
directive
In order to avoid having to create that extra div, we can instead use ng-container directive:
As we can see, the ng-container directive provides us with an element that we can attach a structural directive to a section of the page, without having to create an extra element just for that.
There is another major use case for the ng-container directive: it can also provide a placeholder for injecting a template dynamically into the page.
Dynamic Template Creation with the ngTemplateOutlet
directive
Being able to create template references and point them to other directives such as ngIf is just the beginning.
We can also take the template itself and instantiate it anywhere on the page, using the ngTemplateOutlet
directive:
We can see here how ng-container helps with this use case: we are using it to instantiate on the page the loading
template that we defined above.
We are refering to the loading template via its template reference #loading
, and we are using the ngTemplateOutlet
structural directive to instantiate the template.
We could add as many ngTemplateOutlet
tags to the page as we would like, and instantiate a number of different templates. The value passed to this directive can be any expression that evaluates into a template reference, more on this later.
Now that we know how to instantiate templates, let's talk about what is accessible or not by the template.
Template Context
One key question about templates is, what is visible inside them?
Does the template have its own separate variable scope, what variables can the template see?
Inside the ng-template tag body, we have access to the same context variables that are visible in the outer template, such as for example the variable lessons
.
And this is because all ng-template instances have access also to the same context on which they are embedded.
But each template can also define its own set of input variables! Actually, each template has associated a context object containing all the template-specific input variables.
Let's have a look at an example:
Here is the breakdown of this example:
- this template, unlike the previous templates also has one input variable (it could also have several)
- the input variable is called
lessonsCounter
, and it's defined via a ng-template property using the prefixlet-
- The variable lessonsCounter is visible inside the ng-template body, but not outside
- the content of this variable is determined by the expression that its assigned to the property
let-lessonsCounter
- That expression is evaluated against a context object, passed to
ngTemplateOutlet
together with the template to instantiate - This context object must then have a property named
estimate
, for any value to be displayed inside the template - the context object is passed to
ngTemplateOutlet
via the context property, that can receive any expression that evaluates to an object
Given the example above, this is what would get rendered to the screen:
Approximately 10 lessons ...
This gives us a good overview of how to define and instantiate our own templates.
Another thing that we can also do is interact with a template programmatically at the level of the component itself: let's see how we can do that.
Template References
The same way that we can refer to the loading template using a template reference, we can also have a template injected directly into our component using the ViewChild
decorator:
As we can see, the template can be injected just like any other DOM element or component, by providing the template reference name defaultTabButtons
to the ViewChild
decorator.
This means that templates are accessible also at the level of the component class, and we can do things such as for example pass them to child components!
An example of why we would want to do that is to create a more customizable component, where can pass to it not only a configuration parameter or configuration object: we can also pass a template as an input parameter.
Configurable Components with Template Partial @Inputs
Let's take for example a tab container, where we would like to give the user of the component the possibility of configuring the look and feel of the tab buttons.
Here is how that would look like, we would start by defining the custom template for the buttons in the parent component:
And then on the tab container component, we could define an input property which is also a template named headerTemplate
:
A couple of things are going on here, in this final combined example. Let's break this down:
- there is a default template defined for the tab buttons, called
defaultTabButtons
- This template will be used only if the input property
headerTemplate
remains undefined - If the property is defined, then the custom input template passed via
headerTemplate
will be used to display the buttons instead - the headers template is instantiated inside a ng-container placeholder, using the
ngTemplateOutlet
property - the decision of which template to use (default or custom) is taken using a ternary expression, but if that logic was complex we could also delegate this to a controller method
The end result of this design is that the tab container will display a default look and feel for the tab buttons if no custom template is provided, but it will use the custom template if its available.
Summary and Conclusions
The core directives ng-container, ng-template and ngTemplateOutlet all combine together to allow us to create highly dynamical and customizable components.
We can even change completely the look and feel of a component based on input templates, and we can define a template and instantiate on multiple places of the application.
And this is just one possible way that these features can be combined!
I hope that this post helped to get familiar with some of the more advanced features of Angular core, if you have some questions please let me know in the comments below and I will get back to you.
And if you would like to know about more advanced Angular Core features, we recommend checking the Angular Core Deep Dive course, where Angular Templates are covered in much more detail.
To get notified when more posts like this come out, I invite you to subscribe to our newsletter:
If you are just getting started learning Angular, have a look at the Angular for Beginners Course:
Other posts on Angular
Have also a look also at other popular posts that you might find interesting:
- Getting Started With Angular - Development Environment Best Practices With Yarn, the Angular CLI, Setup an IDE
- Why a Single Page Application, What are the Benefits ? What is a SPA ?
- Angular Smart Components vs Presentation Components: What's the Difference, When to Use Each and Why?
- Angular Router - How To Build a Navigation Menu with Bootstrap 4 and Nested Routes
- Angular Router - Extended Guided Tour, Avoid Common Pitfalls
- Angular Components - The Fundamentals
- How to build Angular apps using Observable Data Services - Pitfalls to avoid
- Introduction to Angular Forms - Template Driven vs Model Driven
- Angular ngFor - Learn all Features including trackBy, why is it not only for Arrays ?
- Angular Universal In Practice - How to build SEO Friendly Single Page Apps with Angular
- How does Angular Change Detection Really Work ?