@Input and @Output — Component Communication

@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+)
Note: Signal inputs (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.
Tip: Use @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.
Warning: Never mutate an @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>()

🧠 Test Yourself

A child component uses task = input.required<Task>(). It needs to display the task title in uppercase only when a highlight signal input is true. What is the most idiomatic Angular signals approach?