Skip to the solution
MediumFrontend9 min read

How does two-way binding work under the hood?

427 views

The problem

Question presented to candidate: "Explain what Angular actually does with [(size)]="fontSize". What does it compile to, how does it work with both signals and plain fields, and what does a component need to support it?"

What a strong answer should cover:

  • [(x)]="v" is syntactic sugar for a property binding [x] plus an event binding (xChange) that writes $event back.
  • Any component with an input x and an output xChange supports it: @Input + @Output, or one model().
  • The compiler emits ɵɵtwoWayProperty and ɵɵtwoWayListener; the listener calls ɵɵtwoWayBindingSet(target, $event), which calls .set() on a writable signal or falls back to assignment.
  • Hence the right-hand side must be writable: a signal reference or a field, not v() (NG5002) or a computed.
  • ngModel is just a directive with input ngModel and output ngModelChange.
  • The data flow stays one-way in each direction; there is no dirty-checked shared variable as in AngularJS.

Clarifying questions expected:

  • "Do you want the template-level desugaring or the compiled instructions?"

Code / implementation expected: Yes — a custom two-way bindable component, plus the hand-written equivalent of the sugar.

  • two-way-binding

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

9 min read · 12 sections

Read the code

AngularA two-way bindable component, the desugared form, and ngModel on one signal
import { Component, EventEmitter, Input, Output, signal } from "@angular/core";
import { FormsModule } from "@angular/forms";

// A hand-made two-way bindable component: input "size" + output "sizeChange".
// That naming pair is the whole contract behind [(size)].
@Component({
  selector: "app-sizer",
  template: `
    <button (click)="resize(-1)">-</button>
    <span [style.font-size.px]="size">{{ size }}px</span>
    <button (click)="resize(+1)">+</button>
  `,
})
export class SizerComponent {
  @Input() size = 16;
  @Output() sizeChange = new EventEmitter<number>();

  resize(delta: number) {
    // The child never writes the parent value. It announces the new one.
    this.sizeChange.emit(Math.min(40, Math.max(8, this.size + delta)));
  }
}

@Component({
  selector: "app-root",
  imports: [SizerComponent, FormsModule],
  template: `
    <!-- 1. Sugar -->
    <app-sizer [(size)]="fontSize" />
    <!-- 2. What the sugar means, written by hand -->
    <app-sizer [size]="fontSize()" (sizeChange)="fontSize.set($event)" />
    <!-- 3. The same pattern on a native input, via the ngModel directive -->
    <input type="number" [(ngModel)]="fontSize" />
    <p>Parent fontSize = {{ fontSize() }}</p>
  `,
})
export class AppComponent {
  fontSize = signal(16);
}

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 60 of 121 in Angular

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

All Angular questions
Up nextMedium

How do you read route parameters?

Continue