input()
– for receiving values (read-only),model()
– for two-way bound values (read-write),@Output()
– for emitting events (classic Angular pattern).
Here’s a detailed comparison and explanation of input()
, model()
, and @Output()
in Angular, including how they relate to each other, their use cases, component attribute usage, and examples.
🔹 1. input()
— One-way Input (Read-Only)
Purpose:
Used to receive data from a parent component. It is a read-only signal.
Syntax:
import { input } from '@angular/core';
@Component({ ... })
export class CustomSlider {
value = input(0); // default value 0
}
Template usage:
<custom-slider [value]="50"></custom-slider>
Notes:
- Returns an
InputSignal<T>
. - Use
value()
to access the actual value. - Cannot be updated from the component itself.
Use case:
Any value passed from parent to child and not changed by the child.
🔹 2. model()
— Two-way Binding Input (Read + Write)
Purpose:
Used to receive a value from a parent and propagate changes back to the parent. It’s a writable signal.
Syntax:
import { model } from '@angular/core';
@Component({ ... })
export class CustomSlider {
value = model(0);
increment() {
this.value.update(v => v + 10); // Update value and propagate
}
}
Template usage (parent):
<custom-slider [(value)]="volume"></custom-slider>
Notes:
model()
returns a writable signal.- Angular automatically creates an implicit output named
<property>Change
(e.g.,valueChange
). - Use
.set()
or.update()
to modify the value and notify the parent. - Two-way binding uses
[(property)]="signalInstance"
syntax.
Use case:
Custom form controls, sliders, checkboxes — any input that modifies data internally and must reflect back to the parent.
🔹 3. @Output()
— Event Emission (Old-style)
Purpose:
Used to emit events from child to parent manually.
Syntax:
import { Output, EventEmitter } from '@angular/core';
@Component({ ... })
export class CustomSlider {
@Output() valueChange = new EventEmitter<number>();
increment() {
this.valueChange.emit(10);
}
}
Template usage (parent):
<custom-slider (valueChange)="onValueChanged($event)"></custom-slider>
Notes:
- This is the traditional Angular approach.
- You manage the event manually and emit values explicitly.
- Used often in legacy code and still valid.
🔄 Comparison Table
Feature | input() | model() | @Output() |
---|---|---|---|
Direction | Parent → Child | Two-way: Parent ⇄ Child | Child → Parent (event-based) |
Writable by child | ❌ No | ✅ Yes | ✅ Yes |
Signal-based | ✅ Yes | ✅ Yes | ❌ No |
Implicit output | ❌ No | ✅ Yes (<prop>Change ) | ❌ No (you define manually) |
Two-way binding | ❌ No | ✅ Yes | ❌ No (manual binding) |
Decorator usage | ❌ No | ❌ No | ✅ Yes (@Output ) |
Template binding | [value]="x" | [(value)]="x" | (valueChange)="handler($event)" |
🔧 Advanced Options
Aliasing:
value = input(0, { alias: 'sliderValue' });
<custom-slider [sliderValue]="x" />
Required Inputs:
value = input.required<number>();
// must be set in the parent template
Transforms:
import { booleanAttribute } from '@angular/core';
disabled = input(false, { transform: booleanAttribute });
✅ When to Use What?
Scenario | Use |
---|---|
You want to accept data from the parent but not modify it | input() |
You want the component to modify the value and reflect the change to the parent | model() |
You want to emit custom events for things like clicks, status changes | @Output() |
🧪 Quick Example
volume-slider.component.ts
@Component({
selector: 'volume-slider',
template: `<button (click)="increment()">+</button>`
})
export class VolumeSlider {
value = model(0); // Two-way bound
increment() {
this.value.update(v => v + 5);
}
}
media-player.component.ts
@Component({
template: `<volume-slider [(value)]="volume" />`
})
export class MediaPlayer {
volume = signal(10); // signal passed as model
}
Angular allows specifying inherited @Input()
and @Output()
bindings through the inputs
and outputs
metadata arrays in a component, even if the base class doesn't declare them in the derived class explicitly.
Here's a focused example demonstrating that:
✅ Inheriting @Input()
and Aliasing It
🔹 Base Component (with @Input()
and @Output()
)
// base-slider.ts
import { Input, Output, EventEmitter } from '@angular/core';
export class BaseSlider {
@Input() disabled: boolean = false;
@Output() valueChanged = new EventEmitter<number>();
}
🔹 Derived Component with Aliased @Input()
and @Output()
// custom-slider.component.ts
import { Component } from '@angular/core';
import { BaseSlider } from './base-slider';
@Component({
selector: 'app-custom-slider',
template: `
<input
type="range"
[disabled]="disabled"
(input)="onInput($event)"
/>
`,
// Aliasing inherited Input and Output names
inputs: ['disabled: sliderDisabled'],
outputs: ['valueChanged: onSliderChange']
})
export class CustomSliderComponent extends BaseSlider {
onInput(event: Event) {
const value = +(event.target as HTMLInputElement).value;
this.valueChanged.emit(value);
}
}
🔹 Usage in a Parent Component
<!-- parent.component.html -->
<app-custom-slider
[sliderDisabled]="true"
(onSliderChange)="handleSliderChange($event)">
</app-custom-slider>
// parent.component.ts
handleSliderChange(value: number) {
console.log('Slider changed to:', value);
}
✅ Key Takeaways
inputs: ['disabled: sliderDisabled']
maps the inherited disabled
input to the alias sliderDisabled
.outputs: ['valueChanged: onSliderChange']
maps the inherited output to the alias onSliderChange
.- This enables cleaner public APIs for your components while reusing base class logic.