📧 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

AngularKit

Suite d'outils pour développeurs Angular francophones. Apprends, modernise tes réflexes, audite ta codebase.

Produits

Contact

Légal

© 2026 AngularKit. Tous droits réservés.