Front-end angularforall.com

- Angular 22 : composants selectorless expliqués

Angular Angular-22 Selectorless Composants Directives Type-Safety Refactoring Standalone Template Imports Migration Signals Front-End Typescript
Angular 22 : composants selectorless expliqués

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.

En une phrase : selectorless transforme la balise <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.

Le piège du faux positif : une balise mal orthographiée comme <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);
    }
}
Convention de nommage : en selectorless, la casse fait le tri. Une balise en 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 {}
Bon à savoir : une directive selectorless n'a plus de sélecteur d'attribut, donc elle ne peut pas s'appliquer « par accident » à des éléments qui contiendraient cet attribut. Chaque application est volontaire et visible dans le template, ce qui supprime toute une catégorie de surprises liées aux sélecteurs trop larges.

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.';
}
Ce que selectorless supprime de votre code :
  • La propriété selector des composants et directives
  • Le tableau imports du 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 {}
Impact sur les tests : moins de bugs d'intégration arrivent jusqu'aux tests, car les erreurs de câblage entre composants sont attrapées par le compilateur. Vos tests unitaires peuvent se concentrer sur le comportement plutôt que sur la vérification de bindings que le type-checker garantit déjà.

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
Cas des librairies : si vous publiez une bibliothèque de composants consommée par d'autres équipes, conservez des sélecteurs string sur l'API publique tant que vos consommateurs ne sont pas tous sur Angular 22. Selectorless brille surtout dans le code applicatif que vous maîtrisez de bout en bout.

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 UserTile plutôt que UserTileComponent allè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
Faut-il tout migrer aujourd'hui ? Non. Commencez par écrire votre nouveau code en selectorless et mesurez le confort gagné. La conversion de l'existant peut se faire au fil de l'eau, sans deadline, puisque les deux modes coexistent sans friction.

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 @ : @Tooltip attache la directive de façon explicite
  • Imports implicites : l'import TypeScript suffit, le tableau imports disparaît
  • Coexistence totale : aucune migration forcée, les deux styles vivent ensemble
Et après ? Selectorless s'inscrit dans la même logique que les Signals et le zoneless : retirer la cérémonie pour rapprocher Angular du JavaScript et du TypeScript natifs. À mesure que l'outillage (language service, schematics) mûrit, on peut imaginer une migration assistée qui retirera automatiquement sélecteurs et tableaux 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.

Partager