Préface

Le but de ce document est d'aider à concevoir des applications angular organisée et qualitative. Pour atteindre ce but, ce document définit des principles directeurs, une organisation de fichiers, des conventions de développement.

Principles directeurs

Stack driven

Évitez de faire du code maison si vous pouvez utiliser des features de la stack/framework/librairies largement adoptées pour, au final, arriver à un résultat équivalent. Le framework et la communauté sont mature et fournissent déjà une solution aux problèmes commun de développement d'application.
Ne réinventez pas la roue !
Cela vous permettra d'économiser des heures de code, débogage, testing, documentation.

Une des clés pour produire un code durable est de normaliser l'utilisation des features du framework.

Gardez une logique commune et suivez des conventions clairement définies pour produire un code base intuitif et organisé.

Angular propose une série de features fondamentales au développement d'application web:

  • les components pour découper les éléments d'interface en unités autonomes et réutilisables.
  • le data binding pour afficher ou intéragir avec les valeurs.
  • Le property binding pour setter les attributs des composants dynamiquement.
  • l'event binding pour partager les données et les événements entre les composants.
  • les directives pour étendre les éléments du DOM.
  • les pipes pour appliquer des transformations à vos données de présentation.
  • le routing pour gérer le changement de route.
  • les services pour séparer la logique métier.

Beaucoup d'autres features clés sont disponibles:

  • les guards pour tester une condition avant le routage.
  • les résolvers pour charger des données avant le routage.
  • les modules pour découper l'application en morceaux distinct et controler leur chargement.
  • ...

Documentation: Introduction to Angular concepts
Documentation: Complete Angular Tutorial

KISS

On pourrait résumer ce principe en 1 phrase: Les solutions les plus simples sont souvent les meilleures.

Le principle KISS peut être traduit de plusieurs manières: 'Keep it Short and Simple', 'Keep it Simple and Straightforward', 'Keep It Simple and Stupid', ...
D'un point de vue programmation, un code simple se matérialise par un code clean.

Pour appliquer le principe KISS dans un projet Angular, voici quelques conseils :

  • Évitez l'abstraction prématurée du code. En d'autres mots, évitez de prédire les besoins futurs et limitez exclusivement l'abstraction aux parties réutilisables du code clairement identifiées.
  • Limitez la quantité de code répétitif Si nécessaire, refactorisez votre code en petit morceau réutilisable.
  • Privilégiez la lisibilité du code Déclarez des types et des noms explicites. Préférez une approche déclarative et non impérative. Factorisez votre code en méthodes/functions courte.
  • Documentez et testez votre code. Cela aidera les autres membres de l'équipe à comprendre votre code et augmentera la réutilisabilité.

Documentation: Keep It Simple, Stupid (KISS)

Try to be DRY

Privilégiez la réutisabilité du code sans sacrifier la clarté/lisibilité du code. Evitez d'avoir un code générique avec des conditions mutuellement exclusives. Cela rend le code complexe, difficile à tester et difficile à modifier.
Si vous êtes dans cette situation, alors refactorisez en plusieurs éléments distinct même si il y a une duplication partielle du code est très probablement une meilleur option.

Documentation: T-DRY (Try to be DRY)

Single Responsability Principle

Chaque morceau de code ne devrait se concentrer que sur une seule chose et le faire bien.

Application du principe SRP dans une application Angular :

  • Au niveau composant:
    • Gardez les composants petits
    • Suivez le pattern smart/dumb component pour séparez la logique de présentation, de la logique de gestion des états.
    • Factorisez vos composants en unité autonome pour les combiner/réutiliser.
    • Déléguez la logique complexe à des services
  • Au niveau service:
    • Divisez vos services en services spécifiques à un domaine.
    • Définissez des interfaces au niveau de vos services pour établir un contract clair. Cela facilite la mise en place de l'injection de dépendance et le testing.
  • Au niveau module:
    • Organisez votre application en features modules. Cela aura pour effet d'encapsuler ensemble des composants, des services et d'autres ressources associées à un domaine unique.
    • Utiliser un module dédié pour les composants partagés (ou les standalones components).

Component-Driven Development

Le développement piloté par composants (CDD) est une méthodologie de développement qui se concentre sur la création d'applications en créant et en assemblant des composants réutilisables.
Cette approche encourage la création de petits composants autonomes pouvant être facilements réutilisés et combinés pour créer des interfaces utilisateur plus complexes.

Documentation: Component Driven User Interfaces
Documentation: An overview of component driven development and atomic design principles

Smart/Dumb components

L'utilisation du pattern smart/dumb components dans un projet Angular contribue à avoir un code organisé car il sépare la gestion du state et le code de présentation. En séparant les responsabilités de présentation et de gestion des données, les composants dumb & smart adhèrent au principe de responsabilité unique.

Les composants dumb se concentrent uniquement sur la présentation et le rendu des éléments de l'interface utilisateur. Les composants de présentation sont conçus pour être réutilisables dans l'application. Ils reçoivent leurs données via les @Inputs et émettent des événements via @Output.

Tandis que les composants smart gèrent le state. Les composants containeurs sont de par nature plus spécifique à l'application. Ces composants joue alors le role de controlleur.

Documentation: Smart Components vs Presentational Components

La structure du projet

LIFT

La structure proposée s'appuie sur la recommandation officielle LIFT.
Do structure the application such that you can Locate code quickly, Identify the code at a glance, keep the Flattest structure you can, and Try to be DRY.
  • Locate:

    Organisez le code pour que la localisation du code soit intuitive, simple et rapide.

  • Identify:

    Nommez le fichier pour identifier instantanément ce qu'il représente et ce qu'il contient. Pour ce faire, le nom du fichier doit être descriptif. Evitez les fichiers mélangeant plusieurs composants, services, ...

  • Flat:

    Conservez la structure la plus à plat aussi longtemps que possible. Envisagez de créer des sous-dossiers lorsqu'un dossier atteint sept fichiers ou plus.

  • T-DRY (Try to be DRY):

    Respectez le principe DRY. Evitez toutefois de sacrifier la lisibilité juste pour suivre à la lettre le principe DRY.

L'arborescence des fichiers

  • app
    • core
      • constants
        • constant-a.ts
        • constant-b.ts
      • enums
        • enum-a.ts
        • enum-b.ts
      • guards
        • auth.guard.ts
        • module-import.guard.ts
        • no-auth.guard.ts
      • helpers
        • helper-a.ts
        • helper-b.ts
      • interceptors
        • token.interceptor.ts
        • error.interceptor.ts
      • models
        • model-a.ts
        • model-b.ts
      • services
        • service-a.service.ts
        • service-b.service.ts
    • features
      • feature-a
        • components
          • scoped-component-a
            • scoped-component-a.component.html|scss|ts
          • scoped-component-b
            • scoped-component-b.component.html|scss|ts
        • models
          • scoped-model-a.model.ts
          • scoped-model-b.model.ts
        • pages
          • page-a
            • page-a.component.html|scss|ts
          • page-b
            • page-b.component.html|scss|ts
        • services
          • scoped-service-a.service.ts
          • scoped-service-b.service.ts
        • feature-a-routing.module.ts
        • feature-a.module.ts
        • feature-a.component.html|scss|ts
    • shared
      • components
        • shared-button
          • shared-button.component.html|scss|ts
      • directives
        • shared-directive.ts
      • pages
        • page-not-found
          • page-not-found.component.html|scss|ts
      • pipes
        • shared-pipe.ts
      • shared.module.ts
    • app.component.html|scss|ts
    • app.component.module.ts
    • app.routes.ts
  • assets
    • i18n
      • en.json
      • fr.json
      • nl.json
    • documents
      • file-a.pdf
      • file-b.pdf
    • icons
      • custom-icon-a.svg
      • custom-icon-b.svg
    • images
      • image-a.svg
      • image-b.svg
  • styles
    • abstract
      • _variables.scss
    • base
      • _color.scss
      • _typographie.scss
      • _utilities.scss
    • components
      • component-a.scss
      • component-b.scss
    • shame.scss
  • favicon.icon
  • index.html
  • maint.ts
  • styles.scss

Alignez les noms de vos dossiers features sur vos routes. Cette approche simplifie la compréhension et la navigation entre la structure des dossiers et les routes de l'application. De plus on identifie rapidement le dossier contenant les composants et les ressources liés à un itinéraire spécifique.

Exemple concret

si vous avez les routes suivantes dans votre application :

- /dashboard
- /users
    - /users/list
    - /users/detail/:id
                        

Alors organisez votre application comme ceci:

src/
|-- app/
|   |-- core/                       # Core module for singleton services and core functionality
|   |-- shared/                     # Shared module for reusable components, directives, and pipes
|   |-- features/
|   |   |-- dashboard/              # Feature module for the /dashboard route
|   |   |   |-- pages/              # Folder for page components in the dashboard feature
|   |   |   |-- components/         # Folder for reusable components specific to the dashboard feature
|   |   |   |-- services/           # Folder for services specific to the dashboard feature
|   |   |-- users/                  # Feature module for the /users route
|   |   |   |-- pages/              # Folder for page components in the users feature
|   |   |   |   |-- list/           # Component for the /users/list child route
|   |   |   |   |-- detail/         # Component for the /users/detail/:id child route
|   |   |   |-- components/         # Folder for reusable components specific to the users feature
|   |   |   |-- services/           # Folder for services specific to the users feature
|-- ...
                    

Dans cet exemple, le routing module correspondant (users-routing.module.ts) doit définir à la fois la route parent et les routes enfants :

import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { UsersComponent } from './users.component';
import { UsersListComponent } from './list/users-list.component';
import { UsersDetailComponent } from './detail/users-detail.component';

const routes: Routes = [
  {
    path: 'users',
    component: UsersComponent,
    children: [
      { path: 'list', component: UsersListComponent },
      { path: 'detail/:id', component: UsersDetailComponent },
    ],
  },
];

@NgModule({
  imports: [RouterModule.forChild(routes)],
  exports: [RouterModule],
})
export class UsersRoutingModule {}
                    

Documentation: Angular File Structure and Best Practices
Documentation: Tips for Structuring Your Angular Project
Documentation: How to define a highly scalable folder structure for your Angular project
Documentation: Angular schematics extension VS Code
Documentation: Github: Angular Folder Structure
Documentation: Angular App Architecture: Don’t get Lost in Structure
Documentation: How to structure your Angular apps like a Googler
Documentation: Angular.io: Workspace and project file structure
Documentation: Angular.io Styleguide: Application structure and NgModules
Documentation: Angular project structure best practice

Modules

En Angular, un module est un moyen de regrouper tout le code associé à un aspect dans une unité modulaire. Chaque module peut contenir des composants, des services, des directives, des pipes, .... Toute application Angular a au moins un module, le module racine (root module). Ce root module est appelé par convention AppModule. Même si il est possible pour les petites applications de n'avoir qu'un seul module, la pluspart des applications ont de nombreux modules.

Voici une liste non exhaustive des modules courant dans une application Angular:

  • Core module

    Le CoreModule est un module qui contient du code essentiel à l'application. Ce core module ne doit être importé qu'une seule fois, généralement dans l'AppModule. Ce module inclut des guards, helpers, des services utilisés dans l'ensemble de l'application, tels que le service d'authentification (auth service) ou de journalisation (loggin service).

  • Feature modules

    Les FeatureModules sont des modules dédiés à des fonctionnalités ou à des domaines de fonctionnalité spécifiques au sein de l'application.
    Ces modules aident à garder le code organisé et modulaire à mesure que l'application se développe. Par exemple, on pourrait avoir un AccountModule pour gérer les comptes d'utilisateurs ou encore un ProductModule pour afficher des informations sur les produits.

  • Shared module

    Le SharedModule est un module qui contient du code partagé entre plusieurs feature modules.
    Cela peut inclure des composants communs, des directives et des pipes qui sont utilisés à plusieurs endroits dans l'application. Par exemple, le SharedModule pourrait contenier un composant de bouton custom ou encore un pipe de formatage de date.

    Si vous constatez que votre SharedModule est devenu trop grand et difficile à gérer en raison du nombre de composants, de pipes et de directives qu'il contient, alors envisagez d'utiliser une stratégie d'organisation plus avancée. Une approche possible consiste à diviser le module en plusieurs morceaux soit:

    • en suivant une organisation de type SCAM
    • en utilisant la feature Standalone component stabilisée dans la version 15 de Angular.

SCAM - Single Component Angular Module

SCAM, ou Single Component Angular Module, est un modèle de conception utilisé dans les applications Angulars qui consiste à créer un module contenant un seul composant.
L'approche du module à composant unique implique la création d'un module pour chaque composant de l'application.
Chaque module contiendra le composant, ainsi que tous les services, pipes ou directives dont dépend le composant.
Le module est ensuite importé dans tout autre module devant utiliser le composant.

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { GreetingComponent } from './greeting.component';

@NgModule({
  declarations: [GreetingComponent],
  imports: [CommonModule],
  exports: [GreetingComponent],
})
export class GreetingModule {}

Standalone components

Un composant 'standalone' est un composant conçu pour être autonome et indépendant. Le composant encapsule alors ses propres fonctionnalités, données et interface utilisateur.

import { Component, Input } from '@angular/core';

@Component({
  selector: 'app-greeting',
  template: '<h1>Hello, {{name}}!</h1>',
})
export class GreetingComponent {
  @Input() name: string;
}

Documentation Introduction to modules
Documentation Angular modularity
Documentation Standalone components

Conventions

Rxjs: $ suffix

Suffixez vos observables rxjs par un $.

API: Request & Response

Suffixez les objets envoyé à une API en tant que payload par Request.
Suffixez les objets reçu suite à un appel à une API par Response.
Typez explicitement les objets réponses des APIs.

API: Enums

Privilégiez les enums pour tous les champs réponses d'une API dont la valeur est une série de constantes prédéfinis.
En effet, une api peut renvoyer une série de constantes sous forme numérique ou sous chaine de caractère (en lower ou upper case). Utilisez des enums est la garantie d'avoir un nom explicite et des conventions uniformes.