An Introduction to Angular Template Syntax
We are going to look at some special things Angular brings to the template in
just a moment. Before I get into that though, I think it’s useful to know about
the APIs that each and every DOM element (that is, a single node in your HTML
like <button>
) have. These are features of DOM elements that exist regardless
of Angular.
Although this will only be a brief look into how native JavaScript and the DOM work, it is helpful to be able to draw lines between when you are utilising features of Angular and when you are just using standard browser/JavaScript features. I would encourage you to continue expanding your knowledge of fundamental JavaScript concepts, as well as learning Angular itself.
Attributes, Properties, Events, and Methods
Let’s imagine we’ve grabbed a single node in an HTML template by using something
like getElementById('myInput')
in JavaScript. That node will have
attributes, properties, methods and events.
NOTE: We would not typically grab a reference to an element in an Angular
application using getElementById
— there are Angular specific ways to do this.
This is just a generic example of how these concepts apply outside of the
Angular framework environment.
An attribute like id
below can be used to supply an initial value to
a property:
<input id="some-input" value="hello">
This attribute is used to set an initial value for the id
property on the
element. Attributes can only ever be strings.
What the browser will do is parse all of the HTML on the page, and it will add
nodes to the DOM for each element. This node will be an object that
has properties. In the case of our <input>
element above, an
HTMLInputElement
object will be added to the DOM along with all of the
properties that an HTMLInputElement
object would have. Since we supplied an
id
attribute, when this object is created it will take the value we provided,
which was some-input
, and it will initialise the id
property of the
HTMLInputElement
object to that value.
Whilst an attribute is just used to supply a simple initial value, a property of a DOM object is much more open to manipulation.
const myInput = document.getElementById('some-input');
console.log(myInput.value); // hello
myInput.value = "What's up?";
console.log(myInput.value); // What's up?
myInput.value = new Object(); // We also are not limited to just using strings
A method is a function that we can call on the element, like this:
myInput.addEventListener('input', doSomething);
By using the addEventListener
method, we would now be listening for the
input
event on that input field and will call the doSomething
function when
that happens. This relies on the element’s ability to fire events.
As well as the input
event, an element can also fire events like focus
,
blur
, click
and so on — elements can also fire custom events. Having a basic
understanding of these native JavaScript concepts should help to shape your
understanding of what Angular adds on top of that.
Once again, our goal is just to gain a basic understanding here and we will continue to learn these concepts throughout the course. Just throwing a ton of concepts at you all at once without practice isn’t going to be that helpful.
NOTE: As we discuss these syntax concepts, try them out in your own
application. If you still have your my-app
application in a working state you
can use that, or you can create an entirely new application. You can just dump
these examples directly into your root AppComponent
, play around with them,
and see what happens.
Property Binding
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
template: ` <input [value]="firstName" /> `,
})
export class AppComponent {
firstName = 'Josh';
}
This looks similar to the concepts we just discussed, but now we have these
strange square brackets around the value
attribute. Just like with an
attribute initialising a property this will also initialise the value
property of our input
element.
However, there are two important differences now.
First, this will set the element’s value
property to the expression firstName
(which is defined in the class definition for the component as a member
variable). Note that firstName
is an expression, not a string. Without the
use of the square brackets, firstName
would be treated literally as the string
“firstName” rather than whatever it is that firstName
evaluates to (in this
case: Josh
). Surrounding the property with square brackets allows us to
bind the property to an expression (most often a value from our
class).
The second important difference is that the value of the property will be
updated when firstName
changes. Our firstName
class member might
initially be Josh
and so the input will have a value of Josh
. But later the
firstName
variable might change to Kathy
, and when that happens the value
property will be updated to Kathy
. For now, you can just take this for
granted. But this ties into Angular’s change detection system that we will
discuss in detail in a later lesson.
NOTE: Angular also has a special way to make dealing with forms and inputs easier — we are not using that in this example, but we will later. You would not typically bind to an input element manually in Angular this way.
Binding an event to a method
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
template: `<button (click)="handleClick($event)">click me</button>`,
})
export class AppComponent {
handleClick(ev: MouseEvent) {
console.log('I was clicked!');
console.log(ev);
}
}
We can place an event name inside of (parenthesis)
to trigger a particular
method in our class when that event is fired. In this case, when the click
event is triggered on the button
it will call our handleClick
method that we
defined in the class. We use the special $event
parameter to pass in the
details of the event to our method, but we don’t need to pass this in if we
don’t want. You can replace click
with any native or custom event you like.
This isn’t particularly relevant to this section, but notice that we are also
manually specifying that our ev
parameter will have a type of
MouseEvent
. Often TypeScript can implicitly tell what type something is, so we
don’t need to manually define it. In this case, TypeScript does not know what
type of parameter we might pass into the handleClick
function. So, we manually
tell TypeScript that we will be passing a MouseEvent
to this method, which is
the type of event that is triggered when the button is clicked.
In the beginning, it is going to be hard to know how to type certain things. If
you are unsure, you can just give it a type of any
:
handleClick(ev: any) {
console.log('I was clicked!');
console.log(ev);
}
This is not something you want to be doing long term, as it defeats the
benefits of TypeScript. But, in the beginning, you might not want to waste time
messing around with types when you have tons of other concepts to learn as well.
So, just use any
if you need to, and dedicate some time to applying proper
types later.
Interpolations
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
template: `<p>Hi, {{ name }}</p>`,
})
export class AppComponent {
name = 'Josh';
}
This is very similar to the concept of binding to a property:
<input [value]="name" />
Except instead of binding to a property, we can render an expression anywhere in our template. In fact, an interpolation will even work for binding to a value instead of using the square bracket syntax:
<input value="{{name}}" />
BUT!… you should not do this for property binding. It may work in some
cases, but the interpolation syntax {{ }}
will render the expression as
a string
, whereas the square bracket syntax [ ]
will bind the value to the
property as whatever type it actually is. For example, if you were passing in
a number
, the {{ }}
syntax will convert it to a string — which might not be
what you want. So, in short, use []
for binding to properties, and use {{ }}
for displaying values in your template.
Two Way Data Binding
So far, we have been looking at how to take a value from our class and display it in the template. You can think of this as one-way data binding. Let’s back up a little and re-consider this example of property binding from before:
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
template: ` <input [value]="firstName" /> `,
})
export class AppComponent {
firstName = 'Josh';
}
This works great because our input will use whatever value we define for
firstName
in the class…
But this is an input. When we put an <input>
on the page we generally
expect the user to change its value. What happens then? In the example above,
when the user changes the <input>
by typing into the field, the input they see
will change. But, the value of firstName
in the class will remain the
same.
This is where Angular’s concept of two-way data binding comes into play.
When we update the firstName
variable in the class we want that change to
be reflected in the template, and when the value is changed in the
template we also want the value to be changed in the class. It should
work both ways.
We can set up two-way data binding in Angular like this:
<input [value]="firstName" (input)="firstName = $event.target.value">
This is reasonably intuitive and utilises concepts we have already covered. We
set the value
with a property binding to update the value in the template, and
then we listen for the input
event to update the value of firstName
in the
class when it is triggered.
NOTE: You can access any class member variable you define in your class in
your template as we are doing with firstName
above.
It makes sense, but it is a bit awkward to set up. Fortunately, Angular provides an alternative syntax for us:
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
@Component({
selector: 'app-root',
template: ` <input [(ngModel)]="firstName" /> `,
imports: [FormsModule],
})
export class AppComponent {
firstName = 'Josh';
}
This is just a shortcut for the code we wrote above but is obviously a lot
easier to write (they call this the “banana in a box” syntax). This ngModel
comes from Angular’s FormsModule
which provides a bunch of functionality
related to creating forms.
NOTE: If you do not add FormsModule
to the imports
array the ngModel
syntax will not work
This is one approach you can take to forms, and is generally referred to as
template driven forms, but we won’t actually be making use of ngModel
throughout this course. Instead, we will be making use of the
ReactiveFormsModule
which we will cover later.
Creating a Template Variable
<p #myParagraph></p>
This one is not used as often, but can come in useful in certain situations. The code above creates a local template variable that we can use to access the element it is attached to, so if I wanted to add some content to this paragraph I could do the following:
<button (click)="myParagraph.innerHTML = 'Once upon a time...'">Click me</button>
This example is pretty contrived, but the situations in which this concept can be useful are a bit more complex than I think we should get into right now.
Embedded Templates
This is not a feature you will use all that often yourself (we will actually use this for creating loading templates later), but it is the concept behind the structural directives provided by Angular that are used quite often. We will discuss those structural directives in just a moment, but in the hope of making them seem a little less magical, let’s briefly cover the concept of an embedded template:
<ng-template #myTemplate>
<p>Hello there</p>
</ng-template>
Go ahead and add that to the example application in the template and see what happens.
Click here to reveal solution
Solution
You should find that nothing is displayed. The general idea here is that an
<ng-template>
allows us to define a chunk of the DOM, but Angular won’t
actually render it. What we are doing is creating a sort of “prefabrication” or
“stamp”, and we can stamp out that template dynamically. We might want to stamp
it out multiple times, or we might want to display a template based on some
condition (like if some data is loading in, for example).
To use this template, we can use the following syntax:
<ng-container *ngTemplateOutlet="myTemplate"></ng-container>
NOTE: Using ngTemplateOutlet
will require adding it to the imports
of
your component:
import { Component } from '@angular/core';
import { NgTemplateOutlet } from '@angular/common';
@Component({
selector: 'app-root',
template: `
<ng-template #myTemplate>
<p>Hello there</p>
</ng-template>
<ng-container *ngTemplateOutlet="myTemplate"></ng-container>
`,
imports: [NgTemplateOutlet],
})
export class AppComponent {}
Notice that we are referring to the template variable myTemplate
that we
added to our <ng-template>
. The <ng-container>
used here is actually
a separate concept — technically we could have just used a <div>
.
Unlike the <div>
approach, the <ng-container>
approach is generally better
because it won’t render an extra element in the DOM. The <ng-container>
is
useful in other circumstances as well and we will make use of this later.
Basically, any time we need some container element to attach some behaviour
to, but we don’t want to render an additional element in the DOM, we can use
<ng-container>
. In this particular case, it doesn’t actually matter, even if
we did use a <div>
the *ngTemplateOutlet
won’t actually render the <div>
in the DOM.
At this point, you should be able to see the <ng-template>
being rendered out
in the DOM. But you might be thinking this is all pretty weird and pointless at
the moment… why bother with the template?
Well, one thing we could do now is stamp out multiple instances of this template if we want, e.g:
<ng-container *ngTemplateOutlet="myTemplate"></ng-container>
<ng-container *ngTemplateOutlet="myTemplate"></ng-container>
<ng-container *ngTemplateOutlet="myTemplate"></ng-container>
That’s kind of fun — we don’t need to re-add the whole template each time. Still, not super interesting. We could make this configurable though:
<ng-template #myTemplate let-greeting="greeting">
<p>{{ greeting }} there</p>
</ng-template>
<ng-container
*ngTemplateOutlet="myTemplate; context: { greeting: 'Hi' }"
></ng-container>
<ng-container
*ngTemplateOutlet="myTemplate; context: { greeting: 'Hello' }"
></ng-container>
<ng-container
*ngTemplateOutlet="myTemplate; context: { greeting: 'Yo' }"
></ng-container>
Our template can take in a context
. Here we are passing in a greeting
and
changing what we render out based on the greeting passed in.
You can do all sorts of clever stuff with this but to be honest, I don’t use this concept a whole lot. If I wanted some configurable/repeatable element like the above, I would generally just create a custom component. But I still think it is important to understand as a concept, since this is the basic idea behind the structural directives Angular provides us that are used a lot.
Directives
Before we get to structural directives it is useful to understand what
a standard directive is and how we might use it. We will look at actually
creating a directive later, but a key point to understand is that a directive
defines a selector
:
@Directive({
selector: '[myDirective]'
})
export class MyDirective {}
NOTE: A directive can be standalone as well — just like a component. If
it is not marked as standalone: false
it will be standalone by default.
Otherwise, it will need to be included as part of an @NgModule
A @Directive
is like a @Component
except that it does not have
a template. Instead of having its own template, it attaches its behaviour to
some other element that already exists. Like a little parasite. A helpful
parasite.
The selector
is defined using a CSS style selector, so our particular
selector
of [myDirective]
will match any element that has a myDirective
attribute. If we had made the selector
.my-directive
instead then it would
match any element with a CSS class of my-directive
.
As an example, we might add some code to this directive such that it will make
whatever element it is attached to have a background colour of red
. We could
then do this in our template:
<my-component myDirective></my-component>
NOTE: Remember, to use a standalone directive you will need to add it to the
imports
array of the component you are using it in — otherwise Angular will
not know that it exists
Since <my-component>
has a myDirective
attribute, our MyDirective
directive will be attached to it, and it will make the component’s background
colour red
.
In short: directives are useful for modifying the behaviour of existing components/elements.
Structural Directives
<section *ngIf="showSection"></section>
<li *ngFor="let item of items">{{ item }}</li>
To use *ngIf
and *ngFor
you must import the CommonModule
(you can also
import NgIf
and NgFor
individually if you prefer):
import { CommonModule } from '@angular/common';
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
template: ` <p *ngIf="shouldDisplay">hello</p> `,
imports: [CommonModule],
})
export class AppComponent {
shouldDisplay = true;
}
NOTE: There is a new syntax for handling control flow in Angular such as conditionally showing/hiding elements or looping over elements. The syntax above is the “old” way, but is still used by many Angular applications. The “old” syntax uses structural directives for the purpose of control flow, but structural directives can also be used in other situations as well. It’s still useful to understand structural directives, even if you are using the new control flow syntax.
Directives allow you to attach some behaviour to an existing element, and structural directives specifically allow you to affect the structure of the DOM. As we saw in the previous section, we can control if/when/how a pre-defined template should be rendered out into the DOM.
The two examples of structural directives above, *ngIf
and *ngFor
, are
provided by Angular and they are used a lot. These will be directly replaced
by the @if
and @for
syntax we will discuss in a moment, but you will still
see this “old” syntax used a lot for a long time.
In the first example, we are conditionally showing the <section>
based on
whether or not the showSection
expression evaluates to be true
, and in the
second example, we are looping over all items in the items array and
creating a list item for all of them. If there were 5
items in the items
array in our class, and let’s pretend this array had elements with the numbers
1
through to 5
, then the following would be rendered out:
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
<li>5</li>
These specific examples are “structural directives” because they use the *
syntax and modify the DOM with <ng-template>
behind the scenes.
@-Syntax for Control Flow
This new @-syntax
is a direct replacement for the concept of *ngIf
and
*ngFor
. Unlike the structural directive approach, the @-syntax
is baked in
by default and does not need to be imported.
The examples from above would translate to:
@if (showSection) {
<section></section>
}
and:
@for (item of items; track $index){
<li>{{ item }}</li>
}
These do the exact same thing — the @if
will conditionally display something,
and the @for
will loop over an array and render something for each item in the
array.
One difference here is the use of this strange track $index
statement. This is
for performance reasons — Angular is better able to render items in a list if it
has a unique identifier to track each item with.
Let’s say you were looping over an item object that had its own unique key:
items = [
{id: '1', title: 'hello'},
{id: '2', title: 'world'}
]
You could use the items id
as the tracking mechanism:
@for (item of items; track item.id){
<li>{{ item }}</li>
}
However, we might not always have a unique id
to track by, and in this case we
can just use the $index
of the item in the array as the unique identifier.
This is necessary to use with the new syntax because this performance aspect is
enforced by default. It is also possible to write a custom trackBy
function
for the old *ngFor
syntax, but it is not enforced and it is harder to do.
A nicer thing about this new syntax is that it also makes else
and else if
conditions quite easy to implement:
@if (someCondition){
<p>display this</p>
} @else if (someOtherCondition){
<p>display this instead</p>
} @else {
<p>last resort</p>
}
We have not discussed everything there is to know about this control flow syntax, but it is a good amount to get us started and we will cover more as we go.
Recap
We’ve covered a lot in this lesson, and again, I don’t expect all this to stick at once. We will continue revisiting these concepts throughout the course.
The best syntax for binding a variable to a property is...
Nice!
Although this will work in some cases, the interpolation syntax will convert whatever you are trying to bind to a string, which may not be desired. It is safer to use the square bracket syntax.
If we want to bind a click event to some method we would do...
Incorrect
Correct!
Incorrect
If we wanted to render out the result of 1 + 1 in the template, which of the following would work?
Incorrect
Incorrect
Correct!
What is the correct way to set up two way data binding for a form control?
Incorrect. This will only achieve one way data binding.
Correct!
Incorrect
What is the correct way to set up a template variable?
Correct!
Incorrect
Incorrect
Select the component that a directive with a selector of [myDirective] would apply to
Incorrect
Correct!
Incorrect
The *ngIf structural directive or @if syntax will...
Correct!
Incorrect
Incorrect
The *ngFor structural directive or @for syntax will...
Incorrect
Correct!
Incorrect