Components
Basic Component
Section titled “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
Section titled “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}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 inputartist = 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
Section titled “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);}<!-- ✅ 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
Section titled “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 }}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 = playCountKey differences from @Output():
- ✅ No need to import
EventEmitter - ✅ Simpler syntax:
output<T>() - ✅ Type-safe by default
input() + output() Combined
Section titled “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(); }}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()
Section titled “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')}`; });}<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()
Section titled “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
Section titled “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 });}<!-- 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
Section titled “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
Section titled “Component Communication Patterns”Pattern 1: Simple Parent-Child
Section titled “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
Section titled “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
Section titled “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
Section titled “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()
Section titled “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()
Section titled “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 |

