Skip to content
Angular ng signals 4 min read

Model Inputs

Two-way binding used to require a matching pair: an @Input() for the value coming down and an @Output() valueChange event going back up, wired by hand for every property. The model() function (stable since Angular 17.2) collapses that ceremony into a single writable signal. A model() is simultaneously a signal input, a signal output, and a mutable piece of local state — which is exactly what the [(value)] “banana-in-a-box” syntax needs. This page covers how to declare model inputs, read and write them, mark them required, alias them, and the gotchas to watch for.

What model() actually creates

Calling model() returns a ModelSignal<T>. That single object plays three roles at once:

  • It is a writable signal, so you call value() to read and value.set(...) / value.update(...) to write.
  • It is a signal input, so a parent can bind into it with [value]="...".
  • It implicitly declares a matching output named <field>Change, so the parent can listen with (valueChange)="..." — or combine both into [(value)].

Because the output is generated automatically, you never write valueChange = output<T>() yourself.

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

@Component({
  selector: 'app-toggle',
  standalone: true,
  template: `
    <button type="button" (click)="toggle()">
      {{ checked() ? 'On' : 'Off' }}
    </button>
  `,
})
export class ToggleComponent {
  // ModelSignal<boolean> with an initial value of false
  checked = model(false);

  toggle() {
    this.checked.update((v) => !v);
  }
}

The parent binds it two-way. When the user clicks, the child updates checked, which emits checkedChange, which flows back into the parent’s darkMode signal — no event handler boilerplate required.

<app-toggle [(checked)]="darkMode" />

Reading and writing inside the component

Unlike input(), which is strictly read-only inside the component, a model() is fully writable. This is the whole point: the component owns mutations to the value, and each mutation propagates to the parent.

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

@Component({
  selector: 'app-stepper',
  standalone: true,
  template: `
    <button (click)="dec()" [disabled]="value() <= min()">-</button>
    <span class="value">{{ value() }}</span>
    <button (click)="inc()">+</button>
  `,
})
export class StepperComponent {
  value = model(0);
  min = model(0);

  inc() { this.value.update((v) => v + 1); }
  dec() { this.value.update((v) => Math.max(this.min(), v - 1)); }
}
<app-stepper [(value)]="quantity" [min]="1" />

Here value is bound two-way while min is bound one-way — a model() does not force the parent to use [(...)]. The parent can bind [min] only and simply ignore the minChange event.

Required model inputs

When a two-way binding is mandatory, use model.required<T>(). It has no initial value and the compiler forces the parent to provide a binding.

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

@Component({
  selector: 'app-rating',
  standalone: true,
  template: `
    @for (star of stars; track star) {
      <button (click)="rating.set(star)">
        {{ star <= rating() ? '★' : '☆' }}
      </button>
    }
  `,
})
export class RatingComponent {
  rating = model.required<number>();
  protected stars = [1, 2, 3, 4, 5];
}

If a consumer forgets to bind it, the build fails rather than leaving an undefined value at runtime.

Output:

NG8008: Required input 'rating' from component RatingComponent must be specified.

Aliasing a model

Pass an alias to expose a public binding name that differs from the class field. The generated change event uses the alias too, so the alias becomes <alias>Change.

checkedState = model(false, { alias: 'checked' });
// parent binds [(checked)] and listens for (checkedChange)

Tip: Unlike input(), model() does not accept a transform option. The value must round-trip unchanged so the two-way contract holds — a transform would mean the value written back differs from the value read, breaking the binding.

Subscribing to changes

A model() is a signal, so the idiomatic way to react to changes is computed() or effect() rather than subscribing to the generated output. The output exists for template binding; inside the class, treat the model as state.

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

@Component({ selector: 'app-search', standalone: true, template: `<input />` })
export class SearchComponent {
  query = model('');

  constructor() {
    effect(() => {
      console.log('query is now:', this.query());
    });
  }
}

model() vs input() vs output()

Concerninput()output()model()
Directionparent → childchild → parentboth
Writable in componentno (read-only)n/a (emit only)yes
Template syntax[x](x)[(x)], [x], or (xChange)
Returned typeInputSignal<T>OutputEmitterRef<T>ModelSignal<T>
Required variantinput.required<T>()n/amodel.required<T>()
Supports transformyesn/ano

Warning: Writing to a model() inside a computed() or during change detection can cause ExpressionChangedAfterItHasBeenChecked-style issues. Mutate models in response to user events or within effect(), not while deriving a value.

Best practices

  • Reach for model() only when the component genuinely owns and mutates a value the parent also cares about (form fields, toggles, sliders); for one-way notifications prefer output().
  • Prefer model.required<T>() over an arbitrary default when “no value” is not a meaningful state — let the compiler enforce the binding.
  • React to model changes with computed()/effect() inside the class; reserve the generated <field>Change output for template bindings.
  • Use .update() for derived mutations (v => v + 1) and .set() for absolute assignments so intent is clear.
  • Remember a model() can be bound one-way ([value]) too — don’t force consumers into [(...)] when they only read.
  • Don’t try to coerce model values with transform; if you need coercion, use a one-way input() plus a separate output().
Last updated June 14, 2026
Was this helpful?