Angular Signal Queries : viewChild et contentChild

Front-end Mezgani said
Angular Signals Viewchild Contentchild Angular 19
Angular Signal Queries : viewChild et contentChild

Maîtrisez viewChild(), viewChildren(), contentChild() et contentChildren() d'Angular 19 : requêtes signal-based sans décorateurs, avec effect() et computed().

@ViewChild vs viewChild() : le changement de paradigme

Depuis Angular 1 jusqu'à Angular 16, accéder à un élément du template depuis le TypeScript passait obligatoirement par les décorateurs @ViewChild et @ContentChild. Ces décorateurs fonctionnent, mais ils introduisent une contrainte importante : les valeurs ne sont disponibles qu'à partir de AfterViewInit.

Voici un exemple classique — un lecteur audio où on contrôle l'élément <audio> natif depuis le composant :

// ❌ Ancienne approche — décorateurs + cycle de vie AfterViewInit
import { Component, ViewChild, ElementRef, AfterViewInit } from '@angular/core';

@Component({
  selector: 'app-lecteur-audio',
  template: `
    <audio #lecteur src="/assets/podcast-episode1.mp3"></audio>
    <button (click)="lire()">Lire</button>
    <button (click)="pause()">Pause</button>
  `
})
export class LecteurAudioComponent implements AfterViewInit {
  // @ViewChild — la valeur est undefined jusqu'à AfterViewInit
  @ViewChild('lecteur') lecteurRef!: ElementRef<HTMLAudioElement>;

  ngAfterViewInit() {
    // Seulement ICI la valeur est disponible — pas avant
    console.log('Durée :', this.lecteurRef.nativeElement.duration);
  }

  lire()  { this.lecteurRef.nativeElement.play(); }
  pause() { this.lecteurRef.nativeElement.pause(); }
}

Ce code a deux problèmes : il force l'implémentation de AfterViewInit, et si on oublie que la valeur est undefined avant ce cycle, on obtient des erreurs runtime difficiles à déboguer.

Voici le même composant avec l'API Signal Query d'Angular 17+ :

// ✅ Nouvelle approche Angular 19 — viewChild() signal-based
import { Component, viewChild, ElementRef, effect } from '@angular/core';

@Component({
  selector: 'app-lecteur-audio',
  standalone: true,
  template: `
    <audio #lecteur src="/assets/podcast-episode1.mp3"></audio>
    <button (click)="lire()">Lire</button>
    <button (click)="pause()">Pause</button>
  `
})
export class LecteurAudioComponent {
  // viewChild() retourne un Signal — pas besoin d'AfterViewInit
  lecteur = viewChild<ElementRef<HTMLAudioElement>>('lecteur');

  constructor() {
    effect(() => {
      // effect() réagit quand le signal devient disponible
      const el = this.lecteur();
      if (el) {
        console.log('Lecteur prêt, durée :', el.nativeElement.duration);
      }
    });
  }

  lire()  { this.lecteur()?.nativeElement.play(); }
  pause() { this.lecteur()?.nativeElement.pause(); }
}
Le signal est undefined puis se remplit : Avant le rendu de la vue, viewChild() retourne undefined. Après le rendu, il retourne la référence. C'est pourquoi on utilise ?. (optional chaining) lors de l'accès. La variante viewChild.required() garantit que la valeur est toujours définie après l'init — pas besoin de ?..
Critère @ViewChild (décorateur) viewChild() (signal)
Disponibilité Après AfterViewInit Signal — undefined puis défini
Cycle de vie requis implements AfterViewInit Aucun
Réactivité Manuelle (ngAfterViewChecked) Automatique via effect() / computed()
Typage TypeScript T | undefined — assert ! souvent utilisé Signal<T | undefined> — sûr par design
Testabilité Complexe (simuler le cycle de vie) Simple (signal = valeur observable)

viewChild() : requêter son propre template

viewChild() permet d'accéder à un élément ou composant défini dans le template du composant courant. Il existe deux façons de cibler l'élément : par une template reference variable (#nom) ou par le type du composant.

Par template reference variable

// Accéder à un élément HTML natif avec une référence #nom
@Component({
  selector: 'app-editeur-texte',
  standalone: true,
  template: `
    <div class="editeur-container">
      <textarea
        #zoneTexte
        class="form-control"
        rows="8"
        placeholder="Commencez à écrire..."
        [(ngModel)]="contenu"
      ></textarea>
      <div class="mt-2 text-muted">
        {{ nombreCaracteres() }} / 500 caractères
      </div>
      <button class="btn btn-sm btn-outline-primary mt-2" (click)="focusZone()">
        Mettre le focus
      </button>
    </div>
  `
})
export class EditeurTexteComponent {
  // 'zoneTexte' = nom de la template reference variable
  // ElementRef<HTMLTextAreaElement> = type attendu
  zoneTexte = viewChild<ElementRef<HTMLTextAreaElement>>('zoneTexte');

  contenu = signal('');

  nombreCaracteres = computed(() => this.contenu().length);

  focusZone() {
    // ?. car le signal peut être undefined si le template n'est pas rendu
    this.zoneTexte()?.nativeElement.focus();
  }

  selectionnerTout() {
    const textarea = this.zoneTexte()?.nativeElement;
    if (textarea) {
      textarea.select(); // Sélectionner tout le texte
    }
  }
}

Par type de composant enfant

// Accéder à une INSTANCE de composant enfant (pas juste le DOM)
import { Component, viewChild } from '@angular/core';
import { GraphiqueBarresComponent } from './graphique-barres.component';

@Component({
  selector: 'app-tableau-de-bord',
  standalone: true,
  imports: [GraphiqueBarresComponent],
  template: `
    <h2>Ventes du mois</h2>
    <app-graphique-barres [donnees]="donnéesVentes"></app-graphique-barres>

    <button class="btn btn-outline-secondary mt-3" (click)="exporterGraphique()">
      Exporter en PNG
    </button>
  `
})
export class TableauDeBordComponent {
  donnéesVentes = signal([120, 95, 140, 88, 163, 177]);

  // Cibler par TYPE — retourne l'instance du composant enfant
  graphique = viewChild(GraphiqueBarresComponent);

  exporterGraphique() {
    // Appeler une méthode publique du composant enfant directement
    this.graphique()?.exporterEnPng('ventes-mensuelles');
  }
}

viewChild.required() — garantie de présence

// required() : Angular garantit que l'élément est toujours présent
@Component({
  selector: 'app-carte-interactive',
  standalone: true,
  template: `
    <!-- Cet élément est TOUJOURS dans le template — required() est safe -->
    <div #conteneurCarte class="map-container"></div>
    <p>Zoom actuel : {{ niveauZoom() }}x</p>
  `
})
export class CarteInteractiveComponent {
  // required() — pas de undefined, pas de ?. nécessaire
  conteneurCarte = viewChild.required<ElementRef<HTMLDivElement>>('conteneurCarte');

  niveauZoom = signal(12);

  ngAfterViewInit() {
    // Accès direct sans ?. — TypeScript sait que la valeur est toujours définie
    const largeur = this.conteneurCarte().nativeElement.offsetWidth;
    console.log(`Conteneur carte initialisé : ${largeur}px`);
  }
}
Quand utiliser required() ? Utilisez viewChild.required() uniquement si l'élément est toujours présent dans le template — jamais dans un @if ou un @for. Si l'élément peut être absent (rendu conditionnel), utilisez viewChild() standard et gérez le cas undefined.

viewChildren() : requêter plusieurs éléments

Quand votre template contient plusieurs éléments du même type (liste d'items, grille de cartes, onglets), viewChildren() retourne un signal contenant un tableau de toutes les références correspondantes.

// Composant de galerie photo avec gestion de sélection multiple
import { Component, viewChildren, ElementRef, signal, computed } from '@angular/core';

interface Photo {
  id: number;
  url: string;
  titre: string;
  selectionnee: boolean;
}

@Component({
  selector: 'app-galerie-photos',
  standalone: true,
  template: `
    <div class="d-flex justify-content-between align-items-center mb-3">
      <h3>Galerie — {{ photosSelectionnees() }} sélectionnée(s)</h3>
      <button class="btn btn-primary btn-sm" (click)="telechargerSelection()"
              [disabled]="photosSelectionnees() === 0">
        Télécharger la sélection
      </button>
    </div>

    <div class="row g-3">
      @for (photo of photos(); track photo.id; let i = $index) {
        <div class="col-md-3">
          <div class="card cursor-pointer" [class.border-primary]="photo.selectionnee"
               (click)="toggleSelection(i)">
            <!-- #photoImg est placé sur chaque image — viewChildren les capture tous -->
            <img #photoImg
                 [src]="photo.url"
                 [alt]="photo.titre"
                 class="card-img-top"
                 loading="lazy"
                 style="height: 180px; object-fit: cover">
            <div class="card-body py-2">
              <p class="card-text small">{{ photo.titre }}</p>
            </div>
          </div>
        </div>
      }
    </div>
  `
})
export class GaleriePhotosComponent {
  // viewChildren retourne Signal<readonly ElementRef[]>
  // Le signal se met à jour si des photos sont ajoutées ou supprimées
  photoImgs = viewChildren<ElementRef<HTMLImageElement>>('photoImg');

  photos = signal<Photo[]>([
    { id: 1, url: '/photos/paysage-alpes.jpg',   titre: 'Alpes au lever du soleil', selectionnee: false },
    { id: 2, url: '/photos/portrait-artiste.jpg', titre: 'Portrait - Studio 12',    selectionnee: false },
    { id: 3, url: '/photos/ville-nuit.jpg',       titre: 'Paris la nuit',           selectionnee: false },
    { id: 4, url: '/photos/macro-fleur.jpg',      titre: 'Macro - Rose de jardin',  selectionnee: false },
  ]);

  photosSelectionnees = computed(() =>
    this.photos().filter(p => p.selectionnee).length
  );

  toggleSelection(index: number) {
    this.photos.update(liste =>
      liste.map((p, i) => i === index ? { ...p, selectionnee: !p.selectionnee } : p)
    );
  }

  telechargerSelection() {
    // Accéder aux éléments DOM des images sélectionnées
    const imgs = this.photoImgs(); // tableau de toutes les ElementRef
    this.photos().forEach((photo, i) => {
      if (photo.selectionnee && imgs[i]) {
        // Logique de téléchargement via l'URL src de l'image
        console.log('Téléchargement :', imgs[i].nativeElement.src);
      }
    });
  }
}

viewChildren avec type de composant

// Accéder à plusieurs instances du même composant enfant
import { Component, viewChildren } from '@angular/core';
import { PanneauAccordeonComponent } from './panneau-accordeon.component';

@Component({
  selector: 'app-faq',
  standalone: true,
  imports: [PanneauAccordeonComponent],
  template: `
    <h2>Questions fréquentes</h2>
    @for (item of faqItems; track item.question) {
      <app-panneau-accordeon
        [titre]="item.question"
        [contenu]="item.reponse">
      </app-panneau-accordeon>
    }
    <button class="btn btn-sm btn-link mt-2" (click)="toutFermer()">
      Tout replier
    </button>
  `
})
export class FaqComponent {
  // Signal contenant toutes les instances de PanneauAccordeonComponent
  panneaux = viewChildren(PanneauAccordeonComponent);

  faqItems = [
    { question: 'Quels sont les délais de livraison ?', reponse: '...' },
    { question: 'Comment retourner un article ?',       reponse: '...' },
    { question: 'Le paiement est-il sécurisé ?',        reponse: '...' },
  ];

  toutFermer() {
    // Appeler la méthode fermer() sur chaque instance de panneau
    this.panneaux().forEach(panneau => panneau.fermer());
  }
}

contentChild() : requêter le contenu projeté

contentChild() est l'équivalent de viewChild() mais pour le contenu projeté via ng-content. C'est ce que le composant parent insère à l'intérieur de votre composant.

La distinction est fondamentale et souvent source de confusion pour les débutants :

API Interroge... Accessible après...
viewChild() Template du composant lui-même AfterViewInit (ou signal devient défini)
contentChild() Contenu injecté via ng-content AfterContentInit (ou signal devient défini)
// Composant "conteneur carte" qui accepte du contenu projeté
// Le parent décide ce qui va dans l'en-tête et le corps

// carte-dashboard.component.ts
import { Component, contentChild, ElementRef } from '@angular/core';

@Component({
  selector: 'app-carte-dashboard',
  standalone: true,
  template: `
    <div class="card shadow-sm">
      <div class="card-header d-flex justify-content-between align-items-center">
        <!-- ng-content avec select : emplacement nommé -->
        <ng-content select="[entete]"></ng-content>
        <button class="btn btn-sm btn-outline-secondary" (click)="toggleContenu()">
          {{ ouvert() ? '▲' : '▼' }}
        </button>
      </div>
      @if (ouvert()) {
        <div class="card-body">
          <ng-content select="[corps]"></ng-content>
        </div>
      }
    </div>
  `
})
export class CarteDashboardComponent {
  ouvert = signal(true);

  // contentChild() — interroge ce que le PARENT a projeté avec [entete]
  entete = contentChild<ElementRef>('[entete]');

  toggleContenu() { this.ouvert.update(v => !v); }

  obtenirTitreEntete(): string {
    // Lire le texte de l'élément projeté en en-tête
    return this.entete()?.nativeElement.textContent?.trim() ?? 'Sans titre';
  }
}
// Utilisation du composant parent — il projette du contenu
@Component({
  selector: 'app-page-accueil',
  standalone: true,
  imports: [CarteDashboardComponent],
  template: `
    <app-carte-dashboard>
      <!-- Le parent projette ces éléments via ng-content -->
      <h5 entete>Statistiques du jour</h5>
      <div corps>
        <div class="row text-center">
          <div class="col"><span class="display-6 fw-bold">1 248</span><p>Visites</p></div>
          <div class="col"><span class="display-6 fw-bold text-success">94</span><p>Ventes</p></div>
          <div class="col"><span class="display-6 fw-bold text-warning">12</span><p>Retours</p></div>
        </div>
      </div>
    </app-carte-dashboard>
  `
})
export class PageAccueilComponent {}
Règle mnémotechnique : viewChild = ce que TU as écrit dans ton template. contentChild = ce que TON PARENT t'a donné à afficher. Si vous voyez <ng-content> dans votre template, vous aurez besoin de contentChild().

contentChildren() : plusieurs contenus projetés

Comme viewChildren() pour le template propre, contentChildren() collecte tous les éléments projetés correspondant à un type ou une référence.

// Composant de liste d'onglets — les onglets sont projetés par le parent
import { Component, contentChildren, signal, computed } from '@angular/core';

// Composant enfant simple représentant un onglet
@Component({
  selector: 'app-onglet',
  standalone: true,
  template: `<ng-content></ng-content>`
})
export class OngletComponent {
  @Input() libelle = '';
  @Input() icone   = '';
  actif = signal(false);
}

// Composant conteneur — gère la navigation entre onglets
@Component({
  selector: 'app-navigation-onglets',
  standalone: true,
  imports: [OngletComponent],
  template: `
    <!-- Barre des onglets -->
    <ul class="nav nav-tabs mb-3" role="tablist">
      @for (onglet of onglets(); track onglet.libelle; let i = $index) {
        <li class="nav-item" role="presentation">
          <button class="nav-link" [class.active]="ongletActifIndex() === i"
                  (click)="activerOnglet(i)" role="tab">
            @if (onglet.icone) { <i class="bi bi-{{ onglet.icone }} me-1"></i> }
            {{ onglet.libelle }}
          </button>
        </li>
      }
    </ul>

    <!-- Contenu des onglets projetés -->
    <div class="tab-content">
      <ng-content></ng-content>
    </div>
  `
})
export class NavigationOngletsComponent {
  // contentChildren collecte TOUS les OngletComponent projetés
  onglets = contentChildren(OngletComponent);

  ongletActifIndex = signal(0);

  nombreOnglets = computed(() => this.onglets().length);

  activerOnglet(index: number) {
    this.ongletActifIndex.set(index);
    // Mettre à jour l'état actif sur chaque onglet
    this.onglets().forEach((onglet, i) => {
      onglet.actif.set(i === index);
    });
  }
}
// Utilisation — le parent projette les onglets
@Component({
  selector: 'app-profil-utilisateur',
  standalone: true,
  imports: [NavigationOngletsComponent, OngletComponent],
  template: `
    <app-navigation-onglets>
      <app-onglet libelle="Informations" icone="person">
        <h5>Profil personnel</h5>
        <p>Nom : Alice Dupont</p>
      </app-onglet>

      <app-onglet libelle="Commandes" icone="bag">
        <h5>Historique des commandes</h5>
        <p>12 commandes passées</p>
      </app-onglet>

      <app-onglet libelle="Sécurité" icone="shield-lock">
        <h5>Paramètres de sécurité</h5>
        <p>Authentification à 2 facteurs : activée</p>
      </app-onglet>
    </app-navigation-onglets>
  `
})
export class ProfilUtilisateurComponent {}

Réactivité avec effect() et computed()

L'un des grands avantages des Signal Queries est leur intégration naturelle avec effect() et computed(). Vous pouvez dériver des valeurs et réagir aux changements sans ngAfterViewInit ni ngAfterViewChecked.

Réagir au montage d'un élément avec effect()

// Initialiser une librairie JS externe quand l'élément est disponible
import { Component, viewChild, ElementRef, effect, signal } from '@angular/core';

@Component({
  selector: 'app-editeur-code',
  standalone: true,
  template: `
    <div class="mb-2">
      <select class="form-select form-select-sm w-auto d-inline"
              (change)="changerLangue($event)">
        <option value="javascript">JavaScript</option>
        <option value="typescript">TypeScript</option>
        <option value="html">HTML</option>
      </select>
    </div>
    <!-- L'éditeur CodeMirror sera monté sur cet élément -->
    <div #zoneEditeur class="border rounded" style="min-height: 200px"></div>
  `
})
export class EditeurCodeComponent {
  zoneEditeur = viewChild<ElementRef<HTMLDivElement>>('zoneEditeur');
  langueActive = signal('javascript');
  private editeurInstance: any = null;

  constructor() {
    effect(() => {
      const el = this.zoneEditeur();
      if (!el) return; // Pas encore rendu — on attend

      if (!this.editeurInstance) {
        // Initialiser CodeMirror une seule fois quand l'élément est prêt
        // this.editeurInstance = CodeMirror(el.nativeElement, { ... });
        console.log('Éditeur initialisé sur :', el.nativeElement);
      }
    });

    effect(() => {
      // Réagir au changement de langue — l'éditeur doit déjà exister
      const langue = this.langueActive();
      if (this.editeurInstance) {
        // this.editeurInstance.setOption('mode', langue);
        console.log('Langue changée :', langue);
      }
    });
  }

  changerLangue(event: Event) {
    this.langueActive.set((event.target as HTMLSelectElement).value);
  }
}

Dériver des données avec computed()

// Calculer des statistiques sur les composants enfants
@Component({
  selector: 'app-liste-taches',
  standalone: true,
  template: `
    <div class="d-flex gap-3 mb-4 p-3 bg-light rounded">
      <div>Total : <strong>{{ stats().total }}</strong></div>
      <div class="text-success">Terminées : <strong>{{ stats().terminees }}</strong></div>
      <div class="text-warning">En cours : <strong>{{ stats().enCours }}</strong></div>
      <div>Progression : <strong>{{ stats().pourcentage }}%</strong></div>
    </div>

    @for (tache of taches; track tache.id) {
      <app-tache-item [tache]="tache"></app-tache-item>
    }
  `
})
export class ListeTachesComponent {
  tacheItems = viewChildren(TacheItemComponent);

  taches = [
    { id: 1, titre: 'Concevoir la maquette', statut: 'termine' },
    { id: 2, titre: 'Développer le back-end', statut: 'en-cours' },
    { id: 3, titre: 'Écrire les tests',       statut: 'en-cours' },
    { id: 4, titre: 'Déployer en production', statut: 'a-faire'  },
  ];

  // computed() recalcule automatiquement quand tacheItems change
  stats = computed(() => {
    const items   = this.tacheItems();
    const total      = items.length;
    const terminees  = items.filter(t => t.statut() === 'termine').length;
    const enCours    = items.filter(t => t.statut() === 'en-cours').length;
    const pourcentage = total > 0 ? Math.round((terminees / total) * 100) : 0;
    return { total, terminees, enCours, pourcentage };
  });
}

Le paramètre read : accéder à différentes interfaces

Par défaut, viewChild() retourne l'instance du composant ou une ElementRef pour les éléments HTML natifs. Mais un même élément du template peut exposer plusieurs interfaces : le composant, sa ElementRef, une directive appliquée dessus, ou un TemplateRef. Le paramètre read vous laisse choisir.

import { Component, viewChild, ElementRef, ViewContainerRef, TemplateRef } from '@angular/core';
import { NgModel } from '@angular/forms';

@Component({
  selector: 'app-formulaire-avance',
  standalone: true,
  imports: [FormsModule],
  template: `
    <input
      #champEmail
      type="email"
      class="form-control"
      [(ngModel)]="email"
      required
      email
    >

    <!-- Template de référence pour injection dynamique -->
    <ng-template #gabaritErreur>
      <div class="alert alert-danger mt-2">Email invalide</div>
    </ng-template>

    <div #emplacementErreur></div>
  `
})
export class FormulaireAvanceComponent {
  email = signal('');

  // Accéder à l'ElementRef de l'input (élément DOM)
  champEmailEl = viewChild<ElementRef<HTMLInputElement>>('champEmail');

  // Accéder à la directive NgModel appliquée sur ce MÊME input
  champEmailModel = viewChild('champEmail', { read: NgModel });

  // Accéder au TemplateRef d'un ng-template
  gabaritErreur = viewChild('gabaritErreur', { read: TemplateRef });

  // Accéder au ViewContainerRef d'un élément (pour injection dynamique)
  emplacementErreur = viewChild('emplacementErreur', { read: ViewContainerRef });

  afficherErreur() {
    const container = this.emplacementErreur();
    const template  = this.gabaritErreur();
    if (container && template) {
      container.clear(); // Vider les erreurs précédentes
      container.createEmbeddedView(template); // Injecter le template d'erreur
    }
  }

  verifierValidite() {
    const ngModel = this.champEmailModel();
    if (ngModel?.invalid) {
      this.afficherErreur();
    }
  }
}
read est rarement nécessaire : Dans la majorité des cas, vous n'utilisez pas read. Il devient utile quand vous avez besoin d'accéder à une directive spécifique appliquée sur un élément, ou quand vous manipulez dynamiquement des templates avec ViewContainerRef.

Migration depuis les décorateurs et bonnes pratiques

Guide de migration rapide

Ancienne syntaxe (décorateur) Nouvelle syntaxe (signal)
@ViewChild('ref') el!: ElementRef el = viewChild<ElementRef>('ref')
@ViewChild(MonComp) comp!: MonComp comp = viewChild(MonComp)
@ViewChildren(MonComp) items!: QueryList<MonComp> items = viewChildren(MonComp)
@ContentChild('ref') el!: ElementRef el = contentChild<ElementRef>('ref')
@ContentChildren(MonComp) items!: QueryList<MonComp> items = contentChildren(MonComp)

Supprimer les cycles de vie inutiles

// Avant la migration — AfterViewInit et AfterContentInit requis
@Component({ /* ... */ })
export class AvantMigration implements AfterViewInit, AfterContentInit {
  @ViewChild('canvas')   canvas!: ElementRef;
  @ContentChild(LegendeComponent) legende!: LegendeComponent;

  ngAfterViewInit() {
    // Initialiser le canvas seulement ici
    this.initialiserCanvas(this.canvas.nativeElement);
  }

  ngAfterContentInit() {
    // Accéder à la légende projetée seulement ici
    console.log(this.legende.texte);
  }
}

// Après la migration — aucun cycle de vie nécessaire
@Component({ /* ... */ })
export class ApresMigration {
  canvas  = viewChild<ElementRef>('canvas');
  legende = contentChild(LegendeComponent);

  constructor() {
    // effect() détecte automatiquement quand les signaux deviennent définis
    effect(() => {
      const el = this.canvas();
      if (el) this.initialiserCanvas(el.nativeElement);
    });

    effect(() => {
      const leg = this.legende();
      if (leg) console.log(leg.texte());
    });
  }
}

Checklist de migration

  • Remplacer @ViewChild par viewChild() importé depuis @angular/core
  • Remplacer @ViewChildren par viewChildren()QueryList n'est plus nécessaire
  • Remplacer @ContentChild par contentChild()
  • Remplacer @ContentChildren par contentChildren()
  • Remplacer ngAfterViewInit par un effect() si la logique ne dépend que du signal
  • Ajouter ?. sur tous les accès aux signaux non-required
  • Utiliser .required() uniquement si l'élément est toujours dans le template (jamais dans un @if)
  • Supprimer les imports AfterViewInit, AfterContentInit si plus utilisés
Résumé : Les Signal Queries (viewChild, viewChildren, contentChild, contentChildren) remplacent les décorateurs Angular classiques par des signaux réactifs. Ils suppriment le besoin de AfterViewInit et AfterContentInit dans la majorité des cas, rendent le code plus lisible et s'intègrent naturellement avec effect() et computed(). Commencez la migration sur vos nouveaux composants standalone, et progressez sur les existants.

Partager