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.
| Syntax | Direction | Name |
|---|---|---|
{{ expr }} | Class → view (as text) | Interpolation |
[prop]="expr" | Class → view (as property) | Property binding |
(event)="handler()" | View → class | Event binding |
[(prop)]="value" | Both directions | Two-way binding |
bind-prop="expr" | Class → view | Long form of [prop] |
on-event="handler()" | View → class | Long form of (event) |
bindon-prop="value" | Both | Long 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:
| Binding | Sets | Type |
|---|---|---|
<img src="{{ url }}"> | src attribute | String |
<img [src]="url"> | src property | Original 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:
| Target | Example |
|---|---|
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
srcon an<img>oruseron 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:
| Event | Fires when |
|---|---|
click | Element clicked |
dblclick | Double click |
input | Input value changes |
change | Value committed |
keyup / keydown | Key pressed/released |
keyup.enter | Enter key (key filter) |
submit | Form submitted |
focus / blur | Focus gained/lost |
mouseenter / mouseleave | Mouse enters/leaves |
scroll | Element scrolled |
touchstart / touchend | Touch 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
$eventis 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.@Outputis 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:
ngModelrequiresFormsModuleto 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() xand@Output() xChangesupports[(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/@Outputpair.
Binding to attributes vs properties vs events — quick rules
| Goal | Use |
|---|---|
| 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
| Syntax | Direction | Purpose |
|---|---|---|
{{ expr }} | Class → view | Render text |
[prop]="expr" | Class → view | Set property |
(event)="handler()" | View → class | Listen for event |
[(prop)]="value" | Both | Two-way bind |
Long Forms
| Short | Long |
|---|---|
[prop] | bind-prop |
(event) | on-event |
[(prop)] | bindon-prop |
Property Binding Targets
| Target | Example |
|---|---|
| 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
| Modifier | Effect |
|---|---|
.enter / .escape / .tab | Key filters |
.shift / .control / .alt / .meta | Modifier keys |
.left / .right / .middle | Mouse buttons |
Common Events
| Event | Fires on |
|---|---|
click | Click |
input | Value change |
change | Value committed |
keyup / keydown | Key press |
submit | Form submit |
focus / blur | Focus change |
mouseenter / mouseleave | Mouse enter/leave |
scroll | Scroll |
Two-Way Binding
| Binding | Requires |
|---|---|
[(ngModel)] | FormsModule |
[(prop)] on component | @Input() prop + @Output() propChange |
[(signal)] | model() signal |
$event Types
| Event | Type | Access value via |
|---|---|---|
(input) | Event | (e.target as HTMLInputElement).value |
(click) | MouseEvent | e.clientX, e.target |
(keyup) | KeyboardEvent | e.key |
@Output | Whatever 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
| Pitfall | Problem | Solution |
|---|---|---|
disabled="{{ flag }}" | String "false" is truthy | [disabled]="flag" |
[(ngModel)] without FormsModule | Runtime error | Import FormsModule |
Forgetting name on ngModel in form | Angular error | Add name="..." |
| Wrong output name for two-way | Binding doesn’t work | <prop>Change suffix |
$event.target.value without cast | TypeScript error | Cast or use reference var |
[attr.disabled] vs [disabled] | Attribute vs property | Use property for DOM props |
Binding to [value] for two-way | One-way only | Use [(ngModel)] |
preventDefault on wrong element | Form still submits | Bind 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
| Binding | Direction | Syntax |
|---|---|---|
| Interpolation | Class → view (text) | {{ expr }} |
| Property | Class → view (property) | [prop]="expr" |
| Attribute | Class → view (attribute) | [attr.name]="expr" |
| Class | Class → view (single class) | [class.name]="expr" |
| Style | Class → view (single style) | [style.name]="expr" |
| Event | View → class | (event)="handler()" |
| Two-way | Both | [(prop)]="value" |
| Custom output | View → class (component) | (output)="handler($event)" |
| Component input | Class → 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 thanngClass/ngStyle - Use
[attr.name]when there’s no matching DOM property (colspan,aria-*,data-*) $eventcarries the event payload — castevent.targetor use a reference variable- Event modifiers (
.enter,.right,.control) filter events without handler code [(ngModel)]requiresFormsModuleand anameattribute inside a form- Custom two-way binding needs
@Input() xand@Output() xChange model()is the modern signal-based two-way binding — noEventEmitterboilerplate
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!