Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

Two-way binding: model()

Sometimes both sides must change a value: the parent sets the initial filter, and the filter component changes it on click. Instead of an input-plus-output pair there is a model input for this — the model() function and two-way binding [(...)].

The filter component

ng generate component todo-filter
// src/app/todo-filter/todo-filter.ts
import { Component, model } from '@angular/core';
 
export type Filter = 'all' | 'active' | 'done';
 
@Component({
  selector: 'app-todo-filter',
  templateUrl: './todo-filter.html',
  styleUrl: './todo-filter.css',
})
export class TodoFilter {
  value = model<Filter>('all');
}

model() creates a signal that you can both read and change with set(). The change travels back to the parent.

<!-- src/app/todo-filter/todo-filter.html -->
<button [class.active]="value() === 'all'" (click)="value.set('all')">All</button>
<button [class.active]="value() === 'active'" (click)="value.set('active')">Active</button>
<button [class.active]="value() === 'done'" (click)="value.set('done')">Done</button>

Three buttons; the selected one has the active class.

Using it in the parent

Two-way binding is written with square brackets and parentheses together — the "banana in a box". On the right goes the parent's signal, without call parentheses:

// src/app/todo-list/todo-list.ts
import { Component, computed, signal } from '@angular/core';
import { TodoItem } from '../todo-item/todo-item';
import { Filter, TodoFilter } from '../todo-filter/todo-filter';
import { Todo } from '../todo';
 
@Component({
  selector: 'app-todo-list',
  imports: [TodoItem, TodoFilter],
  templateUrl: './todo-list.html',
})
export class TodoList {
  todos = signal<Todo[]>([]);
  filter = signal<Filter>('all');
 
  visible = computed(() => {
    const f = this.filter();
    return this.todos().filter((t) =>
      f === 'all' ? true : f === 'done' ? t.done : !t.done,
    );
  });
}
<!-- src/app/todo-list/todo-list.html -->
<app-todo-filter [(value)]="filter" />
 
<ul>
  @for (todo of visible(); track todo.id) {
    <li><app-todo-item [todo]="todo" /></li>
  }
</ul>

Click "Done" — the filter component calls value.set('done'), the parent's filter signal gets the new value, visible recalculates, and only completed tasks remain in the list.

What happens inside

[(value)]="filter" is shorthand for two bindings:

<!-- src/app/todo-list/todo-list.html -->
<app-todo-filter [value]="filter()" (valueChange)="filter.set($event)" />

model() automatically creates an output named valueChange. You can write it this way, but the short form is more convenient.

When to use what

SituationWhat to use
The child only displays datainput()
The child reports an actionoutput()
The child edits the parent's valuemodel()

This is how custom form controls are built: toggles, date pickers, star ratings.

Common mistake

Calling the signal in the binding: [(value)]="filter()". Then just the string 'all' goes into the component, and there is nowhere to write the change back — the build reports an error. In two-way binding pass the signal itself: [(value)]="filter".

Thanks for reading!

Donate to the site — we work hard for you.