Angular 19/20 : standalone par défaut, fin des NgModules

🏷️ Front-end 📅 17/04/2026 14:00:00 👤 Mezgani said
Angular Angular 19 Angular 20 Standalone Components Ngmodules Migration Bootstrapapplication Angular 19 Nouveautés
Angular 19/20 : standalone par défaut, fin des NgModules

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 {}
A retenir : dans Angular 19+, omettre 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
    }
  }
}
Note : si tu travailles sur un projet Angular 15-18 existant avec des NgModules, ajouter "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();
  }
}
A retenir : depuis Angular 19, 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.

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
Note : certains NgModules ne peuvent pas être supprimés automatiquement — par exemple les modules qui exportent des services tiers ou qui configurent des bibliothèques externes (Material, NgRx). Le schematic les laisse en place et t'en informe dans la sortie console.

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));
A retenir : exécute les 3 passes dans l'ordre indiqué. Chaque passe doit être validée par un 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[] = [];
}
Conseil : en Angular 17+, préfère le nouveau control flow (@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
    )
  ]
};
A retenir : chaque 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 {}
Note : 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)
    )
  ]
};
A retenir : avec 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' });
  });
});
A retenir : dans les tests standalone, remplace 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, role et aria-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;
  }
}
Note : avec Angular 17+ et le nouveau control flow (@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.

A retenir : en Angular 19+, un composant sans 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.