Le guide de style officiel a été réécrit en 2025 (avec la v20) et la CLI a changé les conventions par défaut : les fichiers n’ont plus de suffixes de type (.component.ts, .service.ts), et les classes ne se terminent plus par Component. L’idée : le nom décrit ce qu’est la chose, pas quel type d’artefact Angular c’est.
| Moderne (CLI v20+, guide de style actuel) | Classique (avec suffixes) | |
|---|---|---|
| Fichier | user-profile-card.ts / .html / .scss | user-profile-card.component.ts |
| Classe / fonction | UserProfileCard | UserProfileCardComponent |
| Sélecteur / nom | app-user-profile-card | app-user-profile-card |
| Commande CLI | ng generate component user-profile-card # ng g c … | |
Fichiers
user-profile.ts ← le composant (classe UserProfile)
user-profile.html ← template
user-profile.scss ← styles
user-profile.spec.ts ← test
le même nom de base = facile à trouver, facile à déplacer ensemble
auth-store.ts ← service avec état
users-api.ts ← service d’accès aux données
time-ago-pipe.ts ← pipe (classe TimeAgoPipe)
auth-guard.ts ← guard fonctionnel (authGuard)Les projets existants avec suffixes peuvent rester ainsi. Ce qui compte, c’est la cohérence. Si vous voulez conserver les suffixes pour les nouveaux fichiers générés :
// Vous préférez les anciens suffixes (.component.ts, .service.ts) ? Vous pouvez les garder dans angular.json :
{
"schematics": {
"@schematics/angular:component": { "type": "component" },
"@schematics/angular:directive": { "type": "directive" },
"@schematics/angular:service": { "type": "service" },
"@schematics/angular:guard": { "typeSeparator": "." },
"@schematics/angular:pipe": { "typeSeparator": "." }
}
}
// Ce qui compte, c’est d’être COHÉRENT dans tout le projet.L’ordre des membres dans un composant
@Component({
selector: 'app-user-profile',
imports: [Avatar, DatePipe],
templateUrl: './user-profile.html',
styleUrl: './user-profile.scss',
host: { '[class.compact]': 'compact()' },
})
export class UserProfile {
// 1. L’API du composant : inputs, models, outputs (readonly)
readonly userId = input.required<number>();
readonly compact = input(false, { transform: booleanAttribute });
readonly edit = output<number>();
// 2. Queries (readonly)
private readonly nameInput = viewChild<ElementRef<HTMLInputElement>>('name');
// 3. Dépendances (private readonly)
private readonly users = inject(UsersStore);
// 4. État et valeurs dérivées — protected si seul le template les utilise
protected readonly user = computed(() => this.users.byId(this.userId()));
protected readonly editing = signal(false);
// 5. Méthodes — nommées d’après CE QU’ELLES FONT, pas d’après l’événement qui les déclenche
protected startEditing(): void {
this.editing.set(true);
}
}Règles courtes
// ✓ inject() plutôt que le constructeur
private readonly http = inject(HttpClient);
// ✓ protected pour les membres utilisés uniquement dans le template (hors API publique)
protected readonly fullName = computed(() => `${this.first()} ${this.last()}`);
// ✓ readonly pour tout ce qu’Angular initialise : input, output, model, viewChild, signal
readonly value = model(0);
// ✓ host: {} plutôt que @HostBinding / @HostListener
host: { '[attr.aria-expanded]': 'open()', '(keydown.escape)': 'close()' }
// ✓ [class.x] / [style.x] plutôt que ngClass / ngStyle
// ✓ @if / @for / @switch plutôt que *ngIf / *ngFor / ngSwitch
// ✓ lifecycle hooks minces : appelez des méthodes aux noms clairs
ngOnInit() { this.loadInitialFilters(); }Nommer les handlers
<!-- ✗ le nom décrit l’événement -->
<button (click)="handleClick()">Enregistrer</button>
<input (keydown.enter)="onKeydown()" />
<!-- ✓ le nom décrit l’action -->
<button (click)="saveProfile()">Enregistrer</button>
<input (keydown.enter)="search()" />Résumé du guide
| Règle | Pourquoi |
|---|---|
| Un concept par fichier | Facile à trouver, à tester et à déplacer. |
| Le même nom pour .ts/.html/.scss/.spec.ts | Regroupement visuel, recherche rapide. |
| Organiser par fonctionnalité, pas par type | Ce qui change ensemble reste ensemble. |
Un préfixe sur les sélecteurs (app-, adm-) | Pas de collision avec les éléments natifs ou ceux des bibliothèques. |
inject() plutôt que le constructeur | Lisible, fonctionne avec l’héritage et les fonctions. |
protected pour les membres du template | Sépare l’API publique des détails d’affichage. |
readonly pour les propriétés Angular | Évite d’écraser un signal par accident. |
| Logique simple dans le template | Les calculs vont dans computed(). |
| Handlers nommés d’après l’action | saveProfile() dit ce qu’il fait ; onClick() non. |
Comment regrouper les fichiers en dossiers : Structure des dossiers ; comment nommer types et modèles : Interfaces et modèles.
Sélecteurs et préfixes
Les composants ont des sélecteurs d’élément en kebab-case, avec le préfixe du projet (app-user-card). Les directives ont des sélecteurs d’attribut en camelCase, avec le même préfixe (appTooltip). Les pipes ont des noms en camelCase, sans préfixe. Le préfixe se règle une fois dans angular.json, et la CLI l’utilise à chaque ng generate. Les règles @angular-eslint/component-selector et directive-selector le vérifient dans le lint.
@Component({ selector: 'app-user-card' }) // élément, kebab-case, avec préfixe
@Directive({ selector: '[appTooltip]' }) // attribut, camelCase, avec préfixe
@Pipe({ name: 'relativeTime' }) // camelCase, sans préfixe
// angular.json → projects.<app>.prefix: "app"Migrations automatiques vers le style moderne
Inutile de réécrire un ancien projet à la main. Angular CLI propose des migrations qui modifient le code pour vous, fichier par fichier. Toutes existent dans Angular 22.2 :
ng generate @angular/core:standalone # NgModule → standalone
ng generate @angular/core:control-flow # *ngIf / *ngFor → @if / @for
ng generate @angular/core:inject # constructeur → inject()
ng generate @angular/core:signal-inputs # @Input → input()
ng generate @angular/core:outputs # @Output → output()
ng generate @angular/core:signal-queries # @ViewChild & co. → viewChild()
ng generate @angular/core:signals # les trois ci-dessus, en une étape
ng generate @angular/core:service # @Injectable → @Service (v22)
ng generate @angular/core:route-lazy-loading # component: → loadComponent
ng generate @angular/core:self-closing-tag # <app-x></app-x> → <app-x />
ng generate @angular/core:ngclass-to-class # [ngClass] → [class]
ng generate @angular/core:ngstyle-to-style # [ngStyle] → [style]
ng generate @angular/core:cleanup-unused-imports # imports inutilisés dans les composantsLancez-les une par une, sur une branche propre, et vérifiez le diff et les tests après chacune. La plupart acceptent --path pour ne migrer qu’un dossier : vous pouvez avancer progressivement, feature par feature.
Pièges fréquents
- Deux styles mélangés dans le même dossier (
user.component.tsà côté deuser-card.ts) : pire que l’un ou l’autre appliqué de façon cohérente. - Une énorme migration dans une seule PR : personne ne peut la relire. Avancez dossier par dossier et séparez la migration des changements de logique.
- Des noms génériques (
utils.ts,helpers.ts,common.service.ts) qui finissent par tout accumuler. Un nom doit dire ce que fait le fichier.
Aide-mémoire
user-card.ts / .html / .css / .spec.ts un concept par fichier, sans suffixe de type
class UserCard sans Component à la fin
selector: app-user-card [appTooltip] préfixe sur les sélecteurs
inject() protected readonly dans les composants
saveProfile() not onClick() handlers nommés d’après l’action
ng generate @angular/core:<migration>