Decouvrez les composants selectorless d'Angular 22 : importez composants et directives sans selecteur ni tableau imports, pour un code type-safe, concis et refactorable.
Selectorless : pourquoi ce changement
Depuis sa première version, Angular relie un composant à son template grâce à une chaîne de sélecteur : une chaîne de caractères comme app-profile-card que l'on déclare dans le décorateur, puis que l'on écrit dans le HTML. Ce mécanisme fonctionne, mais il introduit une indirection que rien ne valide à la compilation : le sélecteur est du texte libre, déconnecté de la classe TypeScript qu'il représente.
Avec Angular 22, l'équipe pousse une approche radicalement différente, baptisée selectorless : on référence un composant directement par le nom de sa classe, sans sélecteur intermédiaire et sans tableau imports. Le compilateur fait le lien à partir des imports TypeScript déjà présents en haut du fichier. Résultat : moins de cérémonie, et un template enfin couvert par le vérificateur de types.
<app-profile-card> (une chaîne) en <ProfileCard> (un symbole réel), ce qui rend le rename, le « go to definition » et le tree-shaking aussi fiables dans le template que dans le code TypeScript.
Ce que vous gagnez concrètement
- Zéro boilerplate : plus de sélecteur à inventer, plus de tableau
importsà maintenir à jour - Navigation native : Ctrl+clic sur une balise saute directement à la classe du composant
- Refactoring sûr : renommer la classe met à jour les usages dans les templates, comme n'importe quel symbole
- Code mort détectable : un composant jamais référencé devient un import inutilisé, signalé par l'éditeur
Voyons d'abord pourquoi le modèle historique, malgré sa simplicité apparente, coûte cher sur un projet qui grandit.
Le coût caché des sélecteurs string
Prenons un composant standalone classique tel qu'on l'écrit jusqu'à Angular 21. Pour l'afficher quelque part, il faut accomplir trois actions distinctes et faciles à désynchroniser.
// Angular 21 et avant — le composant à réutiliser
import { Component, input } from '@angular/core';
@Component({
// 1) On invente une chaîne de sélecteur, sans aucune garantie d'unicité
selector: 'app-price-badge',
standalone: true,
template: `
<span class="badge">{{ amount() | currency:'EUR' }}</span>
`,
})
export class PriceBadgeComponent {
amount = input.required<number>();
}
// Angular 21 — le composant parent qui veut l'utiliser
import { Component } from '@angular/core';
import { PriceBadgeComponent } from './price-badge.component';
@Component({
selector: 'app-product-row',
standalone: true,
// 2) On doit penser à l'ajouter au tableau imports
imports: [PriceBadgeComponent],
// 3) On écrit la chaîne du sélecteur, qui n'est PAS reliée à la classe
template: `
<app-price-badge [amount]="product.price" />
`,
})
export class ProductRowComponent {
product = { price: 19.9 };
}
Trois sources de vérité pour une seule relation. Si vous renommez PriceBadgeComponent, le sélecteur app-price-badge ne bouge pas. Si vous oubliez la ligne du tableau imports, le template compile parfois en silence (la balise est interprétée comme un élément HTML inconnu) et vous récupérez un bug à l'exécution, pas une erreur de build claire.
<app-price-bagde> ne déclenche aucune erreur tant qu'aucun schéma strict n'est configuré. Angular la considère comme un élément custom inconnu et l'ignore. Le composant ne s'affiche pas, et rien ne vous l'explique.
Le problème s'aggrave avec l'échelle
Sur une application de quelques centaines de composants, deux frictions deviennent quotidiennes : les collisions de sélecteurs (deux équipes choisissent app-card) et les tableaux imports qui enflent. Un composant de page qui orchestre dix sous-composants déclare dix lignes d'import dans son décorateur — des lignes qu'aucun outil ne garde automatiquement synchronisées avec ce que le template utilise réellement.
Composants par nom de classe
Avec selectorless, on supprime purement et simplement la chaîne de sélecteur. Le composant n'a plus besoin de se nommer pour le HTML : c'est sa classe qui sert de référence. Reprenons l'exemple précédent, version Angular 22.
// Angular 22 — composant selectorless : plus de propriété selector
import { Component, input } from '@angular/core';
@Component({
// Aucun selector : la classe PriceBadge EST le point de référence
template: `
<span class="badge">{{ amount() | currency:'EUR' }}</span>
`,
})
export class PriceBadge {
amount = input.required<number>();
}
// Angular 22 — le parent référence le composant par son nom de classe
import { Component } from '@angular/core';
import { PriceBadge } from './price-badge'; // un import TypeScript normal
@Component({
// Pas de tableau imports, pas de chaîne de sélecteur
template: `
<!-- La balise utilise directement le nom de la classe (PascalCase) -->
<PriceBadge [amount]="product.price" />
`,
})
export class ProductRow {
product = { price: 19.9 };
}
La règle est simple : une balise en PascalCase correspond à une classe de composant importée. Angular ne cherche plus dans une table de sélecteurs ; il résout le symbole PriceBadge exactement comme TypeScript résout n'importe quelle variable de votre fichier. Si l'import manque, vous obtenez une vraie erreur de compilation, pas un silence trompeur.
Inputs, outputs et bindings : aucune nouveauté à apprendre
La syntaxe des liaisons reste identique. Seul le nom de la balise change. Voici un composant légèrement plus riche, avec un input et un output, consommé en selectorless.
// Angular 22 — un composant avec input et output, sans sélecteur
import { Component, input, output } from '@angular/core';
@Component({
template: `
<article class="user-tile">
<h3>{{ name() }}</h3>
<button type="button" (click)="select.emit(name())">
Choisir
</button>
</article>
`,
})
export class UserTile {
name = input.required<string>(); // entrée typée
select = output<string>(); // sortie typée
}
// Angular 22 — consommation : les bindings ne changent pas
import { Component, signal } from '@angular/core';
import { UserTile } from './user-tile';
@Component({
template: `
@for (person of people(); track person) {
<!-- [name] et (select) fonctionnent comme avant -->
<UserTile [name]="person" (select)="onSelect($event)" />
}
`,
})
export class TeamPicker {
people = signal(['Amira', 'Lucas', 'Sofia']);
onSelect(name: string) {
console.log('Sélection :', name);
}
}
PascalCase (<UserTile>) désigne un composant ; une balise en minuscules (<div>, <section>) reste un élément HTML natif. Aucune ambiguïté possible pour le compilateur.
Directives selectorless avec @
Les directives posent un problème différent : elles ne sont pas des balises, elles s'appliquent à un élément existant. Angular 22 introduit donc une syntaxe dédiée — le préfixe @ suivi du nom de la classe — pour attacher une directive sans passer par un sélecteur d'attribut.
// Angular 22 — une directive selectorless
import { Directive, input, ElementRef, inject, effect } from '@angular/core';
@Directive() // aucun selector d'attribut
export class Tooltip {
text = input.required<string>();
private host = inject(ElementRef<HTMLElement>);
constructor() {
// Met à jour l'attribut d'accessibilité quand le texte change
effect(() => {
this.host.nativeElement.setAttribute('aria-label', this.text());
});
}
}
// Angular 22 — application de la directive avec la syntaxe @
import { Component } from '@angular/core';
import { Tooltip } from './tooltip';
@Component({
template: `
<!-- @Tooltip attache la directive Tooltip à ce bouton -->
<button type="button" @Tooltip="'Enregistrer le brouillon'">
💾
</button>
`,
})
export class EditorToolbar {}
La lecture devient limpide : @Tooltip dans le template renvoie à la classe Tooltip importée juste au-dessus. Plus besoin de deviner quel attribut HTML déclenche quelle directive — la relation est explicite et navigable.
Plusieurs directives, et des inputs de directive
On peut empiler plusieurs directives selectorless sur un même élément, et leur passer des inputs avec la syntaxe habituelle entre crochets.
// Angular 22 — deux directives appliquées au même élément
import { Component } from '@angular/core';
import { Tooltip } from './tooltip';
import { Highlight } from './highlight';
@Component({
template: `
<span
@Tooltip="'Statut du paiement'"
@Highlight
[color]="'gold'">
Premium
</span>
`,
})
export class BadgeRow {}
La fin du tableau imports
C'est le gain le plus visible au quotidien. Avec les composants standalone, le tableau imports du décorateur listait tout ce que le template utilisait : composants, directives, pipes. Selectorless rend cette liste implicite — l'import TypeScript en haut du fichier suffit.
// AVANT (Angular 21) — un composant de page et son inventaire d'imports
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { HeaderBar } from './header-bar.component';
import { SideNav } from './side-nav.component';
import { ProductRow } from './product-row.component';
import { Pagination } from './pagination.component';
@Component({
selector: 'app-catalog',
standalone: true,
// Tableau imports à tenir à jour manuellement, ligne par ligne
imports: [CommonModule, HeaderBar, SideNav, ProductRow, Pagination],
templateUrl: './catalog.component.html',
})
export class CatalogComponent {}
// APRÈS (Angular 22) — les imports TypeScript suffisent
import { Component } from '@angular/core';
import { HeaderBar } from './header-bar';
import { SideNav } from './side-nav';
import { ProductRow } from './product-row';
import { Pagination } from './pagination';
@Component({
// Plus de selector, plus de tableau imports : tout est implicite
templateUrl: './catalog.html',
})
export class Catalog {}
La conséquence est subtile mais puissante : le décorateur ne peut plus mentir. Avant, un composant pouvait rester dans le tableau imports longtemps après avoir disparu du template — du bruit que personne ne nettoyait. Désormais, si Pagination n'est plus utilisé dans le template, son import devient inutilisé et votre linter le signale immédiatement.
Et les pipes ?
Les pipes suivent la même logique de symbole importé. Vous importez la classe du pipe, et vous l'utilisez par son nom dans le template. Le pipe n'a plus besoin d'un name déclaré comme chaîne séparée du symbole.
// Angular 22 — un pipe selectorless utilisé par sa classe
import { Component } from '@angular/core';
import { TruncatePipe } from './truncate-pipe';
@Component({
template: `
<p>{{ description | TruncatePipe:80 }}</p>
`,
})
export class ArticleCard {
description = 'Un résumé potentiellement très long à raccourcir proprement.';
}
- La propriété
selectordes composants et directives - Le tableau
importsdu décorateur@Component - La duplication entre nom de classe et chaîne de sélecteur
- Les imports « fantômes » jamais nettoyés du décorateur
Type-safety et refactoring à grande échelle
Le vrai bénéfice de selectorless n'est pas l'économie de quelques lignes : c'est que le template rejoint enfin le monde typé. Une balise <UserTile> est une référence au symbole UserTile, donc tous les outils qui comprennent TypeScript comprennent maintenant vos templates.
Renommage propagé partout
Renommer un composant via « Rename Symbol » dans l'éditeur met à jour la classe, ses imports et ses usages dans les templates, en une seule opération. Avec les sélecteurs string, le renommage de la classe laissait les balises HTML inchangées : il fallait une recherche-remplacement textuelle, fragile et risquée.
// Démonstration — l'éditeur garde tout cohérent en selectorless
// 1) Vous renommez la classe : UserTile → MemberTile
@Component({ template: `...` })
export class MemberTile { /* ... */ }
// 2) L'import est mis à jour automatiquement
import { MemberTile } from './member-tile';
// 3) Et la balise dans le template suit aussi
// <UserTile ... /> devient <MemberTile ... />
// Aucune recherche-remplacement manuelle nécessaire
Inputs vérifiés à la compilation
Comme le compilateur connaît la classe exacte derrière chaque balise, il valide les noms d'inputs et leurs types. Une faute de frappe ou un type incompatible devient une erreur de build, pas un binding silencieusement ignoré.
// Angular 22 — erreurs détectées AVANT l'exécution
import { Component, input } from '@angular/core';
@Component({ template: `...` })
export class PriceBadge {
amount = input.required<number>();
}
// Dans le parent :
@Component({
template: `
<!-- ❌ Erreur de compilation : 'amont' n'existe pas sur PriceBadge -->
<PriceBadge [amont]="12" />
<!-- ❌ Erreur de compilation : string non assignable à number -->
<PriceBadge [amount]="'douze'" />
<!-- ✅ Correct -->
<PriceBadge [amount]="12" />
`,
})
export class ProductRow {}
Coexistence et migration progressive
Bonne nouvelle pour les projets existants : selectorless n'est pas un mode exclusif. Les deux styles cohabitent, y compris dans le même template. Vous adoptez la nouvelle syntaxe là où ça vous arrange, sans réécrire l'application d'un bloc.
// Angular 22 — selectorless et sélecteur classique côte à côte
import { Component } from '@angular/core';
import { PriceBadge } from './price-badge'; // composant selectorless
@Component({
// LegacyBanner garde son sélecteur 'app-legacy-banner' et son import classique
imports: [LegacyBanner],
template: `
<!-- Ancien style : sélecteur string -->
<app-legacy-banner />
<!-- Nouveau style : nom de classe -->
<PriceBadge [amount]="29.9" />
`,
})
export class MixedPage {}
Une stratégie de migration sans douleur
Inutile de tout convertir en un sprint. Une approche réaliste consiste à introduire selectorless sur le nouveau code, puis à nettoyer l'existant au gré des modifications.
| Étape | Action | Effort |
|---|---|---|
| 1. Nouveau code | Écrire les nouveaux composants en selectorless dès maintenant | Nul (c'est le défaut) |
| 2. Composants feuilles | Convertir d'abord les composants sans enfants (badges, boutons, tuiles) | Faible |
| 3. Composants de page | Retirer les tableaux imports devenus implicites |
Moyen |
| 4. Directives et pipes | Basculer vers la syntaxe @ et le nom de classe |
Moyen |
| 5. Bibliothèques publiques | Garder un sélecteur si l'API publique l'exige (compat externe) | À évaluer |
Limites, pièges et bonnes pratiques
Selectorless est élégant, mais quelques points méritent votre attention pour éviter les mauvaises surprises pendant l'adoption.
Le contenu projeté reste à surveiller
La projection de contenu via ng-content reposait souvent sur des sélecteurs CSS pour cibler des slots. Vérifiez vos composants qui utilisent select sur ng-content : la logique de projection n'est pas affectée, mais c'est l'occasion de relire ces composants pour clarifier leur API.
// Le slotting par ng-content fonctionne toujours
import { Component } from '@angular/core';
@Component({
template: `
<div class="card">
<header><ng-content select="[card-title]" /></header>
<div class="body"><ng-content /></div>
</div>
`,
})
export class Card {}
// Utilisation : on projette par attribut, comme avant
// <Card>
// <h2 card-title>Titre</h2>
// <p>Contenu projeté dans le slot par défaut</p>
// </Card>
Collisions de noms entre fichiers
Comme la balise est un symbole importé, deux composants nommés Card dans des dossiers différents entrent en conflit dans un même fichier. La parade est celle de TypeScript : l'alias d'import.
// Angular 22 — désambiguïser deux composants de même nom
import { Card as ProductCard } from './product/card';
import { Card as ArticleCard } from './blog/card';
@Component({
template: `
<ProductCard [item]="product" />
<ArticleCard [post]="article" />
`,
})
export class HomePage {
product = { /* ... */ };
article = { /* ... */ };
}
Bonnes pratiques à adopter
- Nommez vos classes proprement : le nom de classe devient le nom public dans le template, donc
UserTileplutôt queUserTileComponentallège le HTML - Un composant par fichier : facilite la résolution de symbole et la lisibilité des imports
- Réservez les sélecteurs string aux composants exposés en dehors de votre application (web components, librairies publiques)
- Laissez le linter travailler : activez la règle des imports inutilisés pour profiter de la détection automatique de composants morts
Conclusion et perspectives
Selectorless est l'une de ces évolutions discrètes mais structurantes : elle ne change pas ce que font vos composants, mais comment vous les reliez. En remplaçant une chaîne de caractères par un véritable symbole, Angular 22 fait entrer les templates dans le périmètre du type-checker — avec à la clé un refactoring plus sûr, une navigation immédiate et un code débarrassé de son boilerplate de plomberie.
Les points à retenir :
- Composants par nom de classe :
<ProfileCard />remplace<app-profile-card> - Directives avec
@:@Tooltipattache la directive de façon explicite - Imports implicites : l'import TypeScript suffit, le tableau
importsdisparaît - Coexistence totale : aucune migration forcée, les deux styles vivent ensemble
imports de tout un projet.
Le meilleur moyen de juger : créez votre prochain composant sans selector ni tableau imports, et observez à quel point le template devient lisible. Pour approfondir l'écosystème Angular 22, poursuivez avec nos guides sur les Signal Forms, l'inject() moderne et les composants standalone.