Skip to content

Using Model Signals for Two-Way Binding in Angular

Problem Statement

Angular developers transitioning to signals often encounter a common scenario: attempting to modify an input signal (input()) from within a component. Many expect syntax like:

typescript
this.selected.set(true);  // Doesn't work!
this.selected().set(true); // Also doesn't work!

These attempts fail, leaving developers confused about how to manage component-internal updates that should propagate to parent components. This occurs due to a fundamental characteristic of Angular's signal-based inputs.

Why Input Signals Are Immutable

Angular's input() creates a read-only signal designed only to receive values from parent components. Its value cannot be modified from the child component, as this would violate the unidirectional data flow principle.

Solution Overview

For two-way data binding where a child component needs to both:

  1. Receive initial values from a parent
  2. Update that value and notify the parent

Use Angular's model() signal instead of input(). Model signals provide readable and writable signals that automatically synchronize with parent components using two-way binding syntax.

How Model Signals Work

Defining a Model Signal in a Child Component

typescript
import { model } from '@angular/core';

export class MyComponent {
  // Define a model signal with default value
  selected = model(false);
  
  toggleSelection() {
    // Update the model signal's value
    this.selected.update(current => !current);
  }
}

Binding in Parent Template

html
<app-child [(selected)]="isChildSelected" />

In the parent component:

typescript
isChildSelected = signal(true);

Key Characteristics

  • model() signals are writable (unlike input() signals)
  • Changes propagate to both parent and child components
  • Uses familiar banana-in-the-box [( )] syntax

Why Model Signals (Not Input Signals)

Featureinput()model()
Writable from child
Parent bindingOne-way ([prop])Two-way ([(prop)])
Initial valueRequired/defaultableRequired/defaultable
Value propagationParent → ChildParent ↔ Child

Full Example Implementation

Child Component (counter.component.ts):

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

@Component({
  selector: 'app-counter',
  template: `
    <button (click)="increment()">Count: {{ value() }}</button>
  `,
  standalone: true
})
export class CounterComponent {
  value = model(0);  // Writable model signal
  
  increment() {
    this.value.update(current => current + 1);
  }
}

Parent Component (app.component.ts):

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

@Component({
  selector: 'app-root',
  template: `
    <p>Current count: {{ count() }}</p>
    <app-counter [(value)]="count" />
  `
})
export class AppComponent {
  count = signal(5);  // Initialized with 5
}

Common Use Cases

  1. Form controls (toggles, inputs, selects)
  2. Interactive widgets (sliders, rating controls)
  3. Expand/collapse components
  4. Any scenario needing child-initiated value updates

When to Use Input Signals Instead

Stick with input() when:

  • The value should only be set by the parent
  • You need a read-only value inside the component
  • You're using transform functions for input processing
  • You require input validation using input.required()

Additional Consideration

If you cannot use model() (Angular versions < 17.2), implement a two-way binding manually:

typescript
// Child Component
@Input() selected = false;
@Output() selectedChange = new EventEmitter<boolean>();

updateSelection(newValue: boolean) {
  this.selected = newValue;
  this.selectedChange.emit(newValue);
}

Best Practice

For modern Angular projects (v17.2+), prefer model() over manual input/output pairs for cleaner implementation of two-way binding scenarios.

The model() API provides an elegant solution for child-to-parent value synchronization using Angular's signals paradigm. By understanding the distinction between input() (read-only) and model() (writable) signals, you can architect cleaner component communication patterns.