Component Interactions: Input, Output, and Two-Way Binding
In real apps, some components work without any inputs, but many are designed for reuse — their display and behavior depend on properties passed in from a parent. Imagine a progress bar for file uploads or video playback: one reusable component, configured differently at each call site.
This article covers passing data into child components (@Input), emitting events out to parents (@Output), building custom two-way binding, and a lesser-known pattern: using an Observable directly as an @Output.
Passing data from parent to child with @Input
First, let's generate a progress-bar component:
ng g c progress-barOr explicitly:
ng generate component progress-barThe @Input decorator
To accept a progress value from a parent, declare a property and add the @Input decorator:
export class ProgressBarComponent implements OnInit {
@Input() progress = 0;
constructor() {}
ngOnInit() {}
}Angular now knows this component accepts a progress property with a default of 0. We can add more inputs for colors:
export class ProgressBarComponent implements OnInit {
@Input() backgroundColor: string;
@Input() progressColor: string;
@Input() progress = 0;
constructor() {}
ngOnInit() {}
}@Input is a property decorator — it attaches metadata to the property immediately following it. Without @Input, Angular won't bind values from parent templates; the property stays a plain class field.
Use property binding in the parent template:
<app-progress-bar
[progress]="15"
[backgroundColor]="'#9e9e9e'"
[progressColor]="'#2e8b57'"
>
</app-progress-bar>Full component example:
import { Component, OnInit, Input } from '@angular/core';
@Component({
selector: 'app-progress-bar',
template: `
<div
class="progress-bar-container"
[style.backgroundColor]="backgroundColor"
>
<div
class="progress"
[style]="{
backgroundColor: progressColor,
width: progress + '%'
}"
></div>
</div>
`,
styles: [
`
.progress-bar-container,
.progress {
height: 20px;
}
.progress-bar-container {
width: 100%;
}
`,
],
})
export class ProgressBarComponent implements OnInit {
@Input() backgroundColor: string;
@Input() progressColor: string;
@Input() progress = 0;
constructor() {}
ngOnInit() {}
}ngOnInit vs constructor
Angular components have a lifecycle. The constructor runs once when an instance is created. ngOnInit runs after the constructor and after inputs have been bound.
If a parent binds a property in its template, the child won't receive that value in its constructor — but it will in ngOnInit. Angular recommends keeping constructors minimal and doing setup in ngOnInit.
Reacting to input changes
What if you need to validate inputs (for example, when a parent passes any)? ngOnInit handles the first value, but not subsequent changes. Use ngOnChanges:
export class ProgressBarComponent implements OnInit, OnChanges {
@Input() backgroundColor: string;
@Input() progressColor: string;
@Input() progress = 0;
constructor() {}
ngOnChanges(changes: SimpleChanges) {
if ('progress' in changes) {
if (typeof changes['progress'].currentValue !== 'number') {
const progress = Number(changes['progress'].currentValue);
if (Number.isNaN(progress)) {
this.progress = 0;
} else {
this.progress = progress;
}
}
}
}
ngOnInit() {}
}Alternatively, use a getter/setter:
export class ProgressBarComponent implements OnInit {
@Input() backgroundColor: string;
@Input() progressColor: string;
private $progress = 0;
@Input()
get progress(): number {
return this.$progress;
}
set progress(value: number) {
if (typeof value !== 'number') {
const progress = Number(value);
if (Number.isNaN(progress)) {
this.$progress = 0;
} else {
this.$progress = progress;
}
} else {
this.$progress = value;
}
}
constructor() {}
ngOnInit() {}
}Emitting events from child to parent with @Output
In HTML, you listen for events like click on a button. Custom components can emit their own events using EventEmitter and @Output.
Generate two components:
ng g c author-list
ng g c author-detailAuthorListComponent holds a list of authors and passes each one to AuthorDetailComponent:
export interface Author {
id: number;
firstName: string;
lastName: string;
email: string;
gender: string;
ipAddress: string;
}import { Component, OnInit } from '@angular/core';
import { authors } from '../authors';
@Component({
selector: 'app-author-list',
template: `<app-author-detail
*ngFor="let author of authors"
[author]="author"
></app-author-detail>`,
styles: [``],
})
export class AuthorListComponent implements OnInit {
authors = authors;
constructor() {}
ngOnInit() {}
}AuthorDetailComponent receives an author via @Input:
import { Component, OnInit, Input } from '@angular/core';
import { Author } from '../authors';
@Component({
selector: 'app-author-detail',
template: `
<div *ngIf="author">
<strong>{{ author.firstName }} {{ author.lastName }}</strong>
<button (click)="handleDelete()">x</button>
</div>
`,
styles: [``],
})
export class AuthorDetailComponent implements OnInit {
@Input() author: Author;
constructor() {}
ngOnInit() {}
handleDelete() {}
}The child shouldn't delete data it doesn't own. Instead, emit an event so the parent can handle removal:
export class AuthorDetailComponent implements OnInit {
@Input() author: Author;
@Output() deleteAuthor = new EventEmitter<Author>();
constructor() {}
ngOnInit() {}
handleDelete() {
this.deleteAuthor.emit(this.author);
}
}The parent listens and filters the list:
@Component({
selector: 'app-author-list',
template: `<app-author-detail
*ngFor="let author of authors"
[author]="author"
(deleteAuthor)="handleDelete($event)"
>
</app-author-detail>`,
styles: [``],
})
export class AuthorListComponent implements OnInit {
authors = authors;
constructor() {}
ngOnInit() {}
handleDelete(author: Author) {
this.authors = this.authors.filter((item) => item.id !== author.id);
}
}Custom two-way binding
We covered two-way binding in data binding. Custom two-way binding pairs an @Input with an @Output whose name is the input name plus Change — the same pattern as ngModel and ngModelChange.
ngModel recap
ngModel comes from FormsModule. Import it in the module that declares your component:
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
import { AppComponent } from './app.component';
@NgModule({
imports: [BrowserModule, FormsModule],
declarations: [AppComponent],
bootstrap: [AppComponent],
})
export class AppModule {}Then use [(ngModel)] in the template:
app.component.html
<p>Your name: {{ name }}</p>
<input type="text" [(ngModel)]="name" />app.component.ts
@Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
})
export class AppComponent {
name = 'Tiep Phan';
}[(ngModel)] is shorthand for:
<input type="text" [ngModel]="name" (ngModelChange)="name = $event" />For custom two-way binding, create @Input() value and @Output() valueChange.
Toggle component with two-way binding
Generate a toggle component:
ng g c toggletoggle.component.ts
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
@Component({
selector: 'app-toggle',
templateUrl: './toggle.component.html',
styleUrls: ['./toggle.component.css'],
})
export class ToggleComponent implements OnInit {
@Input() checked = false;
@Output() checkedChange = new EventEmitter<boolean>();
constructor() {}
ngOnInit() {}
toggle() {
this.checked = !this.checked;
this.checkedChange.emit(this.checked);
}
}toggle.component.html
<div
class="toggle-wrapper"
[class.checked]="checked"
tabindex="0"
(click)="toggle()"
>
<div class="toggle"></div>
</div>Use it like ngModel:
<app-toggle [(checked)]="checked"></app-toggle>
Observable as @Output
Here's a pattern many Angular developers miss because it's not prominently documented: you can assign an Observable directly to an @Output instead of manually subscribing and re-emitting through EventEmitter.
Before
@Output() someEvent = new EventEmitter();
this.someSource$.pipe(
// additional logic/transformation
).subscribe(data => this.someEvent.emit(data));After
@Output() someEvent = this.someSource$.pipe(
/* additional logic transformation */
);The use case: emit values to a parent when someSource$ produces data. Why does the "After" version work?
Two concepts explain it:
Event binding syntax ()
Event binding uses parentheses and an event name: <child (someEvent)="invokeSomeEvent()"></child>. The Angular compiler parses templates into an AST and knows what type of binding is in play. In the output interpreter source, if the bound event is a SubscribeObservable, the compiler subscribes to that source automatically.

So event binding () automatically subscribes when the output is an Observable or Subject.
EventEmitter class
EventEmitter is a subclass of Subject. Since Subject is both an Observable and an Observer, @Output works through the compiler's auto-subscribe behavior described above.
Use cases
- Interval output
- State selectors (NgRx)
- Any stream you can think of
Summary
We've covered @Input for parent-to-child data, @Output and EventEmitter for child-to-parent events, custom two-way binding with the property + propertyChange convention, and using an Observable directly as an output.
Further reading:
- https://angular.io/guide/component-interaction
- https://angular.io/guide/lifecycle-hooks
- https://angular.io/api/forms/NgModel
- Passing data to components with Input (Vietnamese)
- Component events with EventEmitter and Output (Vietnamese)
- Custom two-way data binding (Vietnamese)


