Signal Inputs & Outputs (v18)
📚 Signal-based Inputs & Outputs
Angular 18 made signal inputs and outputs stable:
- input(): Creates signal input (optional, can have default)
- input.required(): Required signal input
- output(): Creates output emitter (replaces @Output)
- model(): Two-way binding signal (input + output combined)
- Signal-based: Inputs are signals, use () to read
🎯 Interview Questions
- Q1: What is the difference between input() and @Input()?
- A: input() returns a signal that tracks changes automatically. @Input is a property that requires manual change detection.
- Q2: How do you create a required signal input?
- A: Use input.required<T>(). The returned signal is guaranteed to have a value (no undefined).
- Q3: What is the model() function?
- A: Creates two-way binding. Combines input signal with automatic output for changes. Parent uses [(model)]="value".
- Q4: Can you transform input values?
- A: Yes, use transform option: input(0, { transform: numberAttribute }) to convert string attributes to numbers.
🔧 Live Demo - Parent Component
Parent Controls
5
Child Component:
Hello World
Count: 42
Select a value (two-way bound):
Event Log:
- No events yet
📋 API Comparison
| Old (Decorator) | New (Function) | Type |
|---|---|---|
| @Input() name: string | name = input<string>() | InputSignal<string | undefined> |
| @Input() name = 'default' | name = input('default') | InputSignal<string> |
| @Input({ required: true }) name!: string | name = input.required<string>() | InputSignal<string> |
| @Output() click = new EventEmitter() | click = output<MouseEvent>() | OutputEmitterRef<MouseEvent> |
| @Input() + @Output() value | value = model(0) | ModelSignal<number> |
💻 Signal Inputs/Outputs Code
import { Component, input, output, model } from '@angular/core';
@Component({
selector: 'app-product-card',
template: `
<div>
<h2>{{ name() }}</h2>
<p>Price: ${{ price() }}</p>
<p>Quantity: {{ quantity() }}</p>
<button (click)="addToCart.emit(name())">Add</button>
<!-- Two-way binding with model -->
<input
type="number"
[value]="quantity()"
(input)="quantity.set(+$event.target.value)"
/>
</div>
`
})
export class ProductCardComponent {
// Required input - must be provided
name = input.required<string>();
// Optional input with default
price = input(0);
// Optional input without default
description = input<string>();
// Input with transform (string attribute to number)
maxItems = input(10, { transform: numberAttribute });
// Input with alias
productId = input.required<string>({ alias: 'id' });
// Output
addToCart = output<string>();
// Two-way binding with model()
quantity = model(1);
}
// ===== Usage in parent template =====
<app-product-card
[name]="product.name"
[price]="product.price"
[id]="product.id"
[maxItems]="5"
(addToCart)="handleAddToCart($event)"
[(quantity)]="selectedQuantity"
/>
// ===== Reading signal inputs =====
// In component class:
fullName = computed(() => `${this.firstName()} ${this.lastName()}`);
// inputs are signals, so they work with computed/effect
effect(() => {
console.log('Name changed:', this.name());
});
// ===== Input transforms =====
import { booleanAttribute, numberAttribute } from '@angular/core';
@Component({...})
export class MyComponent {
// Transform "true"/"false" string to boolean
disabled = input(false, { transform: booleanAttribute });
// Transform string to number
count = input(0, { transform: numberAttribute });
// Custom transform
tags = input<string[]>([], {
transform: (value: string) => value.split(',')
});
}
// Usage: <my-comp disabled="true" count="5" tags="a,b,c" />