Angular course · Module 2: Components
Data binding - how the class talks to the template
In this lesson5
When the samurai takes a hit and their health drops from 100 to 70, the screen has to show 70, not the old value. When the learner clicks the attack button, the class has to find out, so it can take hit points away from the opponent.
Without the framework's help you would have to do this by hand: look up page elements, rewrite their text after every data change and attach click listeners. Every forgotten step means a screen that lies. Angular does it for you with data binding. In the template you describe once what is connected to what, and Angular makes sure the class and the view always say the same thing.
On the battlefields of old, a commander communicated with the troops through banners and drums. A banner carried an order from the commander to the soldiers, and a drum carried a report back the other way. Data binding works in a similar way and comes in four forms:
| Syntax | Direction | Example |
|---|---|---|
{{ expression }} | class → template (text) | {{ name }} |
[property]="expression" | class → template (element property) | [disabled]="isDefeated" |
(event)="statement" | template → class | (click)="attack()" |
[(ngModel)]="field" | both ways | [(ngModel)]="samuraiName" |
We will go through them one by one, starting with the simplest.
Interpolation - the message on the banner
Interpolation inserts a value from the class into the text of the page. You write the expression in double curly braces, and Angular evaluates it and displays the result in that spot:
<h2>{{ name }}</h2>
<p>Level: {{ level }}</p>
<p>Strike power: {{ level * 10 }}</p>
<p>{{ name.toUpperCase() }}</p>The braces can hold a property name, a calculation or a method call. It has to be an expression, though, meaning something that returns a value, not a statement. {{ level = 5 }} will not compile, because Angular forbids assignments in interpolation. The braces are for showing data, not for changing it.
The result of interpolation always lands on the page as text. If the name field contains a <b> tag, the learner sees it literally instead of bold text. This is a deliberate safeguard: data typed in by a user cannot inject its own HTML into the page this way. When the value in the class changes, Angular swaps the text on the screen by itself.
Property binding - setting properties
Interpolation inserts text, but sometimes you need to set a property of an element: the address of an image, the state of a button or a CSS class. That is what property binding is for: the property name in square brackets and an expression after the equals sign:
<img [src]="bannerUrl" [alt]="name">
<button [disabled]="isDefeated">Attack!</button>
<li [class.active]="isActive">Musashi</li>The square brackets tell Angular: evaluate whatever stands in the quotes as an expression. [src]="bannerUrl" sets the image's src property to the value of the bannerUrl field from the class. [disabled]="isDefeated" disables the button for as long as isDefeated is true. The special form [class.active] adds the active CSS class when the expression is true and removes it when it is false.
Without brackets, the right-hand side is a plain string. This is where a trap awaits many beginners, so compare these two buttons:
<!-- button STILL disabled: the disabled attribute exists -->
<button disabled="false">Attack!</button>
<!-- button enabled: the property gets the value false -->
<button [disabled]="false">Attack!</button>In the first line, false is just the text of the attribute, and for the browser only the presence of the disabled attribute counts, so the button stays disabled. In the second line, Angular evaluates the expression false and sets the element's property to a real boolean, so the button works. That is why everything that should change along with the data belongs in square brackets.
For text, brackets are not needed. [title]="'Musashi'" and title="Musashi" have exactly the same effect, because in the first one the expression is simply a string in single quotes. Brackets are essential when the value comes from the class or is not text, for example with the number [level]="10", which you will pass to a child component in the last lesson of the module.
Event binding - a report from the battlefield
Property binding passes data from the class to the template. Event binding works the other way: when something happens on the page, such as a click, Angular calls the method of the class you pointed to. You write the event name in round brackets:
<button (click)="attack()">Attack!</button>
<input (input)="onNameInput($event)">The first line calls the attack() method on every click. The second reacts to every change of the text in the field and passes the special variable $event to the method. For browser events, $event is the event object, from which you can read, for example, the element where the event happened:
export class SamuraiComponent {
name = 'Musashi';
onNameInput(event: Event): void {
const field = event.target as HTMLInputElement;
this.name = field.value;
}
}The event.target property points to the text field, and as HTMLInputElement tells TypeScript that it really is an input element, so you can read its value. After the new name is assigned, Angular refreshes every place in the template that shows name.
Watch out for three notations that look similar but are not Angular event bindings. [click]="attack()" is a property binding: Angular treats click as a property of the element, not as an event. onclick="attack()" is a plain HTML attribute that cannot see the methods of your class. @click="attack()" is Vue syntax, which Angular does not understand. In Angular, you always write an event in round brackets.
Two-way binding - a conversation in both directions
A form field needs both directions at once. The class has to show the samurai's current name in the field, and when the learner types a new one, the class has to know it right away. Two-way binding connects the two, written as round brackets inside square ones: [(ngModel)]. The ngModel directive comes from FormsModule, so the component must have it on its imports list:
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
@Component({
selector: 'app-samurai-form',
standalone: true,
imports: [FormsModule],
template: `
<input [(ngModel)]="samuraiName">
<p>Samurai name: {{ samuraiName }}</p>
`
})
export class SamuraiFormComponent {
samuraiName = '';
}Every letter typed into the field goes straight to the samuraiName property, and the interpolation in the paragraph shows the new value. It works the other way too: if a method of the class assigns a new name to samuraiName, the text field displays it by itself. Without FormsModule on the imports list, the compiler reports that it cannot bind ngModel, because the input element has no such property.
Developers call this notation a banana in a box: the banana, the round brackets, lies in a box, the square brackets. It is a shortcut that Angular expands into two bindings you already know:
<input [ngModel]="samuraiName" (ngModelChange)="samuraiName = $event">The property binding [ngModel] writes the value from the class into the field, and the event binding (ngModelChange) assigns the new value from $event back to the class on every change. Both notations work identically, but the shortcut reads faster and leaves less room for mistakes. The order of the brackets matters: the reversed ([ngModel]) is not two-way binding, and the Angular compiler warns that the round brackets belong inside.
Full example - the training panel
Let us gather all four bindings in one component. The training panel lets you enter a number of repetitions, do a training session and track the total of all repetitions:
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
@Component({
selector: 'app-training',
standalone: true,
imports: [FormsModule],
template: `
<div class="training-panel">
<h2>{{ trainingName }}</h2>
<input [(ngModel)]="reps" type="number">
<button
[disabled]="reps <= 0"
(click)="doTraining()">
Train!
</button>
<p>Completed repetitions: {{ totalReps }}</p>
</div>
`
})
export class TrainingComponent {
trainingName = 'Sword training';
reps = 10;
totalReps = 0;
doTraining(): void {
this.totalReps += this.reps;
}
}Follow this code step by step. The interpolation {{ trainingName }} shows the name of the training in the heading. The two-way binding [(ngModel)]="reps" connects the number field with the reps property, so every number you type goes straight to the class. The property binding [disabled]="reps <= 0" disables the button when the number of repetitions is zero or negative. The event binding (click)="doTraining()" calls the method that adds reps to totalReps, and the interpolation in the last paragraph shows the new total.
Notice what this class does not do: it does not look up elements on the page and it does not rewrite the text of the paragraph. It only changes its own ordinary properties, and the bindings take care of the rest. My advice: keep the logic in the methods of the class and only call it from the template, because long calculations written into the template's quotes quickly become unreadable.
In the last lesson of this module you will pass data to a child component with property binding, and in the sixth module you will build the same panel with signals.
The training hall below is the same component as in the first lesson. Find its Data Binding Demo section: the interpolation of the samurai's name and clan, the property binding [value] of the health bar, the event binding of the Attack! and Heal buttons, and the two-way binding of the name field.
Code for this lesson: src/app/app.component.ts
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
interface Samurai {
id: number;
name: string;
clan: string;
level: number;
health: number;
}
@Component({
selector: 'app-root',
standalone: true,
imports: [FormsModule],
template: `
<div class="dojo-training">
<header>
<h1>Dojo Training Hall</h1>
<p>Module 2: Angular Components</p>
</header>
<section class="training-section">
<h2>Data Binding Demo</h2>
<!-- Interpolation -->
<div class="demo-box">
<h3 ngNonBindable>Interpolation {{ }}</h3>
<p>Name: {{ samurai.name }}</p>
<p>Clan: {{ samurai.clan }}</p>
</div>
<!-- Property Binding -->
<div class="demo-box">
<h3>Property Binding [ ]</h3>
<progress [value]="samurai.health" max="100"></progress>
<p>Health: {{ samurai.health }}%</p>
</div>
<!-- Event Binding -->
<div class="demo-box">
<h3>Event Binding ( )</h3>
<button (click)="attack()">Attack!</button>
<button (click)="heal()">Heal</button>
</div>
<!-- Two-way Binding -->
<div class="demo-box">
<h3>Two-way Binding [( )]</h3>
<input [(ngModel)]="samurai.name" placeholder="Samurai name">
<p>Current name: {{ samurai.name }}</p>
</div>
</section>
<section class="control-flow-section">
<h2>Control Flow Demo</h2>
<!-- @if -->
<div class="demo-box">
<h3>@if / @else</h3>
@if (samurai.health > 50) {
<p class="status good">Samurai in good shape!</p>
} @else if (samurai.health > 20) {
<p class="status warning">Samurai weakened</p>
} @else {
<p class="status danger">Samurai in critical condition!</p>
}
</div>
<!-- @for -->
<div class="demo-box">
<h3>@for</h3>
<ul>
@for (s of samuraiList; track s.id) {
<li [class.selected]="s.id === selectedId" (click)="selectSamurai(s.id)">
{{ s.name }} (Lvl {{ s.level }})
</li>
} @empty {
<li>No samurai</li>
}
</ul>
</div>
<!-- @switch -->
<div class="demo-box">
<h3>@switch</h3>
@switch (getRank(samurai.level)) {
@case ('master') {
<span class="badge master">Master</span>
}
@case ('advanced') {
<span class="badge advanced">Advanced</span>
}
@default {
<span class="badge beginner">Beginner</span>
}
}
</div>
</section>
</div>
`,
styles: [`
.dojo-training {
font-family: 'Segoe UI', sans-serif;
max-width: 900px;
margin: 0 auto;
padding: 20px;
background: #1a1a2e;
color: #eee;
}
header {
text-align: center;
border-bottom: 2px solid #c9a227;
padding-bottom: 20px;
margin-bottom: 20px;
}
h1 { color: #c9a227; }
h2 { color: #c9a227; margin-top: 30px; }
h3 { color: #888; font-size: 0.9rem; margin-bottom: 10px; }
.demo-box {
background: rgba(255,255,255,0.05);
border: 1px solid #333;
border-radius: 8px;
padding: 15px;
margin-bottom: 15px;
}
button {
background: #c9a227;
color: #1a1a2e;
border: none;
padding: 10px 20px;
margin: 5px;
border-radius: 4px;
cursor: pointer;
}
button:hover { opacity: 0.9; }
input {
padding: 10px;
border: 1px solid #c9a227;
border-radius: 4px;
background: #16213e;
color: #eee;
width: 200px;
}
progress {
width: 100%;
height: 20px;
}
.status { padding: 10px; border-radius: 4px; }
.status.good { background: #2d6a4f; }
.status.warning { background: #b45309; }
.status.danger { background: #991b1b; }
ul { list-style: none; padding: 0; }
li {
padding: 10px;
margin: 5px 0;
background: #16213e;
border-radius: 4px;
cursor: pointer;
}
li:hover { background: #1e3a5f; }
li.selected { border: 2px solid #c9a227; }
.badge {
padding: 5px 15px;
border-radius: 20px;
font-weight: bold;
}
.badge.master { background: #c9a227; color: #1a1a2e; }
.badge.advanced { background: #7c3aed; }
.badge.beginner { background: #4a5568; }
`]
})
export class AppComponent {
samurai: Samurai = {
id: 1,
name: 'Musashi',
clan: 'Niten Ichi-ryū',
level: 8,
health: 100
};
samuraiList: Samurai[] = [
{ id: 1, name: 'Musashi', clan: 'Niten', level: 10, health: 100 },
{ id: 2, name: 'Kojiro', clan: 'Ganryū', level: 9, health: 85 },
{ id: 3, name: 'Jubei', clan: 'Yagyu', level: 7, health: 90 }
];
selectedId = 1;
attack(): void {
this.samurai.health = Math.max(0, this.samurai.health - 15);
}
heal(): void {
this.samurai.health = Math.min(100, this.samurai.health + 20);
}
selectSamurai(id: number): void {
this.selectedId = id;
}
getRank(level: number): string {
if (level >= 9) return 'master';
if (level >= 5) return 'advanced';
return 'beginner';
}
}Remember: braces show data, square brackets set properties, round brackets bring reports about events, and the banana in a box carries a conversation in both directions.
Docs entries used in this lesson
Spotted a mistake in this lesson?
Check yourself
Answer the questions from this lesson. Pick an answer to see right away whether it is correct.
1. Which syntax is used for interpolation in an Angular template?
2. Which syntax is used to listen for events in Angular?
3. Which syntax is used for property binding in Angular?
Hands-on tasks in the game
- Horizontal ordering
Arrange the single-line tokens to create a two-way binding of an input field value to the samuraiName property:
- Code editor
Complete the code: add a property binding for [disabled] and an event binding for (click) on the attack button.
- Click in order
Arrange the characters to build a property binding for the src attribute: [src]
- Horizontal ordering
Arrange the single-line tokens to bind the onAttack() method to a button click: