Angular 19 rend standalone: true implicite. Découvrez ce qui change concrètement : ng generate, bootstrapApplication, lazy loading et migration automatique.
Standalone: true implicite en Angular 19
Avant Angular 19, déclarer un composant standalone nécessitait d'écrire explicitement standalone: true dans le décorateur @Component. Depuis Angular 19, cette valeur est implicite par défaut : tout composant, directive ou pipe généré par le CLI est standalone sans qu'il soit nécessaire de le préciser.
Ce changement découle d'une décision de l'équipe Angular annoncée fin 2024 : les NgModules ne sont plus la voie recommandée pour les nouvelles applications. Angular 19 entérine cette orientation en modifiant le comportement du CLI à la racine.
Avant Angular 19 (déclaration explicite requise)
// Composant Angular 17/18 — standalone doit être déclaré manuellement
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-profil',
standalone: true, // Obligatoire avant Angular 19
imports: [CommonModule],
template: `<p>Profil utilisateur</p>`
})
export class ProfilComponent {}
Depuis Angular 19 (standalone implicite)
// Composant Angular 19/20 — standalone: true est la valeur par défaut
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-profil',
// standalone: true est désormais implicite — pas besoin de le déclarer
imports: [CommonModule],
template: `<p>Profil utilisateur</p>`
})
export class ProfilComponent {}
standalone: true ne signifie pas que le composant est déclaré dans un NgModule. Il est standalone par défaut. Pour forcer le comportement legacy (NgModule), il faut écrire explicitement standalone: false.
L'option schematics dans angular.json
Ce comportement est contrôlé par la configuration schematics dans angular.json. Angular 19 initialise les nouveaux projets avec cette configuration automatiquement :
// angular.json — configuration générée par défaut dans Angular 19+
{
"schematics": {
"@schematics/angular:component": {
// standalone: true est implicite, ce champ n'est plus nécessaire
// mais peut être forcé à false pour les projets legacy qui veulent le comportement NgModule
"standalone": false
}
}
}
"standalone": false dans les schematics de ton angular.json préserve l'ancien comportement lors des ng generate.
ng generate sans NgModule : nouveaux fichiers générés
La différence la plus visible au quotidien concerne les fichiers produits par ng generate component. Dans Angular 19, plus aucun NgModule n'est généré ni attendu. Voici ce qui change concrètement.
Comparaison avant / après Angular 19
| Commande | Angular < 19 (avec NgModules) | Angular 19+ (standalone par défaut) |
|---|---|---|
ng g c user-card |
user-card.component.ts + module parent modifié | user-card.component.ts uniquement |
ng g d highlight |
highlight.directive.ts + module parent modifié | highlight.directive.ts uniquement |
ng g p currency-fr |
currency-fr.pipe.ts + module parent modifié | currency-fr.pipe.ts uniquement |
ng g s auth |
auth.service.ts (inchangé) | auth.service.ts (inchangé) |
ng g m feature |
feature.module.ts créé | Toujours possible, mais déprécié |
Exemple : ng generate component en Angular 19
# Génère un composant standalone sans toucher aucun NgModule
ng generate component features/dashboard/user-card
// Fichier généré : src/app/features/dashboard/user-card/user-card.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-user-card',
// Aucun standalone: true — il est implicite depuis Angular 19
imports: [], // Les dépendances s'importent ici directement
templateUrl: './user-card.component.html',
styleUrl: './user-card.component.scss'
})
export class UserCardComponent {
// Logique du composant — aucune déclaration dans un NgModule requise
}
Générer une directive standalone
# Directive standalone générée sans NgModule
ng generate directive shared/directives/auto-focus
// src/app/shared/directives/auto-focus.directive.ts
import { Directive, ElementRef, OnInit } from '@angular/core';
@Directive({
selector: '[appAutoFocus]'
// standalone est implicite : la directive s'importe directement dans les composants
})
export class AutoFocusDirective implements OnInit {
constructor(private el: ElementRef) {}
ngOnInit(): void {
// Met le focus sur l'élément hôte au montage du composant
this.el.nativeElement.focus();
}
}
ng generate ne modifie plus aucun NgModule existant. Chaque artefact est autonome et s'importe directement dans le tableau imports des composants qui l'utilisent.
Migration automatique : les 3 étapes du schematic
Angular fournit un schematic officiel pour automatiser la migration d'une application NgModule vers le mode standalone. Ce schematic s'exécute en 3 passes successives, chacune ciblant un aspect précis de la migration.
@angular/core en version 19.x minimum avant de lancer les commandes.
Etape 1 — Convertir les composants, directives et pipes
La première passe transforme chaque composant, directive et pipe déclaré dans un NgModule en artefact standalone. Elle ajoute automatiquement les imports nécessaires.
# Passe 1 : conversion des composants/directives/pipes en standalone
# Le flag --mode=convert-to-standalone cible uniquement les déclarations NgModule
ng generate @angular/core:standalone --mode=convert-to-standalone
// AVANT la migration — composant déclaré dans un NgModule
import { Component } from '@angular/core';
@Component({
selector: 'app-header',
templateUrl: './header.component.html'
// Pas de standalone: true — déclaré dans AppModule
})
export class HeaderComponent {}
// APRES la passe 1 — composant converti en standalone automatiquement
import { Component } from '@angular/core';
import { RouterLink, RouterLinkActive } from '@angular/router';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-header',
standalone: true, // Ajouté par le schematic
imports: [RouterLink, RouterLinkActive, CommonModule], // Dépendances détectées automatiquement
templateUrl: './header.component.html'
})
export class HeaderComponent {}
Etape 2 — Supprimer les NgModules inutiles
Une fois tous les composants convertis, la deuxième passe supprime les NgModules qui n'ont plus de raison d'exister. Elle détecte automatiquement les modules vides ou ne servant qu'à déclarer des composants.
# Passe 2 : suppression des NgModules devenus inutiles
ng generate @angular/core:standalone --mode=prune-ng-modules
Etape 3 — Migrer le bootstrap vers l'API standalone
La dernière passe transforme le fichier main.ts pour utiliser bootstrapApplication() à la place de l'ancien platformBrowserDynamic().bootstrapModule(AppModule).
# Passe 3 : migration du point d'entrée main.ts
ng generate @angular/core:standalone --mode=standalone-bootstrap
// AVANT — main.ts avec NgModule (Angular 14 et antérieur)
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
// Bootstrap via le NgModule racine — ancienne méthode
platformBrowserDynamic()
.bootstrapModule(AppModule)
.catch(err => console.error(err));
// APRES — main.ts généré par le schematic (Angular 19 standalone)
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
import { appConfig } from './app/app.config';
// Bootstrap standalone : AppComponent est le composant racine, appConfig fournit les providers
bootstrapApplication(AppComponent, appConfig)
.catch(err => console.error(err));
git commit avant de passer à la suivante — cela facilite le rollback en cas de problème.
Erreurs fréquentes et comment les corriger
La migration vers standalone peut générer des erreurs non évidentes. Voici les plus courantes et leurs solutions concrètes.
Erreur 1 — Component is not standalone
// Erreur au runtime ou à la compilation :
// Error: Component UserCardComponent is not standalone,
// it cannot be imported directly into another component's imports array.
Cause : le composant importé n'a pas encore été migré en standalone. Il est encore déclaré dans un NgModule.
// SOLUTION : ajouter standalone: true (ou laisser implicite en Angular 19)
// et déplacer les imports du NgModule vers le composant lui-même
@Component({
selector: 'app-user-card',
// standalone: true est implicite en Angular 19, mais peut être écrit explicitement
standalone: true,
imports: [CommonModule], // Déplacer les dépendances depuis le NgModule
templateUrl: './user-card.component.html'
})
export class UserCardComponent {}
Erreur 2 — Can't bind to 'ngIf' since it isn't a known property
// Erreur à la compilation :
// Can't bind to 'ngIf' since it isn't a known property of 'div'.
Cause : dans un composant standalone, CommonModule ou NgIf n'est pas importé. Les composants standalone ne bénéficient plus des imports globaux d'un NgModule.
// SOLUTION : importer explicitement NgIf (ou CommonModule) dans le composant
import { Component } from '@angular/core';
import { NgIf, NgFor } from '@angular/common'; // Imports granulaires — recommandé
@Component({
selector: 'app-liste',
standalone: true,
imports: [NgIf, NgFor], // Remplace l'import global via NgModule
template: `
<ul>
<li *ngFor="let item of items">{{ item }}</li>
</ul>
<p *ngIf="items.length === 0">Liste vide</p>
`
})
export class ListeComponent {
items: string[] = [];
}
@if, @for) qui ne nécessite aucun import. Tu n'as alors plus besoin de NgIf ni NgFor.
Erreur 3 — NullInjectorError : No provider for HttpClient
// Erreur au runtime dans les tests ou l'application :
// NullInjectorError: No provider for HttpClient!
Cause : HttpClientModule n'est plus importé dans un NgModule (qui a été supprimé). Il faut désormais fournir HttpClient via provideHttpClient() dans la configuration bootstrap.
// SOLUTION : ajouter provideHttpClient() dans app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes),
// Remplace l'ancien HttpClientModule dans AppModule
provideHttpClient(withInterceptorsFromDi())
]
};
Erreur 4 — forRoot() perdu lors de la migration
// Situation : RouterModule.forRoot(routes) dans AppModule supprimé
// Conséquence : aucun router fourni → routes inactives
// SOLUTION : remplacer RouterModule.forRoot() par provideRouter()
import { provideRouter, withHashLocation, withViewTransitions } from '@angular/router';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [
// Equivalent de RouterModule.forRoot(routes, { useHash: true })
provideRouter(
routes,
withHashLocation(), // Optionnel : active le mode hash (#)
withViewTransitions() // Optionnel : transitions de vues natives
)
]
};
ModuleX.forRoot() a son équivalent provideX() dans l'écosystème Angular 15+. La migration consiste à remplacer les appels forRoot par les fonctions provide* correspondantes.
bootstrapApplication() : configuration complète
bootstrapApplication() est le point d'entrée de toute application Angular standalone. Elle remplace platformBrowserDynamic().bootstrapModule(AppModule) et centralise tous les providers globaux de l'application.
Structure recommandée : séparer app.config.ts
La bonne pratique Angular 19 est de centraliser la configuration dans un fichier dédié app.config.ts, puis de le référencer depuis main.ts. Cela facilite les tests et la lisibilité.
// src/app/app.config.ts — configuration centralisée de l'application
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { provideRouter, withComponentInputBinding, withViewTransitions } from '@angular/router';
import { provideHttpClient, withInterceptors, withFetch } from '@angular/common/http';
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
import { routes } from './app.routes';
import { authInterceptor } from './core/interceptors/auth.interceptor';
import { loggingInterceptor } from './core/interceptors/logging.interceptor';
export const appConfig: ApplicationConfig = {
providers: [
// Optimisation zone.js : coalesce les événements avant détection de changements
provideZoneChangeDetection({ eventCoalescing: true }),
// Router : lie les params de route aux @Input() du composant + transitions natives
provideRouter(
routes,
withComponentInputBinding(), // route.params → @Input() directement
withViewTransitions() // transitions CSS natives entre routes
),
// HTTP : fetch API sous-jacente + interceptors fonctionnels
provideHttpClient(
withFetch(), // Utilise fetch() au lieu de XMLHttpRequest
withInterceptors([authInterceptor, loggingInterceptor])
),
// Animations : chargées en lazy pour réduire le bundle initial
provideAnimationsAsync()
]
};
// src/main.ts — point d'entrée minimal
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
import { appConfig } from './app/app.config';
// Lance l'application avec le composant racine et la configuration centralisée
bootstrapApplication(AppComponent, appConfig)
.catch(err => console.error('Erreur au démarrage de l\'application :', err));
app.component.ts dans Angular 19
// src/app/app.component.ts — composant racine standalone
import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
@Component({
selector: 'app-root',
// standalone implicite — pas besoin de le déclarer en Angular 19
imports: [
RouterOutlet // Seul import nécessaire : le router outlet pour les vues enfants
],
template: `
<header><app-navbar /></header>
<main>
<!-- RouterOutlet rend le composant correspondant à la route active -->
<router-outlet />
</main>
<footer><app-footer /></footer>
`
})
export class AppComponent {}
withComponentInputBinding() est une fonctionnalité majeure d'Angular 16+ qui permet de recevoir les paramètres de route (:id, query params, data) directement via @Input() dans le composant, sans injecter ActivatedRoute.
Lazy loading standalone : loadComponent et loadChildren
Le lazy loading en mode standalone se fait sans NgModule intermédiaire. Angular 19 propose deux approches selon le niveau de granularité souhaité : loadComponent() pour une page individuelle et loadChildren() pour un groupe de routes.
loadComponent() — lazy loading d'un composant seul
// src/app/app.routes.ts — routes de l'application
import { Routes } from '@angular/router';
export const routes: Routes = [
{
path: '',
// Chargement immédiat du composant home (pas de lazy)
loadComponent: () =>
import('./features/home/home.component').then(m => m.HomeComponent)
},
{
path: 'dashboard',
// Chargement lazy du dashboard — bundle séparé créé par le CLI
loadComponent: () =>
import('./features/dashboard/dashboard.component').then(m => m.DashboardComponent),
canActivate: [() => inject(AuthGuard).canActivate()] // Guard inline fonctionnel
},
{
path: 'profil/:id',
// Binding automatique du param :id vers @Input() grace à withComponentInputBinding()
loadComponent: () =>
import('./features/profil/profil.component').then(m => m.ProfilComponent)
}
];
loadChildren() — lazy loading d'un groupe de routes
// app.routes.ts — route parent avec lazy loading du sous-routing
export const routes: Routes = [
{
path: 'admin',
// Charge le fichier de routes admin en lazy — tout le module admin est dans un chunk séparé
loadChildren: () =>
import('./features/admin/admin.routes').then(m => m.adminRoutes)
}
];
// src/app/features/admin/admin.routes.ts — routes du périmètre admin
import { Routes } from '@angular/router';
// Tableau de routes exporté directement — aucun NgModule nécessaire
export const adminRoutes: Routes = [
{
path: '',
// Composant layout de l'admin chargé en lazy
loadComponent: () =>
import('./admin-layout/admin-layout.component').then(m => m.AdminLayoutComponent),
children: [
{
path: 'users',
loadComponent: () =>
import('./users/users-list.component').then(m => m.UsersListComponent)
},
{
path: 'users/:id',
loadComponent: () =>
import('./users/user-detail.component').then(m => m.UserDetailComponent)
},
{
path: 'settings',
loadComponent: () =>
import('./settings/settings.component').then(m => m.SettingsComponent)
}
]
}
];
Préchargement des routes avec PreloadAllModules
// app.config.ts — stratégie de préchargement pour améliorer l'UX
import { provideRouter, withPreloading, PreloadAllModules } from '@angular/router';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(
routes,
// Précharge tous les chunks lazy en arrière-plan après le chargement initial
withPreloading(PreloadAllModules)
)
]
};
loadComponent(), Angular crée automatiquement un chunk JS séparé pour chaque composant lazy. Avec loadChildren(), tous les composants du sous-routing partagent un seul chunk — préférable pour grouper les pages d'un même domaine fonctionnel.
Tests unitaires : TestBed sans NgModule
Les tests unitaires de composants standalone sont plus simples que les anciens tests avec NgModule. La configuration de TestBed se rapproche de ce que le composant déclare dans son tableau imports.
Avant : test d'un composant déclaré dans un NgModule
// Ancienne approche — TestBed avec NgModule
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { UserCardComponent } from './user-card.component';
import { SharedModule } from '../../shared/shared.module'; // Import du NgModule entier
describe('UserCardComponent', () => {
let fixture: ComponentFixture<UserCardComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [UserCardComponent], // Déclaration obligatoire
imports: [SharedModule] // NgModule complet requis — lourd
}).compileComponents();
fixture = TestBed.createComponent(UserCardComponent);
});
it('devrait créer le composant', () => {
expect(fixture.componentInstance).toBeTruthy();
});
});
Depuis Angular 19 : test d'un composant standalone
// Nouvelle approche — TestBed standalone, plus léger et précis
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { UserCardComponent } from './user-card.component';
import { provideHttpClient } from '@angular/common/http';
import { provideHttpClientTesting } from '@angular/common/http/testing';
import { of } from 'rxjs';
import { UserService } from '../../core/services/user.service';
describe('UserCardComponent', () => {
let fixture: ComponentFixture<UserCardComponent>;
let component: UserCardComponent;
// Mock du service utilisateur
const mockUserService = {
getUser: (id: number) => of({ id, name: 'Alice', email: 'alice@test.com' })
};
beforeEach(async () => {
await TestBed.configureTestingModule({
// Importer directement le composant standalone — pas de declarations[]
imports: [UserCardComponent],
providers: [
// Fournir les dépendances nécessaires via provide*
provideHttpClient(),
provideHttpClientTesting(),
{ provide: UserService, useValue: mockUserService }
]
}).compileComponents();
fixture = TestBed.createComponent(UserCardComponent);
component = fixture.componentInstance;
fixture.detectChanges(); // Déclenche le cycle de vie initial
});
it('devrait créer le composant sans erreur', () => {
expect(component).toBeTruthy();
});
it('devrait afficher le nom de l\'utilisateur', () => {
// Assigner une valeur d'entrée au composant
component.userId = 1;
fixture.detectChanges();
// Vérifier le rendu dans le DOM
const compiled = fixture.nativeElement as HTMLElement;
expect(compiled.querySelector('[data-testid="user-name"]')?.textContent).toContain('Alice');
});
});
Tester un service avec inject() dans un contexte standalone
// Test d'un service Angular 19 qui utilise inject() au lieu du constructeur
import { TestBed } from '@angular/core/testing';
import { AuthService } from './auth.service';
import { provideHttpClient } from '@angular/common/http';
import { provideHttpClientTesting, HttpTestingController } from '@angular/common/http/testing';
describe('AuthService', () => {
let service: AuthService;
let httpMock: HttpTestingController;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
AuthService,
provideHttpClient(),
provideHttpClientTesting() // Remplace HttpClientTestingModule (déprécié)
]
});
service = TestBed.inject(AuthService);
httpMock = TestBed.inject(HttpTestingController);
});
afterEach(() => {
// Vérifie qu'aucune requête non gérée ne reste en suspens
httpMock.verify();
});
it('devrait retourner un token après login', () => {
service.login('user@test.com', 'password').subscribe(res => {
expect(res.token).toBe('jwt-token-mock');
});
// Simule la réponse du serveur
const req = httpMock.expectOne('/api/auth/login');
expect(req.request.method).toBe('POST');
req.flush({ token: 'jwt-token-mock' });
});
});
declarations par imports et utilise provideHttpClientTesting() à la place de HttpClientTestingModule (déprécié depuis Angular 18).
Accessibilité et responsive Bootstrap 4
La migration vers standalone ne doit pas dégrader l'accessibilité ni la mise en page responsive. Voici les points de vigilance spécifiques à cette migration.
Points de vigilance accessibilité lors de la migration
- Vérifier que les composants migratés conservent leurs attributs
aria-label,roleetaria-live. - Les modales et drawers doivent conserver le focus trap — tester au clavier après migration.
- Les messages d'erreur de formulaire doivent rester liés aux champs via
aria-describedby. - Tester avec un lecteur d'écran (NVDA, VoiceOver) les flux les plus critiques.
- Les lazy-loaded components doivent annoncer le changement de page via
aria-live="polite".
Exemple : composant formulaire accessible et standalone
// Formulaire de contact standalone avec bonnes pratiques accessibilité
import { Component } from '@angular/core';
import { ReactiveFormsModule, FormBuilder, Validators } from '@angular/forms';
import { NgIf } from '@angular/common';
@Component({
selector: 'app-contact-form',
// standalone implicite en Angular 19
imports: [
ReactiveFormsModule, // Formulaires réactifs — import direct, sans NgModule
NgIf // Ou utiliser @if du nouveau control flow
],
template: `
<form [formGroup]="form" (ngSubmit)="onSubmit()" novalidate>
<div class="form-group">
<!-- label explicitement lié au champ via for/id -->
<label for="email" class="fw-bold">Adresse email *</label>
<input
id="email"
type="email"
formControlName="email"
class="form-control"
[class.is-invalid]="form.get('email')?.invalid && form.get('email')?.touched"
aria-required="true"
aria-describedby="email-error"
placeholder="vous@exemple.fr"
/>
<!-- Message d'erreur lié au champ via aria-describedby -->
<div
id="email-error"
class="invalid-feedback"
role="alert"
*ngIf="form.get('email')?.invalid && form.get('email')?.touched"
>
Veuillez saisir une adresse email valide.
</div>
</div>
<button
type="submit"
class="btn btn-primary"
[disabled]="form.invalid"
aria-label="Envoyer le formulaire de contact"
>
Envoyer
</button>
</form>
`
})
export class ContactFormComponent {
// Injection via inject() — pattern Angular moderne sans constructeur
private fb = inject(FormBuilder);
form = this.fb.group({
email: ['', [Validators.required, Validators.email]]
});
onSubmit(): void {
if (this.form.valid) {
console.log('Formulaire soumis :', this.form.value);
}
}
}
Responsive Bootstrap 4 avec les composants standalone
Les composants standalone n'ont aucun impact sur le rendu Bootstrap 4. Les classes utilitaires et la grille fonctionnent exactement comme avant. La seule différence est que les composants Angular Material ou ng-bootstrap doivent être importés directement dans chaque composant standalone qui les utilise.
// Composant avec grille Bootstrap 4 — layout responsive standard
import { Component } from '@angular/core';
import { NgFor } from '@angular/common';
@Component({
selector: 'app-cards-grid',
imports: [NgFor],
template: `
<!-- Grille Bootstrap 4 responsive — col-12 mobile, col-md-6 tablette, col-lg-4 desktop -->
<div class="row">
<div
class="col-12 col-md-6 col-lg-4 mb-4"
*ngFor="let item of items; trackBy: trackById"
>
<div class="card h-100 shadow-sm">
<div class="card-body d-flex flex-column">
<h3 class="card-title">{{ item.title }}</h3>
<p class="card-text flex-grow-1">{{ item.description }}</p>
<a [href]="item.url" class="btn btn-outline-primary mt-auto"
[attr.aria-label]="'Lire : ' + item.title">
Lire l'article
</a>
</div>
</div>
</div>
</div>
`
})
export class CardsGridComponent {
items = [
{ id: 1, title: 'Angular 19', description: 'Standalone par défaut', url: '/posts/angular-19' }
];
// trackBy améliore les performances du NgFor en évitant les re-rendus inutiles
trackById(index: number, item: { id: number }): number {
return item.id;
}
}
@for), trackBy est remplacé par le paramètre track directement dans la syntaxe : @for (item of items; track item.id). Aucun import de NgFor n'est nécessaire.
Conclusion
Angular 19 marque un tournant décisif : le standalone n'est plus une option expérimentale mais le mode par défaut. En rendant standalone: true implicite, en supprimant la génération de NgModules par le CLI, et en fournissant un schematic de migration automatique en 3 étapes, Angular simplifie radicalement l'architecture des nouvelles applications et facilite la modernisation des projets existants.
La transition vers bootstrapApplication(), loadComponent() et les fonctions provide*() peut sembler importante au premier abord, mais elle suit une logique cohérente : chaque artefact est autonome, les dépendances sont explicites, et les tests sont plus simples à configurer. Commence par migrer les composants les moins critiques, valide avec des tests, puis étends progressivement à l'ensemble de l'application.
standalone: false explicite est standalone par défaut. Le NgModule n'est pas supprimé du framework — il reste disponible — mais il n'est plus la voie recommandée pour les nouvelles applications ni pour les projets en cours de modernisation.