| |

Angular 7 🅰️ Property, Event, and Two-Way Binding

Interpolation renders text. Bindings do everything else — they push values into the DOM, listen for events coming out of it, and (in the two-way case) keep a value and a form control in sync both ways. Angular has three binding directions: property binding ([prop]="expr") sends data from the class into the template, event binding ((event)="handler()") sends events from the template back into the class, and two-way binding ([(ngModel)]="prop") does both at once. Every interaction in an Angular app is built from these three.

Key point: The brackets and parentheses are not decoration — they tell Angular which direction data flows. [ ] is down (class → view), ( ) is up (view → class), and [( )] is both. Knowing which direction you need is the whole skill.


The three binding directions

Angular’s binding syntax encodes direction.

SyntaxDirectionName
{{ expr }}Class → view (as text)Interpolation
[prop]="expr"Class → view (as property)Property binding
(event)="handler()"View → classEvent binding
[(prop)]="value"Both directionsTwo-way binding
bind-prop="expr"Class → viewLong form of [prop]
on-event="handler()"View → classLong form of (event)
bindon-prop="value"BothLong form of [(prop)]

The long forms (bind-, on-, bindon-) exist for cases where the short syntax can’t be parsed — rare, but worth knowing.

Why direction matters: A property binding is a one-way pipe from class to DOM. An event binding is a one-way pipe from DOM to class. Mixing them up — trying to read a value from a [ ] binding, or send data through a ( ) binding — is the most common beginner mistake.


Property binding — [prop]="expr"

Property binding sets a DOM property or a component/directive input on an element.

export class ImageComponent {
  imageUrl = '/assets/logo.png';
  imageAlt = 'Company logo';
  isLoaded = false;
}
<img [src]="imageUrl" [alt]="imageAlt">
<button [disabled]="!isLoaded">Save</button>

Angular reads imageUrl and assigns it to the src property of the <img>. When imageUrl changes, the property updates. No string conversion — the value keeps its type.

Property binding vs interpolation:

BindingSetsType
<img src="{{ url }}">src attributeString
<img [src]="url">src propertyOriginal type
<button disabled="{{ flag }}">Attribute (string)"false" is truthy — bug
<button [disabled]="flag">Property (boolean)Correct

For anything that isn’t a string, use property binding.

Common property bindings:

TargetExample
src<img [src]="url">
href<a [href]="link">
disabled<button [disabled]="flag">
value<input [value]="text">
class<div [class.active]="isActive">
style<div [style.color]="color">
attr.*<td [attr.colspan]="span">
class.*<div [class.warn]="hasWarning">
style.*<div [style.width.px]="width">

Why [attr.*] and [class.*]: Some HTML attributes don’t have matching DOM properties — colspan, aria-label, data-*. [attr.colspan] sets the attribute directly. [class.foo] toggles a single class; [style.foo] sets a single style property. Cleaner than string concatenation.


Class and style bindings

Class and style have dedicated binding forms.

Single class:

<div [class.active]="isActive">...</div>

Adds active when isActive is truthy, removes it otherwise.

Multiple classes via object:

<div [class]="{ active: isActive, error: hasError, muted: isMuted }">...</div>

Each key is a class name; the value decides whether it’s applied.

Class list via array:

<div [class]="['card', size, theme]">...</div>

Each element is a class name. Empty strings and null are ignored.

Single style:

<div [style.color]="textColor">...</div>

Style with unit:

<div [style.width.px]="width">...</div>
<div [style.font-size.em]="size">...</div>

The .px, .em, .rem suffix appends the unit — no string concatenation.

Multiple styles via object:

<div [style]="{ color: textColor, 'font-size': fontSize + 'px' }">...</div>

Why dedicated forms: [class.foo] and [style.foo] are the fastest and clearest way to toggle a single class or style. The object and array forms handle multiple at once. Avoid [ngClass] and [ngStyle] (legacy directives) unless you need their specific behavior.


Property binding to components

Property binding isn’t just for DOM elements. It sets inputs on child components.

// child
@Component({
  selector: 'app-user-card',
  standalone: true,
  template: `<h3>{{ user.name }}</h3>`
})
export class UserCardComponent {
  @Input() user!: User;
  @Input() compact = false;
}
<!-- parent -->
<app-user-card [user]="currentUser" [compact]="true"></app-user-card>

Angular assigns currentUser to the child’s user input. Whenever currentUser changes, the child’s input updates and ngOnChanges fires.

Input binding is covered in depth in the component communication chapters — for now, know that [ ] works identically on DOM elements and components.

Why the same syntax: Property binding is uniform. Whether you’re binding to src on an <img> or user on a component, the syntax and mental model are the same. That consistency is deliberate.


Event binding — (event)="handler()"

Event binding listens for a DOM event or a component output and calls a method on the class.

export class ClickerComponent {
  count = 0;
  lastKey = '';

  increment(): void {
    this.count++;
  }

  onKey(event: KeyboardEvent): void {
    this.lastKey = (event.target as HTMLInputElement).value;
  }

  reset(): void {
    this.count = 0;
  }
}
<button (click)="increment()">Clicked {{ count }} times</button>
<input (keyup)="onKey($event)" placeholder="Type...">
<p>Last input: {{ lastKey }}</p>
<button (click)="reset()">Reset</button>

Angular listens for the event, and when it fires, calls the method. $event is the event object — pass it to access target, key, value, etc.

Common DOM events:

EventFires when
clickElement clicked
dblclickDouble click
inputInput value changes
changeValue committed
keyup / keydownKey pressed/released
keyup.enterEnter key (key filter)
submitForm submitted
focus / blurFocus gained/lost
mouseenter / mouseleaveMouse enters/leaves
scrollElement scrolled
touchstart / touchendTouch events

Key modifiers: Append .enter, .escape, .tab, .space, .shift, .control, .alt, .meta — Angular only fires when that key (or modifier) is pressed.

<input (keyup.enter)="submit()" (keyup.escape)="cancel()">

Mouse button modifiers: .left, .right, .middle.

<button (click.right)="onRightClick()">Right click</button>

Why event binding matters: This is how user input reaches your class. Without it, the component is a static display. Every click, keypress, and mouse move that your app reacts to goes through ( ).


$event — the event object

$event is Angular’s placeholder for the event payload. Its type depends on the event.

<input (input)="onInput($event)">
onInput(event: Event): void {
  const value = (event.target as HTMLInputElement).value;
  console.log(value);
}

You usually need to cast event.target because the browser types it as EventTarget | null. Angular’s strict mode won’t let you access .value without narrowing.

Cleaner alternative: Use a template reference variable and pass the value directly.

<input #name (input)="onName(name.value)">
onName(value: string): void {
  console.log(value);
}

No casting, no Event type juggling.

Why $event is typed loosely: DOM events are messy — the same event type can have different targets. TypeScript can’t know what element fired it. Cast or use a reference variable.


Custom event binding — @Output

Event binding also works on component outputs. A child component emits an event; the parent listens.

// child
@Component({
  selector: 'app-counter',
  standalone: true,
  template: `<button (click)="increment()">+</button>`
})
export class CounterComponent {
  @Output() countChange = new EventEmitter<number>();
  private count = 0;

  increment(): void {
    this.count++;
    this.countChange.emit(this.count);
  }
}
<!-- parent -->
<app-counter (countChange)="onCountChange($event)"></app-counter>
<p>Count: {{ count }}</p>
count = 0;

onCountChange(value: number): void {
  this.count = value;
}

The child’s countChange is an EventEmitter. The parent binds to it with (countChange). $event carries the emitted value — here, a number.

The syntax is identical to DOM events. That’s the point.

Why the same syntax: Angular unifies DOM events and component outputs under one binding form. Learning ( ) once covers both. @Output is covered fully in the component communication chapters.


Two-way binding — [(prop)]="value"

Two-way binding is property binding and event binding combined. The banana-in-a-box syntax — [( )] — is the giveaway.

export class NameComponent {
  name = 'Alice';
}
<input [(ngModel)]="name">
<p>Hello, {{ name }}!</p>

Typing in the input updates name. Changing name updates the input. Both directions, one binding.

How it expands:

<input [ngModel]="name" (ngModelChange)="name = $event">

[(ngModel)] is sugar for [ngModel] plus (ngModelChange). The value flows in, changes flow out.

Requirements:

  • ngModel requires FormsModule to be imported
  • For a custom component, the component needs an @Input() and matching @Output() named <prop>Change

Custom two-way binding:

// child
@Component({
  selector: 'app-rating',
  standalone: true,
  template: `<button (click)="setRating(5)">★★★★★</button>`
})
export class RatingComponent {
  @Input() rating = 0;
  @Output() ratingChange = new EventEmitter<number>();

  setRating(value: number): void {
    this.rating = value;
    this.ratingChange.emit(value);
  }
}
<!-- parent -->
<app-rating [(rating)]="userRating"></app-rating>

The ratingChange output name must match the rating input with Change appended. Angular’s two-way syntax expands [(rating)]="userRating" to:

<app-rating [rating]="userRating" (ratingChange)="userRating = $event"></app-rating>

Why [( )] is sugar: It’s not a separate feature — it’s a naming convention. Any component with @Input() x and @Output() xChange supports [(x)]. That’s the whole rule.


Two-way binding without ngModel — signals and models

Modern Angular provides model() — a signal-based two-way binding primitive.

import { Component, model } from '@angular/core';

@Component({
  selector: 'app-toggle',
  standalone: true,
  template: `<button (click)="toggle()">{{ checked() ? 'On' : 'Off' }}</button>`
})
export class ToggleComponent {
  checked = model(false);

  toggle(): void {
    this.checked.set(!this.checked());
  }
}
<app-toggle [(checked)]="isEnabled"></app-toggle>

model() creates an input and output pair automatically. No EventEmitter, no Change suffix. Signals and model() are covered in later chapters — the point here is that two-way binding has a modern form.

Why model() exists: It removes boilerplate. [(value)] on a signal-based component just works, without manually writing an @Input/@Output pair.


Binding to attributes vs properties vs events — quick rules

GoalUse
Set a DOM property[prop]="expr"
Set an HTML attribute[attr.name]="expr"
Toggle a single class[class.name]="expr"
Set a single style[style.name]="expr"
Set multiple classes[class]="expr"
Set multiple styles[style]="expr"
Listen for an event(event)="handler()"
Two-way bind[(prop)]="value"
Render text{{ expr }}

If you’re unsure of the direction, ask: “Does the class send this, or does the view send this?” Class → view is [ ]. View → class is ( ). Both is [( )].


A full example

A small form that uses all three binding types.

The class:

import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';

@Component({
  selector: 'app-signup',
  standalone: true,
  imports: [FormsModule],
  templateUrl: './signup.component.html',
  styleUrl: './signup.component.css'
})
export class SignupComponent {
  name = '';
  email = '';
  agreed = false;
  submitted = false;

  onSubmit(): void {
    this.submitted = true;
    console.log('Signup:', this.name, this.email, this.agreed);
  }

  onReset(): void {
    this.name = '';
    this.email = '';
    this.agreed = false;
    this.submitted = false;
  }
}

The template:

<form (submit)="onSubmit(); $event.preventDefault()">
  <label>
    Name:
    <input [(ngModel)]="name" name="name" required>
  </label>

  <label>
    Email:
    <input [(ngModel)]="email" name="email" type="email" required>
  </label>

  <label>
    <input [(ngModel)]="agreed" name="agreed" type="checkbox">
    I agree to the terms
  </label>

  <button type="submit" [disabled]="!agreed || !name || !email">
    Sign up
  </button>
  <button type="button" (click)="onReset()">Reset</button>
</form>

<p [class.success]="submitted">
  {{ submitted ? 'Thanks, ' + name + '!' : '' }}
</p>

The styles:

.success {
  color: green;
  font-weight: bold;
}

Three bindings at work: [(ngModel)] keeps the inputs and class in sync, [disabled] disables the button when the form is incomplete, (click) and (submit) handle actions, and [class.success] toggles a class.

Why this shape: Each binding does one job. [(ngModel)] for form values, [disabled] for state-driven UI, (click)/(submit) for actions, [class.*] for conditional styling. That’s the whole toolkit.


Complete Example Session

# ============================================
# PART 1: GENERATE A COMPONENT
# ============================================

ng generate component demo
# [ CREATE src/app/demo/demo.component.ts ]
# [ CREATE src/app/demo/demo.component.html ]
# [ CREATE src/app/demo/demo.component.css ]

# ============================================
# PART 2: WRITE THE CLASS
# ============================================

cat > src/app/demo/demo.component.ts << 'EOF'
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';

@Component({
  selector: 'app-demo',
  standalone: true,
  imports: [FormsModule],
  templateUrl: './demo.component.html',
  styleUrl: './demo.component.css'
})
export class DemoComponent {
  url = 'https://via.placeholder.com/150';
  alt = 'Placeholder';
  isLoaded = false;
  count = 0;
  text = '';

  increment(): void {
    this.count++;
  }

  reset(): void {
    this.count = 0;
    this.text = '';
  }
}
EOF

# ============================================
# PART 3: WRITE THE TEMPLATE
# ============================================

cat > src/app/demo/demo.component.html << 'EOF'
<h2>Property binding</h2>
<img [src]="url" [alt]="alt" [class.loaded]="isLoaded">

<h2>Event binding</h2>
<button (click)="increment()">Clicked {{ count }} times</button>
<input #field (input)="text = field.value" placeholder="Type...">
<p>You typed: {{ text }}</p>

<h2>Two-way binding</h2>
<input [(ngModel)]="text" placeholder="Two-way...">
<p>Bound: {{ text }}</p>

<button (click)="reset()">Reset</button>
EOF

# ============================================
# PART 4: STYLE IT
# ============================================

cat > src/app/demo/demo.component.css << 'EOF'
img { border: 2px solid #ccc; }
img.loaded { border-color: green; }
button { margin-right: 0.5rem; }
EOF

# ============================================
# PART 5: SERVE
# ============================================

ng serve
# [ Local:   http://localhost:4200/ ]

Quick Reference

Binding Directions

SyntaxDirectionPurpose
{{ expr }}Class → viewRender text
[prop]="expr"Class → viewSet property
(event)="handler()"View → classListen for event
[(prop)]="value"BothTwo-way bind

Long Forms

ShortLong
[prop]bind-prop
(event)on-event
[(prop)]bindon-prop

Property Binding Targets

TargetExample
DOM property[src]="url"
Attribute[attr.colspan]="span"
Single class[class.active]="flag"
Multiple classes[class]="objOrArray"
Single style[style.color]="color"
Style with unit[style.width.px]="w"
Multiple styles[style]="obj"
Component input[user]="currentUser"

Event Modifiers

ModifierEffect
.enter / .escape / .tabKey filters
.shift / .control / .alt / .metaModifier keys
.left / .right / .middleMouse buttons

Common Events

EventFires on
clickClick
inputValue change
changeValue committed
keyup / keydownKey press
submitForm submit
focus / blurFocus change
mouseenter / mouseleaveMouse enter/leave
scrollScroll

Two-Way Binding

BindingRequires
[(ngModel)]FormsModule
[(prop)] on component@Input() prop + @Output() propChange
[(signal)]model() signal

$event Types

EventTypeAccess value via
(input)Event(e.target as HTMLInputElement).value
(click)MouseEvente.clientX, e.target
(keyup)KeyboardEvente.key
@OutputWhatever is emitted$event directly

Best Practices

Do This:

<!-- Use property binding for non-strings -->
<button [disabled]="isLoading">Save</button>          <!-- ✅ -->

<!-- Use [class.name] for single classes -->
<div [class.active]="isActive">...</div>              <!-- ✅ -->

<!-- Use [style.name.unit] for styles with units -->
<div [style.width.px]="width">...</div>               <!-- ✅ -->

<!-- Use event modifiers -->
<input (keyup.enter)="submit()">                      <!-- ✅ -->

<!-- Use template reference vars to avoid casting -->
<input #email (input)="save(email.value)">            <!-- ✅ -->

<!-- Use [(ngModel)] for simple two-way -->
<input [(ngModel)]="name">                            <!-- ✅ -->

<!-- Use $event.preventDefault() in the template -->
<form (submit)="$event.preventDefault(); save()">     <!-- ✅ -->

<!-- Prefer model() for signal-based components -->
checked = model(false);                               <!-- ✅ -->

Don’t Do This:

<!-- Don't interpolate booleans -->
<button disabled="{{ flag }}">Save</button>           <!-- ❌ "false" is truthy -->

<!-- Don't use ngClass/ngStyle for single toggles -->
<div [ngClass]="{ active: flag }">...</div>           <!-- ⚠️  use [class.active] -->

<!-- Don't call preventDefault outside the template or handler -->
<button (click)="doThing()">...</button>              <!-- ✅ inside doThing() -->

<!-- Don't bind to arbitrary attributes -->
<div [foo]="bar">...</div>                            <!-- ❌ unless [attr.foo] -->

<!-- Don't forget the Change suffix for custom two-way -->
@Output() valueChanged = new EventEmitter()           <!-- ⚠️  must be valueChange -->

<!-- Don't use $event without typing -->
(event.target as any).value                           <!-- ⚠️  cast properly -->

<!-- Don't mix event syntax with property syntax -->
<input (value)="name">                                <!-- ❌ value is not an event -->

Common Pitfalls

PitfallProblemSolution
disabled="{{ flag }}"String "false" is truthy[disabled]="flag"
[(ngModel)] without FormsModuleRuntime errorImport FormsModule
Forgetting name on ngModel in formAngular errorAdd name="..."
Wrong output name for two-wayBinding doesn’t work<prop>Change suffix
$event.target.value without castTypeScript errorCast or use reference var
[attr.disabled] vs [disabled]Attribute vs propertyUse property for DOM props
Binding to [value] for two-wayOne-way onlyUse [(ngModel)]
preventDefault on wrong elementForm still submitsBind on (submit)

Real-World Examples

1. Bind an image source

<img [src]="user.avatar" [alt]="user.name">

2. Disable a button conditionally

<button [disabled]="!formValid">Submit</button>

3. Toggle a class

<div [class.highlight]="selected">...</div>

4. Toggle multiple classes

<div [class]="{ active: isActive, error: hasError }">...</div>

5. Set a style with a unit

<div [style.width.px]="boxWidth">...</div>

6. Handle a click

<button (click)="save()">Save</button>

7. Read an input’s value

<input #email (input)="emailValue = email.value">

8. Enter-key submit

<input (keyup.enter)="submit()">

9. Right-click handler

<div (click.right)="onContextMenu($event)">...</div>

10. Two-way bind a simple value

<input [(ngModel)]="name" name="name">

11. Custom two-way with a component

<app-rating [(rating)]="userRating"></app-rating>

12. Two-way with a signal

<app-toggle [(checked)]="isEnabled"></app-toggle>

13. Listen to a child output

<app-counter (countChange)="onCount($event)"></app-counter>

14. Bind a component input

<app-user-card [user]="currentUser" [compact]="true"></app-user-card>

15. Prevent default and handle

<form (submit)="$event.preventDefault(); onSubmit()">

Visual: Binding Directions

┌──────────────────────────────────────────────┐
│  CLASS                                       │
│                                              │
│  name = 'Alice'                              │
│  count = 0                                   │
│                                              │
└──────────────────────────────────────────────┘
        │                          ▲
        │                          │
   [ ]  │  class → view            │  ( )  view → class
        │                          │
        ▼                          │
┌──────────────────────────────────────────────┐
│  TEMPLATE                                    │
│                                              │
│  <p>{{ name }}</p>                           │
│  <button (click)="inc()">+</button>          │
│                                              │
└──────────────────────────────────────────────┘

   [( )] — both directions at once

Visual: Two-Way Binding Expansion

┌──────────────────────────────────────────────┐
│  You write:                                  │
│                                              │
│  <input [(ngModel)]="name">                  │
│                                              │
└──────────────────────────────────────────────┘
                  │
                  ▼
┌──────────────────────────────────────────────┐
│  Angular expands to:                         │
│                                              │
│  <input [ngModel]="name"                     │
│         (ngModelChange)="name = $event">     │
│                                              │
└──────────────────────────────────────────────┘
                  │
                  ▼
┌──────────────────────────────────────────────┐
│  Custom component:                           │
│                                              │
│  @Input()  rating                            │
│  @Output() ratingChange                      │
│                                              │
│  → [(rating)] works automatically            │
│                                              │
└──────────────────────────────────────────────┘

Visual: Property vs Attribute Binding

┌──────────────────────────────────────────────┐
│  [src]="url"                                 │
│                                              │
│  → sets the DOM PROPERTY `src`               │
│  → keeps the original type                   │
│  → use for booleans, numbers, objects        │
│                                              │
└──────────────────────────────────────────────┘

┌──────────────────────────────────────────────┐
│  [attr.colspan]="span"                       │
│                                              │
│  → sets the HTML ATTRIBUTE `colspan`         │
│  → always a string                           │
│  → use when no matching property exists      │
│                                              │
└──────────────────────────────────────────────┘

┌──────────────────────────────────────────────┐
│  [class.active]="flag"                       │
│  [style.width.px]="w"                        │
│                                              │
│  → dedicated class and style bindings        │
│  → cleaner than string concatenation         │
│                                              │
└──────────────────────────────────────────────┘

Visual: Event Binding with Modifiers

┌──────────────────────────────────────────────┐
│  (click)="handler()"                         │
│       │                                      │
│       ▼                                      │
│  Fires on every click                        │
│                                              │
└──────────────────────────────────────────────┘

┌──────────────────────────────────────────────┐
│  (keyup.enter)="submit()"                    │
│       │                                      │
│       ▼                                      │
│  Fires only when Enter is released           │
│                                              │
└──────────────────────────────────────────────┘

┌──────────────────────────────────────────────┐
│  (click.right)="menu()"                      │
│       │                                      │
│       ▼                                      │
│  Fires only on right-click                   │
│                                              │
└──────────────────────────────────────────────┘

Summary

BindingDirectionSyntax
InterpolationClass → view (text){{ expr }}
PropertyClass → view (property)[prop]="expr"
AttributeClass → view (attribute)[attr.name]="expr"
ClassClass → view (single class)[class.name]="expr"
StyleClass → view (single style)[style.name]="expr"
EventView → class(event)="handler()"
Two-wayBoth[(prop)]="value"
Custom outputView → class (component)(output)="handler($event)"
Component inputClass → view (component)[input]="expr"

Key takeaways:

  • [ ] sends data down — from class into the DOM or a child component
  • ( ) sends data up — from the DOM or a child component into the class
  • [( )] does both — sugar for a property binding plus an event binding
  • Property binding keeps the original type; interpolation converts to string
  • Use [class.name] and [style.name.unit] for single toggles — cleaner than ngClass/ngStyle
  • Use [attr.name] when there’s no matching DOM property (colspan, aria-*, data-*)
  • $event carries the event payload — cast event.target or use a reference variable
  • Event modifiers (.enter, .right, .control) filter events without handler code
  • [(ngModel)] requires FormsModule and a name attribute inside a form
  • Custom two-way binding needs @Input() x and @Output() xChange
  • model() is the modern signal-based two-way binding — no EventEmitter boilerplate

Remember: Every interaction in Angular is one of three directions. Class → view uses [ ]. View → class uses ( ). Both uses [( )]. Learn to read the brackets as arrows, and the whole binding system becomes obvious. Everything else — forms, components, routing — builds on this.


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!