Front-end angularforall.com

- Angular 20 : toutes les nouveautés

Angular Angular 20 Nouveautes Front-End
Angular 20 : toutes les nouveautés

Découvrez les nouveautés d'Angular 20 : Signals stables, afterRenderEffect(), signal queries, améliorations des forms réactifs et nouvelles APIs CLI pour.

API Signals complète en stable

Angular 20 marque la stabilisation de l'intégralité de l'API des Signals. Le travail entamé avec Angular 16 (preview) est maintenant finalisé — toutes les primitives sont stables et recommandées sans restriction pour la production.

APIStable depuisDescription
signal(), computed()Angular 17Primitives de base
effect()Angular 17Effets de bord réactifs
input(), output()Angular 19Inputs/outputs basés sur signaux
model()Angular 19Two-way binding avec signaux
viewChild(), viewChildren()Angular 19Queries de template réactives
linkedSignal()Angular 20Signal dérivé modifiable (voir section suivante)
resource(), httpResource()Angular 20Requêtes async réactives
// Composant complet "Angular 20 style" — utilise toutes les APIs stables
import { Component, signal, computed, input, output, model, effect, viewChild } from '@angular/core';

@Component({
  selector: 'app-product-filter',
  standalone: true,  // standalone par défaut en Angular 20
  template: `
    <input [value]="searchQuery()" (input)="searchQuery.set($event.target.value)">
    @for (product of filteredProducts(); track product.id) {
      <app-product-card [product]="product" (select)="onSelect(product)" />
    } @empty {
      <p>Aucun résultat pour "{{ searchQuery() }}"</p>
    }
  `
})
export class ProductFilterComponent {
  // input() = signal en lecture seule
  products = input.required<Product[]>();
  // output() = remplacement EventEmitter
  productSelected = output<Product>();

  // Signal interne de recherche
  searchQuery = signal('');

  // computed() = signal dérivé, se recalcule quand products() ou searchQuery() change
  filteredProducts = computed(() =>
    this.products().filter(p =>
      p.name.toLowerCase().includes(this.searchQuery().toLowerCase())
    )
  );

  onSelect(product: Product) {
    this.productSelected.emit(product);
  }
}

linkedSignal() — signal dérivé modifiable

linkedSignal() crée un signal dont la valeur initiale est dérivée d'un autre signal, mais qui peut aussi être modifiée manuellement. C'est le chaînon manquant entre signal() (modifiable mais pas dérivé) et computed() (dérivé mais en lecture seule).

import { Component, signal, linkedSignal, input } from '@angular/core';

@Component({
  selector: 'app-paginated-list',
  template: `
    <button (click)="previousPage()">Précédent</button>
    <span>Page {{ currentPage() }} / {{ pageCount() }}</span>
    <button (click)="nextPage()">Suivant</button>
  `
})
export class PaginatedListComponent {
  items = input.required<Item[]>();
  pageSize = input<number>(10);

  // pageCount dépend de items() et pageSize()
  pageCount = computed(() => Math.ceil(this.items().length / this.pageSize()));

  // linkedSignal : valeur initiale dérivée mais modifiable par l'utilisateur
  // Quand items() change (nouvelle recherche), currentPage se remet à 1 automatiquement
  // Mais l'utilisateur peut aussi changer la page manuellement
  currentPage = linkedSignal(() => {
    void this.items();  // déclare la dépendance à items()
    return 1;           // valeur initiale/reset
  });

  previousPage() {
    if (this.currentPage() > 1) {
      this.currentPage.update(p => p - 1);  // modification manuelle
    }
  }

  nextPage() {
    if (this.currentPage() < this.pageCount()) {
      this.currentPage.update(p => p + 1);
    }
  }
}

// Autre cas d'usage : sélection qui se réinitialise quand la source change
@Component({ /* ... */ })
export class ItemSelectorComponent {
  availableItems = input<Item[]>([]);

  // Quand availableItems change, selectedItem se réinitialise à null
  // Mais l'utilisateur peut sélectionner un item manuellement
  selectedItem = linkedSignal<Item | null>(() => {
    void this.availableItems();
    return null;
  });
}
linkedSignal vs computed : computed() est strictement en lecture seule. linkedSignal() peut être réinitialisé par sa source ET modifié manuellement. Cas typiques : pagination qui se remet à 1 quand les données changent, sélection qui se vide quand la liste change, formulaire qui se réinitialise quand l'entité active change.

resource() et httpResource() stables

L'API resource() gère le cycle de vie complet d'une opération asynchrone réactive : déclenchement automatique quand les dépendances changent, états loading/error/success, et annulation automatique des requêtes périmées.

import { httpResource } from '@angular/core';  // pas HttpClient directement

@Component({
  selector: 'app-product-catalog',
  template: `
    @if (productsResource.isLoading()) {
      <app-skeleton-grid />
    } @else if (productsResource.error()) {
      <app-error [message]="productsResource.error()?.message" />
    } @else {
      @for (product of productsResource.value() ?? []; track product.id) {
        <app-product-card [product]="product" />
      }
    }
    <span>Total : {{ productsResource.value()?.length ?? 0 }} produits</span>
  `
})
export class ProductCatalogComponent {
  // Filtres utilisateur
  category = signal<string>('all');
  sortBy = signal<'price' | 'name' | 'date'>('name');
  page = signal(1);

  // httpResource : se relance automatiquement quand category(), sortBy() ou page() change
  // La requête précédente est annulée (AbortController)
  productsResource = httpResource<Product[]>(() => ({
    url: '/api/products',
    params: {
      category: this.category(),
      sort: this.sortBy(),
      page: String(this.page()),
      limit: '20'
    }
  }));

  // resource() pour des opérations async non-HTTP
  analyticsResource = resource({
    request: () => ({ productIds: this.selectedIds() }),
    loader: ({ request }) =>
      fetch(`/api/analytics?ids=${request.productIds.join(',')}`)
        .then(r => r.json())
  });
}

Revalidation et mutation avec resource()

// resource() supporte la revalidation manuelle et les mutations
const userResource = httpResource<User>(() => ({
  url: `/api/users/${userId()}`
}));

// Revalider (refetch) manuellement
async function refreshUser() {
  await userResource.reload();
}

// Mise à jour optimiste : modifier localement avant confirmation serveur
async function updateEmail(newEmail: string) {
  const current = userResource.value();
  if (!current) return;

  // Mise à jour optimiste immédiate
  userResource.set({ ...current, email: newEmail });

  try {
    await updateUserApi(userId(), { email: newEmail });
    await userResource.reload();  // sync avec le serveur
  } catch {
    userResource.set(current);    // rollback si erreur
  }
}

Zoneless change detection stable

Le mode zoneless était expérimental depuis Angular 18 sous le nom provideExperimentalZonelessChangeDetection(). Il est maintenant stable dans Angular 20 avec un nouveau nom. Zone.js n'est plus inclus dans les nouveaux projets générés par Angular CLI 20.

// app.config.ts — Angular 20 zoneless
import { ApplicationConfig } from '@angular/core';
import { provideZonelessChangeDetection } from '@angular/core';  // nouveau nom stable
import { provideRouter } from '@angular/router';

export const appConfig: ApplicationConfig = {
  providers: [
    provideZonelessChangeDetection(),  // remplace provideExperimentalZonelessChangeDetection()
    provideRouter(routes),
  ]
};

// angular.json — supprimer zone.js du polyfills
// AVANT (avec Zone.js) :
// "polyfills": ["zone.js"]
// APRÈS (zoneless) :
// "polyfills": []

// Compatibilité avec les bibliothèques tierces utilisant Zone.js :
// La plupart des bibliothèques populaires (NgRx, Angular Material, PrimeNG)
// ont été mises à jour pour fonctionner sans Zone.js en Angular 20.

Impact performances

// En mode zoneless, Angular ne fait la détection de changements que quand :
// 1. Un Signal est modifié (via .set() ou .update())
// 2. Une promesse async se résout dans un composant
// 3. Un event handler DOM est appelé via (click), (input), etc.
// 4. markForCheck() ou detectChanges() est appelé manuellement

// Gains typiques mesurés sur des applications Angular réelles :
// - Temps de bootstrap : -15 à -25%
// - CPU usage au repos : -40 à -60% (plus de polling Zone.js)
// - Mémoire : -5 à -10% (pas de patch des API navigateur par Zone.js)

Host directives améliorées

Les host directives (Angular 15+) permettent de composer des comportements réutilisables en attachant des directives à un composant depuis son décorateur. Angular 20 améliore leur ergonomie.

// Directive réutilisable : comportement de tooltip
@Directive({ selector: '[tooltip]', standalone: true })
export class TooltipDirective {
  tooltipText = input.required<string>({ alias: 'tooltip' });
  tooltipPosition = input<'top' | 'bottom' | 'left' | 'right'>('top');
  tooltipShown = output<boolean>();
}

// Composant qui compose plusieurs comportements via hostDirectives
@Component({
  selector: 'app-info-button',
  standalone: true,
  hostDirectives: [
    {
      directive: TooltipDirective,
      inputs: ['tooltip: helpText', 'tooltipPosition'],  // renommage d'input
      outputs: ['tooltipShown: helpVisible']             // renommage d'output
    },
    {
      directive: AnalyticsDirective,  // tracking automatique des clics
      inputs: ['analyticsEvent: trackAs']
    }
  ],
  template: `
    <button class="btn btn-outline-secondary btn-sm">
      <ng-content />
    </button>
  `
})
export class InfoButtonComponent {}

<!-- Utilisation : les inputs/outputs des host directives sont exposés -->
<app-info-button
  helpText="Aide contextuelle pour ce champ"
  tooltipPosition="right"
  trackAs="help_button_click"
  (helpVisible)="onHelpToggle($event)">
  ❓ Aide
</app-info-button>

Standalone par défaut et suppressions

Angular CLI 20 ne génère plus le flag standalone: true dans le décorateur car c'est désormais le défaut. Les NgModules sont toujours supportés mais marqués comme "legacy".

// ng generate component dashboard
// Angular 19 et avant :
@Component({
  selector: 'app-dashboard',
  standalone: true,  // explicitement requis
  imports: [CommonModule, RouterModule],
  template: `...`
})
export class DashboardComponent {}

// Angular 20 — standalone est le défaut, le flag est omis :
@Component({
  selector: 'app-dashboard',
  // pas de standalone: true — c'est implicite
  imports: [RouterLink, RouterOutlet],  // CommonModule non nécessaire (control flow intégré)
  template: `...`
})
export class DashboardComponent {}

// ng generate module legacy-module --no-standalone reste possible
// mais affiche un warning "NgModule is a legacy pattern"

Suppressions et dépréciations Angular 20

// Supprimé en Angular 20 :
// - platformBrowserDynamic() pour les apps basées sur NgModule (remplacé par bootstrapApplication())
// - AnimationsModule (import) → remplacé par provideAnimationsAsync()
// - HttpClientModule (import) → remplacé par provideHttpClient()
// - RouterModule.forRoot() → remplacé par provideRouter()
// - BrowserModule (dans les composants standalone) → n'est plus nécessaire

// Migration vers les providers fonctionnels
// AVANT (NgModule style) :
@NgModule({
  imports: [BrowserModule, HttpClientModule, RouterModule.forRoot(routes)],
  bootstrap: [AppComponent]
})
export class AppModule {}

// APRÈS (Angular 20) :
bootstrapApplication(AppComponent, {
  providers: [
    provideHttpClient(withInterceptors([authInterceptor])),
    provideRouter(routes, withPreloading(PreloadAllModules)),
    provideAnimationsAsync()
  ]
});

Angular DevTools — nouveaux inspecteurs

L'extension Angular DevTools (Chrome/Firefox) reçoit des améliorations majeures alignées avec les nouvelles fonctionnalités d'Angular 20.

Signals Inspector

// Le Signals Inspector affiche en temps réel :
// - La valeur courante de chaque signal dans un composant
// - Le graphe de dépendances entre signals (qui dépend de quoi)
// - Les effets actifs et quand ils ont été exécutés
// - Les linkedSignal et leur source

// Comment utiliser :
// 1. Ouvrir DevTools → onglet "Angular"
// 2. Sélectionner un composant dans l'arbre
// 3. Cliquer sur "Signals" pour voir tous les signaux du composant
// 4. Modifier une valeur de signal directement depuis DevTools pour tester
  • Signals Inspector : graphe de dépendances des signaux, modification en direct depuis DevTools
  • Hydration Debugger : colorisation des composants hydratés/non-hydratés, erreurs de mismatch SSR
  • Performance Profiler : temps de render par composant, détection des re-renders inutiles
  • DI Tree : visualisation de l'arbre d'injection, résolution des tokens par environnement injector
  • @defer Inspector : état (placeholder/loading/loaded/error) de chaque bloc defer dans l'application

Breaking changes Angular 20

Breaking changeImpactMigration
Zone.js non inclus par défaut (nouveaux projets)Projets existants non impactésOpt-in via "polyfills": ["zone.js"]
provideExperimentalZonelessChangeDetection() suppriméProjets angular 18-19 zonelessRenommer en provideZonelessChangeDetection()
platformBrowserDynamic() suppriméProjets NgModule bootstrapMigrer vers bootstrapApplication()
TypeScript < 5.5 non supportéProjets sur TS 5.4npm install typescript@5.6
Node.js < 20 non supportéCI sur Node 18Mettre à jour les runners CI

Migrer vers Angular 20 étape par étape

# Étape 1 : Mettre à jour Angular CLI, Core et autres packages Angular
ng update @angular/core @angular/cli @angular/common @angular/forms @angular/router

# Étape 2 : Migrer vers signal inputs (optionnel mais recommandé)
ng generate @angular/core:signal-input-migration

# Étape 3 : Migrer les @Output EventEmitter vers output() (optionnel)
ng generate @angular/core:output-migration

# Étape 4 : Migrer le control flow *ngIf/*ngFor vers @if/@for
ng generate @angular/core:control-flow

# Étape 5 : Migrer les composants vers standalone (si encore sur NgModules)
ng generate @angular/core:standalone

# Étape 6 : Migrer vers zoneless (optionnel, mais recommandé pour les nouveaux projets)
# Remplacer dans app.config.ts :
# provideExperimentalZonelessChangeDetection() → provideZonelessChangeDetection()
# Ou ajouter provideZonelessChangeDetection() et supprimer zone.js des polyfills

# Vérification finale
ng build --configuration=production  # s'assurer que le build passe
ng test                               # s'assurer que les tests passent
Stratégie de migration : Angular 20 conserve une rétrocompatibilité complète — aucune migration n'est obligatoire sauf les 5 breaking changes listés ci-dessus. Migrez progressivement en priorité les composants les plus actifs (gros composants, pages fréquemment visitées), pas tout en une seule PR.

Partager