@Input() and @Output() are the primary API of a component — they define the contract between a parent and a child component. @Input() passes data down; @Output() sends events up. Angular 16 introduced signal inputs with input() and input.required(), providing a reactive, type-safe alternative to decorator-based inputs that integrates seamlessly with the signals system. Understanding the full input/output API — including aliasing, transforms, change detection hooks, and the model() two-way binding — is essential for building well-structured component trees.
@Input() Options and Variants
| Form | Syntax | Notes |
|---|---|---|
| Basic | @Input() title = '' |
Optional — defaults to initialiser if not provided |
| Required | @Input({ required: true }) task!: Task |
Error if parent omits this input (Angular 16+) |
| Alias | @Input('taskItem') task!: Task |
Parent binds as [taskItem], class uses this.task |
| Transform | @Input({ transform: booleanAttribute }) disabled = false |
Coerce attribute string to boolean (Angular 16+) |
| Signal Input | task = input<Task>() |
Returns a Signal — reactive, read with task() |
| Required Signal Input | task = input.required<Task>() |
No default — Angular errors if not bound |
| Aliased Signal Input | task = input<Task>(undefined, { alias: 'taskItem' }) |
Signal input with alias |
| Two-way (model) | value = model<string>('') |
Enables [(value)] from parent (Angular 17+) |
@Output() Options and Patterns
| Pattern | Syntax | Use When |
|---|---|---|
| Basic output | @Output() clicked = new EventEmitter<void>() |
No data to emit |
| With payload | @Output() deleted = new EventEmitter<string>() |
Emit an ID, value, or object |
| With alias | @Output('taskDeleted') deleted = new EventEmitter<string>() |
Parent binds as (taskDeleted) |
| Object payload | @Output() saved = new EventEmitter<{id: string, data: Partial<Task>}>() |
Multiple values in one event |
| output() signal | deleted = output<string>() |
Modern signal-based output (Angular 17+) |
input() and input.required()) are reactive by nature — they are Signals and can be used in computed() and effect(). Traditional @Input() requires ngOnChanges() or a setter to react to changes. With signal inputs, the change is automatically tracked: filteredItems = computed(() => this.items().filter(...)) automatically recomputes when items input changes. This makes signal inputs significantly more powerful for derived state in presentational components.@Input({ required: true }) or input.required() for inputs that are truly required — task!: Task with the non-null assertion is a band-aid that silences the TypeScript error at the cost of runtime null reference errors. With required inputs, Angular gives you a compile-time error if a parent template forgets to bind the input, preventing entire categories of bugs before the code even runs.@Input() value directly. If a parent passes [tasks]="tasks" and the child does this.tasks.push(newTask), the parent’s array is mutated — Angular’s change detection may not detect this, the UI may not update, and the parent loses control of its own state. Treat inputs as read-only. If you need to derive a modified version, create a new array or object: this.sortedTasks = [...this.tasks].sort(...).Complete Input/Output Examples
// ── Traditional @Input / @Output ──────────────────────────────────────────
import {
Component, Input, Output, EventEmitter,
OnChanges, SimpleChanges, booleanAttribute, numberAttribute,
} from '@angular/core';
import { Task } from '../../models/task.model';
@Component({ selector: 'app-task-form', standalone: true, ... })
export class TaskFormComponent implements OnChanges {
// Required input — Angular compile error if parent omits [task]
@Input({ required: true }) task!: Task;
// Optional with default
@Input() submitLabel = 'Save Task';
@Input() showCancel = true;
// Built-in transform: coerce attribute strings to correct type
@Input({ transform: booleanAttribute }) disabled = false;
@Input({ transform: numberAttribute }) maxTitleLength = 200;
// Aliased: parent uses [taskData], class uses this.taskData
@Input('taskData') incomingData?: Partial<Task>;
// Outputs
@Output() submitted = new EventEmitter<Partial<Task>>();
@Output() cancelled = new EventEmitter<void>();
@Output() changed = new EventEmitter<{ field: string; value: unknown }>();
// Local form state (not an input — managed internally)
formData: Partial<Task> = {};
// React to @Input changes
ngOnChanges(changes: SimpleChanges): void {
if (changes['task']) {
// Shallow copy — never mutate the input directly
this.formData = { ...changes['task'].currentValue };
}
}
onFieldChange(field: string, value: unknown): void {
this.formData = { ...this.formData, [field]: value };
this.changed.emit({ field, value });
}
onSubmit(): void {
this.submitted.emit(this.formData);
}
onCancel(): void {
this.cancelled.emit();
}
}
// ── Signal Inputs (Angular 16+) ───────────────────────────────────────────
import { Component, input, output, model, computed, effect } from '@angular/core';
@Component({ selector: 'app-task-card', standalone: true, ... })
export class TaskCardComponent {
// Signal inputs — reactive, no ngOnChanges needed
task = input.required<Task>(); // required — no default
isDeleting= input(false); // optional — default false
maxChars = input(150); // optional with default
// Signal outputs (Angular 17+)
completed = output<string>();
deleted = output<string>();
edited = output<string>();
// Derived state using computed() — auto-updates when task() changes
isOverdue = computed(() =>
!!this.task().dueDate
&& new Date(this.task().dueDate!) < new Date()
&& this.task().status !== 'completed'
);
truncatedTitle = computed(() => {
const t = this.task().title;
const max = this.maxChars();
return t.length > max ? t.substring(0, max) + '...' : t;
});
// effect() runs when signal inputs change — replaces ngOnChanges
constructor() {
effect(() => {
console.log('Task changed:', this.task().title);
// Effect tracks this.task() — runs whenever task input changes
});
}
markComplete(): void { this.completed.emit(this.task()._id); }
requestDelete(): void { this.deleted.emit(this.task()._id); }
requestEdit(): void { this.edited.emit(this.task()._id); }
}
// ── model() — two-way bindable signal (Angular 17+) ───────────────────────
@Component({
selector: 'app-search-input',
standalone: true,
template: `
<input
[value]="query()"
(input)="query.set($any($event.target).value)"
[placeholder]="placeholder()">
<button *ngIf="query()" (click)="query.set('')">Clear</button>
`,
})
export class SearchInputComponent {
query = model(''); // two-way bindable signal
placeholder = input('Search...');
}
// Parent usage:
// <app-search-input [(query)]="searchQuery"></app-search-input>
// searchQuery is a writable signal — updated automatically when user types
// ── @Input setter — run logic when input changes ──────────────────────────
@Component({ selector: 'app-priority-icon', standalone: true, ... })
export class PriorityIconComponent {
iconPath = '';
colour = '';
@Input()
set priority(value: string) {
this.iconPath = `/assets/icons/priority-${value}.svg`;
this.colour = value === 'high' ? '#dc2626'
: value === 'medium' ? '#d97706' : '#16a34a';
}
}
// Setter runs every time the priority input changes — no ngOnChanges needed
How It Works
Step 1 — @Input() Is a Property Binding Target
When you decorate a class property with @Input(), Angular registers it as a bindable property. A parent template can bind to it with [propertyName]="expression". During change detection, if the expression’s value has changed, Angular updates the property on the child component instance. Without the decorator, Angular cannot find the property as a binding target and reports a template error.
Step 2 — Signal Inputs Are Always Reactive
A signal input created with input() is a read-only Signal. Any computed() or effect() that reads the signal’s value tracks it as a dependency — they automatically re-run when the input changes. This replaces the need for ngOnChanges() in most cases. The signal’s value is read with the call syntax: this.task() instead of this.task.
Step 3 — @Output() EventEmitter Wraps RxJS Subject
EventEmitter<T> is a thin wrapper around RxJS Subject<T>. When you call this.emitter.emit(value), it calls subject.next(value). Angular subscribes to the EventEmitter when the parent template binds to the output: (outputName)="handler($event)". The $event in the template expression is the value passed to emit(). Angular manages the subscription lifecycle automatically.
Step 4 — booleanAttribute and numberAttribute Enable HTML Attribute Usage
HTML attributes are always strings — <app-button disabled> passes the string "" (empty string) as the disabled input, not true. The built-in booleanAttribute transform converts attribute strings to booleans: empty string becomes true, "false" becomes false. Similarly, numberAttribute converts "42" to 42. These transforms make your components usable as HTML attributes without property binding syntax.
Step 5 — model() Implements the Two-Way Binding Convention
model<T>(default) creates a writable signal that follows Angular’s two-way binding convention. It generates both an input (named X) and an output (named XChange) automatically. A parent can bind with [(X)]="signal" — Angular subscribes to the output and updates the parent’s signal when the child calls X.set(value). This eliminates the boilerplate of creating a matching @Input X and @Output XChange for every two-way property.
Common Mistakes
Mistake 1 — Mutating @Input() arrays or objects directly
❌ Wrong — modifies the parent’s data, Angular may not detect the change:
@Input() tasks: Task[] = [];
addTask(task: Task): void {
this.tasks.push(task); // mutates parent's array — DO NOT DO THIS
}
✅ Correct — emit an event and let the parent update its own state:
@Output() taskAdded = new EventEmitter<Task>();
addTask(task: Task): void {
this.taskAdded.emit(task); // parent creates new array with the task added
}
Mistake 2 — Emitting on @Output in ngOnInit before parent is ready
❌ Wrong — emitting during initialisation can cause ExpressionChangedAfterItHasBeenCheckedError:
ngOnInit(): void {
this.selectedChange.emit(this.defaultValue); // parent not ready for events during child init
}
✅ Correct — use a setTimeout or emit only in response to user actions:
ngAfterViewInit(): void {
setTimeout(() => this.selectedChange.emit(this.defaultValue));
}
Mistake 3 — Using required decorator but not @Input({ required: true })
❌ Wrong — non-null assertion silences TS but does not give compile-time safety:
@Input() task!: Task; // TypeScript non-null assertion — no compile error if parent omits [task]
✅ Correct — use required input for true compile-time enforcement:
@Input({ required: true }) task!: Task; // Angular error if parent omits [task]
// OR modern:
task = input.required<Task>(); // signal input — required + reactive
Quick Reference
| Need | Use |
|---|---|
| Optional input with default | @Input() title = '' or title = input('') |
| Required input | @Input({ required: true }) task!: Task or task = input.required<Task>() |
| Coerce HTML attribute | @Input({ transform: booleanAttribute }) disabled = false |
| React to input change | ngOnChanges() or computed() with signal input |
| Run logic on change | @Input() set value(v) { this.process(v) } or effect() |
| Emit event up | @Output() saved = new EventEmitter<Task>() |
| Emit without data | @Output() cancelled = new EventEmitter<void>() |
| Two-way binding | value = model('') → parent uses [(value)] |
| Modern output | saved = output<Task>() |