Angular Templates Guide
Angular Templates Guide
You have probably already come across with the ng-template Angular
core directive, such as for example while using ngIf /else, or ngSwitch .
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:
1
2 @Component({
3 selector: 'app-root',
4 template: `
5 <ng-template>
6 <button class="tab-button"
7 (click)="login()">{{loginText}}</button>
8 <button class="tab-button"
9 (click)="signUp()">{{signUpText}}</button>
10 </ng-template>
11 `})
13 loginText = 'Login';
16
17 login() {
18 console.log('Login');
19 }
20
21 signUp() {
22 console.log('Sign Up');
23 }
24 }
25
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.
1
2 <div class="lessons-list" *ngIf="lessons else loading">
3 ...
4 </div>
5
6 <ng-template #loading>
7 <div>Loading...</div>
8 </ng-template>
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:
1
2 <ng-template [ngIf]="lessons" [ngIfElse]="loading">
3 <div class="lessons-list">
4 ...
5 </div>
6 </ng-template>
7
8 <ng-template #loading>
9 <div>Loading...</div>
10 </ng-template>
11
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.
How does this work if there are multiple structural directives applied to the
same element?
1
2 <div class="lesson" *ngIf="lessons"
6 </div>
7 </div>
8
This would not work! Instead, we would get the following error message:
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:
1
2 <div *ngIf="lessons">
4 <div class="lesson-detail">
5 {{lesson | json}}
6 </div>
7 </div>
8 </div>
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!
1
2 <ng-container *ngIf="lessons">
6 </div>
7 </div>
8 </ng-container>
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.
We can also take the template itself and instantiate it anywhere on the
page, using the ngTemplateOutlet directive:
1
2 <ng-container *ngTemplateOutlet="loading"></ng-container>
3
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.
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.
1
2 @Component({
3 selector: 'app-root',
4 template: `
5
6 <ng-template #estimateTemplate let-lessonsCounter="estimate">
13 `})
14 export class AppComponent {
15
16 totalEstimate = 10;
17 ctx = {estimate: this.totalEstimate};
18
19 }
20
Given the example above, this is what would get rendered to the screen:
This gives us a good overview of how to define and instantiate our own
templates.
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:
1
2 @Component({
3 selector: 'app-root',
4 template: `
5 <ng-template #defaultTabButtons>
6
7 <button class="tab-button" (click)="login()">
8 {{loginText}}
9 </button>
10
11 <button class="tab-button" (click)="signUp()">
12 {{signUpText}}
13 </button>
14
15 </ng-template>
16 `})
17 export class AppComponent implements OnInit {
18
19 @ViewChild('defaultTabButtons')
20 private defaultTabButtonsTpl: TemplateRef<any>;
21
22 ngOnInit() {
23 console.log(this.defaultTabButtonsTpl);
24 }
25
26 }
27
28
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.
Here is how that would look like, we would start by defining the custom
template for the buttons in the parent component:
1
2 @Component({
3 selector: 'app-root',
4 template: `
5 <ng-template #customTabButtons>
6 <div class="custom-class">
9 </button>
12 </button>
13 </div>
14 </ng-template>
15 <tab-container [headerTemplate]="defaultTabButtons"></tab-container>
16 `})
17 export class AppComponent implements OnInit {
18
19 }
20
And then on the tab container component, we could define an input
property which is also a template named headerTemplate :
1
2 @Component({
3 selector: 'tab-container',
4 template: `
5
6 <ng-template #defaultTabButtons>
7
8 <div class="default-tab-buttons">
9 ...
10 </div>
11
12 </ng-template>
13
14 <ng-container
16
17 </ng-container>
18
19 ... rest of tab container component ...
20
21 `})
23 @Input()
24 headerTemplate: TemplateRef<any>;
25 }
26
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.
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!