Skip to content

Angular

Mathieu Lavigne edited this page Aug 7, 2026 · 52 revisions

ControlValueAccessor

Composant personnalisé avec gestion du formControl simple : https://medium.com/angular-in-depth/dont-reinvent-the-wheel-when-implementing-controlvalueaccessor-a0ed4ad0fafd

Résultat final :

<app-slide-toggle formControlName="automatic">Automatique ?</app-slide-toggle>

Exemple : https://github.com/Bludwarf/CodeBank/tree/master/Angular/ControlValueAccessor

Détection de changements

Détection à l'intérieur d'une propriété (comme un array ou un object) avec KeyValueDiffers : https://stackoverflow.com/a/46330816/1655155.

Ensuite pour rafraichir les MatTableDataSource, on peut utiliser cette technique : https://stackoverflow.com/a/49224747/1655155.

Exemple : https://gist.github.com/Bludwarf/1334767e0124f78141f42dfa60e530c8.

Alternative : utiliser uniquement des objets non mutables (comme un ReadonlyArray), pour forcer la création d'une nouvelle instance.

ControlValueAccessor : si on veut détecter une modification sur un control, il ne faut pas affecter directement la valeur du control mais faire appel à la méthode setValue. Exemple :

const control = this.formGroup.controls.myControl;
control.setValue(newValue);
control.markAsDirty(); // Pour marquer le formulaire comme modifié par l'utilisateur

Pagination

Récupérer tous les éléments de toutes les pages d'une API : PageResultUtils

Attribut booléen

On peut utiliser un attribut booléen sous cette forme dans le template :

<app-component monAttribut></app-component>

Ce qui est équivalent à :

<app-component [monAttribut]="true"></app-component>

Dans ce cas, l'attribut a pour valeur la chaîne vide. Cela se traduit comme suit dans le composant :

    private monAttributBoolean = false;

    @Input() set monAttribut(value: boolean|string) {
        this.monAttributBoolean = value === '' || !!value;
    }

    get monAttribut(): boolean {
        return this.monAttribut;
    }

ExpressionChangedAfterItHasBeenCheckedError

Article détaillé

@ViewChildren

Exemple de code :

export class ParentComponent {
    @ViewChildren(ChildComponent)
    children: QueryList<ChildComponent>;
}
<app-other-component [otherProperty]="children.length" />

L'erreur apparaît parce que la valeur de otherProperty change, puisque'au 1er cycle children n'est pas encore initialisé, alors qu'au 2e cycle, elle l'est.

Solution avec ngAfterViewInit

Quand un composant parent se base sur ses enfants pour calculer ses propriétés, on peut empêcher l'erreur en déclenchant manuellement la détection de changements, juste après l'initialisation des enfants annotés @ViewChildren.

Exemple :

export class ParentComponent implements AfterViewInit {

    @ViewChildren(ChildComponent)
    children: QueryList<ChildComponent>;

    ngAfterViewInit(): void {
        this.changeDetectorRef.detectChanges();
    }

}

C'est une des solutions possibles détaillées dans l'article, mais indiquée comme problématique (cf. avant dernière section de l'article).

Solution avec OnPush

Source Doc

@Component({
    changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ParentComponent implements AfterContentChecked {

    @ViewChildren(ChildComponent)
    children: QueryList<ChildComponent>;

    ngAfterContentChecked(): void {
        this.changeDetectorRef.markForCheck();
    }

}

Solution acceptable ? Ça paraît bizarre de faire un markForCheck dans un ngAfterContentChecked...

Solution avec OnPush sans markForCheck

Si le composant le permet, on peut uniquement changer le mode de détection de changement, sans le déclencher manuellement.

@Component({
    changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ParentComponent {

    @ViewChildren(ChildComponent)
    children: QueryList<ChildComponent>;

}

QueryList

Par défaut, QueryList ne gère que les composants concrets. Si on souhaite utiliser une interface ou une classe abstraite, il faut ajouter l'annotation suivante dans chaque composant concret :

@Component({
    // ...
    // Indique à Angular que ce composant doit apparaître dans une QueryList<AbstractComponent>
    providers: [{provide: AbstractComponent, useExisting: forwardRef(() => ConcreteComponent)}],
})
export class ConcreteComponent extends AbstractComponent

Karma

Ajouter la config Karma pour ng test :

{
  "projects": {
    "<mon-projet>": {
      "architect": {
        "test": {
          "options": {
            "karmaConfig": "./karma.conf.js"
          }
        }
      }
    }
  }
}

Source

Jasmine

Lancer certains tests :

ng test --include='**/notes.spec.ts'

setTimeout en boucle

Lancer un évènement setTimeout dans un composant déclenche une détection de changement Angular, qui peut conduire à un boucle sans fin. Dans ce cas utiliser plutôt :

this.ngZone.runOutsideAngular(() => {
  // code...
})
  • Différence entre runOutsideAngular et run ?

Migration

Erreur ERESOLVE

Exemple d'erreur lors du début de la migration 10 -> 11 :

npm error code ERESOLVE
npm error ERESOLVE could not resolve
npm error
npm error While resolving: cuetube@0.1.0
npm error Found: @angular-devkit/build-angular@0.1002.4
npm error node_modules/@angular-devkit/build-angular
npm error   dev @angular-devkit/build-angular@"~0.1102.19" from the root project
npm error
npm error Could not resolve dependency:
npm error dev @angular-devkit/build-angular@"~0.1102.19" from the root project
npm error
npm error Conflicting peer dependency: @angular/compiler-cli@11.2.14
npm error node_modules/@angular/compiler-cli
npm error   peer @angular/compiler-cli@"^11.0.0 || ^11.2.0-next" from @angular-devkit/build-angular@0.1102.19
npm error   node_modules/@angular-devkit/build-angular
npm error     dev @angular-devkit/build-angular@"~0.1102.19" from the root project
npm error
npm error Fix the upstream dependency conflict, or retry
npm error this command with --force or --legacy-peer-deps
npm error to accept an incorrect (and potentially broken) dependency resolution.
npm error
npm error
npm error For a full report see:
npm error C:\Users\MLA20849\AppData\Local\npm-cache\_logs\2025-12-12T16_35_53_260Z-eresolve-report.txt
npm error A complete log of this run can be found in: C:\Users\MLA20849\AppData\Local\npm-cache\_logs\2025-12-12T16_35_53_260Z-debug-0.log

Le 1er bloc indique que la version installée (dans node_modules) (0.1002.4) ne correspond pas à la version demandée (par le package.json) (~0.1102.19) :

npm error Found: @angular-devkit/build-angular@0.1002.4
npm error node_modules/@angular-devkit/build-angular
npm error   dev @angular-devkit/build-angular@"~0.1102.19" from the root project

Il y a donc un conflit de versions.

La solution consiste donc à supprimer la dépendance dans le dossier node_modules et supprimer le fichier package-lock.json (attention avec ça ! on peut faire des montées de version de lib non souhaitées) puis retenter la migration.

  • Pour éviter le problème, devrait-on exécuter la migration Angular et Material via une seule commande ?

Guide

Variables d'environnement sécurisées

  1. Mettre en place un système pour surcharger les environnements Angular par des variables d'environnements (exemple)
  2. Ajouter les secrets correspondants côté CI/CD (exemple) (doc)

Données de navigation

Méthode Source de la donnée Liée au routeur* Visible dans l'URL Accessible hors constructeur
ActivatedRouteSnapshot.data Config de route statique (data: {...})
resolve (Resolvers) Fonction async exécutée avant activation, alimente .data
getCurrentNavigation().extras.state Passé impérativement via navigate(..., { state })
history.state / Location.getState() Même state que ci-dessus, lu autrement
ActivatedRoute.paramMap Paramètres de route (:id)
ActivatedRoute.queryParamMap Query params (?foo=bar)
withComponentInputBinding() Bind auto de params/queryParams/data vers @Input() ✅ (selon la source)
Matrix params (;param=value) Segment d'URL, via paramMap
Service partagé / store État applicatif, hors router ❌*

* Colonne "Liée au routeur" :

  • ✅ : Ancrées à l'URL/la config de route (data, resolve, params, withComponentInputBinding)
  • ✅ : Deep-link / Refresh OK : cette donnée est reconstructible depuis l'URL seule.
  • ✅ : Recalculées à l'identique à chaque activation, peu importe comment on est arrivé.
  • ❌ : Ancrées à l'événement de transition (state, service partagé)
  • ❌ : N'existent que le temps d'une navigation précise (rien en deep-link, pas de navigation préalable)
  • ❌ : Seul canal capable de transporter une donnée d'une route à une autre lors d'une redirection.
  • ❌* : Exception "Service partagé / store" : Survit au refresh si sessionStorage/localStorage (mais pas de deep-link)

Pour choisir rapidement :

  • Donnée qui doit survivre à un refresh ou être partageable → data de route, resolve, params/queryParams.
  • Donnée volatile, sensible ou complexe (objet non sérialisable en URL) transmise entre deux vues dans la même session → state de navigation ou service partagé.
  • Donnée qui doit être prête avant l'affichage du composant → resolve.

Clone this wiki locally