Angular Basics #3: Data Binding and Events

8 min read

Last time, we looked at the structure of an Angular component and the basics of template syntax. We also displayed values declared on the class with {{ }}. But a real app doesn’t end at simply showing values. The user clicks buttons, types into inputs, and the screen has to update accordingly.

This time, we’ll cover the four ways Angular passes data between a component class and its template — data binding — along with Signals, the reactive state tool of modern Angular.

Four binding styles at a glance #

Angular’s data binding splits into four kinds based on “which direction does it flow?”

KindSyntaxFlow
Interpolation{{ value }}Class → Template
Property binding[prop]="value"Class → Template (DOM property)
Event binding(event)="handler()"Template → Class
Two-way binding[(ngModel)]="value"Both directions

The bracket shapes in the syntax themselves indicate flow. [ ] is an arrow coming in (left ←), ( ) is an arrow going out (right →), and [( )] is the two combined for two-way (↔). It’s designed so you can intuitively tell which direction data flows just from the shape.

Now let’s look at each one in detail.

Interpolation review #

The most basic binding style. It plugs a value from the component class straight into the template.

src/app/app.component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  standalone: true,
  template: `
    <h1>Hello, {{ name }}!</h1>
    <p>In 10 years you'll be {{ age + 10 }}.</p>
  `,
})
export class AppComponent {
  name = 'Cheolsu';
  age = 30;
}

Anything that’s an expression can go inside {{ }} — arithmetic, method calls, ternary operators. But you can’t put assignment statements (=) or control-flow statements like if in there.

Interpolation ultimately gets converted to text and printed on the screen. You can also use it for HTML attribute values, but for attributes, the Property binding below is often a better fit.

Property binding — pass values to DOM properties with brackets #

The [propertyName]="expression" form binds a value to a DOM property.

src/app/app.component.ts
@Component({
  selector: 'app-root',
  standalone: true,
  template: `
    <img [src]="imageUrl" [alt]="imageAlt" />
    <button [disabled]="isLoading">Submit</button>
    <input [value]="defaultName" />
  `,
})
export class AppComponent {
  imageUrl = '/images/logo.svg';
  imageAlt = 'Logo image';
  isLoading = true;
  defaultName = 'Cheolsu';
}

[src]="imageUrl" evaluates the imageUrl variable and passes its value to the src property. If you write src="{{ imageUrl }}" without brackets inside quotes, it works similarly, but there’s a subtle difference between the two.

The difference
<!-- Interpolation: result is always a string -->
<button disabled="{{ isLoading }}">Submit</button>

<!-- Property binding: the expression result is passed as-is -->
<button [disabled]="isLoading">Submit</button>

For things like disabledbooleans, numbers, objects, anything non-string — you must use property binding ([disabled]). Interpolation turns every value into a string, so the literal string disabled="false" gets passed in, and a bug like the button being disabled against your intent is easy to introduce.

Tip
A simple rule of thumb: “always use brackets for properties.” Use interpolation only when you really just need a plain string label, and use property binding for everything else.

Event binding — receive user input with parentheses #

Now the opposite direction: receiving the user’s actions in the component class. You use the form (eventName)="handler()".

src/app/app.component.ts
@Component({
  selector: 'app-root',
  standalone: true,
  template: `
    <p>Current count: {{ count }}</p>
    <button (click)="increment()">+1</button>
    <button (click)="reset()">Reset</button>
  `,
})
export class AppComponent {
  count = 0;

  increment() {
    this.count = this.count + 1;
  }

  reset() {
    this.count = 0;
  }
}

(click)="increment()" means: every time the button is clicked, call the component class’s increment() method. Standard DOM events like click, input, keyup, and submit all use the same form.

If you need the event object, access it via the special variable $event.

src/app/app.component.ts
@Component({
  selector: 'app-root',
  standalone: true,
  template: `
    <input (input)="onInput($event)" placeholder="Enter your name" />
    <p>Input: {{ name }}</p>
  `,
})
export class AppComponent {
  name = '';

  onInput(event: Event) {
    const input = event.target as HTMLInputElement;
    this.name = input.value;
  }
}

The pattern is to cast event.target to HTMLInputElement and pull out .value. If writing this every time feels tedious, you’re right — the two-way binding next is exactly what relieves that tedium.

Two-way binding — both directions at once #

The value the user types into an input flows into a variable, and the variable’s value also shows up on the screen — two-way binding ([(ngModel)]) ties these two flows together at once. The shape that combines brackets and parentheses visually conveys “two-way.” Because of the shape, it’s commonly called “banana in a box.”

ngModel lives in FormsModule, so in a standalone component you have to add it to the imports array.

src/app/app.component.ts
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [FormsModule],
  template: `
    <input [(ngModel)]="name" placeholder="Enter your name" />
    <p>Hello, {{ name }}!</p>
  `,
})
export class AppComponent {
  name = '';
}

Compared with the previous example, both the (input) handler and the event.target cast are gone. As the user types into the input, name updates automatically, and the updated value flows back into the <p> tag.

Note
Two-way binding looks magical, but internally it’s just property binding and event binding combined. [(ngModel)]="name" is shorthand for writing [ngModel]="name" (class → input) and (ngModelChange)="name = $event" (input → class) at the same time.

Simple reactive state with Signals #

In the examples so far, we’ve used regular class properties (count = 0) directly. That works just fine. But in modern Angular (v17+), there’s a clearer tool for simple reactive state: the Signal.

src/app/app.component.ts
import { Component, signal } from '@angular/core';

@Component({
  selector: 'app-root',
  standalone: true,
  template: `
    <p>Current count: {{ count() }}</p>
    <button (click)="increment()">+1</button>
    <button (click)="reset()">Reset</button>
  `,
})
export class AppComponent {
  count = signal(0);

  increment() {
    this.count.update(value => value + 1);
  }

  reset() {
    this.count.set(0);
  }
}

Three things changed.

  1. count = 0count = signal(0): the state is wrapped in a signal.
  2. In the template, {{ count }}{{ count() }}: signals are called like functions to read their value.
  3. To change the value, use set() or update() instead of this.count = ....

set(value) swaps the value wholesale for a new one. update(prev => ...) is a functional update — it takes the previous value and returns a new one. For things like a counter, where the next value is computed from the previous one, update() is safer.

Why signals are nice #

“It works fine with a regular property — why use signals?” is a fair question. Signals offer the following advantages.

  • Changes are explicit: the value can only change through set()/update(), so it’s easier to trace “where did who change this value?”
  • Change detection becomes more precise: Angular knows exactly which signal is used where, so when a signal changes, only the parts that use that signal get redrawn. This helps performance in large apps.
  • Extends to computed values (computed) and side effects (effect): starting from simple state, you can naturally express derived values and effects on the same model.

For new projects, I recommend starting with signals from the beginning whenever possible. The rest of this series will use signals by default too.

Class binding and style binding #

There are two common applications of property binding worth seeing. Patterns for adding CSS classes conditionally and for changing inline styles dynamically.

src/app/app.component.ts
import { Component, signal } from '@angular/core';

@Component({
  selector: 'app-root',
  standalone: true,
  template: `
    <p
      [class.active]="isActive()"
      [class.disabled]="isDisabled()"
      [style.color]="textColor()"
      [style.font-size.px]="fontSize()"
    >
      Sample text
    </p>
    <button (click)="toggle()">Toggle active</button>
  `,
  styles: [`
    .active { font-weight: bold; }
    .disabled { opacity: 0.4; }
  `],
})
export class AppComponent {
  isActive = signal(false);
  isDisabled = signal(false);
  textColor = signal('tomato');
  fontSize = signal(16);

  toggle() {
    this.isActive.update(v => !v);
  }
}
  • [class.active]="isActive()": if the expression is truthy, the active class is added; if falsy, it’s removed.
  • [style.color]="textColor()": changes the color style dynamically.
  • [style.font-size.px]="fontSize()": appending the unit (px) with a dot lets you pass just the number, and the unit is attached automatically.

To handle multiple classes or styles at once, you can use the object form too.

Object form
<p [ngClass]="{ active: isActive(), disabled: isDisabled() }">Text</p>
<p [ngStyle]="{ color: textColor(), 'font-size': fontSize() + 'px' }">Text</p>

[ngClass]/[ngStyle] are directives that live in CommonModule, so a standalone component must add it to imports. For simple cases, [class.x]/[style.x] are lighter and easier to read, and are used more often.

Signals vs regular properties — when do you use which? #

In simplified terms:

  • Use a signal for values that change over time and need the screen to follow that change. Counts, loading state, user name, toggle state, etc.
  • Regular properties are fine for config values that don’t change after the component is created, methods, and constants.

At first, “every state that can change is a signal” is a fine rule of thumb. You can refine the distinction once you’re comfortable.

Summary: four shapes and their flows #

For review, let’s look at it once more.

Four bindings in one line
{{ value }}                    <!-- Class → Template (text) -->
<img [src]="value" />          <!-- Class → Template (property) -->
<button (click)="fn()" />      <!-- Template → Class (event) -->
<input [(ngModel)]="value" />  <!-- Two-way -->

Just remember that the bracket shapes are the arrow direction, and even unfamiliar code reads at a glance for which way the data flows.

Recap #

In this post, we covered Angular’s four data binding styles and signals — modern Angular’s reactive state tool. To recap:

  • {{ }} outputs a value as text
  • [ ] is class → template (property, class, style)
  • ( ) is template → class (event)
  • [( )] is two-way (ngModel requires FormsModule)
  • signal()/set()/update() for explicit reactive state management

In the next post, “Angular Basics #4: Directives and Pipes,” we’ll cover Directives — the tools that take template expressiveness up a notch (*ngIf, *ngFor, and the new syntax @if/@for) — along with Pipes, which transform values right before they’re displayed on the screen.

X