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.
How would you implement two-way binding for a custom component (Input/Output naming convention), and what can go wrong?
Use guided tracks for structured prep, then practice company-specific question sets when you want targeted interview coverage.
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 |
Practical notes
Watch for edge case behavior, common pitfalls, and trade-offs between clarity and performance. Mention accessibility and testing considerations when the concept affects UI output or event timing.
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.