logo le blog invivoo blanc

Devoxx “Angular : le renouveau”

19 septembre 2024 | Java | 0 comments

Angular, héritier d’AngularJS, est un framework de développement des single page applications (SPA) développé et maintenu par Google. Le passage de la version v1 d’AngularJS à la version v2 en 2016 a été marqué par une réécriture complète du framework donnant naissance à Angular 2 ou tout simplement Angular. Depuis, Angular sort régulièrement des versions majeures apportant des améliorations aux framework. En novembre 2023, la version v17 est sortie avec de nouvelles fonctionnalités significatives. Lors de la Devoxx Paris 2024, Amadou Sall, Google Developer Expert en Angular, nous a présenté à travers sa conférence intitulée « Angular : le renouveau » le nouvel aspect du framework.

Dans cet article je vous propose de revenir sur quelques nouveautés du framework et de partager avec vous quelques notes de la présentation.

Pourquoi Angular ?

Angular est le framework officiel chez Google, il met à disposition une plateforme solide de développement et offre plusieurs outils, API et bibliothèques simplifiant le développement frontend. Il permet de développer des applications rapides et robustes qui se mettent à l’échelle à la fois en taille d’équipe et en code source.

Parmi les principales briques d’Angular, nous pouvons citer :

  • Angular Material : bibliothèque de composants graphiques implémentant le design system chez Google, Material Design
  • Angular CDK : un kit de développement de composants graphiques personnalisés et réutilisables
  • Angular Animation : une bibliothèque facilitant l’ajout des animations pour les composants
  • Client http : un client pour la communication avec le backend
  • Router : un module de routage pour la navigation
  • Formulaire : deux modules de gestion de formulaire, template driven form et reactive form
  • I18n : l’internationalisation
  • Language service : permet l’intégration d’Angular avec les différents IDEs
  • Angular CLI: un client Angular en ligne de commande

Angular est :

  • Un projet open source avec une forte communauté de développeurs et une grande quantité de ressources en ligne
  • Une plateforme robuste : tous les changements déclenchent une batterie de tests réduisant ainsi le risque de bug
  • Une plateforme evergreen : Angular sort une nouvelle version chaque 6 mois qui est maintenue environ 18 mois. Il offre également un utilitaire pour faciliter la montée de version, ng-update

Concernant les points faibles d’Angular, nous distinguons les points suivants :

  • Angular est un framework dogmatique : il limite les alternatives de développement en offrant souvent une unique approche standard à suivre
  • Angular est un framework inadapté aux sites nécessitant une indexation pour le référencement
  • Une courbe d’apprentissage souvent jugée élevée
  • Un outillage fermé

Nouveautés Angular ?

Component Standalone

Dans les premières versions d’Angular, les composants devaient être rattachés à un module pour être utilisables. Livré dans la version 15 (en preview dans la v14), il est depuis possible de déclarer les composants, les directives et les pipes ‘standalone’. Cela a pour objectif de les centraliser dans l’application, plus besoin d’utiliser un module pour les embarquer. Cela améliore considérablement l’expérience développeur et facilite l’intégration des nouveaux composants. Désormais, un composant peut être déclaré standalone, se comportant ainsi comme un module et important lui-même ses dépendances. L’ajout du flag standalone dans le décorateur @Component permet de déclarer le composant auto-suffisant et l’utilisation de la propriété imports permet de préciser ses dépendances :

@Component({
  standalone: true,
  imports: [JsonPipe],
  ..
})

De plus, pour faciliter la migration progressive, les composants, directives et pipes ainsi déclarés ‘standalone’ peuvent cohabiter avec les modules existants, mais il est également possible de développer une application entièrement constituée de composants standalone. La fonction bootstrapApplication() permet de démarrer directement l’application depuis un composant sans passer par un module.

import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';

bootstrapApplication(AppComponent);

Pour l’injection des dépendances au niveau de l’application, une liste de fonctions (provide-prefixed) a été ajoutée permettant de configurer explicitement les providers sans passer par l’import des modules :

bootstrapApplication(MyAppComponent, {
  providers: [
    {provide: BACKEND_URL, useValue: 'https://../api'},
    provideRouter([/* app routes */]),
    // ...
  ]
}); 

Concernant les bibliothèques, n’ayant pas encore de fonction provide like, nous pouvons utiliser la fonction importProvidersFrom :

import {LibraryModule} from 'ngmodule-based-library';

bootstrapApplication(PhotoAppComponent, {
  providers: [
    {provide: BACKEND_URL, useValue: 'https://../api'},
    importProvidersFrom(
      LibraryModule.forRoot()
    ),
  ]
});

L’API Router a été également mise à jour pour supporter le chargement tardif des composants standalone, à titre d’exemple, un composant standalone peut être également chargé tardivement sans passer par un module :

export const ROUTES: Route[] = [
  {path: 'admin', loadComponent: () => import('./admin/admin.component').then(mod => mod.AdminComponent)},
  // ...
];

Angular fournit également un schematic qui permet de simplifier la migration des projets existants vers la nouvelle API standalone :

ng generate @angular/core:standalone

Contrôle de flux

La version 17 d’Angular a introduit un nouveau mécanisme améliorant la gestion de flux de contrôle.

ngIf

Dans les versions précédentes, Angular se basait sur la syntaxe suivante pour l’affichage conditionnel d’une partie de la vue via la directive structurelle ngIf avec un else optionnel :

<div *ngIf=’condition; else elseTemplate’>
…
</div>
< ng-template #elseTemplate>
…
</ng-template>

Désormais, la nouvelle syntaxe est comme suit :

<@if (condition1) { 
…	
}@else if (condition2) {
…
}@else {
…
}

La déclaration d’un alias est toujours possible avec cette syntaxe :

<@if (items$ | async; as items) { {{ items.length }} }

Ce code est plus lisible mais permet aussi d’améliorer la performance et de s’affranchir de l’import du ngIf directive (ou CommonModule pour les versions précédentes) réduisant ainsi la taille finale du package.

ngSwitch

Angular a également apporté une nouvelle syntaxe pour les switchs. L’ancienne syntaxe de switch utilisée comme suit :

<ng-container [ngSwitch]="tab">
  <div *ngSwitchCase="1">Content 1</div>
  <div *ngSwitchCase="2">Content 2</div>
  <div *ngSwitchDefault>Default</div>
</ng-container>

a été remplacée par la syntaxe suivante :

@switch (tab) {
  @case (1) {
    <div>Content 1</div>
  }
  @case (2) {
    <div>Content 2</div>
  }
  @default {
    <div>Default</div>
  }
}

Discriminated Union

Il s’agit de discriminer le type se basant sur un attribut commun à l’union de types, comme dans l’exemple suivant :

export type Response = {status: ‘ok’, data: string} | {status: ‘ko’, error: string}

Typescript permet d’inférer le bon type à la suite d’une vérification du discriminant, comme précisé dans l’exemple suivant :

response: Response;
…
if (response.status === ‘ok’) {
	console.log(response.data); // Typescript reconnaît l’attribut data
}

En revanche, Angular, au niveau template, n’infère pas le bon type :

<ng-container [ngSwitch]="response.status">
  <div *ngSwitchCase="’ok’">Data: {{response.data}}</div> <!-- error -->
  ...
</ng-container>

Habituellement, pour remédier à ce problème, nous utilisons le cast au niveau template comme suit :

<ng-container [ngSwitch]="response.status">
  <div *ngSwitchCase="’ok’">Data: {{$any(response).data}}</div>
  …
</ng-container>

La nouvelle syntaxe supporte l’inférence de type. Ainsi, le code suivant s’exécute sans problème :

@switch (response) {
  @case (‘ok’) {
    <div>Data: {{response.data}}</div>
  }
  @case (‘ko’) {
    <div>Error: {{response.error}}</div>
  }
  @default {
    <div>Default</div>
  }
}

ngFor

De même Angular a introduit une nouvelle syntaxe pour gérer les boucles dans la vue. La syntaxe suivante utilisée dans les versions antérieures :

<li *ngFor="let item of items">{{ item.name }}</li>

a été remplacée par celle-ci (avec un bloc @empty optionnel) :

@for (item of items; track item.name) {
  <li>{{ item.name }}</li>
} @empty {
  <li>There are no items</li>
}

Il est à noter que la propriété track est devenue obligatoire.

En plus, plusieurs variables implicites sont maintenant disponibles tels que $index, $even…

@for (item of items; track item.name) {
  <span [class]=”{‘bg-odd’: $odd, ‘bg-even’: $even}”> {{ item.name }}</span>
}

De même, avec la nouvelle syntaxe, plus besoin d’importer le module NgForOf (ou CommonModule).

Change detection

Zone.js est une bibliothèque qui, via du monkey patching, change les APIs asynchrones du navigateur et permet d’intercepter les événements et déclencher les traitements associés. Angular se base sur Zone.js pour mieux :

  • Détecter les changements
  • Gérer les tâches asynchrones
  • Gérer les erreurs

Default change detection

Par défaut, à la suite d’un événement, Angular parcourt l’arborescence des composants de la racine et vérifie chaque composant lors de chaque change detection.

On push change detection

Avec cette stratégie, Angular ne met à jour que les composants pour lesquels l’une des conditions suivantes est remplie :

  • Une référence d’un @Input du composant a changé
  • Un événement dans le composant ou sa sous arborescence a été émis
  • Le composant est marqué explicitement dirty
  • Async lié au template a émis une nouvelle valeur

Inconvénient de Zone.js

L’utilisation de zone.js par Angular présente quelques limitations :

  • Manque de précision : quelque chose a changé mais quoi ?
  • Change detection exécuté trop souvent
  • Dépendance externe (100kb)
  • Monkey patching d’API (présente des limitations, async/await est modifié en Promize pour pouvoir être patchés)
  • ExpressionChangedAfterItHasBeenCheckedError souvent incompréhensible

Les signaux

Pour dépasser les limitations de zone.js et améliorer à la fois la performance et l’expérience développeur, Angular a introduit dans la version 18 les signaux comme nouvelle primitive réactive.

« Un signal est un objet qui enveloppe une valeur et qui notifie ceux qui en dépendent lorsque celle-ci change »

Let a = signal(1) ;
Let b = signal(2) ;
Let c = computed(()=> a() + b());
Console.log(c()); // 3
a.set(5);
console.log(c()); // 7

L’utilisation du signal dans un composant Angular se traduit comme suit :

title: signal(‘ng new’);
<span> {{ title() }}

De plus, le signal est générique, nous pouvons préciser le paramètre du type :

title = signal<string>(‘ng-teams');

La modification du signal s’effectue, comme suit :

this.title.set(‘’); // mise à jour
this.title.update(value => value + ‘’); // mise à jour en utilisant l’ancienne valeur

Signal dérivé

Un signal dérivé est un signal dont la valeur est dérivée et basée sur d’autres signaux. La déclaration d’un signal dérivé se fait en utilisant le mot clé computed.

count = computed(()=> this.title().length);

Le signal est lazy, son code ne s’exécute qu’à la première utilisation et le résultat est caché (memoized) et ne change que si les signaux d’origine changent.

Effect

Pour pouvoir réagir au changement d’un signal (ou plusieurs), nous pouvons ajouter un effet secondaire, pour cela nous utilisons un Effect, par exemple : 

effect(()=> { 
  console.log(‘val = ‘, this.count());
});
Signal Input

Aujourd’hui, Angular supporte les inputs de type signal dans les composants. Nous pouvons déclarer les signal inputs comme suit :

  • Un signal input optionnel :
title = input<string>() ;
  • Un signal input obligatoire :
title = input.required<string>() ;
  • L’ajout d’alias :
value = input<string>(‘Exemple’, {alias : ‘title’}) ;
  • L’ajout d’un transformateur
value = input<number>(1, {transform: (value: number)=> value * 1000}) ;

L’utilisation du signal depuis le template se résume tout simplement à l’appel du signal comme suit :

<span> title : {{ title() }} </span>

Pour le passage du paramètre de type signal, la propriété passée en input reste de type simple.

<app-my-comp [title]= “input signal” />
Signal Output

Concernant les outputs, Angular, pour des besoins de cohérence a introduit une nouvelle API output. Cette dernière n’est pas basée sur les signaux :

titleChange = output<string>();

 Par défaut, les signal inputs sont en lecture seule, pour pouvoir modifier un signal input, nous pouvons utiliser les models :

title = model<string>() ;
<app-my-comp [(title)]= “title” />

Signaux et RxJS

Les signaux et RxJS se complètent, nous pouvons utiliser les signaux pour la gestion d’état au niveau des composants et continuer à utiliser les observables pour la gestion des événements.

La transformation d’un observable en signal s’effectue en utilisant la méthode toSignal, comme suit :

title$ = new Subject<string>();
$title = toSignal(this.title$);


Un signal contient toujours une valeur, undefined si non fournie. L’affectation de la valeur initiale du signal
est comme suit :

$title = toSignal(this.title$, initialValue: ‘’);

Le passage d’un signal vers un observable est également possible via la méthode toObservable :

title$ = $title.toObservable();

En général, le passage d’un observable vers un signal est utile mais l’inverse a moins de sens.

Conclusion

Angular poursuit son évolution en mettant l’accent sur la performance et l’expérience développeur. Les nouvelles évolutions récemment ajoutées améliorent considérablement le framework et simplifient le développement des applications réactives. Ainsi, le nouveau Angular continue à figurer parmi les frameworks les plus populaires et largement utilisés pour le développement frontend.