Signal-Based Component APIs
Signal-based inputs and outputs provide reactive component APIs without decorator boilerplate.
input() — Reactive Input
import { Component, input } from '@angular/core';
@Component({
selector: 'app-card',
template: `
<div class="card">
<h3>{{ title() }}</h3>
<p>{{ description() }}</p>
</div>
`,
})
export class CardComponent {
title = input<string>('Untitled');
description = input<string>('');
variant = input<'primary' | 'secondary'>('primary');
}
<app-card [title]="cardTitle" [description]="cardDescription"></app-card>
input.required() — Mandatory Input
@Component({
selector: 'app-alert',
template: `<div class="alert">{{ message() }}</div>`,
})
export class AlertComponent {
message = input.required<string>();
}
If a parent does not provide a required input, Angular throws a compile-time error.
output() — Typed Events
import { Component, output } from '@angular/core';
@Component({
selector: 'app-child',
template: `<button (click)="selected.emit('user-123')">Select</button>`,
})
export class ChildComponent {
selected = output<string>();
}
<app-child (selected)="onSelected($event)"></app-child>
model() — Two-Way Binding
import { Component, model } from '@angular/core';
@Component({
selector: 'app-counter',
template: `
<button (click)="count.update(c => c - 1)">-</button>
<span>{{ count() }}</span>
<button (click)="count.update(c => c + 1)">+</button>
`,
})
export class CounterComponent {
count = model<number>(0);
}
<app-counter [(count)]="parentCount"></app-counter>
This is equivalent to:
<app-counter [count]="parentCount" (countChange)="parentCount = $event"></app-counter>
Migrating from Decorators to Signals
Signal-based APIs can be adopted incrementally alongside existing @Input/@Output decorators.
Migration Comparison
| Feature | Decorator | Signal |
|---|---|---|
| Input | @Input() title: string |
title = input<string>() |
| Required input | @Input({ required: true }) |
input.required<string>() |
| Output | @Output() selected = new EventEmitter<string>() |
selected = output<string>() |
| Two-way | @Input() count + @Output() countChange |
count = model<number>(0) |
| Alias | @Input('alias') |
input({ alias: 'alias' }) |
Input with Transform
import { Component, input, booleanAttribute } from '@angular/core';
@Component({
selector: 'app-toggle',
template: `<span [class.active]="isActive()">Toggle</span>`,
})
export class ToggleComponent {
isActive = input(false, { transform: booleanAttribute });
}
<app-toggle [isActive]="true"></app-toggle>
<app-toggle isActive></app-toggle>
Input with Alias
@Component({
selector: 'app-user-avatar',
template: `<img [src]="avatarUrl()" [alt]="altText()">`,
})
export class UserAvatarComponent {
avatarUrl = input<string>(undefined, { alias: 'src' });
altText = input<string>('Avatar', { alias: 'alt' });
}
<app-user-avatar [src]="user.imageUrl" [alt]="user.name"></app-user-avatar>
When to Use Signals vs Decorators
- New components — prefer signal-based APIs
- Existing components — @Input/@Output work fine; migrate incrementally
- Library authors — signal APIs work in both standalone and NgModule contexts
Quiz
1. How do you declare a signal-based input?
2. What does model() create?
3. How do you make a signal input required?
Flashcards
Question
How do you declare a signal input?
Click to reveal answer
Answer
title = input<string>() — read with title().
Question
What does model() create?
Click to reveal answer
Answer
A two-way bindable signal. Use [(syntax)] in parent template.
Question
How do you make an input required?
Click to reveal answer
Answer
input.required<string>() — compile-time error if not provided.
Revision Notes
Key Takeaways
- 1. input() declares reactive component inputs
- 2. input.required() enforces mandatory inputs
- 3. model() creates two-way bindable signals
- 4. output() replaces EventEmitter for typed events
Interview Tips
- • Explain the benefits of signal-based component APIs
- • Be ready to describe model() for two-way binding
- • Mention incremental migration from decorators
Cheat Sheet
Signal Inputs and Outputs Cheat Sheet
Input:
title = input<string>('Untitled');
Required input:
message = input.required<string>();
Output:
selected = output<string>();
Two-way binding:
count = model<number>(0);
<app-counter [(count)]="parentCount"></app-counter>