Salta al contenuto principale

4 - Standalone Components: Angular senza NgModules

Scopri come sviluppare in Angular senza NgModules: standalone components, routing con loadComponent, bootstrapApplication() e migrazione automatica.

Cosa ne pensi di questo articolo?

4 - Standalone Components: Angular senza NgModules
Angular ModernoFoto:Bibek ghoshPexels

In Breve

Scopri come sviluppare in Angular senza NgModules: standalone components, routing con loadComponent, bootstrapApplication() e migrazione automatica.

Standalone Components: Angular senza NgModules

Con l'introduzione degli Standalone Components in Angular 14 e la loro progressiva adozione come standard a partire da Angular 15, il framework ha compiuto una svolta architetturale fondamentale: l'eliminazione dell'obbligo di utilizzare NgModules per organizzare il codice. Questa evoluzione ha semplificato drasticamente lo sviluppo, ridotto il boilerplate e reso Angular molto più accessibile ai nuovi sviluppatori.

In questo quarto articolo della serie Angular Moderno esploreremo gli Standalone Components in profondità: dal problema che risolvono alla migrazione completa, passando per il routing, il bootstrapping, la dependency injection e i pattern avanzati. Se hai mai lottato con declarations, imports e exports nei moduli Angular, questo articolo ti mostrera come liberartene definitivamente.

Cosa Imparerai

  • perchè i NgModules rappresentavano un problema e come gli Standalone Components lo risolvono
  • Come creare componenti, direttive e pipe standalone
  • Gestire le dipendenze con imports direttamente nel componente
  • Configurare routing e lazy loading senza moduli
  • Bootstrapping dell'applicazione con bootstrapApplication()
  • Dependency Injection nel mondo standalone
  • Migrare un progetto esistente con lo schematic ufficiale
  • Pattern avanzati e best practice per applicazioni enterprise

Panoramica della Serie Angular Moderno

# Articolo Focus
1Angular SignalsReattività fine-grained
2Zoneless Change DetectionEliminare Zone.js
3Nuovi Template @if, @for, @deferControl flow moderno
4Sei qui - Standalone ComponentsArchitettura senza NgModule
5Signal FormsForm reattivi con Signals
6SSR e Incremental HydrationServer-Side Rendering
7Core Web Vitals in AngularPerformance e metriche
8Angular PWAProgressive Web App
9Dependency Injection AvanzataDI tree-shakable
10Migrare da Angular 17 a 21Guida alla migrazione

1. Il Problema dei NgModules

Sin dalla versione 2, Angular ha utilizzato i NgModules come unita fondamentale di organizzazione del codice. Ogni componente, direttiva o pipe doveva essere dichiarato esattamente in un modulo tramite l'array declarations, e le dipendenze tra moduli venivano gestite tramite imports e exports. Questo sistema, pur fornendo una struttura chiara, ha generato problemi crescenti man mano che le applicazioni diventavano più complesse.

Tipico NgModule con molto boilerplate

// app.module.ts - L'approccio tradizionale
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { HttpClientModule } from '@angular/common/http';
import { ReactiveFormsModule } from '@angular/forms';
import { RouterModule } from '@angular/router';

import { AppComponent } from './app.component';
import { HeaderComponent } from './header/header.component';
import { FooterComponent } from './footer/footer.component';
import { HomeComponent } from './home/home.component';
import { ProductListComponent } from './products/product-list.component';
import { ProductDetailComponent } from './products/product-detail.component';
import { CartComponent } from './cart/cart.component';
import { CheckoutComponent } from './checkout/checkout.component';
import { HighlightDirective } from './directives/highlight.directive';
import { CurrencyPipe } from './pipes/currency.pipe';

@NgModule({
  declarations: [
    AppComponent,
    HeaderComponent,
    FooterComponent,
    HomeComponent,
    ProductListComponent,
    ProductDetailComponent,
    CartComponent,
    CheckoutComponent,
    HighlightDirective,
    CurrencyPipe
  ],
  imports: [
    BrowserModule,
    HttpClientModule,
    ReactiveFormsModule,
    RouterModule.forRoot(routes)
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }
    

Problemi Comuni con i NgModules

  • Boilerplate eccessivo: Ogni componente richiede l'importazione e la dichiarazione nel modulo. In progetti grandi si arriva a centinaia di righe solo per le dichiarazioni
  • Dipendenze circolari: Quando due moduli si importano a vicenda si creano errori difficili da diagnosticare
  • Confusione declarations vs imports: I nuovi sviluppatori confondono costantemente dove mettere componenti (declarations) e dove mettere moduli (imports)
  • Errore "Component is not part of any NgModule": Uno degli errori più frustranti per i principianti, causato dalla dimenticanza di dichiarare un componente
  • Accoppiamento: I componenti sono accoppiati al modulo che li dichiara, rendendo difficile il riutilizzo e il test unitario
  • Lazy loading complesso: Per caricare una feature in modo lazy bisogna creare un modulo dedicato con la sua configurazione di routing

Consideriamo un esempio tipico di dipendenza circolare che affliggeva molti progetti Angular:

Dipendenza circolare tra moduli

// shared.module.ts
@NgModule({
  declarations: [UserAvatarComponent],
  imports: [ProductModule],  // Importa ProductModule
  exports: [UserAvatarComponent]
})
export class SharedModule { }

// product.module.ts
@NgModule({
  declarations: [ProductCardComponent],
  imports: [SharedModule],   // Importa SharedModule → CIRCOLARE!
  exports: [ProductCardComponent]
})
export class ProductModule { }
    

NgModules vs Standalone: Confronto Rapido

Aspetto NgModules Standalone
Dichiarazione componenti In declarations del modulo Self-contained con standalone: true
Gestione dipendenze Tramite moduli intermedi Direttamente nel componente
Lazy loading Richiede NgModule dedicato loadComponent() diretto
Boilerplate Alto (modulo + dichiarazioni) Minimo
Test unitari TestBed con modulo di test Configurazione minima
Dipendenze circolari Frequenti tra moduli Quasi impossibili
Curva di apprendimento Ripida (declarations, imports, exports, providers, bootstrap) Dolce (standalone: true, imports)

2. Cosa sono gli Standalone Components

Uno Standalone Component è un componente Angular che dichiara se stesso come autosufficiente tramite il flag standalone: true nel decoratore @Component. Questo significa che il componente non ha bisogno di essere dichiarato in nessun NgModule: gestisce le proprie dipendenze direttamente nel suo array imports.

A partire da Angular 19, tutti i componenti, le direttive e le pipe sono standalone per default. Non serve nemmeno specificare standalone: true perchè è il comportamento predefinito. Questa è la dimostrazione definitiva che il team Angular considera gli Standalone Components il futuro del framework.

Standalone Component: prima e dopo Angular 19

// Angular 14-18: standalone esplicito
@Component({
  selector: 'app-user-card',
  standalone: true,
  template: `<div class="user-card">
    <h3>{{ user().name }}</h3>
    <p>{{ user().email }}</p>
  </div>`,
  styles: [`
    .user-card {
      padding: 1rem;
      border: 1px solid #ccc;
      border-radius: 8px;
    }
  `]
})
export class UserCardComponent {
  user = input.required<User>();
}

// Angular 19+: standalone e il default, non serve specificarlo
@Component({
  selector: 'app-user-card',
  template: `<div class="user-card">
    <h3>{{ user().name }}</h3>
    <p>{{ user().email }}</p>
  </div>`,
  styles: [`
    .user-card {
      padding: 1rem;
      border: 1px solid #ccc;
      border-radius: 8px;
    }
  `]
})
export class UserCardComponent {
  user = input.required<User>();
}
    

Nota su standalone: true in Angular 19+

Da Angular 19, se ometti la proprietà standalone, il componente è standalone per default. Se vuoi esplicitamente creare un componente non-standalone (per compatibilità con codice legacy), devi specificare standalone: false. In questo articolo useremo sempre standalone: true per chiarezza, ma ricorda che in Angular 21 e opzionale.

3. Creare Componenti, Direttive e Pipe Standalone

Il concetto di "standalone" non si applica solo ai componenti. Anche le direttive e le pipe possono essere standalone, seguendo lo stesso principio di autosufficienza.

Componente Standalone

Componente standalone completo

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

@Component({
  selector: 'app-todo-item',
  standalone: true,
  imports: [CommonModule],  // Dipendenze gestite qui
  template: `
    <div class="todo-item" [class.completed]="todo().completed">
      <input type="checkbox"
             [checked]="todo().completed"
             (change)="onToggle()" />
      <span>{{ todo().title }}</span>
      <button (click)="onDelete()">Elimina</button>
    </div>
  `
})
export class TodoItemComponent {
  todo = input.required<Todo>();
  toggled = output<number>();
  deleted = output<number>();

  onToggle() {
    this.toggled.emit(this.todo().id);
  }

  onDelete() {
    this.deleted.emit(this.todo().id);
  }
}
    

Direttiva Standalone

Direttiva standalone per evidenziare elementi

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

@Directive({
  selector: '[appHighlight]',
  standalone: true
})
export class HighlightDirective {
  appHighlight = input<string>('yellow');

  constructor(private el: ElementRef) {
    effect(() => {
      this.el.nativeElement.style.backgroundColor = this.appHighlight();
    });
  }
}
    

Pipe Standalone

Pipe standalone per formattare le date in modo relativo

import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
  name: 'timeAgo',
  standalone: true
})
export class TimeAgoPipe implements PipeTransform {
  transform(value: Date): string {
    const seconds = Math.floor((new Date().getTime() - value.getTime()) / 1000);

    if (seconds < 60) return 'pochi secondi fa';
    if (seconds < 3600) return Math.floor(seconds / 60) + ' minuti fa';
    if (seconds < 86400) return Math.floor(seconds / 3600) + ' ore fa';
    return Math.floor(seconds / 86400) + ' giorni fa';
  }
}
    

4. Gestire le Dipendenze con imports

La chiave degli Standalone Components è l'array imports nel decoratore @Component. Qui si dichiarano tutte le dipendenze del template: altri componenti, direttive, pipe e anche NgModules interi (per compatibilità con librerie legacy). Ogni componente sa esattamente di cosa ha bisogno, senza intermediari.

Gestione delle dipendenze nel componente

import { Component } from '@angular/core';
import { RouterLink, RouterOutlet } from '@angular/router';
import { AsyncPipe, DatePipe, CurrencyPipe } from '@angular/common';
import { ReactiveFormsModule } from '@angular/forms';

// Importa componenti standalone direttamente
import { UserCardComponent } from './user-card.component';
import { LoadingSpinnerComponent } from '../shared/loading-spinner.component';
import { HighlightDirective } from '../directives/highlight.directive';
import { TimeAgoPipe } from '../pipes/time-ago.pipe';

@Component({
  selector: 'app-dashboard',
  standalone: true,
  imports: [
    // Moduli Angular (per compatibilità)
    ReactiveFormsModule,

    // Pipe standalone
    AsyncPipe,
    DatePipe,
    CurrencyPipe,
    TimeAgoPipe,

    // Componenti standalone
    UserCardComponent,
    LoadingSpinnerComponent,

    // Direttive standalone
    HighlightDirective,

    // Router
    RouterLink,
    RouterOutlet
  ],
  templateUrl: './dashboard.component.html'
})
export class DashboardComponent {
  // Il componente conosce tutte le sue dipendenze
}
    

Confronto: Dipendenze con NgModules vs Standalone

Con NgModules Con Standalone
Componente dichiarato in un modulo Componente autosufficiente
Dipendenze importate nel modulo Dipendenze importate nel componente
Tutte le dichiarazioni del modulo vedono le importazioni Solo il componente vede le sue importazioni
Rischio di importazioni non utilizzate nel modulo Ogni importazione è esplicita e necessaria

Attenzione: Non importare più del necessario

Con gli Standalone Components potresti essere tentato di importare CommonModule intero per comodita. Tuttavia, è meglio importare solo le singole pipe e direttive di cui hai bisogno (es. NgIf, NgFor, AsyncPipe). Con la nuova sintassi @if e @for di Angular 17+, molte di queste importazioni non sono più necessarie perchè il control flow è nativo nel template.

5. Routing senza NgModules

Uno dei maggiori vantaggi degli Standalone Components è la semplificazione del routing. Con i moduli, per caricare una feature in modo lazy bisognava creare un modulo dedicato con RouterModule.forChild(). Con gli Standalone Components, si può fare lazy loading di un singolo componente direttamente con loadComponent.

loadComponent: Lazy Loading di un Singolo Componente

Routing con lazy loading standalone

// app.routes.ts - Configurazione routing completa senza moduli
import { Routes } from '@angular/router';

export const routes: Routes = [
  {
    path: '',
    loadComponent: () =>
      import('./home/home.component').then(m => m.HomeComponent)
  },
  {
    path: 'products',
    loadComponent: () =>
      import('./products/product-list.component').then(m => m.ProductListComponent)
  },
  {
    path: 'products/:id',
    loadComponent: () =>
      import('./products/product-detail.component').then(m => m.ProductDetailComponent)
  },
  {
    path: 'cart',
    loadComponent: () =>
      import('./cart/cart.component').then(m => m.CartComponent)
  },
  {
    path: '**',
    loadComponent: () =>
      import('./not-found/not-found.component').then(m => m.NotFoundComponent)
  }
];
    

loadChildren: Lazy Loading di un Gruppo di Route

Per feature complesse con sotto-route, si può usare loadChildren puntando a un file che esporta un array di Routes, senza bisogno di un modulo wrapper.

Lazy loading di sotto-route standalone

// app.routes.ts
export const routes: Routes = [
  {
    path: 'admin',
    loadChildren: () =>
      import('./admin/admin.routes').then(m => m.ADMIN_ROUTES)
  }
];

// admin/admin.routes.ts - File di route standalone
import { Routes } from '@angular/router';

export const ADMIN_ROUTES: Routes = [
  {
    path: '',
    loadComponent: () =>
      import('./admin-layout.component').then(m => m.AdminLayoutComponent),
    children: [
      {
        path: 'dashboard',
        loadComponent: () =>
          import('./dashboard/dashboard.component').then(m => m.DashboardComponent)
      },
      {
        path: 'users',
        loadComponent: () =>
          import('./users/users.component').then(m => m.UsersComponent)
      },
      {
        path: 'settings',
        loadComponent: () =>
          import('./settings/settings.component').then(m => m.SettingsComponent)
      }
    ]
  }
];
    

Lazy Loading: NgModules vs Standalone

Approccio NgModules Standalone
Lazy load singolo componente Impossibile senza modulo wrapper loadComponent: () => import(...)
Lazy load feature loadChildren con NgModule loadChildren con array Routes
File necessari feature.module.ts + feature-routing.module.ts feature.routes.ts (solo)
Granularità A livello di modulo A livello di singolo componente

6. Bootstrapping con bootstrapApplication()

Con gli Standalone Components, anche il modo in cui si avvia l'applicazione cambia radicalmente. Non serve più un AppModule con il metodo platformBrowserDynamic().bootstrapModule(). Si utilizza invece bootstrapApplication(), che accetta il componente root e una configurazione di provider.

Il Vecchio Approccio con NgModule

main.ts tradizionale con NgModule

// main.ts - Approccio tradizionale
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';

platformBrowserDynamic().bootstrapModule(AppModule)
  .catch(err => console.error(err));
    

Il Nuovo Approccio Standalone

main.ts con bootstrapApplication()

// main.ts - Approccio standalone
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
import { appConfig } from './app/app.config';

bootstrapApplication(AppComponent, appConfig)
  .catch(err => console.error(err));
    
app.config.ts - Configurazione centralizzata dei provider

// app.config.ts
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { provideRouter, withViewTransitions } from '@angular/router';
import { provideHttpClient, withInterceptors, withFetch } from '@angular/common/http';
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
import { provideClientHydration, withEventReplay } from '@angular/platform-browser';

import { routes } from './app.routes';
import { authInterceptor } from './interceptors/auth.interceptor';
import { errorInterceptor } from './interceptors/error.interceptor';

export const appConfig: ApplicationConfig = {
  providers: [
    // Change detection (o provideExperimentalZonelessChangeDetection())
    provideZoneChangeDetection({ eventCoalescing: true }),

    // Router con view transitions
    provideRouter(routes, withViewTransitions()),

    // HTTP client con interceptors funzionali
    provideHttpClient(
      withFetch(),
      withInterceptors([authInterceptor, errorInterceptor])
    ),

    // Animazioni asincrone
    provideAnimationsAsync(),

    // Hydration per SSR
    provideClientHydration(withEventReplay())
  ]
};
    

Le Funzioni provide*

Con l'approccio standalone, Angular introduce le funzioni provide* che sostituiscono i vecchi moduli di configurazione. Ecco le principali:

Funzione provide* Sostituisce
provideRouter() RouterModule.forRoot()
provideHttpClient() HttpClientModule
provideAnimationsAsync() BrowserAnimationsModule
provideClientHydration() Configurazione SSR nel modulo
provideZoneChangeDetection() Configurazione implicita di Zone.js

7. Dependency Injection nel Mondo Standalone

La Dependency Injection (DI) di Angular funziona perfettamente con gli Standalone Components, ma offre nuovi pattern per configurare i provider a diversi livelli dell'applicazione.

providedIn: 'root' - Il Pattern Consigliato

Il modo più semplice e raccomandato per fornire un servizio è utilizzare providedIn: 'root' nel decoratore @Injectable. Questo registra il servizio come singleton nell'injector root, disponibile ovunque nell'applicazione. Il vantaggio è che il servizio è tree-shakable: se nessun componente lo inietta, viene rimosso dal bundle di produzione.

Servizio con providedIn: 'root'

import { Injectable, signal } from '@angular/core';

@Injectable({
  providedIn: 'root'  // Singleton tree-shakable
})
export class CartService {
  private _items = signal<CartItem[]>([]);
  readonly items = this._items.asReadonly();

  readonly totalItems = computed(() => this._items().length);
  readonly totalPrice = computed(() =>
    this._items().reduce((sum, item) => sum + item.price * item.quantity, 0)
  );

  addItem(product: Product) {
    this._items.update(items => [...items, {
      ...product,
      quantity: 1
    }]);
  }

  removeItem(id: number) {
    this._items.update(items => items.filter(item => item.id !== id));
  }
}
    

Provider a Livello di Route

Per servizi che devono essere condivisi solo all'interno di una feature, si possono definire i provider direttamente nella configurazione della route. Questo crea un environment injector dedicato a quella sezione dell'applicazione.

Provider a livello di route

// admin/admin.routes.ts
export const ADMIN_ROUTES: Routes = [
  {
    path: '',
    providers: [
      // Questo servizio è disponibile SOLO nei componenti admin
      AdminAuthService,
      // Anche i provider configurabili funzionano
      {
        provide: API_BASE_URL,
        useValue: '/api/admin'
      }
    ],
    children: [
      {
        path: 'dashboard',
        loadComponent: () =>
          import('./dashboard.component').then(m => m.DashboardComponent)
      },
      {
        path: 'users',
        loadComponent: () =>
          import('./users.component').then(m => m.UsersComponent)
      }
    ]
  }
];
    

Provider a Livello di Componente

Provider nel componente standalone

@Component({
  selector: 'app-editor',
  standalone: true,
  providers: [
    // Nuova istanza per ogni componente editor
    EditorStateService,
    {
      provide: UNDO_STACK_SIZE,
      useValue: 50
    }
  ],
  template: `...`
})
export class EditorComponent {
  private editorState = inject(EditorStateService);
}
    

Gerarchia dei Provider nel Mondo Standalone

Livello Configurazione Scope Caso d'uso
Root providedIn: 'root' Tutta l'app (singleton) AuthService, HttpService, StateStore
App Config appConfig.providers Tutta l'app Router, HttpClient, Animations
Route route.providers Feature/sezione AdminService, FeatureConfig
Componente component.providers Singolo componente FormState, EditorState

8. Migrare da NgModules a Standalone

Angular fornisce uno schematic ufficiale di migrazione che automatizza gran parte del processo di conversione da NgModules a Standalone Components. La migrazione avviene in tre fasi progressive, permettendo di procedere in modo sicuro e incrementale.

Fase 1: Convertire i Componenti in Standalone

Il primo passo converte tutti i componenti, le direttive e le pipe in standalone, aggiungendo standalone: true e gli imports necessari.

Eseguire la migrazione automatica - Fase 1

# Fase 1: Converti componenti, direttive e pipe in standalone
ng generate @angular/core:standalone-migration --mode=convert-to-standalone

# Cosa fa:
# - Aggiunge standalone: true a tutti i componenti/direttive/pipe
# - Aggiunge gli imports necessari nel decoratore di ogni componente
# - Mantiene i NgModules esistenti per compatibilità
    

Fase 2: Rimuovere i NgModules non Necessari

Eseguire la migrazione automatica - Fase 2

# Fase 2: Rimuovi i NgModules che non sono più necessari
ng generate @angular/core:standalone-migration --mode=prune-ng-modules

# Cosa fa:
# - Rimuove i NgModules che contengono solo declarations standalone
# - Aggiorna le importazioni nei file che li riferivano
# - Mantiene i moduli che definiscono providers o altro
    

Fase 3: Passare a bootstrapApplication()

Eseguire la migrazione automatica - Fase 3

# Fase 3: Converti il bootstrap dell'applicazione
ng generate @angular/core:standalone-migration --mode=standalone-bootstrap

# Cosa fa:
# - Converte main.ts per usare bootstrapApplication()
# - Crea app.config.ts con i provider
# - Rimuove AppModule se non più necessario
# - Migra i provider dal modulo alla configurazione
    

Consigli per una Migrazione Sicura

  • Fai un commit prima di ogni fase: Cosi puoi facilmente tornare indietro se qualcosa va storto
  • Esegui i test dopo ogni fase: Assicurati che l'applicazione funzioni correttamente prima di procedere
  • Verifica le dipendenze circolari: La migrazione potrebbe evidenziare dipendenze circolari nascoste nei moduli
  • Controlla i provider: Alcuni moduli definiscono provider con forRoot() e forChild() che richiedono attenzione manuale
  • Librerie di terze parti: Verifica che le librerie che usi supportino standalone. La maggior parte lo fa esportando i componenti come standalone o fornendo funzioni provide*()

Migrazione Manuale Passo-Passo

Se preferisci migrare manualmente, ecco il processo tipico per convertire un componente:

Prima: Componente dichiarato in un modulo

// PRIMA: product-card.component.ts (in un NgModule)
@Component({
  selector: 'app-product-card',
  templateUrl: './product-card.component.html'
})
export class ProductCardComponent {
  @Input() product!: Product;
  @Output() addToCart = new EventEmitter<Product>();
}

// product.module.ts
@NgModule({
  declarations: [ProductCardComponent, ProductListComponent],
  imports: [CommonModule, RouterModule],
  exports: [ProductCardComponent]
})
export class ProductModule { }
    
Dopo: Componente standalone moderno

// DOPO: product-card.component.ts (standalone)
import { Component, input, output } from '@angular/core';
import { RouterLink } from '@angular/router';
import { CurrencyPipe } from '@angular/common';

@Component({
  selector: 'app-product-card',
  standalone: true,
  imports: [RouterLink, CurrencyPipe],
  templateUrl: './product-card.component.html'
})
export class ProductCardComponent {
  product = input.required<Product>();
  addToCart = output<Product>();
}

// product.module.ts → ELIMINATO!
// Le route puntano direttamente ai componenti
    

9. Pattern Avanzati

Barrel Exports per Componenti Condivisi

Quando si hanno molti componenti standalone condivisi, è utile creare un file barrel (index.ts) che li riesporta tutti. Questo semplifica le importazioni nei componenti che li utilizzano.

Barrel export per componenti condivisi

// shared/ui/index.ts - Barrel export
export { ButtonComponent } from './button/button.component';
export { CardComponent } from './card/card.component';
export { ModalComponent } from './modal/modal.component';
export { TabsComponent } from './tabs/tabs.component';
export { TooltipDirective } from './tooltip/tooltip.directive';
export { TimeAgoPipe } from './pipes/time-ago.pipe';
export { TruncatePipe } from './pipes/truncate.pipe';

// Utilizzo nel componente:
import {
  ButtonComponent,
  CardComponent,
  TooltipDirective,
  TruncatePipe
} from '../shared/ui';

@Component({
  standalone: true,
  imports: [ButtonComponent, CardComponent, TooltipDirective, TruncatePipe],
  // ...
})
export class MyFeatureComponent { }
    

Comporre Array di Importazioni Comuni

Se più componenti condividono le stesse dipendenze, si può creare un array di importazioni comuni da riutilizzare. Questo è un pattern leggero alternativo ai SharedModule.

Array di importazioni condivise

// shared/common-imports.ts
import { RouterLink } from '@angular/router';
import { DatePipe, CurrencyPipe, AsyncPipe } from '@angular/common';
import { ButtonComponent } from './ui/button/button.component';
import { CardComponent } from './ui/card/card.component';
import { LoadingSpinnerComponent } from './ui/loading-spinner/loading-spinner.component';

export const COMMON_IMPORTS = [
  RouterLink,
  DatePipe,
  CurrencyPipe,
  AsyncPipe,
  ButtonComponent,
  CardComponent,
  LoadingSpinnerComponent
] as const;

// Utilizzo:
@Component({
  standalone: true,
  imports: [...COMMON_IMPORTS, ReactiveFormsModule],
  // ...
})
export class CheckoutComponent { }
    

Lazy Loading Granulare con @defer

Combinando gli Standalone Components con il blocco @defer introdotto in Angular 17, si può ottenere un lazy loading ancora più granulare a livello di template:

Lazy loading nel template con @defer

<!-- Il componente HeavyChart viene caricato solo quando entra nel viewport -->
@defer (on viewport) {
  <app-heavy-chart [data]="chartData()" />
} @placeholder {
  <div class="chart-placeholder">Grafico in caricamento...</div>
} @loading (minimum 500ms) {
  <app-loading-spinner />
}

<!-- Il componente Comments viene caricato solo all'interazione -->
@defer (on interaction) {
  <app-comments [postId]="postId()" />
} @placeholder {
  <button>Mostra commenti</button>
}
    

Vantaggi del Lazy Loading Granulare

Con i NgModules, la granularità minima del lazy loading era un intero modulo con tutti i suoi componenti. Con gli Standalone Components, ogni singolo componente può essere caricato in modo indipendente, sia tramite il router (loadComponent) che tramite il template (@defer). Questo riduce significativamente il bundle iniziale e migliora i tempi di caricamento.

10. Best Practices

Dopo aver esplorato tutte le funzionalità degli Standalone Components, ecco le best practice da seguire per organizzare al meglio il codice nelle applicazioni Angular moderne.

Le 10 Regole d'Oro degli Standalone Components

  1. Standalone per default: Crea sempre componenti standalone. Usa NgModules solo per compatibilità con librerie legacy
  2. Importa solo ciò che serve: Evita di importare interi moduli come CommonModule. Importa le singole pipe e direttive
  3. Usa la nuova sintassi di template: Con @if, @for e @switch non hai più bisogno di importare NgIf, NgFor, NgSwitch
  4. Un componente, un file: Ogni componente standalone dovrebbe avere il proprio file. Evita di dichiarare più componenti nello stesso file
  5. Barrel exports per le feature: Usa file index.ts per riesportare i componenti condivisi di una feature
  6. Provider nelle route, non nei componenti: Per servizi di feature, preferisci route.providers rispetto a component.providers
  7. providedIn: 'root' per i servizi globali: Mantieni providedIn: 'root' per servizi singleton e tree-shakable
  8. Lazy loading aggressivo: Usa loadComponent nel router e @defer nel template per caricare i componenti on-demand
  9. Migra gradualmente: Non devi migrare tutto in una volta. Standalone e NgModules possono coesistere indefinitamente
  10. Testa ogni componente in isolamento: Un grande vantaggio degli standalone e la facilità di testing. Sfruttala!

Struttura di Progetto Consigliata

Struttura di un progetto Angular standalone

src/app/
  app.component.ts          # Root component (standalone)
  app.config.ts             # ApplicationConfig con providers
  app.routes.ts             # Route principali

  core/                     # Servizi singleton e guard
    services/
      auth.service.ts       # providedIn: 'root'
      api.service.ts        # providedIn: 'root'
    interceptors/
      auth.interceptor.ts
    guards/
      auth.guard.ts

  shared/                   # Componenti, direttive, pipe riutilizzabili
    ui/
      button/
        button.component.ts     # standalone
      card/
        card.component.ts       # standalone
      modal/
        modal.component.ts      # standalone
      index.ts                  # barrel export
    directives/
      tooltip.directive.ts      # standalone
    pipes/
      time-ago.pipe.ts          # standalone

  features/                 # Feature dell'applicazione
    products/
      product-list.component.ts   # standalone
      product-detail.component.ts # standalone
      product-card.component.ts   # standalone (locale)
      products.routes.ts          # sotto-route
    admin/
      admin-layout.component.ts
      dashboard.component.ts
      admin.routes.ts
    

Testing di Componenti Standalone

Uno dei vantaggi più significativi è la semplificazione del testing. Non serve più configurare un modulo di test complesso con TestBed: il componente porta con se tutte le sue dipendenze.

Test semplificato con standalone

// product-card.component.spec.ts
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ProductCardComponent } from './product-card.component';

describe('ProductCardComponent', () => {
  let fixture: ComponentFixture<ProductCardComponent>;

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      // Con standalone, basta importare il componente!
      imports: [ProductCardComponent]
      // Non serve: declarations, imports di moduli, etc.
    }).compileComponents();

    fixture = TestBed.createComponent(ProductCardComponent);
  });

  it('dovrebbe mostrare il nome del prodotto', () => {
    // Usa ComponentRef per impostare gli input
    fixture.componentRef.setInput('product', {
      id: 1,
      name: 'Angular T-Shirt',
      price: 29.99
    });
    fixture.detectChanges();

    const compiled = fixture.nativeElement as HTMLElement;
    expect(compiled.textContent).toContain('Angular T-Shirt');
  });

  it('dovrebbe emettere addToCart quando si clicca il pulsante', () => {
    fixture.componentRef.setInput('product', {
      id: 1,
      name: 'Angular T-Shirt',
      price: 29.99
    });
    fixture.detectChanges();

    const spy = jest.spyOn(fixture.componentInstance.addToCart, 'emit');
    const button = fixture.nativeElement.querySelector('button');
    button.click();

    expect(spy).toHaveBeenCalledWith(expect.objectContaining({
      id: 1
    }));
  });
});
    

Quando Mantenere i NgModules

Anche se gli Standalone Components sono il futuro, ci sono casi in cui i NgModules sono ancora utili o necessari:

  • Librerie di terze parti: Alcune librerie esportano ancora NgModules. Puoi importarli direttamente nell'array imports del componente standalone
  • Progetti legacy di grandi dimensioni: Se hai un progetto con centinaia di componenti, la migrazione può essere graduale. Non serve fare tutto subito
  • SharedModule con molte dipendenze: Se hai uno SharedModule che importa decine di componenti, potrebbe essere più pratico mantenerlo temporaneamente e sostituirlo con un barrel export quando sei pronto

Riepilogo e Prossimi Passi

In questo articolo abbiamo esplorato in profondità gli Standalone Components di Angular: dalla motivazione alla migrazione, dal routing alla dependency injection. Ecco i punti chiave:

Concetti Chiave

  • Gli Standalone Components eliminano la necessità di NgModules, rendendo ogni componente autosufficiente
  • Da Angular 19, standalone è il comportamento di default per componenti, direttive e pipe
  • L'array imports nel decoratore permette di dichiarare le dipendenze direttamente nel componente
  • loadComponent e loadChildren semplificano il lazy loading senza moduli wrapper
  • bootstrapApplication() e ApplicationConfig sostituiscono il bootstrap tramite NgModule
  • Le funzioni provide*() sostituiscono i moduli di configurazione (RouterModule.forRoot(), etc.)
  • La DI funziona a quattro livelli: root, app config, route e componente
  • Lo schematic di migrazione automatizza la conversione in tre fasi progressive
  • I barrel exports e gli array di importazioni comuni sostituiscono gli SharedModule

Nel prossimo articolo della serie, Signal Forms, vedremo come la rivoluzione dei Signals si estende alla gestione dei form, con un'alternativa moderna ai Reactive Forms che sfrutta la reattività fine-grained per creare form performanti e dichiarativi.

Domande frequenti

Cos'è il problema principale dei NgModules in Angular?

I NgModules possono generare problemi crescenti man mano che le applicazioni diventano più complesse, come ad esempio dipendenze circolari tra moduli.

Cosa sono gli Standalone Components in Angular?

Gli Standalone Components sono componenti Angular autosufficienti che dichiarano se stessi tramite il flag nel decoratore e gestiscono le proprie dipendenze direttamente nel loro array.

Come si possono creare componenti, direttive e pipe standalone in Angular?

Si possono creare componenti, direttive e pipe standalone utilizzando lo stesso principio di autosufficienza del decoratore .

Qual è il vantaggio principale degli Standalone Components nel routing in Angular?

Il vantaggio principale degli Standalone Components nel routing in Angular è la semplificazione del routing, che consente di caricare una feature in modo lazy senza dover creare un modulo dedicato.

Fonti e approfondimenti

Prova Questi Strumenti

Strumenti gratuiti correlati a questo argomento.

Commenti

Caricamento commenti...

Accedi per commentare

Discuti questo articolo nella community

Unisciti al gruppo Telegram per discutere con altri sviluppatori, fare domande e condividere le tue esperienze.

Ti è piaciuto questo articolo?

Esplora altri contenuti sul blog o scopri i miei progetti