Introduction
Angular est un framework open-source pour créer des applications web modernes et performantes, avec une gestion efficace des données et une expérience utilisateur fluide.
🚀 Depuis le 20 novembre 2024, Angular 19 est enfin disponible apportant une multitude de nouvelles fonctionnalités et d'améliorations pour optimiser le développement et les performances de vos applications. Découvrez linkedSignal
, resource
, l'hydratation incrémentale et bien plus encore. Plongez dans cet aperçu complet pour découvrir les toutes dernières mises à jour et apprendre comment elles peuvent propulser vos projets à un niveau supérieur !
Standalone par défaut
Avec la sortie d'Angular 19, une nouvelle fonctionnalité majeure a été introduite : les composants en standalone par défaut. Cette notion de standalone, introduite dans la version 14 et stable dans la version 15, vise à simplifier la gestion des dépendances et à améliorer la modularité des applications Angular. Un composant standalone est par définition un composant qui ne dépend pas d'un module Angular pour fonctionner. Il peut être utilisé directement sans avoir besoin de l'importer dans un module.
Avec ceci est introduite l'option de configuration strictStandalone à placer dans angularCompilerOptions. Par défaut strictStandalone est à false.
"angularCompilerOptions": {
"strictStandalone": true,
}
Une erreur est générée à la compilation lorsqu'un composant, une directive ou un pipe n'est pas défini en standalone.
Imports inutilisés dans les composants standalone
Marre des imports inutilisés qui encombrent votre code ? Bonne nouvelle ! Angular 19 introduit une fonctionnalité pour signaler ces imports superflus dans les composants standalone :
Si, pour une raison ou une autre, vous préférez désactiver cette vérification, c'est tout à fait possible. Il vous suffit de mettre à jour votre fichier angular.json
, ou tsconfig.json
, comme suit :
"extendedDiagnostics": {
"checks": {
"unusedStandaloneImports": "suppress"
}
}
Variables locales dans les templates
Introduite en developer preview avec Angular v18.1, la syntaxe @let
a été largement adoptée par la communauté. Angular passe désormais cette fonctionnalité en stable. Pour en savoir plus sur cette fonctionnalité et ses cas d'usage, consultez l'article détaillé de Nicolas :
Découvrez le linkedSignal
Le linkedSignal
, en phase expérimentale, est une nouvelle fonctionnalité qui permet de créer des signaux réactifs liés entre eux. Un signal, dans ce contexte, est une source de données réactive qui peut être observée par d'autres parties de l'application. Lorsqu'un signal change, toutes les parties de l'application qui dépendent de ce signal sont automatiquement mises à jour. Le linkedSignal
permet de lier plusieurs signaux ensemble, facilitant ainsi la gestion des états complexes et des dépendances entre les composants. De plus, unlinkedSignal
peut être défini de manière concise. Par exemple :
multiplier = signal<number>(1);
result = linkedSignal<number>(() => this.multiplier() * 5);
console.log(this.result()); // Affiche 5 (1 * 5)
this.result.set(999);
console.log(this.result()); // Affiche 999
this.multiplier.set(2);
console.log(this.result()); // Affiche 10 (2 * 5)
Avant l'introduction de linkedSignal
, Angular proposait déjà des fonctionnalités réactives comme computed
. Elles recalculent leur valeur en fonction des autres signaux auxquels elles sont liées. Cependant, les computed
sont des signaux en lecture seule, ce qui signifie que leur valeur ne peut pas être directement modifiée par l'utilisateur.
En revanche, le linkedSignal
est un WritableSignal
, ce qui signifie qu'il peut être à la fois lu et écrit. Cela offre une plus grande flexibilité, car les développeurs peuvent non seulement observer les changements de valeur, mais aussi les déclencher directement. Cette caractéristique rend le linkedSignal
particulièrement utile pour les scénarios où les états doivent être à la fois réactifs et modifiables.
Ci-dessous un exemple d'utilisation :
@Component({
selector: 'app-fruits',
imports: [FormsModule, MatFormFieldModule, MatSelectModule, MatButtonModule],
templateUrl: './fruits.component.html',
styleUrl: './fruits.component.scss'
})
export class FruitsComponent {
availableFruits = signal<Fruit[]>(randomFruitsArray());
selectedFruit = linkedSignal<Array<Fruit>, number | undefined>({
source: this.availableFruits,
computation: (fruits, previous) => {
return fruits.some(fruit => fruit.id === previous?.value) ? previous?.value : fruits[0].id;
}
});
changeAvailableFruits() {
this.availableFruits.set(randomFruitsArray());
}
}
Comme on peut le voir ici, on dispose d'un premier signal availableFruits contenant un tableau de fruits aléatoires. Le linkedSignal
selectedFruit est configuré avec deux paramètres principaux :
- source : La source de données réactive, ici availableFruits.
- computation : Une fonction qui détermine la valeur du signal en fonction de la source et de la valeur précédente. Dans ce cas, la fonction vérifie si l'ID du fruit sélectionné précédemment est toujours présent dans le nouveau tableau de fruits. Si oui, il conserve cet ID, sinon il sélectionne l'ID du premier fruit dans le nouveau tableau.
Ainsi on a toujours la main sur la valeur de selectedFruit si on souhaite la mettre à jour manuellement, contrairement au computed
.
Hydratation incrémentale
L'hydratation incrémentale est une technique qui permet de rendre progressivement interactif le contenu d'une application Angular rendue côté serveur (SSR). Traditionnellement, lorsque vous utilisez le SSR, l'application Angular génère une version statique de votre page HTML sur le serveur et l'envoie au client. Ensuite, Angular "hydrate" cette page en ajoutant les comportements interactifs nécessaires, ce qui peut être coûteux en termes de performance pour les grandes applications.
Avec l'hydratation incrémentale, Angular 19 permet d'hydrater seulement les parties de la page qui sont visibles ou nécessaires à l'interaction immédiate, plutôt que d'hydrater toute la page en une seule fois. Cela permet de réduire le temps de chargement initial et d'améliorer la réactivité de l'application.
Pour activer l'hydratation incrémentale il suffit d'ajouter la fonction withIncrementalHydration()
au provider provideClientHydration()
.
import { ApplicationConfig } from '@angular/core';
import { provideClientHydration, withIncrementalHydration } from '@angular/platform-browser';
export const appConfig: ApplicationConfig = {
providers: [
provideClientHydration(withIncrementalHydration())
]
};
Cette technique repose sur l'utilisation de l'annotation @defer
et d'un nouveau déclencheur hydrate
. Ce dernier indique à Angular de charger à la volée le code nécessaire ainsi que ses dépendances pour "hydrater" le composant. Une fois hydraté, Angular rejoue les événements qui se sont produits pendant que le composant était "déshydraté", donnant ainsi l'impression que le composant était déjà interactif depuis le début. Cette méthode utilise la librairie d'envoi d'événements développée par Google Search (Wiz). Nous l'aborderons dans la section suivante. Les déclencheurs d'hydratation sont les mêmes que les conditions @defer
, c'est-à-dire :
Trigger | Description |
---|---|
hydrate on hover |
Se déclenche lorsque la souris survole une zone spécifiée |
hydrate on timer |
Déclenche après une durée donnée |
hydrate on interaction |
Déclenche lorsque l'utilisateur interagit avec l'élément |
hydrate on immediate |
Se déclenche immédiatement après la fin du rendu du contenu non différé |
hydrate on idle |
Se déclenche lorsque le navigateur a atteint un état inactif |
hydrate on viewport |
Se déclenche lorsque le contenu spécifié entre dans la fenêtre d'affichage |
hydrate when {condition} |
Déclenche lorsque l'expression conditionnelle devient vraie |
hydrate never |
Spécifique que le contenu du bloc doit rester "déshydraté", devenant ainsi complètement statique |
Event replay
La fonctionnalité Event Replay, introduite avec Angular 18, est désormais pleinement stable. Lors de la création d'une nouvelle application SSR, la CLI intègre automatiquement l’appel à withEventReplay()
requis pour activer cette fonctionnalité.
Grâce à cette fonctionnalité, les événements utilisateur déclenchés pendant la phase d’hydratation sont capturés et rejoués une fois que l’application est totalement chargée. Cela garantit une continuité des interactions utilisateur, éliminant le risque de perte d’actions durant la transition entre le rendu côté serveur et l’interactivité complète côté client.
Routing et modes de rendu
Le rendu hybride, une nouveauté majeure d'Angular 19, offre la possibilité de configurer des stratégies de rendu personnalisées pour chaque route lorsque le rendu côté serveur est activé. Cette fonctionnalité vous permet d'ajuster le comportement de rendu de vos pages en fonction de leurs besoins spécifiques, améliorant ainsi les performances et l'expérience utilisateur. Les modes de rendu disponibles sont les suivants :
RenderMode.Server
: Génère la page côté serveur à chaque requête pour une sécurité optimaleRenderMode.Client
: Génère la page côté client pour une meilleure expérience utilisateurRenderMode.Prerender
: Génère la page lors du build
Voici un exemple qui utilise tous les modes de rendu :
export const serverRoutes: ServerRoute[] = [
{
path: '/fruits/details/:id',
renderMode: RenderMode.Server
},
{
path: '/dashboard',
renderMode: RenderMode.Client
},
{
path: '/fruits/:id',
renderMode: RenderMode.Prerender,
async getPrerenderParams() {
const fruitService = inject(FruitService);
const fruitIds = await fruitService.getFruitIds();
return fruitIds.map(id => ({ id }));
}
},
{
path: '**',
renderMode: RenderMode.Prerender
}
];
Pour les routes configurées avec RenderMode.Prerender
, il est possible d'utiliser une fonction appelée getPrerenderParams
pour définir les paramètres à pré-rendre. Cette fonction retourne une promesse qui fournit un tableau d'objets, où chaque objet spécifie un nom de paramètre et sa valeur. Par exemple, pour une route définie comme /fruits/:id
, la fonction getPrerenderParams
pourrait renvoyer un tableau tel que [{id: 1}, {id: 2}, {id: 3}]
. Cela permettrait à Angular de générer les pages statiques correspondantes pour les chemins /fruits/1
, /fruits/2
et /fruits/3
.
Resource
Angular introduit une nouvelle API en phase expérimentale, resource
, pour nous permettre de récupérer des données à partir d'une API, de connaître l'état de la demande et de mettre à jour les données localement si nécessaire. Voyons un exemple simple de son utilisation :
import { Component, effect, resource } from '@angular/core';
@Component({ ... })
export class ResourceComponent {
todos = resource<Todo[], unknown>({
loader: async () => {
const todos = await fetch(`${TODO_API_URL}`);
return todos.json();
}
});
constructor() {
effect(() => {
console.log("Value: ", this.todos.value());
console.log("Is loading: ", this.todos.isLoading());
console.log("Status: ", this.todos.status());
console.log("Error: ", this.todos.error());
})
}
}
Elle nous renvoie un WritableResource
contenant les signaux suivants :
isLoading
: indique si la ressource est en cours de chargement ;value
: contient le résultat de la promise ;error
: contient l’erreur si la promise est rejetée ;status
: contient l'état de la ressource.
Côté template cela donnerait ceci :
<mat-card class="resource-discover">
<mat-card-header>
<mat-card-title><h2>Resource discover - {{ todos.status() }}</h2></mat-card-title>
</mat-card-header>
<mat-card-content>
@if (todos.error()) {
<div class="error">{{ todos.error() }}</div>
}
@if (todos.isLoading()) {
<mat-progress-bar mode="query"/>
}
<mat-list>
<div mat-subheader>Todos</div>
@for (todo of todos.value(); track todo.id) {
<mat-list-item>
<mat-icon matListItemIcon>{{ todo.completed ? 'task_alt' : 'cancel' }}</mat-icon>
<div matListItemTitle>{{ todo.title }}</div>
</mat-list-item>
}
</mat-list>
</mat-card-content>
</mat-card>
Il y a une chose importante à savoir sur l'utilisation de la fonction fetch
avec la resource
. Il ne considère pas les codes de statut comme 404 ou 500 pour des erreurs, il faut donc throw une erreur manuellement :
todos = resource<Todo[], unknown>({
loader: async () => {
const todos = await fetch(`${TODO_API_URL}/does-not-exist`);
if (!todos.ok) {
throw new Error(`Could not fetch : ${todos.url}`);
}
return todos.json();
}
});
Le signal status
dans l'API resource
peut correspondre à l'un des états suivants :
ResourceStatus.Idle
, l’état initial ;ResourceStatus.Error
, lorsque la promise est rejetée ;ResourceStatus.Loading
, lorsque la promise est en attente ;ResourceStatus.Resolved
, lorsque la promise est résolue ;ResourceStatus.Local
, lorsque la valeur est définie localement ;ResourceStatus.Reloading
, lorsque la ressource est en cours de rechargement.
Concernant le dernier statut, celui-ci est appliqué à l'appel de méthode reload
qui va relancer l'appel et mettre à jour les données :
<button mat-stroked-button (click)="todos.reload()">Reload todos</button>
Mais plutôt que de recharger manuellement la resource
, cette dernière peut prendre en charge un signal grâce à request
. Lorsqu'elle est fournie, la resource
se rechargera automatiquement si l'un des signaux utilisés dans la request
change. Ici, par exemple, je souhaite que la resource
actualise les données à chaque fois que todoQuery
change :
todoQuery = signal('');
todos = resource<Todo[], { todoQuery: string }>({
request: () => ({ todoQuery: this.todoQuery() }),
loader: async ({request}) => {
const todos = await fetch(`${TODO_API_URL}?title_like=^${request.todoQuery}`);
if (!todos.ok) {
throw new Error(`Could not fetch : ${todos.url}`);
}
return todos.json();
}
});
L'exemple ci-dessus peut encore être amélioré : à chaque caractère tapé dans le champ de recherche, une nouvelle requête est envoyée pour récupérer les tâches correspondantes. Ce qui peut entraîner des appels redondants et inutiles. Il est possible d'annuler la requête précédente si besoin, lorsque la resource
est rechargée, en utilisant abortSignal
en paramètre du loader
:
loader: async ({ request, abortSignal }) => {
const todos = await fetch(`${TODO_API_URL}?title_like=^${request.todoQuery}`, {
signal: abortSignal
});
...
}
Ici nous avons utilisé resource
qui se base sur les Promise, mais Angular fournit également un équivalent RxJS appelée rxResource
. Dans ce cas, la méthode loader
renvoie un Observable, mais toutes les autres propriétés restent des signaux.
todoQuery = signal('');
todosRx = rxResource<Todo[], { todoQuery: string }>({
request: () => ({ todoQuery: this.todoQuery() }),
loader: ({ request }) => {
return this._httpClient.get<Todo[]>(`${TODO_API_URL}?title_like=^${request.todoQuery}`);
}
});
HMR
Avec Angular v19, le remplacement de module à chaud (HMR) est désormais activé par défaut pour le style et disponible en phase expérimentale pour les templates, offrant une amélioration significative pour les développeurs. Avant cette mise à jour, chaque modification de style nécessitait une reconstruction complète de l'application via Angular CLI, suivie d'un rafraîchissement du navigateur. Ce processus pouvait s'avérer long et fastidieux, surtout pour les projets de grande envergure.
Désormais, HMR compile uniquement le style modifié, envoie le résultat directement au navigateur et met à jour l'application en cours sans rafraîchissement de la page ni perte d'état. Voici un exemple de modification de style pour l'élément <h1>
, où nous changeons la taille de la police à 4rem
et la couleur à deeppink
(parce que le rose, c'est cool). Vous remarquerez qu'il n'y a aucun rafraîchissement de la page lors des modifications !
Côté template, il faudra activer le HMR expérimentale avec la commande suivante :
NG_HMR_TEMPLATES=1 ng serve
Les signaux sont désormais stables (ou presque)
La plupart des signaux ne sont plus en developer preview et peuvent être utilisés sans risques. Les fonctions suivantes sont maintenant marquées comme stable :
input()
output()
model()
viewChild()
viewChildren()
contentChild()
contentChildren()
takeUntilDestroyed()
outputFromObservable()
outputToObservable()
Migrer vers ces nouvelles API peut représenter une charge de travail conséquente. Pour cela, Angular a mis à disposition les schematics suivants pour effectuer la migration complète :
ng generate @angular/core:signal-input-migration
ng generate @angular/core:signal-queries-migration
ng generate @angular/core:output-migration
De plus, il est possible d'exécuter toutes ces migrations en une seule fois :
ng generate @angular/core:signals
Juste avant je précisais que "presque" tous les signaux étaient stables, car ce n'est pas le cas pour effect
qui a subi quelques changements et est encore en phase de développement. Il existe désormais deux types d'effect
qui se distinguent par leur moment d'exécution et leur contexte:
- Component effects : créés quand
effect()
est appelé à partir de composants ou directives. Ils s'exécutent en tant qu'événements du cycle de vie du composant pendant le processus de synchronisation (détection des changements) d'Angular. - Root effects : créés quand
effect()
est appelé en dehors d'un composant ou lorsque l'optionforceRoot
est précisée et ne sont donc pas liés directement au cycle de vie des composants.
Un autre changement notable est que vous pouvez maintenant écrire dans les signaux depuis un effect
, sans avoir besoin de spécifier l'option (désormais obsolète) allowSignalWrites à true.
Angular Language Service
En plus des schematics, l'Angular Language Service a été mis à jour pour permettre d'effectuer les migrations depuis l'IDE. Pour rappel, un service linguistique fournit aux éditeurs de code un moyen d'obtenir des complétions, des erreurs, des diagnostics et des conseils fichier par fichier.
Conclusion
Angular 19 marque une étape importante dans l'évolution de ce framework, en apportant des améliorations qui renforcent sa performance, sa flexibilité et sa convivialité pour les développeurs. Parmi les points forts, on retrouve l'hydratation incrémentale, qui optimise le chargement des composants, et l'API resource()
, qui simplifie la gestion des données asynchrones tout en gérant automatiquement les états de chargement et d'erreur.
Angular continue de se positionner comme un outil de choix pour le développement web moderne, et cette version 19 laisse entrevoir un avenir encore plus prometteur avec des innovations qui répondent aux besoins croissants des développeurs. À votre tour de découvrir tout son potentiel dans vos projets ! 🚀