📧 Reste informé(e) !

Reçois les derniers articles et conseils EasyAngularKit directement dans ta boîte mail.

S'inscrire gratuitement

~3 min de lecture

ControlValueAccessor : Crée Tes Propres Inputs Comme Un Pro

Tu veux créer un composant qui fonctionne avec [(ngModel)] et FormControl ? Tu ne sais pas comment faire ?

Dans cet article, je vais t'expliquer POURQUOI tu en as besoin et COMMENT l'implémenter pas à pas.


🤔 POURQUOI : Le Problème Qu'on Veut Résoudre

Le besoin concret

Imagine que tu veux créer un composant de notation (rating) avec des étoiles.

Tu veux pouvoir l'utiliser comme ça :

// Dans un formulaire réactif
const form = this.fb.group({
  rating: [3] // Note par défaut
});
<!-- Dans le template -->
<form [formGroup]="form">
    <app-rating formControlName="rating"/>
</form>

Ou même comme ça :

<!-- Avec ngModel -->
<app-rating [(ngModel)]="userRating"/>

Le problème

Par défaut, ton composant ne sait PAS communiquer avec Angular Forms.

Si tu essayes formControlName="rating" sur ton composant custom, tu auras une erreur :

ERROR: No value accessor for form control with name: 'rating'

La solution

ControlValueAccessor est l'interface qui permet à ton composant de communiquer avec Angular Forms.

C'est le pont entre ton composant et le système de formulaires d'Angular.


📚 C'est Quoi ControlValueAccessor ?

Définition simple

ControlValueAccessor est une interface qui définit 4 méthodes :

  1. writeValue() : Angular te donne une valeur
  2. registerOnChange() : Tu donnes une fonction à Angular pour lui signaler les changements
  3. registerOnTouched() : Tu donnes une fonction à Angular pour lui signaler le "touch"
  4. setDisabledState() : Angular te dit si le champ est disabled

L'analogie simple

Imagine une boîte aux lettres 📬 :

  • writeValue = Quelqu'un met une lettre dans ta boîte (Angular → Toi)
  • registerOnChange = Tu donnes ton numéro de téléphone pour qu'on puisse te prévenir quand tu envoies une lettre ( Toi → Angular)
  • registerOnTouched = Tu préviens qu'on a ouvert la boîte
  • setDisabledState = On te dit que la boîte est fermée

🛠️ COMMENT : Implémentation Pas À Pas

Étape 1 : Créer le composant de base

On va créer un composant de rating avec des étoiles.


@Component({
  selector: 'app-rating-field',
  template: `
      @for (star of stars; track $index) {
        <button 
          type="button"
          (click)="setRating($index + 1)"
          [class.filled]="$index < value">
          ★
        </button>
      }
  `,
  styles: [`
    :host button {
      background: none;
      border: none;
      font-size: 32px;
      color: #ddd;
      cursor: pointer;
    }
    .host button.filled {
      color: gold;
    }
  `]
})
export class RatingField {
  readonly stars = [1, 2, 3, 4, 5];

  value = 0;

  setRating(rating: number): void {
    this.value = rating;
  }
}

Étape 2 : Implémenter ControlValueAccessor

MaintenantOn implémente l'interface :

import { ControlValueAccessor } from '@angular/forms';

@Component({
  selector: 'app-rating-field',
  template: `...`
})
export class RatingField implements ControlValueAccessor {
  readonly stars = [1, 2, 3, 4, 5];

  value = 0;

  // 🔵 1. Angular nous donne une valeur
  writeValue(value: number): void {
    this.value = value || 0;
  }

  // 🔵 2. On garde la fonction pour notifier Angular
  private onChange: (value: number) => void = () => {
  };

  registerOnChange(fn: (value: number) => void): void {
    this.onChange = fn;
  }

  // 🔵 3. On garde la fonction pour notifier le "touch"
  private onTouched: () => void = () => {
  };

  registerOnTouched(fn: () => void): void {
    this.onTouched = fn;
  }

  // 🔵 4. Angular nous dit si on est disabled
  setDisabledState(isDisabled: boolean): void {
    // On gère le disabled (optionnel)
  }

  // Notre logique métier
  setRating(rating: number): void {
    this.value = rating;
    this.onChange(rating); // ✅ On notifie Angular !
    this.onTouched(); // ✅ On notifie le touch
  }
}

Étape 3 : Enregistrer le provider

On dit à Angular que notre composant est un ControlValueAccessor :

import { NG_VALUE_ACCESSOR } from '@angular/forms';

@Component({
  selector: 'app-rating-field',
  // ✅ On enregistre le provider
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => RatingField),
      multi: true
    }
  ],
  template: `...`
})
export class RatingField implements ControlValueAccessor {
  // ...
}

Étape 4 : Utiliser le composant

Maintenant, ça marche ! 🎉


@Component({
  selector: 'app-review-form',
  imports: [ReactiveFormsModule, RatingField],
  template: `
    <form [formGroup]="form" (ngSubmit)="onSubmit()">
      <label>Votre note :</label>
      <app-rating-field formControlName="rating" />
      
      <p>Note sélectionnée : {{ form.value.rating }}</p>
      
      <button type="submit">Envoyer</button>
    </form>
  `
})
export class ReviewFormComponent {
  private readonly fb = inject(FormBuilder);

  readonly form = this.fb.group({
    rating: [0] // Valeur par défaut
  });

  onSubmit(): void {
    console.log(this.form.value);
  }
}

🔍 Les 4 Méthodes Expliquées Simplement

1️⃣ writeValue() : Reçoit la valeur

Quand ? Angular veut donner une valeur à ton composant.

writeValue(value: number): void {
  // Angular dit : "Voilà la valeur : 3"
  this.value = value || 0;
}

Exemple :

const form = this.fb.group({
  rating: [3] // → writeValue(3) est appelé
});

2️⃣ registerOnChange() : Enregistre le callback

Quand ? Angular te donne une fonction pour que tu puisses lui dire quand la valeur change.

private onChange: (value: number) => void = () => {
};

registerOnChange(fn: (value: number) => void): void {
  // Angular dit : "Voici ma fonction, appelle-la quand tu changes"
  this.onChange = fn;
}

// Plus tard, quand l'utilisateur clique
setRating(rating: number): void {
  this.value = rating;
  this.onChange(rating); // ✅ On prévient Angular
}

3️⃣ registerOnTouched() : Enregistre le touch

Quand ? Angular veut savoir quand l'utilisateur a "touché" le champ.

private onTouched: () => void = () => {
};

registerOnTouched(fn: () => void): void {
  // Angular dit : "Appelle-moi quand l'utilisateur touche le champ"
  this.onTouched = fn;
}

// Quand l'utilisateur clique
setRating(rating: number): void {
  this.value = rating;
  this.onChange(rating);
  this.onTouched(); // ✅ On prévient qu'il a touché
}

C'est utile pour les validations touched :


<app-rating-field formControlName="rating"/>
@if (form.controls.rating.touched && form.controls.rating.invalid) {
<span>Note requise</span>
}

4️⃣ setDisabledState() : Gère le disabled

Quand ? Angular veut activer/désactiver ton composant.

setDisabledState(isDisabled: boolean): void {
  this.disabled = isDisabled;
}

Exemple :

this.form.controls.rating.disable(); // → setDisabledState(true)
this.form.controls.rating.enable();  // → setDisabledState(false)

🎯 Exemple Complet : Rating Field Component

Voici l'implémentation complète :

import { Component, forwardRef } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';

@Component({
  selector: 'app-rating-field',
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => RatingComponent),
      multi: true
    }
  ],
  template: `
      @for (star of stars; track $index) {
        <button 
          type="button"
          (click)="setRating($index + 1)"
          [disabled]="disabled"
          [class.filled]="$index < value">
          ★
        </button>
      }
  `,
  styles: [`
    :host {
      display: flex;
      gap: 4px;
    }
    :host button {
      background: none;
      border: none;
      font-size: 32px;
      color: #ddd;
      cursor: pointer;
      transition: color 0.2s;
    }
    :host button:hover:not(:disabled) {
      color: #ffcc00;
    }
    :host button.filled {
      color: gold;
    }
    :host button:disabled {
      cursor: not-allowed;
      opacity: 0.5;
    }
  `]
})
export class RatingField implements ControlValueAccessor {
  readonly stars = [1, 2, 3, 4, 5];

  value = 0;

  disabled = false;

  private onChange: (value: number) => void = () => {
  };

  private onTouched: () => void = () => {
  };

  writeValue(value: number): void {
    this.value = value || 0;
  }

  registerOnChange(fn: (value: number) => void): void {
    this.onChange = fn;
  }

  registerOnTouched(fn: () => void): void {
    this.onTouched = fn;
  }

  setDisabledState(isDisabled: boolean): void {
    this.disabled = isDisabled;
  }

  setRating(rating: number): void {
    if (this.disabled) return;

    this.value = rating;
    this.onChange(rating);
    this.onTouched();
  }
}

✨ Bonus : Validation

Ton composant fonctionne automatiquement avec les validators Angular !

const form = this.fb.group({
  rating: [0, [Validators.required, Validators.min(1)]]
});

<app-rating-field formControlName="rating"/>

@if (form.controls.rating.touched && form.controls.rating.errors?.['required']) {
<span class="error">La note est requise</span>
}

@if (form.controls.rating.errors?.['min']) {
<span class="error">Note minimum : 1</span>
}

🛠️ Autres Exemples D'Usage

Toggle Switch


@Component({
  selector: 'app-toggle-field',
  template: `
    <button 
      type="button"
      (click)="toggle()"
      [class.active]="value">
      {{ value ? 'ON' : 'OFF' }}
    </button>
  `
})
export class ToggleField implements ControlValueAccessor {
  value = false;

  // ... implémentation CVA

  toggle(): void {
    this.value = !this.value;
    this.onChange(this.value);
    this.onTouched();
  }
}

Color Picker


@Component({
  selector: 'app-color-picker',
  template: `
    <input 
      type="color" 
      [value]="value"
      (input)="onColorChange($event)" />
  `
})
export class ColorPicker implements ControlValueAccessor {
  value = '#000000';

  // ... implémentation CVA

  onColorChange(event: Event): void {
    const input = event.target as HTMLInputElement;
    this.value = input.value;
    this.onChange(this.value);
    this.onTouched();
  }
}

❗ Les Erreurs Fréquentes

Erreur 1 : Oublier le provider

// ❌ Sans provider
@Component({
  selector: 'app-rating-field'
})
export class RatingField implements ControlValueAccessor {
}

// ✅ Avec provider
@Component({
  selector: 'app-rating-field',
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => RatingComponent),
      multi: true
    }
  ]
})
export class RatingField implements ControlValueAccessor {
}

Erreur 2 : Oublier d'appeler onChange

typscript

// ❌ Oubli
setRating(rating: number):void {
  this.value = rating;
  // Angular n'est pas notifié !
}

// ✅ Correct
setRating(rating: number): void {
  this.value = rating;
  this.onChange(rating); // ✅
  this.onTouched();
}

Erreur 3 : Modifier directement dans writeValue

// ❌ Mauvais : appeler onChange dans writeValue
writeValue(value: number): void {
  this.value = value;
  this.onChange(value); // ❌ Boucle infinie !
}

// ✅ Bon : juste mettre à jour la valeur
writeValue(value: number): void {
  this.value = value || 0;
}

🎓 Conclusion : Tu Maîtrises ControlValueAccessor

Maintenant, tu sais :

POURQUOI ControlValueAccessor existe (intégration avec Angular Forms)

COMMENT l'implémenter pas à pas

✅ Les 4 méthodes et leur rôle

✅ Comment gérer validation et disabled

✅ Les erreurs à éviter

La règle d'or :

Ton composant devient un vrai input Angular : il fonctionne avec formControlName, [(ngModel)], et toutes les validations.

📧 Reste informé(e) !

Reçois les derniers articles et conseils EasyAngularKit directement dans ta boîte mail.

S'inscrire gratuitement

EasyAngularKit

Formation complète pour maîtriser Angular et développer des applications web modernes.

Navigation

Contact

Légal

© 2026 Easy Angular Kit. Tous droits réservés.