Chapter 10.4โ˜• 15 min read

Model Signals โ€” model()

model() is a writable signal that supports two-way binding with parent components. It combines @Input, @Output, and signal() into a single declarative API.

01What is model()

model() is a special type of signal introduced in Angular 17.3+ that supports two-way binding. Think of it as signal() + @Input() + @Output() combined into one line.

"model = two-way signal โ€” template se change karo ya TypeScript se, dono sync."

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

@Component({
  selector: 'app-biryani-card',
  template: `
    <h3>{{ name() }}</h3>
    <p>โ‚น{{ price() }}</p>
    <input [ngModel]="name()" (ngModelChange)="name.set($event)">
  `
})
export class BiryaniCardComponent {
  // model() = signal + @Input + @Output in one line!
  name = model('Biryani');
  price = model(0);
}

How model() solves the old two-way problem:

  • Old way: @Input() name + @Output() nameChange + EventEmitter = 5+ lines
  • New way: name = model('') = 1 line
  • Parent still uses: [(name)]="myName" โ€” same syntax!
  • Inside component, model() works exactly like signal() โ€” read with (), write with .set()/.update()
02model() in a Component

Using model() in a child component is straightforward:

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

@Component({
  selector: 'app-biryani-card',
  standalone: true,
  template: `
    <div class="card" [class.selected]="selected()">
      <h3>{{ name() }}</h3>
      <span class="price">โ‚น{{ price() }}</span>
      <span class="rating">โญ {{ rating() }}</span>
      <button (click)="toggleSelect()">
        {{ selected() ? 'Deselect' : 'Select' }}
      </button>
    </div>
  `
})
export class BiryaniCardComponent {
  // model() signals โ€” support two-way binding
  name = model.required<string>();
  price = model(0);
  rating = model(4.5);
  selected = model(false);
}

// Parent component uses:
// <app-biryani-card [(name)]="selectedBiryani">

"model() = signal + @Input + @Output combined into one line โ€” 3-in-1."

How it works: model() creates a writable signal AND automatically creates a corresponding @Output() nameChange event emitter. When you call this.name.set('new'), it updates the signal AND emits nameChange so the parent knows to update its binding.

03Using model() in Template

Inside the component's own template, model() can be used with either one-way or two-way binding:

<!-- Reading model() value -->
<h3>{{ name() }}</h3>  

<!-- One-way binding: input displays model, but changes don't update model -->
<input [value]="name()">

<!-- Two-way with plain input: need to handle change manually -->
<input [value]="name()" (input)="name.set($any($event.target).value)">

<!-- Two-way with Angular's [model] directive (shorthand) -->
<input [model]="name">

<!-- With ngModel โ€” still works -->
<input [(ngModel)]="name">

Angular has a special [model] directive that works directly with model() signals โ€” no need for (ngModelChange) or manual event handling.

"[model] directive = model signal ke saath direct two-way โ€” no ngModel needed."

With controls:

<!-- Checkbox -->
<input type="checkbox" [model]="isAvailable">

<!-- Select -->
<select [model]="category">
  <option value="Veg">Veg</option>
  <option value="Non-Veg">Non-Veg</option>
</select>
04model() vs signal() vs @Input/@Output

Here's the comparison between all approaches for child-parent communication:

signal()model()@Input + @Output
Lines of code115-7
Two-way bindingโŒ Noโœ… Yesโœ… Yes (manual)
Writable inside componentโœ… Yesโœ… YesโŒ Input is read-only
Signal ecosystemโœ… Yesโœ… YesโŒ No
Parent usageN/A[(name)]="x"[(name)]="x"
Auto-change detectionโœ… Yesโœ… Yesโš ๏ธ Only with OnPush
// โŒ WRONG: signal() for two-way โ€” parent can't bind
name = signal('Biryani');
// <app-biryani [(name)]="x"> โ€” DOESN'T WORK!

// โœ… RIGHT: model() for two-way
name = model('Biryani');
// <app-biryani [(name)]="x"> โ€” WORKS!

// โŒ OLD: @Input + @Output (boilerplate)
@Input() name = '';
@Output() nameChange = new EventEmitter<string>();
updateName(val: string) { this.name = val; this.nameChange.emit(val); }

"model() = 3-in-1 โ€” signal + input + output. signal() = internal state only."

05Configuring model() with Options

model() supports configuration options for flexibility:

required โ€” must be provided by parent:

// Parent MUST provide this binding โ€” TypeScript error if not
name = model.required<string>();
// <app-biryani [(name)]="someValue"> โ€” REQUIRED

alias โ€” rename for parent usage:

// Component property is 'name', parent uses 'biryaniName'
name = model('', { alias: 'biryaniName' });
// Parent: <app-biryani [(biryaniName)]="myName">

initializer โ€” lazy initialization:

name = model('', { initializer: () => getDefaultName() });
// Uses function result as initial value, not called unnecessarily

Combined example:

export class BiryaniCardComponent {
  // Required model with alias
  biryaniItem = model.required<Biryani>({ alias: 'item' });

  // Required simple value
  name = model.required<string>();

  // Optional with custom initializer
  rating = model(4.5, { initializer: () => loadDefaultRating() });

  // Simple optional
  price = model(0);
}

// Parent usage:
// <app-biryani-card [(item)]="biryaniData" [(name)]="biryaniName">

"Options = customization โ€” alias do, required banao, default value do. model() handles everything."

Key Takeaways

  • โœ… model() = signal + @Input + @Output combined โ€” 3-in-1
  • โœ… Two-way binding: parent uses [(name)]="value" syntax
  • โœ… Writable inside component โ€” .set() and .update() work like signal()
  • โœ… [model] directive โ€” works with model() for template two-way binding
  • โœ… Options: required, alias, initializer
  • โœ… model() replaces @Input + @Output + EventEmitter boilerplate
Course Search
Search across all chapters & stages
๐Ÿ“–

Search the course

Type any topic โ€” branching, stash, rebase, hooks โ€” and jump straight to that chapter.

merge branchesgit stashundo commitrebase