Skip to content
Angular ng components 4 min read

Signal Inputs, Outputs & Models

For years a component’s public API was declared with the @Input() and @Output() decorators. Modern Angular (17.1 for inputs, 17.2 for outputs and models) replaces them with the input(), output(), and model() functions. These are not just cosmetic — inputs become read-only signals you can compute and effect over, outputs gain precise typing, and model() wires up two-way binding without boilerplate. This page covers all three, plus required inputs, aliases, and transforms.

Signal inputs with input()

Calling input() on a class field declares a reactive, read-only InputSignal. The parent still binds with the familiar square-bracket syntax, but inside the component you read the value by calling the signal like any other.

import { Component, input } from '@angular/core';

@Component({
  selector: 'app-user-card',
  standalone: true,
  template: `
    <article class="card">
      <h3>{{ name() }}</h3>
      <p>{{ role() }}</p>
    </article>
  `,
})
export class UserCardComponent {
  // optional input with a default value -> InputSignal<string>
  name = input('Anonymous');
  role = input('Member');
}
<app-user-card [name]="user.fullName" [role]="user.title" />

Because inputs are signals, they compose with the rest of the reactivity system. A computed() derived from an input recalculates automatically whenever the parent pushes a new value — no ngOnChanges lifecycle hook required.

import { Component, input, computed } from '@angular/core';

@Component({
  selector: 'app-price-tag',
  standalone: true,
  template: `<span>{{ formatted() }}</span>`,
})
export class PriceTagComponent {
  amount = input.required<number>();
  currency = input('USD');

  formatted = computed(() =>
    new Intl.NumberFormat('en-US', {
      style: 'currency',
      currency: this.currency(),
    }).format(this.amount()),
  );
}

Required inputs

input.required<T>() declares an input that has no default and must be supplied by the parent. Angular enforces this at compile time, so a missing binding is a build error rather than a runtime undefined.

id = input.required<string>(); // InputSignal<string>

Output:

NG8008: Required input 'id' from component UserCardComponent must be specified.

Aliases and transforms

Both options are passed via the second argument. An alias exposes a different public binding name than the field, and transform coerces the incoming value before it reaches the signal. Angular ships booleanAttribute and numberAttribute for the common cases.

import { Component, input, booleanAttribute, numberAttribute } from '@angular/core';

@Component({ selector: 'app-toggle', standalone: true, template: `` })
export class ToggleComponent {
  disabled = input(false, { transform: booleanAttribute });
  tabIndex = input(0, { transform: numberAttribute, alias: 'tabindex' });
}

With booleanAttribute, <app-toggle disabled> resolves to true exactly like a native HTML boolean attribute.

Outputs with output()

output() declares an OutputEmitterRef<T>. You emit values with .emit(), and the parent listens with the same (eventName) syntax as before. The big win is type inference: the payload type flows straight from the generic.

import { Component, output } from '@angular/core';

@Component({
  selector: 'app-search-box',
  standalone: true,
  template: `<input (input)="onInput($event)" />`,
})
export class SearchBoxComponent {
  search = output<string>();

  onInput(event: Event) {
    const value = (event.target as HTMLInputElement).value;
    this.search.emit(value);
  }
}
<app-search-box (search)="handleQuery($event)" />

Unlike @Output(), output() is not an EventEmitter and is not an RxJS subject — it does not implement Observable. If you need a stream, convert it with outputToObservable() from @angular/core/rxjs-interop.

Two-way binding with model()

model() creates a writable ModelSignal that supports the banana-in-a-box [(value)] syntax. It pairs a signal input with a matching output named <field>Change, and the component can both read and update it.

import { Component, model } from '@angular/core';

@Component({
  selector: 'app-counter',
  standalone: true,
  template: `
    <button (click)="dec()">-</button>
    <span>{{ value() }}</span>
    <button (click)="inc()">+</button>
  `,
})
export class CounterComponent {
  value = model(0); // ModelSignal<number>

  inc() { this.value.update((v) => v + 1); }
  dec() { this.value.update((v) => v - 1); }
}
<app-counter [(value)]="count" />

A change in the child flows back to the parent’s count, and a change in the parent flows down into the child — all without writing an explicit valueChange output. Use model.required<T>() when the parent must always bind.

Comparison at a glance

ConcernDecorator APISignal API
Input@Input() name: stringname = input<string>()
Required inputmanual assertion / { required: true }input.required<string>()
Read inside classplain propertycall the signal: name()
Reacting to changesngOnChangescomputed() / effect()
Output@Output() x = new EventEmitter()x = output<T>()
Two-way bindinginput + xChange outputx = model<T>()

Tip: Signal inputs are read-only inside the component. Attempting to write to one (e.g. this.name.set(...)) is a type error. Use model() when the component itself needs to mutate the value.

Warning: You cannot mix @Input() and input() for the same property, and signal inputs are not available to @ContentChild/@ViewChild queries that rely on the decorator’s static option in the same way. Migrate a property fully rather than half-way.

Best practices

  • Prefer the signal APIs for new components — they are type-safe, work with computed()/effect(), and remove the need for ngOnChanges.
  • Use input.required<T>() instead of defensive undefined checks; let the compiler guarantee the value is present.
  • Reach for transform: booleanAttribute / numberAttribute so template attributes coerce predictably, matching native HTML behavior.
  • Use model() only for genuine two-way state (form controls, toggles); for one-directional notifications, an output() is clearer.
  • Derive view state with computed() over inputs rather than copying input values into separate fields you must keep in sync.
  • Run ng generate @angular/core:signal-input-migration and the related schematics to convert existing decorators automatically.
Last updated June 14, 2026
Was this helpful?