Componenti
Cosa sono i Componenti
I componenti sono i mattoni fondamentali di ogni applicazione Angular. Ogni componente è una classe TypeScript decorata con @Component che controlla una porzione della UI. Un componente è composto da tre parti principali: la classe (logica), il template (vista) e gli stili (aspetto).
import { Component } from '@angular/core';
@Component({
selector: 'app-saluto',
standalone: true,
template: `<h1>Ciao, {{ nome }}!</h1>`,
styles: [`h1 { color: #3b82f6; }`]
})
export class SalutoComponent {
nome = 'Mondo';
}
Il Decoratore @Component
Il decoratore @Component accetta un oggetto di metadati che definisce il comportamento del componente.
Proprietà principali
@Component({
// Il selettore CSS usato per inserire il componente nel template
selector: 'app-profilo',
// Indica che il componente è standalone (non necessita di NgModule)
standalone: true,
// Importazioni di altri componenti, direttive e pipe
imports: [CommonModule, RouterLink],
// Template inline o riferimento a file esterno
template: `<div>Template inline</div>`,
// oppure
templateUrl: './profilo.component.html',
// Stili inline o riferimento a file esterno
styles: [`div { padding: 1rem; }`],
// oppure
styleUrl: './profilo.component.scss',
// Strategia di change detection
changeDetection: ChangeDetectionStrategy.OnPush,
// Incapsulamento degli stili
encapsulation: ViewEncapsulation.Emulated
})
export class ProfiloComponent {}
View Encapsulation
Angular supporta tre modalità di incapsulamento degli stili:
import { Component, ViewEncapsulation } from '@angular/core';
// Emulated (default): emula lo Shadow DOM con attributi
@Component({
encapsulation: ViewEncapsulation.Emulated,
styles: [`p { color: red; }`] // Applicato solo a questo componente
})
// ShadowDom: usa il vero Shadow DOM del browser
@Component({
encapsulation: ViewEncapsulation.ShadowDom,
styles: [`p { color: blue; }`]
})
// None: stili globali, nessun incapsulamento
@Component({
encapsulation: ViewEncapsulation.None,
styles: [`p { color: green; }`] // Applicato globalmente
})
Standalone Components
A partire da Angular 14 (e default da Angular 17), i componenti standalone non necessitano di essere dichiarati in un NgModule. Sono autonomi e dichiarano esplicitamente le proprie dipendenze.
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterLink } from '@angular/router';
import { CardComponent } from '../card/card.component';
@Component({
selector: 'app-dashboard',
standalone: true,
imports: [CommonModule, RouterLink, CardComponent],
template: `
<h2>Dashboard</h2>
@for (item of items; track item.id) {
<app-card [titolo]="item.nome" />
}
<a routerLink="/impostazioni">Impostazioni</a>
`
})
export class DashboardComponent {
items = [
{ id: 1, nome: 'Vendite' },
{ id: 2, nome: 'Utenti' },
{ id: 3, nome: 'Ordini' }
];
}
Input e Output
I decoratori @Input e @Output permettono la comunicazione tra componenti padre e figlio.
@Input - Passare dati al figlio
import { Component, Input } from '@angular/core';
@Component({
selector: 'app-card-prodotto',
standalone: true,
template: `
<div class="card">
<h3>{{ nome }}</h3>
<p class="prezzo">{{ prezzo | currency:'EUR' }}</p>
@if (inOfferta) {
<span class="badge">In Offerta!</span>
}
</div>
`
})
export class CardProdottoComponent {
@Input() nome = '';
@Input() prezzo = 0;
@Input() inOfferta = false;
// Input obbligatorio (Angular 16+)
@Input({ required: true }) id!: number;
// Input con alias
@Input({ alias: 'descrizione-prodotto' }) descrizione = '';
// Input con trasformazione (Angular 16+)
@Input({ transform: booleanAttribute }) attivo = false;
}
Utilizzo nel componente padre:
<app-card-prodotto
[id]="1"
[nome]="'Laptop'"
[prezzo]="999.99"
[inOfferta]="true"
/>
@Output - Emettere eventi al padre
import { Component, Output, EventEmitter } from '@angular/core';
@Component({
selector: 'app-contatore',
standalone: true,
template: `
<div class="contatore">
<button (click)="decrementa()">-</button>
<span>{{ valore }}</span>
<button (click)="incrementa()">+</button>
</div>
`
})
export class ContatoreComponent {
valore = 0;
@Output() valoreModificato = new EventEmitter<number>();
@Output() limiteRaggiunto = new EventEmitter<string>();
incrementa(): void {
this.valore++;
this.valoreModificato.emit(this.valore);
if (this.valore >= 10) {
this.limiteRaggiunto.emit('Limite massimo raggiunto');
}
}
decrementa(): void {
if (this.valore > 0) {
this.valore--;
this.valoreModificato.emit(this.valore);
}
}
}
Utilizzo nel padre:
<app-contatore
(valoreModificato)="onValoreModificato($event)"
(limiteRaggiunto)="mostraAvviso($event)"
/>
Content Projection con ng-content
La content projection permette di inserire contenuto dal componente padre nel template del figlio.
// Componente Card riutilizzabile
@Component({
selector: 'app-card',
standalone: true,
template: `
<div class="card">
<div class="card-header">
<ng-content select="[header]" />
</div>
<div class="card-body">
<ng-content />
</div>
<div class="card-footer">
<ng-content select="[footer]" />
</div>
</div>
`
})
export class CardComponent {}
<!-- Utilizzo con content projection -->
<app-card>
<h3 header>Titolo della Card</h3>
<p>Questo contenuto va nel body della card.</p>
<p>Anche questo paragrafo va nel body.</p>
<button footer>Azione</button>
</app-card>
Lifecycle Hooks
Angular offre diversi hook del ciclo di vita che permettono di eseguire logica in momenti specifici della vita di un componente.
import {
Component, OnInit, OnDestroy, OnChanges,
AfterViewInit, Input, SimpleChanges
} from '@angular/core';
import { Subscription } from 'rxjs';
@Component({
selector: 'app-utente',
standalone: true,
template: `<p>Utente: {{ nome }}</p>`
})
export class UtenteComponent implements OnInit, OnChanges, AfterViewInit, OnDestroy {
@Input() nome = '';
private sottoscrizione?: Subscription;
// Chiamato quando un @Input cambia valore
ngOnChanges(changes: SimpleChanges): void {
if (changes['nome']) {
console.log(
'Nome cambiato da',
changes['nome'].previousValue,
'a',
changes['nome'].currentValue
);
}
}
// Chiamato una volta dopo la creazione del componente
ngOnInit(): void {
console.log('Componente inizializzato');
// Ideale per: fetch dati, sottoscrizioni iniziali
}
// Chiamato dopo che la vista del componente è stata inizializzata
ngAfterViewInit(): void {
console.log('Vista inizializzata');
// Ideale per: accedere a elementi del DOM, inizializzare librerie esterne
}
// Chiamato quando il componente viene distrutto
ngOnDestroy(): void {
console.log('Componente distrutto');
// Fondamentale: cancellare sottoscrizioni, timer, listener
this.sottoscrizione?.unsubscribe();
}
}
Ordine dei Lifecycle Hooks
| Hook | Quando viene chiamato |
|---|---|
ngOnChanges |
Ad ogni cambio di un @Input (anche prima di ngOnInit) |
ngOnInit |
Una volta, dopo il primo ngOnChanges |
ngDoCheck |
Ad ogni ciclo di change detection |
ngAfterContentInit |
Dopo la proiezione del contenuto (ng-content) |
ngAfterContentChecked |
Dopo ogni verifica del contenuto proiettato |
ngAfterViewInit |
Dopo l’inizializzazione della vista e delle viste figlie |
ngAfterViewChecked |
Dopo ogni verifica della vista |
ngOnDestroy |
Immediatamente prima della distruzione del componente |
Accesso agli Elementi del Template
@ViewChild e @ViewChildren
import { Component, ViewChild, ViewChildren, ElementRef, QueryList, AfterViewInit } from '@angular/core';
import { CardComponent } from './card.component';
@Component({
selector: 'app-galleria',
standalone: true,
imports: [CardComponent],
template: `
<input #campoRicerca type="text" placeholder="Cerca...">
@for (item of items; track item.id) {
<app-card [titolo]="item.nome" />
}
`
})
export class GalleriaComponent implements AfterViewInit {
items = [{ id: 1, nome: 'Item 1' }, { id: 2, nome: 'Item 2' }];
// Riferimento a un elemento del template
@ViewChild('campoRicerca') campoRicerca!: ElementRef<HTMLInputElement>;
// Riferimento a tutti i componenti CardComponent figli
@ViewChildren(CardComponent) cards!: QueryList<CardComponent>;
ngAfterViewInit(): void {
// Ora possiamo accedere agli elementi
this.campoRicerca.nativeElement.focus();
console.log(`Trovate ${this.cards.length} card`);
}
}
Comunicazione tra Componenti
Riepilogo dei pattern
// 1. Padre -> Figlio: @Input
// Nel padre:
<app-figlio [messaggio]="'Ciao'" />
// 2. Figlio -> Padre: @Output + EventEmitter
// Nel figlio:
@Output() evento = new EventEmitter<string>();
// Nel padre:
<app-figlio (evento)="gestisci($event)" />
// 3. Tra componenti non correlati: Servizio condiviso
@Injectable({ providedIn: 'root' })
export class MessaggioService {
private messaggioSubject = new BehaviorSubject<string>('');
messaggio$ = this.messaggioSubject.asObservable();
inviaMessaggio(msg: string): void {
this.messaggioSubject.next(msg);
}
}
Best Practices
- Mantieni i componenti piccoli e focalizzati su una singola responsabilità
- Usa standalone components per i nuovi progetti (Angular 17+)
- Preferisci OnPush change detection per migliori prestazioni
- Cancella sempre le sottoscrizioni in
ngOnDestroyper evitare memory leak - Usa
@Input({ required: true })per input obbligatori - Evita logica complessa nei template: spostala nella classe del componente
- Usa content projection (
ng-content) per componenti wrapper riutilizzabili - Separa template e stili in file esterni per componenti con template complessi