Chapter 4: Outputs & Events
Outputs & Events!
This is where the child component gets to talk back to the parent. Up until now, data only flowed down (parent → child via inputs). Now we’ll make it flow up too (child → parent via outputs/events).
Think of it like this:
- Inputs = “Hey child, here’s your name and age”
- Outputs = “Hey parent, the user just clicked ‘Delete me!’ — do something about it!”
In modern Angular (v17+ → 19–21 as of 2026), we use the signal-based output() function instead of the old @Output() + EventEmitter. It’s cleaner, more type-safe, doesn’t require instantiating anything, and integrates nicely with the rest of the signals world.
We’ll cover two main things:
- Emitting custom events with output()
- Two-way binding using model() + the famous “banana-in-a-box” syntax [( )]
1. Emitting Events with output()
output() creates an emitter that the child uses to notify the parent when something interesting happens (click, change, delete, save, etc.).
Key points
- Returns an OutputEmitterRef<T> (you call .emit(value) on it)
- No need to create new EventEmitter<>() anymore — huge win!
- Optional: alias if you want a different event name in templates
- Events do not bubble up the DOM (unlike native click)
- Parent listens with (eventName)=”handler($event)”
Example: Let’s upgrade our UserCard to emit events
We’ll add two outputs:
- delete when user clicks a delete button
- favoriteToggled when they toggle a favorite star
src/app/user-card/user-card.component.ts (add to existing)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
import { Component, input, output } from '@angular/core'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-user-card', standalone: true, imports: [CommonModule], template: ` <div class="card"> <img [src]="avatarUrl()" [alt]="name() + ' avatar'" class="avatar" /> <h3>{{ name() }}</h3> <p>Age: {{ age() }} • {{ isAdult() ? 'Adult' : 'Minor' }}</p> <!-- Favorite toggle --> <button (click)="toggleFavorite()"> {{ isFavorite() ? '★ Favorited' : '☆ Favorite' }} </button> <!-- Delete button --> <button class="delete-btn" (click)="onDelete()">Delete User</button> </div> `, styles: [` /* ... existing styles ... */ .delete-btn { background: #ff5252; color: white; margin-top: 12px; } `] }) export class UserCardComponent { name = input.required<string>(); age = input.required<number>(); avatar = input<string>('https://i.pravatar.cc/120?u=default'); isAdult = computed(() => this.age() >= 18); avatarUrl = computed(() => this.avatar() || 'https://i.pravatar.cc/120?u=fallback'); // New: local state for favorite (just for demo) isFavorite = signal(false); // Outputs! delete = output<string>(); // emits user ID or identifier favoriteToggled = output<boolean>(); // emits new favorite state // Alias example (if you want different name in template) // remove = output<void>({ alias: 'userRemoved' }); toggleFavorite() { this.isFavorite.update(v => !v); this.favoriteToggled.emit(this.isFavorite()); // ← emit! } onDelete() { this.delete.emit(this.name()); // could emit ID, object, etc. } } |
Now in parent (app.component.ts)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
@Component({ selector: 'app-root', standalone: true, imports: [UserCardComponent], template: ` <h1>Family Members</h1> <app-user-card [name]="'Amit Sharma'" [age]="28" (delete)="onUserDelete($event)" (favoriteToggled)="onFavoriteChange($event)" ></app-user-card> <app-user-card name="Priya Patel" [age]="16" (delete)="onUserDelete($event)" ></app-user-card> <p>Last deleted: {{ lastDeleted() || 'None yet' }}</p> <p>Last favorite toggle: {{ lastFavoriteMessage() }}</p> ` }) export class AppComponent { lastDeleted = signal<string | null>(null); lastFavoriteMessage = signal<string>('No changes'); onUserDelete(name: string) { console.log('Parent received delete for:', name); this.lastDeleted.set(name); // In real app: remove from list, call API, etc. } onFavoriteChange(isFav: boolean) { this.lastFavoriteMessage.set(`Favorite is now: ${isFav ? 'Yes' : 'No'}`); } } |
→ Click “Delete User” → parent logs it and shows “Last deleted: Amit Sharma” → Toggle favorite → parent updates the message instantly
No more EventEmitter boilerplate — just output<…>() and .emit().
2. Two-Way Binding with model() + Banana-in-a-Box [( )]
model() is a special input that automatically creates a paired output named xxxChange (e.g. count → countChange).
This enables the classic [(ngModel)]-style two-way binding, but signal-powered and custom to your component.
When to use it?
- Form-like controls: sliders, toggles, color pickers, editable text, counters
- Parent wants to both set initial value and get updates when child changes it
Example: A simple Counter component with two-way binding
Generate it:
|
0 1 2 3 4 5 6 |
ng generate component counter --standalone |
src/app/counter/counter.component.ts
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
import { Component, model } from '@angular/core'; @Component({ selector: 'app-counter', standalone: true, template: ` <div class="counter"> <button (click)="decrement()">-</button> <span>{{ count() }}</span> <button (click)="increment()">+</button> </div> `, styles: [` .counter { display: flex; gap: 12px; align-items: center; font-size: 1.5rem; } button { padding: 8px 16px; font-size: 1.2rem; } `] }) export class CounterComponent { // model() creates: input + output named 'countChange' count = model<number>(0); // default 0, writable! // Optional: required model // count = model.required<number>(); // Optional: transform (e.g. ensure even numbers) // count = model(0, { transform: (v: number) => Math.round(v / 2) * 2 }); increment() { this.count.update(v => v + 1); // ← updates parent automatically! } decrement() { this.count.update(v => v - 1); } } |
Parent usage (app.component.ts)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
template: ` <h2>Two-Way Counter Demo</h2> <!-- Banana-in-a-box: [(count)] --> <app-counter [(count)]="myCount" /> <p>Parent sees: {{ myCount() }} (updates live!)</p> <button (click)="reset()">Reset to 0</button> ` |
|
0 1 2 3 4 5 6 7 8 9 10 |
myCount = signal(5); reset() { this.myCount.set(0); } |
What happens?
- Parent sets initial 5 → child shows 5
- Click + in child → count becomes 6 → parent’s myCount auto-updates to 6
- Click Reset in parent → child instantly shows 0
- Full two-way sync, powered by signals — no manual event handling needed!
Banana-in-a-box = [(count)] is sugar for:
|
0 1 2 3 4 5 6 |
[count]="myCount()" (countChange)="myCount.set($event)" |
But model() makes it automatic and reactive.
Quick Rules of Thumb
- Use output<T>() for one-way notifications (delete, selected, saved)
- Use model<T>() when you want two-way sync (value + onValueChange)
- Always .emit() or .update()/set() on model to notify parent
- model.required<T>() if value must be provided
- You can alias: model(…, { alias: ‘value’ }) → [(value)]
Mini Practice Task
- Add a toggleFavorite output to UserCard (like above)
- Create a new component: app-like-button with liked = model<boolean>(false);
- Template: heart icon that toggles on click, updates liked
- Use it in app.component: <app-like-button [(liked)]=”articleLiked” />
- Show “Article is {{ articleLiked() ? ‘liked ❤️’ : ‘not liked ☹️’ }}”
Play with it — change in one place, see sync everywhere.
You’ve now got downward data flow (inputs), upward events (outputs), and two-way sync (model). Components are talking to each other like real team members!
Next chapter is usually services + dependency injection (shared state, API calls). Want to go there, or deepen outputs/model (more examples, alias/transforms, common gotchas)? Just say! 😄
