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
- Child Component
- Parent Component
- How It Works
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
}
import { Component } from '@angular/core';
import { AlbumCardComponent } from './album-card.component';
@Component({
selector: 'app-parent',
standalone: true,
imports: [AlbumCardComponent],
template: `
<app-album-card
[name]="albumName"
[artist]="albumArtist"
[price]="albumPrice">
</app-album-card>
`
})
export class ParentComponent {
albumName = 'Dark Side of the Moon';
albumArtist = 'Pink Floyd';
albumPrice = 29.99;
}
Step 1: Child declares inputs
name = input<string>(''); // Signal-based input
artist = input<string>('');
price = input<number>(0);
Step 2: Parent passes data
<app-album-card [name]="albumName" [artist]="albumArtist" [price]="albumPrice"></app-album-card>
Step 3: Child reads inputs as signals
<h2>{{ name() }}</h2> <!-- Call as function -->
<p>Artist: {{ artist() }}</p>
<p>{{ price() | currency }}</p>
Key difference from @Input():
- ✅ inputs are signals - call with
() - ✅ Reactive by default
- ✅ Can use in computed(), effect()
input() with Required
- Required Inputs
- Usage
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);
}
<!-- ✅ Valid - title is provided -->
<app-album
[title]="'Abbey Road'"
[artist]="'The Beatles'"
[featured]="true">
</app-album>
<!-- ❌ Compile error - title is required -->
<app-album [artist]="'The Beatles'"></app-album>
Benefits:
- Compile-time checking for required inputs
- No need for
!(non-null assertion) - Type-safe
output() - Child to Parent
- Child Component
- Parent Component
- Data Flow
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
}
}
import { Component } from '@angular/core';
import { TrackPlayerComponent } from './track-player.component';
@Component({
selector: 'app-parent',
standalone: true,
imports: [TrackPlayerComponent],
template: `
<app-track-player (played)="onTrackPlayed($event)"></app-track-player>
<p>Total plays: {{ totalPlays }}</p>
`
})
export class ParentComponent {
totalPlays = 0;
onTrackPlayed(playCount: number) {
this.totalPlays = playCount;
console.log('Track played, total:', playCount);
}
}
1. Click play button in Child
↓
2. Child calls play()
↓
3. Child emits: played.emit(this.playCount)
↓
4. Parent receives: (played)="onTrackPlayed($event)"
↓
5. Parent's onTrackPlayed() is called with play count
↓
6. Parent updates: this.totalPlays = playCount
Key differences from @Output():
- ✅ No need to import
EventEmitter - ✅ Simpler syntax:
output<T>() - ✅ Type-safe by default
input() + output() Combined
- Child Component
- Parent Component
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();
}
}
import { Component, signal } from '@angular/core';
import { AlbumEditorComponent } from './album-editor.component';
@Component({
selector: 'app-parent',
standalone: true,
imports: [AlbumEditorComponent],
template: `
<app-album-editor
[album]="currentAlbum()"
(save)="handleSave($event)"
(cancel)="handleCancel()">
</app-album-editor>
`
})
export class ParentComponent {
currentAlbum = signal<Album>({
id: 1,
name: 'Dark Side of the Moon',
artist: 'Pink Floyd'
});
handleSave(updatedAlbum: Album) {
this.currentAlbum.set(updatedAlbum);
console.log('Album saved:', updatedAlbum);
}
handleCancel() {
console.log('Edit cancelled');
}
}
Using Inputs in computed()
- Component
- Usage
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')}`;
});
}
<app-album-duration
[trackCount]="12"
[avgDuration]="240">
</app-album-duration>
<!-- Output:
🎵 Tracks: 12
⏱️ Avg Duration: 240s
⏰ Total: 2880s (48:00)
-->
Benefits:
totalDuration()andformattedDuration()automatically update when inputs change- No need for ngOnChanges lifecycle hook
- Declarative and reactive
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
- With Transform
- Usage
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
});
}
<!-- Passing values -->
<app-album-formatter
formattedArtist="pink floyd"
releaseYear="1973"
isFeatured="true">
</app-album-formatter>
<!-- Transforms applied:
Artist: PINK FLOYD (uppercase)
Year: 1973 (parsed to number)
Featured: true (parsed to boolean)
-->
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) |
|---|---|---|
| Type | Property | Signal |
| Access | this.name | this.name() |
| Reactivity | Manual (ngOnChanges) | Automatic |
| Required | @Input({ required: true }) | input.required<T>() |
| Default | @Input() x = 5 | input<number>(5) |
| Transform | @Input({ transform }) | input(0, { transform }) |
| Works with | Change detection | Signals, computed(), effect() |
output() vs @Output()
| Feature | @Output() (Old) | output() (Modern) |
|---|---|---|
| Import | EventEmitter | None needed |
| Declaration | new EventEmitter<T>() | output<T>() |
| Emit | .emit(value) | .emit(value) |
| Type safety | Manual | Automatic |