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
| Situation | What to use |
|---|---|
| The child only displays data | input() |
| The child reports an action | output() |
| The child edits the parent's value | model() |
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".