Input vs Model vs Output in Angular

 

  • 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

Featureinput()model()@Output()
DirectionParent → ChildTwo-way: Parent ⇄ ChildChild → 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?

ScenarioUse
You want to accept data from the parent but not modify itinput()
You want the component to modify the value and reflect the change to the parentmodel()
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.

Vikash Chauhan

C# & .NET experienced Software Engineer with a demonstrated history of working in the computer software industry.

Post a Comment

Previous Post Next Post

Contact Form