Custom two-way binding uses an @Input() value and a paired @Output() valueChange event. The binding syntax [(value)] listens to valueChange and updates value. Mismatched names or missing emitters break the binding. Covers: angular, two way binding, input, output, components, forms.
Use this Angular interview question to rehearse a quick answer, common mistake, follow-up, and production pitfall.
How would you implement two-way binding for a custom component (Input/Output naming convention), and what can go wrong?Frontend interview answer
This Angular interview question tests whether you can explain you create two-way binding in a custom Angular component, connect it to production trade-offs, and handle common follow-up questions.
- you create two-way binding in a custom Angular component explanation without falling back to memorized docs wording
- Two Way Binding and Input reasoning, edge cases, and production failure modes
- How you would answer the most likely Angular interview follow-up
Core idea[(x)] works only if Angular can find:
• @Input() x
• @Output() xChange
Then this:<app-cmp [(x)]="state"></app-cmp><br>is equivalent to:<app-cmp [x]="state" (xChange)="state = $event"></app-cmp>
import { Component, EventEmitter, Input, Output, ChangeDetectionStrategy } from '@angular/core';
@Component({
selector: 'app-text-input',
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<label>
<span class="sr-only">Name</span>
<input
[value]="value"
(input)="onInput(($event.target as HTMLInputElement).value)"
/>
</label>
`
})
export class TextInputComponent {
@Input() value = '';
@Output() valueChange = new EventEmitter<string>();
onInput(next: string): void {
// Do NOT rely on mutating @Input() to update parent.
// Always emit the change.
this.valueChange.emit(next);
}
}
<!-- parent.component.html -->
<app-text-input [(value)]="name"></app-text-input>
<p>Preview: {{ name }}</p>
Aliasing still works (but names must still match)
If you alias the input name, the output alias must be that alias + Change.
import { Component, EventEmitter, Input, Output } from '@angular/core';
@Component({
selector: 'app-toggle',
template: `
<button type="button" (click)="toggle()">
{{ checked ? 'ON' : 'OFF' }}
</button>
`
})
export class ToggleComponent {
@Input('checked') checked = false;
@Output('checkedChange') checkedChange = new EventEmitter<boolean>();
toggle(): void {
this.checkedChange.emit(!this.checked);
}
}
// usage:
// <app-toggle [(checked)]="isEnabled"></app-toggle>
What can go wrong | Symptom | Fix |
|---|---|---|
Naming mismatch (e.g., @Output() valueChanged instead of valueChange) | [(value)] fails to compile / template error | Use exact convention: input <prop> + output <prop>Change (or matching aliases) |
You mutate the @Input() and forget to emit | Child UI updates, parent state does not | Treat @Input as read-only; emit through the Output for parent updates |
Emitting at the wrong time (during init hooks) | ExpressionChangedAfterItHasBeenCheckedError / flicker | Emit from user actions or schedule (e.g., queueMicrotask/setTimeout) if you must emit after init |
Two sources of truth (internal state diverges from input) | UI shows stale value or “snaps back” | Render from the @Input; if you keep local state, sync it carefully in ngOnChanges |
Using [(...)] with a non-assignable expression | Template error: can't assign to expression | Bind to a writable field: [(value)]="name" (not "getName()" / pipes / literals) |
Objects/arrays are mutated instead of replaced (especially with OnPush patterns) | Hard-to-debug stale UI / shared state side effects | Emit new references (immutable updates) or clearly own mutation boundaries |
Using custom two-way binding for form controls when you really need Angular Forms integration | ngModel / reactive forms don’t work properly (touched/dirty/validation) | Implement ControlValueAccessor for true form controls |
// Broken alias pair: Angular expands [(checked)] to [checked] + (checkedChange)
@Input('checked') checked = false;
@Output('checkedChanged') checkedChanged = new EventEmitter<boolean>(); // wrong name
// Fixed alias pair
@Output('checkedChange') checkedChange = new EventEmitter<boolean>();
// Shadow-state pattern: sync the local draft from the input
@Input() value = '';
draft = '';
ngOnChanges(): void {
this.draft = this.value;
}
commit(): void {
this.valueChange.emit(this.draft);
}
Boundary to mention in senior answers
Custom two-way binding is enough when the parent just needs [value] plus (valueChange). If you keep a local draft, sync it from the input and emit the next value instead of letting two sources of truth drift apart. It stops being enough when the component must behave like a real Angular form control with disabled state, touched/dirty tracking, validation, and formControlName/ngModel integration. That is the point where ControlValueAccessor becomes the right abstraction.
Custom two-way binding is just [prop] + (propChange). Implement @Input() prop and @Output() propChange, emit on user interaction, avoid mutating inputs as your update mechanism, and use ControlValueAccessor when the component is a real form control.
Use this as one explanation rep, then continue with the Angular interview questions cluster or a guided prep path.