Back to Angular

Two-way binding with model signals

adev/src/content/tutorials/signals/steps/6-two-way-binding-with-model-signals/README.md

22.0.0-next.104.4 KB
Original Source

Two-way binding with model signals

Now that you've learned passing data to components with input signals, let's explore Angular's model() API for two-way binding. Model signals are perfect for UI components like checkboxes, sliders, or custom form controls where the component needs to both receive a value AND update it.

In this activity, you'll create a custom checkbox component that manages its own state while keeping the parent synchronized.

<hr /> <docs-workflow> <docs-step title="Set up the custom checkbox with model signal"> Create a model signal in the `custom-checkbox` component that can both receive and update the parent's value.
ts
// Add imports for model signals
import {Component, model, input, ChangeDetectionStrategy} from '@angular/core';

// Model signal for two-way binding
checked = model.required<boolean>();

// Optional input for label
label = input<string>('');

Unlike input() signals which are read-only, model() signals can be both read and written to. </docs-step>

<docs-step title="Create the checkbox template"> Build the checkbox template that responds to clicks and updates its own model.
html
<label class="custom-checkbox">
  <input type="checkbox" [checked]="checked()" (change)="toggle()" />
  <span class="checkmark"></span>
  {{ label() }}
</label>

The component reads from its model signal and has a method to update it. </docs-step>

<docs-step title="Add the toggle method"> Implement the toggle method that updates the model signal when the checkbox is clicked.
ts
toggle() {
  // This updates BOTH the component's state AND the parent's model!
  this.checked.set(!this.checked());
}

When the child component calls this.checked.set(), it automatically propagates the change back to the parent. This is the key difference from input() signals. </docs-step>

<docs-step title="Set up two-way binding in the parent"> First, uncomment the model signal properties and methods in `app.ts`:
ts
// Parent signal models
agreedToTerms = model(false);
enableNotifications = model(true);

// Methods to test two-way binding
toggleTermsFromParent() {
  this.agreedToTerms.set(!this.agreedToTerms());
}

resetAll() {
  this.agreedToTerms.set(false);
  this.enableNotifications.set(false);
}

Then update the template:

Part 1. Uncomment the checkboxes and add two-way binding:

  • Replace ___ADD_TWO_WAY_BINDING___ with [(checked)]="agreedToTerms" for the first checkbox
  • Replace ___ADD_TWO_WAY_BINDING___ with [(checked)]="enableNotifications" for the second

Part 2. Replace the ??? placeholders with @if blocks:

angular-html
@if (agreedToTerms()) {
  Yes
} @else {
  No
}

@if (enableNotifications()) {
  Yes
} @else {
  No
}

Part 3. Add click handlers to the buttons:

html
<button (click)="toggleTermsFromParent()">Toggle Terms from Parent</button>
<button (click)="resetAll()">Reset All</button>

The [(checked)] syntax creates two-way binding - data flows down to the component AND changes flow back up to the parent by emitting an event that references the signal itself and does not call the signal getter directly. </docs-step>

<docs-step title="Test the two-way binding"> Interact with your app to see two-way binding in action:
  1. Click checkboxes - Component updates its own state and notifies parent
  2. Click "Toggle Terms from Parent" - Parent updates propagate down to component
  3. Click "Reset All" - Parent resets both models and components update automatically

Both the parent and child can update the shared state, and both stay in sync automatically! </docs-step>

</docs-workflow>

Perfect! You've learned how model signals enable two-way binding:

  • Model signals - Use model() and model.required() for values that can be both read and written
  • Two-way binding - Use [(property)] syntax to bind parent signals to child models
  • Perfect for UI components - Checkboxes, form controls, and widgets that need to manage their own state
  • Automatic synchronization - Parent and child stay in sync without manual event handling

When to use model() vs input():

  • Use input() for data that only flows down (display data, configuration)
  • Use model() for UI components that need to update their own value (form controls, toggles)

In the next lesson, you'll learn about using signals with services!