Je suis un peu en retard par rapport à la sortie d'Angular 15 qui est sorti le 16 novembre 2022, mais je prends le temps de vous faire un résumé de ce que je retiens de cette version ! J'en profite pour parler aussi d'Angular 15.1 qui est sorti entre-temps ! 😅

Goodbye NgModules !

Gros changement sur la manière de penser la modularité dans Angular : les standalones sont maintenant en version stable.

Que ce soit les composants, les directives ou les pipes, on peut tout créer en mode standalone. Ce que ça implique c'est qu'on a plus besoin de NgModule, tous ces éléments ont maintenant la possibilité d'être des modules directement.

Pour l'instant les NgModules ne sont pas dépréciés pour autant, mais ça pourrait venir dans le futur, sur ce point ce n'est pas encore très clair. Par contre dans l'idée on peut toujours créer des NgModules pour regrouper nos éléments même standalone pour faire des imports en grappe de plusieurs composants. Mais je pense qu'à l'avenir il faudra apprendre à se passer des NgModule.

Dans le même temps on y gagne sur plein de points. Rien que sur les tests où on aura plus besoin d'indiquer les dépendances explicitement quand on ne veut pas mocker, car la résolution est automatique pour les NgModules dans le TestBed. Comme chaque élément va définir ses dépendances, on pourra aussi plus facilement repérer les éléments qui ne sont plus utiles !

À noter aussi l'introduction d'un flag --standalone à la création des composants, directives et pipes pour directement les créer sans module.

Quelques changements dans le router

On pourra utiliser provideRouter() pour créer son router. Ça change peu de chose d'un point de vue développeur mais sous le capot on peut y gagner jusqu’à 11% de réduction de la taille du bundle, parce que le code devient plus facilement tree-shakable.

Dans le même temps une petite réduction de syntaxe a été ajouté : l'auto-unwrap du default pour les imports dynamiques. L'idée est simple si vous voulez faire un import dynamique, vous n'êtes plus obligé de faire quelque chose du genre .then(m => m.MyModule), vous pouvez juste l'omettre et Angular le fera pour vous. L'exemple ci-dessous l'utilise pour les lazyRoutes.

// lazy.router.ts
import {Routes} from '@angular/router';
import {LazyComponent} from './lazy.component';

export default const lazyRoutes: Routes = [{path: '', component: LazyComponent}];

// app.router.ts
export const appRoutes: Routes = [{
  path: 'lazy',
  loadChildren: () => import('./lazy/lazy.routes')
}];

// main.ts
bootstrapApplication(AppComponent, {
  providers: [
    provideRouter(appRoutes)
  ]
});

Dans le même temps le guard CanLoad est maintenant déprécié (15.1) en faveur de CanMatch qui est plus intéressant : il est activé à chaque navigation (comme CanActivate).

Côté HttpClient

On peut se passer d'HttpClientModule et utiliser provideHttpClient() (et provideHttpClientTesting() pour remplacer HttpClientTestingModule).

Au passage tout devient plus fonctionnel avec la possibilité d'écrire des interceptors sous forme de fonction. Par contre attention aux interceptors qui ne s'applique pas par défaut aux modules lazy loadé, il faut utiliser withRequestsMadeViaParent() pour que ça fonctionne !

import {
  provideHttpClient,
  withRequestsMadeViaParent,
  withInterceptors,
} from '@angular/common/http';

export default [
  {
    path: '',
    providers: [
      provideHttpClient(
        withRequestsMadeViaParent(),
        withInterceptors([
          (req, next) => {
            console.log('TodosComponent Interceptors');
            return next(req);
          },
        ])
      ),
    ],
    loadComponent() {
      return import('./todos/todos.component');
    },
  },
];

Directive Composition

@Component({
  selector: 'mat-menu',
  hostDirectives: [HasColor, {
    directive: CdkMenu,
    inputs: ['cdkMenuDisabled: disabled'],
    outputs: ['cdkMenuClosed: closed']
  }]
})
class MatMenu {}

L'exemple parle de lui-même je trouve : on peut maintenant composer des directives. Ici dans l'exemple on crée une directive MatMenu qui est "amélioré" (en anglais on voit "enhance") via HasColor et CdkMenu. HasColor est reprise intégralement : tous ses Input et Output, ainsi que la logique sera présente dans MatMenu sans rien faire de plus. CdkMenu est reprise partiellement, toute la logique est présente mais seulement une partie des Input est reprise.

Ouvrir cette possibilité ouvre beaucoup de possibilité de réutilisation de code ! Personnellement c'est quelque chose qui me manquait dans pas mal de cas, surtout qu'en React on peut utiliser des HOC (High Order Component) depuis très longtemps.

Cette fonctionnalité n'est disponible que si les directives qu'on passe en composition sont standalones.

Quelques changements plus anecdotiques

Goodbye useless ngOnInit()

Quand on génèrera un nouveau composant, on aura plus de méthode ngOnInit() auto-générée pour rien dans la vaste majorité des cas. Mais ça n'empêche pas de l'ajouter au besoin !

Self closing tag (15.1)

Un changement attendu par beaucoup de gens (en particulier les gens faisant aussi du React je pense). On peut maintenant écrire l'une ou l'autre des variantes :

<my-component />
<my-component></my-component>
<!-- work also with ng-content and ng-container -->
<ng-content />
<ng-container />

Version plus récente de TypeScript

À partir de la version 15 on peut utiliser la version 4.8.2+ de TypeScript, et même 4.9 pour la 15.1. Comme d'habitude quelques améliorations sur la compilation sans avoir à gérer quoi que ce soit.

~ déprécié dans Sass

En passant à la version 15 vous aurez potentiellement cette erreur qui apparaitra :

'src/styles.scss' imports '~font-awesome/scss/font-awesome' with a tilde. Usage of '~' in imports is deprecated.

Rien de grave, il suffit de retirer le ~ et tout devrait fonctionner comme avant. C'est une conséquence de l'utilisation d'une version plus moderne du compilateur Sass qui offre de bien meilleure performance mais par contre il faut repasser sur nos fichiers si vous n'utilisez pas le CLI pour la monter de version, car un schematics le fera pour vous automatiquement !

Sources :

Crédit photo : https://unsplash.com/fr/photos/YMMuClvvvWI