Skip to main content

Components

Basic Component

import { Component } from '@angular/core';

@Component({
selector: 'app-album',
standalone: true,
template: `<h1>{{ title }}</h1>`,
styles: [`h1 { color: blue; }`]
})
export class AlbumComponent {
title = 'Now Playing';
}

Usage:

<app-album></app-album>

input() - Parent to Child

import { Component, input } from '@angular/core';

@Component({
selector: 'app-album-card',
standalone: true,
template: `
<div class="card">
<h2>{{ name() }}</h2>
<p>Artist: {{ artist() }}</p>
<p class="price">{{ price() | currency }}</p>
</div>
`
})
export class AlbumCardComponent {
// Signal-based inputs (Angular 17+)
name = input<string>(''); // Optional with default
artist = input<string>(''); // Optional with default
price = input<number>(0); // Optional with default
}

input() with Required

import { Component, input } from '@angular/core';

@Component({
selector: 'app-album',
standalone: true,
template: `
<h3>{{ name() }}</h3>
<p>Artist: {{ artist() }}</p>
<p *ngIf="featured()">⭐ Featured Album!</p>
`
})
export class AlbumComponent {
// Required input - no default value
name = input.required<string>();

// Optional inputs with defaults
artist = input<string>('Unknown Artist');
featured = input<boolean>(false);
}

output() - Child to Parent

import { Component, output } from '@angular/core';

@Component({
selector: 'app-track-player',
standalone: true,
template: `
<button (click)="play()">▶️ Play Count: {{ playCount }}</button>
`
})
export class TrackPlayerComponent {
playCount = 0;

// Signal-based output (Angular 17.1+)
played = output<number>();

play() {
this.playCount++;
this.played.emit(this.playCount); // Emit to parent
}
}

input() + output() Combined

import { Component, input, output } from '@angular/core';

interface Album {
id: number;
name: string;
artist: string;
description: string;
price: number;
tags: string[];
highlighted?: boolean;
}

@Component({
selector: 'app-album-editor',
standalone: true,
template: `
<input
[value]="album().name"
(input)="onNameChange($event)"
placeholder="Album name">
<input
[value]="album().artist"
(input)="onArtistChange($event)"
placeholder="Artist name">
<input
type="number"
[value]="album().price"
(input)="onPriceChange($event)"
placeholder="Price">
<button (click)="onSave()">💾 Save</button>
<button (click)="onCancel()">❌ Cancel</button>
`
})
export class AlbumEditorComponent {
// Signal-based input (required)
album = input.required<Album>();

// Signal-based outputs
save = output<Album>();
cancel = output<void>();

onNameChange(event: Event) {
const input = event.target as HTMLInputElement;
const updated = { ...this.album(), name: input.value };
this.save.emit(updated);
}

onArtistChange(event: Event) {
const input = event.target as HTMLInputElement;
const updated = { ...this.album(), artist: input.value };
this.save.emit(updated);
}

onPriceChange(event: Event) {
const input = event.target as HTMLInputElement;
const updated = { ...this.album(), price: parseFloat(input.value) };
this.save.emit(updated);
}

onSave() {
this.save.emit(this.album());
}

onCancel() {
this.cancel.emit();
}
}

Using Inputs in computed()

import { Component, input, computed } from '@angular/core';

@Component({
selector: 'app-album-duration',
standalone: true,
template: `
<div>
<p>🎵 Tracks: {{ trackCount() }}</p>
<p>⏱️ Avg Duration: {{ avgDuration() }}s</p>
<p>⏰ Total: {{ totalDuration() }}s ({{ formattedDuration() }})</p>
</div>
`
})
export class AlbumDurationComponent {
trackCount = input<number>(0);
avgDuration = input<number>(0);

// Computed signal derived from inputs
totalDuration = computed(() => {
return this.trackCount() * this.avgDuration();
});

formattedDuration = computed(() => {
const total = this.totalDuration();
const mins = Math.floor(total / 60);
const secs = total % 60;
return `${mins}:${secs.toString().padStart(2, '0')}`;
});
}

Using Inputs in effect()

import { Component, input, effect } from '@angular/core';

@Component({
selector: 'app-track-analytics',
standalone: true,
template: `<p>Now Playing: Track #{{ trackId() }}</p>`
})
export class TrackAnalyticsComponent {
trackId = input<number>(0);

constructor() {
// Effect runs when trackId() changes
effect(() => {
console.log('Track changed to:', this.trackId());
// Side effect: log to analytics, update play history, etc.
});
}
}

Input Transforms

import { Component, input } from '@angular/core';

@Component({
selector: 'app-album-formatter',
standalone: true,
template: `
<p>Artist: {{ formattedArtist() }}</p>
<p>Year: {{ releaseYear() }}</p>
<p>Featured: {{ isFeatured() }}</p>
`
})
export class AlbumFormatterComponent {
// Transform string to uppercase
formattedArtist = input('', {
transform: (value: string) => value.toUpperCase()
});

// Transform string to number
releaseYear = input(0, {
transform: (value: string | number) =>
typeof value === 'string' ? parseInt(value, 10) : value
});

// Transform string to boolean
isFeatured = input(false, {
transform: (value: string | boolean) =>
value === 'true' || value === true
});
}

Input Aliases

import { Component, input } from '@angular/core';

@Component({
selector: 'app-aliased',
standalone: true,
template: `<p>{{ artistName() }}</p>`
})
export class AliasedComponent {
// Property name: artistName
// Template binding: [artist-name]
artistName = input<string>('', {
alias: 'artist-name'
});
}

Usage:

<app-aliased [artist-name]="'Pink Floyd'"></app-aliased>

Component Communication Patterns

Pattern 1: Simple Parent-Child

// Child
@Component({
selector: 'app-track-display',
template: `<p>🎵 {{ trackTitle() }}</p>`
})
export class TrackDisplayComponent {
trackTitle = input<string>('');
}

// Parent
@Component({
template: `<app-track-display [trackTitle]="'Bohemian Rhapsody'"></app-track-display>`
})
export class ParentComponent {}

Pattern 2: Child Notifies Parent

// Child
@Component({
selector: 'app-play-button',
template: `<button (click)="play()">▶️ Play</button>`
})
export class PlayButtonComponent {
trackPlayed = output<void>();

play() {
this.trackPlayed.emit();
}
}

// Parent
@Component({
template: `<app-play-button (trackPlayed)="onTrackPlayed()"></app-play-button>`
})
export class ParentComponent {
onTrackPlayed() {
console.log('Track is now playing!');
}
}

Pattern 3: Two-way Data Flow

// Child
@Component({
selector: 'app-track-editor',
template: `
<input
[value]="title()"
(input)="titleChange.emit($any($event.target).value)"
placeholder="Track title">
`
})
export class TrackEditorComponent {
title = input<string>('');
titleChange = output<string>();
}

// Parent
@Component({
template: `<app-track-editor [(title)]="trackTitle"></app-track-editor>`
})
export class ParentComponent {
trackTitle = signal('Stairway to Heaven');
}

Best Practices

  • ✅ Use input() instead of @Input() - Signal-based, more reactive
  • ✅ Use output() instead of @Output() - Simpler, no EventEmitter import
  • ✅ Use input.required() for required inputs - Compile-time safety
  • ✅ Access inputs as functions: this.name() - They are signals
  • ✅ Use computed() for derived values - Automatic reactivity
  • ✅ Use effect() for side effects - Reacts to input changes
  • ✅ Use transforms for type conversion - Clean separation of concerns
  • ❌ Don't mutate input() values directly - Inputs are read-only
  • ❌ Don't use ngOnChanges with signal inputs - Use computed() or effect()

input() vs @Input()

Feature@Input() (Old)input() (Modern)
TypePropertySignal
Accessthis.namethis.name()
ReactivityManual (ngOnChanges)Automatic
Required@Input({ required: true })input.required<T>()
Default@Input() x = 5input<number>(5)
Transform@Input({ transform })input(0, { transform })
Works withChange detectionSignals, computed(), effect()

output() vs @Output()

Feature@Output() (Old)output() (Modern)
ImportEventEmitterNone needed
Declarationnew EventEmitter<T>()output<T>()
Emit.emit(value).emit(value)
Type safetyManualAutomatic